refactor(ui): shared FilterChipGroup; unify feed filter chips (R13 S2c step 2)

New single-select FilterChipGroup primitive on the canonical rounded-full
"Filters bar" chip look (pill + shadow-xs + press feedback), plus an exported
filterChipClass so multi-select siblings share ONE geometry source of truth.

Migrations:
- feed watch-state chips (F1, Feed.tsx) -> FilterChipGroup, testIds preserved
- feed content-type toggles restyled to filterChipClass (stay multi-select) so
  the whole toolbar row is one chip look; wrapped in their own labelled group
- feed date presets + Custom (F3, Sidebar.tsx) -> FilterChipGroup with a
  behaviour-preserving dispatch (preset toggle-off + Custom editor intact)

Adds aria-label i18n (feed.showLabel/contentLabel, sidebar.datePresetLabel;
HU+EN) and FilterChipGroup.dom.test.tsx (6 tests). Self-E2E'd: watch switch,
date preset on/off toggle, Custom opens the range editor. gate green (vitest
136, tsc/eslint-0/knip/prettier). Branch not merged.
This commit is contained in:
2026-08-15 00:35:22 +02:00
parent 61ee7af9e4
commit 579af9cf89
8 changed files with 201 additions and 56 deletions
+29 -25
View File
@@ -40,6 +40,7 @@ import { LoadingState, StateMessage } from "./QueryState";
import { PageToolbar } from "./PageShell";
import { useWizard } from "./WizardProvider";
import SortControl from "./SortControl";
import FilterChipGroup, { filterChipClass } from "./FilterChipGroup";
import ViewSwitcher from "./ViewSwitcher";
import VirtualFeed from "./VirtualFeed";
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
@@ -563,32 +564,35 @@ export default function Feed({
const toolbar = (
<div className="pb-3">
<div className="flex items-center gap-1.5 gap-y-2 flex-wrap">
{SHOW_IDS.map((id) => (
<button
key={id}
data-testid={`feed-show-${id}`}
onClick={() => setFilters({ ...filters, show: id })}
aria-pressed={filters.show === id}
className="pill text-xs px-3 py-1.5 rounded-full border"
>
{t("sidebar.show." + id)}
</button>
))}
<FilterChipGroup
ariaLabel={t("feed.showLabel")}
options={SHOW_IDS.map((id) => ({
value: id,
label: t("sidebar.show." + id),
testId: `feed-show-${id}`,
}))}
value={filters.show}
onChange={(show) => setFilters({ ...filters, show })}
/>
<span className="mx-1 h-5 w-px bg-border" aria-hidden="true" />
{CONTENT.map((c) => {
const on = filters[c.key];
return (
<button
key={c.key}
data-testid={`feed-content-${c.key}`}
onClick={() => setFilters({ ...filters, [c.key]: !on })}
aria-pressed={on}
className="pill text-xs px-3 py-1.5 rounded-full border"
>
{t(c.label)}
</button>
);
})}
{/* Content-type toggles are MULTI-select (independent booleans) — same chip look via
filterChipClass, but not a single-select group. */}
<div className="flex flex-wrap gap-1.5" role="group" aria-label={t("feed.contentLabel")}>
{CONTENT.map((c) => {
const on = filters[c.key];
return (
<button
key={c.key}
data-testid={`feed-content-${c.key}`}
onClick={() => setFilters({ ...filters, [c.key]: !on })}
aria-pressed={on}
className={filterChipClass}
>
{t(c.label)}
</button>
);
})}
</div>
{/* The Source filter (organic / include-search / search-only) is a global-catalog concept —
it selects how videos ENTERED the library. On a channel-scoped view we already show all
of this one channel's videos (librarySource is pinned to "all"), so the selector would