feat(facets+ui): clickable facet values everywhere, genre prettify, active-filter chips

Frontend half of the faceted-navigation Phase 2. 'Everything filterable
should be clickable': every displayed facet value links to the bookshelf
pre-filtered by its raw stored value; display forms are prettified at
render, never in the DB.

- templates/facet_links.templ: FacetLink (inline links) + FacetBadge
  (badge links) to /bookshelf?<facet>_filter=<value>; the raw value
  rides the param while the shown text is prettified, so matching and
  display can never disagree. Series/tags keep their dedicated pages
  by design; the card media-type icon stays unlinked (a derived kind,
  not a column value — deep-linking it would send wrong values).
- book_detail.templ: publisher, language, edition, genre, imprint
  cells + narrators line + age-rating/story-arc badges are now links.
- prettifyGenre (templates/utils.go + unit tests): title-cases only
  entirely-lowercase values; small words stay down except first
  position; & preserved. TS port for chip labels documents the twin.
- bookshelf.templ + bookshelf.ts: active-filter chips bar — Alpine
  renders chips from client-local form state (sanctioned island);
  one bubbled htmx:afterRequest hook keeps chips + shareable URL in
  sync; removeFilterChip clears the facet and re-applies. URL deep
  links now accept all 18 facet params (hidden inputs where the
  drawer has no field).

Pins changelog: Batch B/C entry in docs/contract_pins.md. Rendering
ownership guard (make lint-rendering) green.
This commit is contained in:
John O'Keefe committed 2026-10-19 18:59:36 -04:00
1 parent 7e586df48a
commit bbedbfb13f
10 files changed
+990 -545

No files matched your search

+22
View File
@@ -180,3 +180,25 @@ cover_image_path, has_conflict, format_group, badge`.
input leaves `genre` empty). Existing rows are unaffected until a
rescan/re-extract.
## 2026-10-19 — Faceted navigation Batch B/C: chips, FacetLink, facets endpoint
- `GET /api/media-items/facets?facet=X&library_id=...` is a NEW pinned
surface (registry-driven value+count listing for the filter panel):
200 → `{facet, values[]: {value, display?, count}}` (display present
only for language codes → names); 400 for unknown facets and for
bool/range facets (they render as toggles, not lists); 403 for an
explicit library_id outside the requester's visible set.
- `GET /api/media-items/search` (JSON) and its HTMX grid partial now
share ONE param parser (`handlers.ParseMediaSearchParams`). Behavior
notes: the HTMX branch previously dropped `publisher_filter` and
`narrator_filter` silently — it now honors every facet param; and an
explicit hidden-library `library_id` is now a 403 on BOTH branches
(was: silent empty set on JSON). Response shapes are untouched; the
search row pin (47 keys) is unchanged.
- `language_filter` now normalizes comma elements through the ingest
language map, so display names ("English") match rows stored as ISO
codes ("en"). The param name is unchanged; single-code values behave
identically.
- Web: the bookshelf gains an active-filter chips bar (client-local
form state rendered by Alpine; templ owns the markup) and
`applyURLFilters` accepts the new facet params.