feat(design): the page band (B4) — one h1 per page, section headings, skip to content, filters fold on a short window (E28 S3d4)

- PageShell names the page with a visually hidden h1; a view with its own title renders it as
  PageHeading and the hidden one steps aside (contract: no hand <h1> inside the shell).
- Section captions are SectionLabel headings (h2, h3 in a dialog), ui/form Section included.
- "Skip to content" is the first Tab stop and focuses the scroller's <main>.
- Below 600px of height the band's filter row folds into "Filters · n" (Feed, Plex, server log),
  the same row shown in place.
- The band holds controls only: Downloads and Notifications lose their title cards, the Config and
  channel-discovery intros scroll with the content, the Stats tabs and the audit pager join the band,
  and the server log's toolbar sticks with the fade under it.
This commit is contained in:
2026-09-26 00:39:01 +02:00
parent 69edeecc45
commit eddeca82d3
41 changed files with 669 additions and 319 deletions
+30 -17
View File
@@ -33,6 +33,8 @@ import { PageToolbar } from "./ui/PageShell";
import ActiveFilterChips from "./ActiveFilterChips";
import SelectedViewChip from "./SelectedViewChip";
import ItemCount from "./ui/ItemCount";
import { FilterFoldButton } from "./ui/FilterFold";
import { useFilterFold } from "../lib/useFilterFold";
import { StateMessage } from "./ui/QueryState";
import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters";
import { usePlex, usePlexActions } from "./plexContext";
@@ -104,6 +106,8 @@ export default function PlexBrowse() {
// Plex's active-filter chip row (above the grid), the shared feed idiom: one removable chip per
// narrowing, plus a Clear-all (shared with the sidebar) that resets filters + watch-state + sort.
const plexChips = usePlexActiveFilters();
// B4: on a short window the filter row folds into one "Filters · n" button (ui/FilterFold).
const fold = useFilterFold();
const clearAllFilters = usePlexClearFilters();
// The provider holds sort scope-agnostically; heal it against THIS scope's options so a movie-only
// key (e.g. duration) left over from before a switch to shows doesn't ask for an unavailable sort.
@@ -523,29 +527,38 @@ export default function PlexBrowse() {
<div className="px-4 pt-2 pb-1 max-w-400 mx-auto">
<div className="flex items-center gap-1.5">
<ItemCount count={browseQ.isLoading ? undefined : total} />
<FilterFoldButton fold={fold} label={t("sidebar.filters")} count={plexChips.length} />
{/* Watch state — a top-level toolbar pill next to the count (moved out of the rail),
the same idiom as the feed's Watch-state FilterSelect. */}
<FilterSelect
label={t("plex.filter.show")}
testId="plex-show"
options={SHOW_OPTS.map((s) => ({ value: s, label: t(`plex.filter.showOpt.${s}`) }))}
selected={[show]}
onChange={(next) => setShow(next[0] ?? "all")}
/>
{!fold.folded && (
<FilterSelect
label={t("plex.filter.show")}
testId="plex-show"
options={SHOW_OPTS.map((s) => ({ value: s, label: t(`plex.filter.showOpt.${s}`) }))}
selected={[show]}
onChange={(next) => setShow(next[0] ?? "all")}
/>
)}
{/* Folded, the band is one row: Sort + the view toggle join it on the right. */}
{fold.folded && (
<div className="ml-auto flex shrink-0 items-center gap-1.5">{sortView}</div>
)}
</div>
{/* The controls row sits directly above the grid: the applied-filter chips flow from the
left (overflowing to a "+N"/count pill so they never crowd the controls), while Sort +
the Discover/Browse toggle stay pinned to the right — the same layout as the Feed. */}
<div className="mt-1 flex flex-wrap items-center gap-2">
{/* The active personal view (if any) leads the applied-filter row, like the feed. */}
<SelectedViewChip />
<ActiveFilterChips
filters={plexChips}
onClearAll={clearAllFilters}
className="flex-1"
/>
<div className="ml-auto flex shrink-0 items-center gap-1.5">{sortView}</div>
</div>
{!fold.folded && (
<div className="mt-1 flex flex-wrap items-center gap-2">
{/* The active personal view (if any) leads the applied-filter row, like the feed. */}
<SelectedViewChip />
<ActiveFilterChips
filters={plexChips}
onClearAll={clearAllFilters}
className="flex-1"
/>
<div className="ml-auto flex shrink-0 items-center gap-1.5">{sortView}</div>
</div>
)}
</div>
</PageToolbar>