The skeleton, not features. Two things felt rushed: the colour had no point of
view, and type and shape came from ad-hoc values.
Design language
- Palette reworked to warm-neutral dark with a single muted gold accent
(#d9a441). The chrome is deliberately colourless because the artwork is the
colour in this app; gold carries emphasis without competing with covers.
Light mode re-tuned to match.
- A real type scale (--fs-xs … --fs-3xl) replaces the ad-hoc rem values, with
fixed line heights and letter-spacing per level. Headings are 620 weight and
tighter; body is a consistent 15px.
- Radii tightened to a 5/8/12 scale, one shadow pair, and the primary button
now uses dark ink on gold for contrast.
Featured carousel
- The banner rotates through up to six items — in progress first, then most
viewed, then newest — drawn only from the libraries the user was granted, so
it can never show something they cannot open.
- Auto-advances every 8s, pauses on hover and focus, and has dots plus arrows
for manual control. Slides crossfade, and the ambient wash changes with them.
Verified in the built bundle: accent resolves to #d9a441, six slides, rotation
confirmed across three items, and hover pauses it.
Not new features — motion, focus, and depth, kept restrained.
- Motion tokens: one easing curve and three durations, used everywhere, so the
app moves as a single thing rather than component by component. Honours
prefers-reduced-motion by disabling all of it.
- View transitions: navigating fades and rises the content instead of swapping
it, and each view starts scrolled to the top.
- Topbar frosts once the content scrolls, so the header separates from the page
without a permanent bar of chrome.
- Featured area on Home: the item you are most likely to act on (something in
progress, else the newest), with the cover as an ambient blurred wash. The
colour comes from the artwork, not a decorative gradient, so it belongs to
whatever is on screen.
- Cards: lift and scale on hover, a scrim so the action glyph always has
contrast, a kind-appropriate glyph (play / read / expand), press feedback,
and a visible keyboard focus ring.
- Shelves: edge fades and arrow buttons that appear only when the row actually
continues that way; the scrollbar is hidden and the row snaps.
- Item page: ambient wash from its own cover.
- Global :focus-visible ring.
Verified in the built bundle: featured renders the in-progress item, hover
scales a 192px card to 197.8px with the glyph visible, and the item page tints
from its artwork.
- All 50 font-size declarations converted from px to rem, so a single root
value scales headings, labels and body together. The root is now 15px, a
deliberate ~7% increase over the previous 14px.
- Card width and grid column width are now --card-w / --grid-min variables,
driven by --card-scale independently of text. Defaults bumped too: shelf
cards 150 -> 160px, grid columns 148 -> 158px.
- Appearance panel (nav item, visible to everyone): Text size and Card size
as segmented controls, a live preview, and a reset. Settings are stored in
localStorage, per device rather than per account, because a TV across the
room wants bigger text than a laptop and the same person uses both.
- nav: 'settings' was missing from the View union after the home rewrite;
added back along with the Appearance entry.
Verified in the built bundle: root font 15px by default; "Larger" scales it to
19.2px; "Large" cards scale 160 to 192px; both persist across a reload and are
independent of each other.
The app looked like an admin panel. This is the product people actually see
when they open their library.
- Home: a landing screen with a Continue shelf (progress bars, straight from
the progress table) and per-type shelves of recent additions. One request
feeds it, so it does not pop in piece by piece.
- Browse: cover grid per media type with type chips, plus browse-all. Powered
by a new GET /items that lists across every visible library, filtered by
kind, through the same access chokepoint.
- Item page: cover, kind, title, authors, progress, a primary action that says
Continue when you have started, description, and the file list for
multi-file items. Cards now open the item page rather than jumping straight
into a reader.
- Search: results grouped by media type, cover-forward.
- Navigation moved from "libraries" to "media types" — you browse Ebooks, not
"the Ebooks folder". Libraries became an admin concern.
- Design system: dark by default, cover-forward, one accent, tighter type
scale, horizontal shelves, skeleton loaders.
- media: GET /home, GET /items, and authors on item detail.
Verified by screenshot in a live instance: home shelves, browse grid, and item
page all render correctly.
The frontend the app needs to be usable at all.
- web/: Svelte 5 + Vite + TypeScript SPA. Login, app shell with per-type nav,
library list/create/scan/delete, item grid with covers, admin user
management, and an epub.js reader that syncs progress.
- Reader resumes by spine index — an integer — which is why the polymorphic
progress table needs no text column.
- Session state uses classic stores in a plain .ts module rather than runes in
a .svelte.ts module, avoiding special module resolution.
- Embedding: web/embed.go embeds web/dist. A committed .gitkeep keeps the
directive valid before the frontend is built, so `go build ./...` never
fails. Unknown non-API paths serve index.html for client routes; /api/* and
/healthz always return JSON and never fall back to HTML.
- Dockerfile builds the UI in a node stage and overlays dist into the Go stage.
- pnpm 12 needs allowBuilds in pnpm-workspace.yaml; esbuild approved, the two
unused transitive scripts explicitly denied so install exits 0.
svelte-check: 0 errors, 0 warnings. Verified served: SPA shell at /, route
fallback, hashed asset, and JSON 404 on /api/*.