refactor(channels): adopt shared Tabs + usePersistedTab for the manager tab row (R13 S2b)

Retire the hand-rolled channel-manager view pills for the shared <Tabs> component (given an
optional className so it embeds in the justify-between header without the default mb-4), and
back view + layout on usePersistedTab instead of manual getAccountRaw/setAccountRaw useState.
That subsumes the account-pin re-hydrate (usePersistedTab now re-reads on the pin), so
ChannelsProvider's bespoke markManagerDirty/useAccountRehydrate is gone; setView keeps the
history-tab logic (_chTab pushState + popstate) layered on the stable raw setter, and
useAccountPersistedState's setter is now memoized so that stable action holds.

Per the user's call, unify every channel-manager pill on the rounded-lg idiom: the view tabs
via <Tabs>, the sync chips and tag chips + the "manage tags" affordance from rounded-full to
rounded-lg — no more odd-one-out rounded-full.

gate-green: tsc + vitest 120 + eslint-0 + knip + prettier. Self-E2E'd: tab switch, browser
Back across tabs (Blocked->Hidden->Subscriptions with localStorage in sync), and a fresh tab
restoring view=hidden on the FIRST load; zero console errors.
This commit is contained in:
2026-08-14 03:50:43 +02:00
parent e6688edbcc
commit 735d08c91d
4 changed files with 42 additions and 54 deletions
+13 -15
View File
@@ -12,6 +12,7 @@ import { buildChannelColumns } from "./channelColumns";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
import DataTable from "./DataTable";
import Tabs, { type TabDef } from "./Tabs";
import ChannelLayoutGrid from "./ChannelLayoutGrid";
import { LoadingState, StateMessage } from "./QueryState";
import { useChannelTagActions } from "../lib/useChannelTagActions";
@@ -299,7 +300,7 @@ export default function Channels() {
key={c.value || "all"}
onClick={() => setFilters({ ...filters, sync: c.value })}
aria-pressed={syncFilter === c.value}
className="pill text-xs px-2.5 py-1 rounded-full border"
className="pill text-xs px-2.5 py-1 rounded-lg border"
>
{t(c.labelKey)}
</button>
@@ -431,20 +432,17 @@ export default function Channels() {
}, [filters, search, sort, resetPage]);
const cardPager = <Pager {...pagerProps} />;
const viewTabs: TabDef[] = (
["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[]
).map((v) => ({ id: v, label: t(`channels.tabs.${v}`) }));
const tabs = (
<div className="px-4 pt-4 max-w-384 mx-auto flex items-center justify-between gap-4 flex-wrap">
<div className="flex flex-wrap items-center gap-1.5">
{(["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[]).map((v) => (
<button
key={v}
onClick={() => setView(v)}
aria-pressed={view === v}
className="pill text-sm px-3 py-1.5 rounded-full border"
>
{t(`channels.tabs.${v}`)}
</button>
))}
</div>
<Tabs
tabs={viewTabs}
active={view}
onChange={(id) => setView(id as ChannelsView)}
className="flex flex-wrap items-center gap-1"
/>
<div className="flex items-center gap-3">
{view === "subscribed" && statsBlock}
{view === "subscribed" && layout === "cards" && sortControl}
@@ -476,7 +474,7 @@ export default function Channels() {
key={tg.id}
onClick={() => toggleTagFilter(tg.id)}
aria-pressed={tagFilter.includes(String(tg.id))}
className="pill text-xs px-2 py-1 rounded-full border"
className="pill text-xs px-2 py-1 rounded-lg border"
>
{tg.name}
</button>
@@ -491,7 +489,7 @@ export default function Channels() {
)}
<button
onClick={() => setTagManagerOpen(true)}
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full border border-dashed border-border text-muted hover:text-accent hover:border-accent transition"
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-lg border border-dashed border-border text-muted hover:text-accent hover:border-accent transition"
>
<Pencil className="w-3 h-3" />
{t("channels.tags.manage")}