feat: draw covers for books without one

Full-bleed serif title on a woven ground, drawn in the browser rather than
stored, sized with container queries so one component works from 36px to full
page. Colour is hashed into a list of bookcloth tones instead of the hue wheel.

The grid and search were falling back to default_cover.jpg because a missing
cover made the src "/api/undefined"; they now check for one first.
This commit is contained in:
2026-08-12 13:44:14 -04:00
parent 96789620bb
commit 7e04826fa5
4 changed files with 203 additions and 30 deletions
@@ -1,6 +1,7 @@
<script lang="ts">
import { resolve } from '$app/paths';
import BookImage from './book-image.svelte';
import GeneratedCover from './generated-cover.svelte';
import { Progress } from '$lib/components/ui/progress/index';
import { getLibraryState } from '$lib/state/library.svelte';
import { getBookSelectionState } from '$lib/state/bookSelection.svelte';
@@ -31,13 +32,30 @@
: ''}"
onclick={handleClick}
>
<BookImage
src="/api/{book.cover_image}"
class="h-full w-full rounded-sm object-cover transition-all duration-200 group-hover:brightness-50 {selected ||
darkened
? 'brightness-50'
: ''}"
/>
<!--
Checked rather than left to the image's onerror: with no cover_image the
src became "/api/undefined", 404'd, and fell back to the generic
default_cover.jpg — which is what made every coverless book look alike in
the grid.
-->
{#if book.cover_image}
<BookImage
src="/api/{book.cover_image}"
class="h-full w-full rounded-sm object-cover transition-all duration-200 group-hover:brightness-50 {selected ||
darkened
? 'brightness-50'
: ''}"
/>
{:else}
<div
class="h-full w-full transition-all duration-200 group-hover:brightness-50 {selected ||
darkened
? 'brightness-50'
: ''}"
>
<GeneratedCover {book} />
</div>
{/if}
<!-- Inside the anchor, which already clips to rounded-sm, so the bar
follows the cover's corners instead of floating below it -->