feat(plex): port the filter bar to the YT-feed pattern

Bring the Plex module's filter surface in line with the feed's final shape:

- Flatten the rail: PlexSidebar passes `collapsible={false}` to FilterRail so
  the widgets become plain always-open label + control rows (no collapse
  chevrons; reorder/hide in edit mode stay). Drop the rail's own "Clear all" —
  the chip row above the grid already owns it.
- Move Sort + the Discover/Browse toggle into the applied-filter chip row,
  pinned right directly above the grid (a `sortView` fragment, like the feed's),
  and give ActiveFilterChips `flex-1` so it gets the shared responsive overflow
  (wide: fit + "+N" pill; narrow: single "N filters" count pill) — the chips can
  no longer crowd the controls off-screen.

ItemCount leads its own toolbar row, matching the feed. No behaviour change to
Discover, which keeps its standalone view toggle.
This commit is contained in:
2026-08-22 07:23:20 +02:00
parent 041cd526b7
commit 21c9b34540
2 changed files with 31 additions and 27 deletions
+28 -13
View File
@@ -369,6 +369,24 @@ export default function PlexBrowse() {
/> />
); );
// Sort + the Discover/Browse toggle are tightly bound to the grid below, so on the grid view they
// live pinned to the right of the applied-filter chip row (directly above the cards), not on their
// own toolbar line — the same idiom as the Feed's `sortView`.
const sortView = (
<>
<SortControl
options={sortOptions}
sort={sortState}
onChange={(next) => setSort(next ?? PLEX_SORT_FALLBACK)}
label={t("plex.filter.sort")}
ascLabel={t("plex.filter.dir.asc")}
descLabel={t("plex.filter.dir.desc")}
/>
<span className="mx-0.5 h-5 w-px bg-border" aria-hidden="true" />
{viewToggle}
</>
);
if (sub.view.kind === "discover") { if (sub.view.kind === "discover") {
return ( return (
<div className="p-4 max-w-[1600px] mx-auto"> <div className="p-4 max-w-[1600px] mx-auto">
@@ -389,25 +407,22 @@ export default function PlexBrowse() {
{/* The title count is fixed chrome — it portals into the shell's fixed band so it stays put {/* The title count is fixed chrome — it portals into the shell's fixed band so it stays put
while only the poster grid scrolls. */} while only the poster grid scrolls. */}
<PageToolbar> <PageToolbar>
<div className="px-4 pt-2 pb-1 max-w-[1600px] mx-auto flex items-center gap-1.5"> <div className="px-4 pt-2 pb-1 max-w-[1600px] mx-auto">
<ItemCount count={browseQ.isLoading ? undefined : total} /> <ItemCount count={browseQ.isLoading ? undefined : total} />
{viewToggle} {/* The controls row sits directly above the grid: the applied-filter chips flow from the
{/* Sort moved out of the filter sidebar to the row above the cards (S3), like the Feed. */} left (overflowing to a "+N"/count pill so they never crowd the controls), while Sort +
<div className="ml-auto flex items-center gap-1.5"> the Discover/Browse toggle stay pinned to the right — the same layout as the Feed. */}
<SortControl <div className="mt-1 flex items-center gap-2">
options={sortOptions} <ActiveFilterChips
sort={sortState} filters={plexChips}
onChange={(next) => setSort(next ?? PLEX_SORT_FALLBACK)} onClearAll={clearAllFilters}
label={t("plex.filter.sort")} className="flex-1"
ascLabel={t("plex.filter.dir.asc")}
descLabel={t("plex.filter.dir.desc")}
/> />
<div className="ml-auto flex shrink-0 items-center gap-1.5">{sortView}</div>
</div> </div>
</div> </div>
</PageToolbar> </PageToolbar>
<ActiveFilterChips filters={plexChips} onClearAll={clearAllFilters} />
{personCards.length > 0 && ( {personCards.length > 0 && (
<div className="mb-4 flex flex-wrap gap-3"> <div className="mb-4 flex flex-wrap gap-3">
{personCards.map((p) => ( {personCards.map((p) => (
+3 -14
View File
@@ -13,7 +13,6 @@ import { type PanelItem } from "./PanelGroups";
import SegmentedToggle from "./SegmentedToggle"; import SegmentedToggle from "./SegmentedToggle";
import FilterChipGroup from "./FilterChipGroup"; import FilterChipGroup from "./FilterChipGroup";
import Combobox, { type ComboboxOption } from "./Combobox"; import Combobox, { type ComboboxOption } from "./Combobox";
import { usePlexClearFilters } from "./usePlexActiveFilters";
import { usePlex, usePlexActions } from "./PlexProvider"; import { usePlex, usePlexActions } from "./PlexProvider";
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups // The Plex module's left filter column — now the shared floating SidePanel with its filter groups
@@ -96,10 +95,8 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
// Scope and sort moved to the header / above-the-grid toolbar (S3); the panel badge now counts only // Scope and sort moved to the header / above-the-grid toolbar (S3); the panel badge now counts only
// the filters that still live in this panel (expanded filters + watch-state). // the filters that still live in this panel (expanded filters + watch-state).
const activeCount = fCount + (show !== "all" ? 1 : 0); const activeCount = fCount + (show !== "all" ? 1 : 0);
const anyActive = activeCount > 0;
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p }); const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
const clearAll = usePlexClearFilters();
const durBucketKey = DURATION_BUCKETS.find( const durBucketKey = DURATION_BUCKETS.find(
(b) => (filters.durationMin ?? null) === b.min && (filters.durationMax ?? null) === b.max, (b) => (filters.durationMin ?? null) === b.min && (filters.durationMax ?? null) === b.max,
@@ -361,17 +358,9 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
layout={layout} layout={layout}
setLayout={setLayout} setLayout={setLayout}
items={items} items={items}
actions={ // The Plex widgets are simple label + control rows now, not collapsible islands (matches the
anyActive ? ( // feed rail). The rail's own "Clear all" is dropped too — the chip row above the grid owns it.
<button collapsible={false}
onClick={clearAll}
className="inline-flex items-center gap-1 text-xs text-muted hover:text-accent transition px-1 whitespace-nowrap"
>
<X className="w-3.5 h-3.5" />
{t("plex.filter.clearAll")}
</button>
) : undefined
}
/> />
); );
} }