Backend
- sort=recent stays a back-compat alias for created (parse_sort); invalid
sort/order now return the structured 422 envelope instead of plain-text 400
- per-field default direction on omitted order (dates desc, text asc), matching
the frontend so a bare ?sort=<field> reads the same in UI and API
- migration 0033: index mangas(updated_at DESC, id) backing the default sort
and its id tie-break; NULLS LAST now applied only to the nullable author key
- tests: recent alias, per-field default (title+author), invalid-value 422,
and a tie-break test that pins ordering by ascending id (mutation-verified)
Frontend
- pure sort helpers extracted to $lib/mangaSort with unit tests; coerceSort
honors the recent alias so pasted/legacy URLs resolve to the same field
- SegmentedControl: roving tabindex + arrow/Home/End keyboard nav, anchored on
focus so rapid keypresses don't stick
- UX: visible "Direction" labels (desktop + mobile), mobile sort-sheet section
headings and a "Done" button; README sort/order contract updated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the two-option `ListSort` enum with an orthogonal sort field
(created/updated/title/author, default updated) and direction
(asc/desc, default desc). The catalog now defaults to last-updated-first
and lets the user order by any field in either direction.
Backend builds ORDER BY from the enums only (no injection seam), with a
NULLS-LAST author subquery and a stable id tie-break. Frontend adds a
direction toggle with per-field defaults (dates desc, text asc) and
labels (Newest/Oldest vs A->Z/Z->A), reusing SegmentedControl; URL state
omits the per-field defaults and validates on hydrate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2 of the mobile redesign: the catalog at `/` adapts to phone
viewports without disrupting the desktop layout. The same filter form
renders inline on desktop and inside a bottom sheet on mobile, and a
dedicated sort sheet replaces the inline `<select>` below 640px.
- MangaCard gains optional `unreadCount` and `progress` props that
render a top-right pill badge and a bottom progress overlay on the
cover. Both are no-ops when omitted, so existing callers don't
change. Counts past 99 cap at "99+".
- The filter form body is extracted into a Svelte 5 snippet and
rendered conditionally — inline desktop panel OR mobile sheet, never
both — so no testid is duplicated and the focus trap can't double-
fire. A matchMedia listener tracks the 640px breakpoint and drives
the snippet target.
- Mobile catalog adds: Filter / Sort chip buttons, an always-visible
active-filter chip row with per-facet remove buttons, full-width
search, and a 2-column grid.
- Auto-expanding the filter panel from URL params is now desktop-only
— on mobile the active-filter chips signal applied facets without
hiding the catalog under a scrim on first paint.
- Vitest suite covers MangaCard badge / overlay behavior including
clamp / hide edge cases. Playwright spec covers the mobile filter +
sort sheet flow at 390px viewport, with a hydration gate to keep
click dispatch from racing the SSR'd-but-not-yet-reactive button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>