Files
website/src/lib/books/BookCover.svelte

134 lines
3.0 KiB
Svelte

<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;
container-type: inline-size;
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;
/* Scale with the cover's width so thumbnails don't break words mid-way. */
font-size: clamp(0.55rem, 12cqi, 1.6rem);
display: -webkit-box;
-webkit-line-clamp: 6;
line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: break-word;
hyphens: auto;
}
.g-author {
font-size: clamp(0.5rem, 6.5cqi, 0.85rem);
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.8;
}
@container (max-width: 90px) {
.g-author {
display: none;
}
}
.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>