books-ui: library, detail page, EPUB/PDF reader, reading status/rating/sessions migration, progress/notes APIs, owner gating and tests
This commit is contained in:
1 parent
2ca2d0d5a4
commit
364c7ee132
65 files changed
+25709
-24
No files matched your search
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
// A book cover: the real image when there is one, otherwise a generated cover in a
|
||||
// colour derived from the title (also used when the image fails to load).
|
||||
import { authorLine, coverHue } from './shared.js';
|
||||
|
||||
let {
|
||||
src,
|
||||
title,
|
||||
authors = [],
|
||||
placeholder = false,
|
||||
size = 'md',
|
||||
class: className = ''
|
||||
}: {
|
||||
src: string | null;
|
||||
title: string;
|
||||
authors?: readonly string[];
|
||||
placeholder?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let failed = $state<string | null>(null);
|
||||
const showImage = $derived(!!src && failed !== src);
|
||||
const hue = $derived(coverHue(title));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={['cover', `cover-${size}`, placeholder && 'is-placeholder', className]}
|
||||
style:--hue={hue}
|
||||
>
|
||||
{#if showImage}
|
||||
<img
|
||||
{src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerpolicy="no-referrer"
|
||||
onerror={() => (failed = src)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="generated" aria-hidden="true">
|
||||
<span class="g-title">{title}</span>
|
||||
{#if authors.length}<span class="g-author">{authorLine(authors)}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cover {
|
||||
position: relative;
|
||||
aspect-ratio: 2 / 3;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 0.375rem 0.625rem 0.625rem 0.375rem;
|
||||
background: var(--site-hover);
|
||||
box-shadow:
|
||||
0 1px 2px rgb(0 0 0 / 0.25),
|
||||
0 8px 24px -12px rgb(0 0 0 / 0.5);
|
||||
}
|
||||
/* Spine highlight, like a real paperback. */
|
||||
.cover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(0 0 0 / 0.28) 0,
|
||||
rgb(255 255 255 / 0.12) 2.5%,
|
||||
rgb(0 0 0 / 0.08) 5%,
|
||||
transparent 9%
|
||||
);
|
||||
border-radius: inherit;
|
||||
}
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.generated {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 14% 10% 10% 14%;
|
||||
color: hsl(var(--hue) 30% 96%);
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 80% at 100% 0%,
|
||||
hsl(calc(var(--hue) + 40) 60% 45% / 0.55),
|
||||
transparent 60%
|
||||
),
|
||||
linear-gradient(160deg, hsl(var(--hue) 45% 32%), hsl(calc(var(--hue) + 20) 50% 16%));
|
||||
}
|
||||
.g-title {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
font-size: 1rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 6;
|
||||
line-clamp: 6;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.g-author {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.cover-sm .g-title {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.cover-sm .g-author {
|
||||
display: none;
|
||||
}
|
||||
.cover-lg .g-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.cover-lg .g-author {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.is-placeholder {
|
||||
box-shadow: none;
|
||||
outline: 1.5px dashed color-mix(in srgb, var(--site-muted) 60%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.is-placeholder img,
|
||||
.is-placeholder .generated {
|
||||
filter: grayscale(0.85) opacity(0.55);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user