refactor(ui): unify Channels + Stats filter chips (R13 S2c step 4)

- channel-manager sync-status chips (F2) -> FilterChipGroup (rounded-lg -> the
  canonical rounded-full), controls-row alignment preserved via className
- channel-manager user-tag chips (multi-select) restyled to filterChipClass
  (rounded-lg -> rounded-full) so the whole manager chip set is one look
- stats system time-range chips (F4) -> FilterChipGroup (text-sm -> canonical)

Adds aria-label i18n (channels.filters.syncLabel, stats.rangeLabel; HU+EN).
Self-E2E'd: sync chips + tag chips rounded-full, view TABS stay rounded-lg
(their own canonical), stats 7/30/90d toggles. gate green (tsc/eslint-0/
vitest 136/knip/prettier). Branch not merged.
This commit is contained in:
2026-08-15 00:53:12 +02:00
parent f26e329ecf
commit 42d0173095
6 changed files with 24 additions and 25 deletions
+9 -13
View File
@@ -19,6 +19,7 @@ import { useChannelTagActions } from "../lib/useChannelTagActions";
import { useChannelPriority } from "../lib/useChannelPriority"; import { useChannelPriority } from "../lib/useChannelPriority";
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher"; import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
import ColumnSortControl from "./ColumnSortControl"; import ColumnSortControl from "./ColumnSortControl";
import FilterChipGroup, { filterChipClass } from "./FilterChipGroup";
import Pager from "./Pager"; import Pager from "./Pager";
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout"; import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
import { sortRows } from "../lib/columnSort"; import { sortRows } from "../lib/columnSort";
@@ -294,18 +295,13 @@ export default function Channels() {
// where they act. // where they act.
const syncFilter = typeof filters.sync === "string" ? filters.sync : ""; const syncFilter = typeof filters.sync === "string" ? filters.sync : "";
const statusChips = ( const statusChips = (
<div className="flex flex-wrap items-center gap-1.5"> <FilterChipGroup
{SYNC_CHIPS.map((c) => ( ariaLabel={t("channels.filters.syncLabel")}
<button className="flex flex-wrap items-center gap-1.5"
key={c.value || "all"} options={SYNC_CHIPS.map((c) => ({ value: c.value, label: t(c.labelKey) }))}
onClick={() => setFilters({ ...filters, sync: c.value })} value={syncFilter}
aria-pressed={syncFilter === c.value} onChange={(sync) => setFilters({ ...filters, sync })}
className="pill text-xs px-2.5 py-1 rounded-lg border" />
>
{t(c.labelKey)}
</button>
))}
</div>
); );
// The catalog-wide stats ride on the right of the tab row (subscribed only), leaving room for // The catalog-wide stats ride on the right of the tab row (subscribed only), leaving room for
@@ -479,7 +475,7 @@ export default function Channels() {
key={tg.id} key={tg.id}
onClick={() => toggleTagFilter(tg.id)} onClick={() => toggleTagFilter(tg.id)}
aria-pressed={tagFilter.includes(String(tg.id))} aria-pressed={tagFilter.includes(String(tg.id))}
className="pill text-xs px-2 py-1 rounded-lg border" className={filterChipClass}
> >
{tg.name} {tg.name}
</button> </button>
+11 -12
View File
@@ -9,6 +9,7 @@ import { notify } from "../lib/notifications";
import { LS, useAccountPersistedState } from "../lib/storage"; import { LS, useAccountPersistedState } from "../lib/storage";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import { LoadingState, StateMessage } from "./QueryState"; import { LoadingState, StateMessage } from "./QueryState";
import FilterChipGroup from "./FilterChipGroup";
import { Section, SettingRow } from "./ui/form"; import { Section, SettingRow } from "./ui/form";
import { useMe } from "../lib/useMe"; import { useMe } from "../lib/useMe";
@@ -228,18 +229,16 @@ function SystemStats() {
<div className="flex items-center justify-between gap-3 mb-4"> <div className="flex items-center justify-between gap-3 mb-4">
<h2 className="text-lg font-semibold">{t("stats.title")}</h2> <h2 className="text-lg font-semibold">{t("stats.title")}</h2>
<div className="flex items-center gap-1"> <FilterChipGroup
{RANGES.map((d) => ( ariaLabel={t("stats.rangeLabel")}
<button className="flex items-center gap-1.5"
key={d} options={RANGES.map((d) => ({
onClick={() => setDays(d)} value: String(d),
aria-pressed={days === d} label: t("stats.rangeDays", { count: d }),
className="pill px-3 py-1.5 rounded-full text-sm border" }))}
> value={String(days)}
{t("stats.rangeDays", { count: d })} onChange={(v) => setDays(Number(v))}
</button> />
))}
</div>
</div> </div>
<p className="text-xs text-muted mb-4 leading-relaxed"> <p className="text-xs text-muted mb-4 leading-relaxed">
@@ -39,6 +39,7 @@
} }
}, },
"filters": { "filters": {
"syncLabel": "Sync status",
"all": "All", "all": "All",
"needsFull": "Needs full history", "needsFull": "Needs full history",
"fullySynced": "Fully synced" "fullySynced": "Fully synced"
+1
View File
@@ -5,6 +5,7 @@
}, },
"title": "API quota usage", "title": "API quota usage",
"rangeDays": "{{count}}d", "rangeDays": "{{count}}d",
"rangeLabel": "Time range",
"introBefore": "YouTube Data API units attributed by who triggered the spend. ", "introBefore": "YouTube Data API units attributed by who triggered the spend. ",
"introSystem": "System", "introSystem": "System",
"introAfter": " is shared background work (scheduled backfill, enrichment, resync) that isn't tied to one person.", "introAfter": " is shared background work (scheduled backfill, enrichment, resync) that isn't tied to one person.",
@@ -39,6 +39,7 @@
} }
}, },
"filters": { "filters": {
"syncLabel": "Szinkron állapot",
"all": "Mind", "all": "Mind",
"needsFull": "Hiányos előzmény", "needsFull": "Hiányos előzmény",
"fullySynced": "Teljesen szinkronizálva" "fullySynced": "Teljesen szinkronizálva"
+1
View File
@@ -5,6 +5,7 @@
}, },
"title": "API-kvóta használat", "title": "API-kvóta használat",
"rangeDays": "{{count}} nap", "rangeDays": "{{count}} nap",
"rangeLabel": "Időtartomány",
"introBefore": "A YouTube Data API-egységek aszerint, hogy ki váltotta ki a felhasználást. A ", "introBefore": "A YouTube Data API-egységek aszerint, hogy ki váltotta ki a felhasználást. A ",
"introSystem": "Rendszer", "introSystem": "Rendszer",
"introAfter": " a megosztott háttérmunka (ütemezett letöltés, gazdagítás, újraszinkronizálás), amely nem köthető egy személyhez.", "introAfter": " a megosztott háttérmunka (ütemezett letöltés, gazdagítás, újraszinkronizálás), amely nem köthető egy személyhez.",