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
+38 -31
View File
@@ -16,6 +16,7 @@ import SidePanel from "./SidePanel";
import PanelGroups, { type PanelItem } from "./PanelGroups";
import ChannelPicker from "./ChannelPicker";
import SegmentedToggle from "./SegmentedToggle";
import FilterChipGroup from "./FilterChipGroup";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
import { useChannelsActions } from "./ChannelsProvider";
import { useMe } from "../lib/useMe";
@@ -150,42 +151,47 @@ export default function Sidebar({
return <SavedViewsWidget filters={filters} onApply={setFilters} />;
case "channel":
return <ChannelPicker filters={filters} setFilters={setFilters} />;
case "date":
case "date": {
// Single-select across the presets + Custom: a preset is active only when its maxAge matches
// and no custom range is open; otherwise Custom owns the selection (or none).
const activePresetDays = DATE_PRESETS.find(
(p) => filters.maxAgeDays === p.days && !customDates,
)?.days;
const dateKey =
customDates || filters.dateFrom || filters.dateTo
? "custom"
: activePresetDays != null
? String(activePresetDays)
: "";
return (
<>
<div className="flex flex-wrap gap-1.5">
{DATE_PRESETS.map((p) => {
const activePreset = filters.maxAgeDays === p.days && !customDates;
return (
<button
key={p.days}
onClick={() =>
setFilters({
...filters,
maxAgeDays: activePreset ? undefined : p.days,
dateFrom: undefined,
dateTo: undefined,
})
}
aria-pressed={activePreset}
className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px"
>
{t("sidebar.datePreset." + p.key)}
</button>
);
})}
<button
onClick={() => {
<FilterChipGroup
ariaLabel={t("sidebar.datePresetLabel")}
value={dateKey}
options={[
...DATE_PRESETS.map((p) => ({
value: String(p.days),
label: t("sidebar.datePreset." + p.key),
})),
{ value: "custom", label: t("sidebar.custom") },
]}
onChange={(key) => {
if (key === "custom") {
const next = !customDates;
setCustomDates(next);
if (next) setFilters({ ...filters, maxAgeDays: undefined });
}}
aria-pressed={!!(customDates || filters.dateFrom || filters.dateTo)}
className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px"
>
{t("sidebar.custom")}
</button>
</div>
return;
}
const days = Number(key);
const activePreset = filters.maxAgeDays === days && !customDates;
setFilters({
...filters,
maxAgeDays: activePreset ? undefined : days,
dateFrom: undefined,
dateTo: undefined,
});
}}
/>
{(customDates || filters.dateFrom || filters.dateTo) && (
<div className="flex flex-col gap-1.5 mt-2">
@@ -233,6 +239,7 @@ export default function Sidebar({
)}
</>
);
}
case "language": {
const chips = visibleChips(languages);
return (