import type { ReactNode } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs'; import { useParam } from '~/router'; /** * The frame the five axis-filtered gallery pages share. * * `/alerts`, `/avatars`, `/badges`, `/buttons` and `/toasts` are all the same * document: a `max-w-7xl` column, a three-line header, and a tab strip choosing * between the kit's own grid layout and CBAR's canvas layout. Each page carried * its own byte-identical copy — about thirty lines apiece — including the line * that decides what an unknown `?tab=` value falls back to. Changing how a tab * behaves meant editing five files and being silently wrong in the four you * forgot; nothing here is covered by a test, so nothing would have said so. * Same argument, and the same pair of helpers, as `chrome/ui/axis-filter.tsx`. * * What stays on the pages: `TABS`, `Tab` and `EVERY_TOKEN`. They look duplicated * too, but their *members* are that page's own vocabulary — the note in * `axis-filter.tsx` already makes this case. So does the tab **content**: this * frame owns the skeleton and nothing inside it. `/buttons` and `/avatars` do * not draw the same Figma tab, and `/buttons` switches `copyAll` off where the * others do not. */ /** * The chosen tab, read from `?tab=` and validated against the page's own list. * * Unfolded from the same expression the five pages each spelled out. Two * behaviours it preserves exactly: an unrecognised value (a hand-edited URL, a * link from an older build) falls back rather than rendering an empty page, and * the fallback is `tabs[0]`, which is what `useParam` drops from the hash — so * the default view still has a clean URL to copy out of the address bar. * * It stays in this file because `GalleryPage` is its only consumer: the pages * never need the value, they only name the views. */ function useTabParam(tabs: readonly [T, ...T[]]): [T, (next: string) => void] { const fallback = tabs[0]; const [raw, setTab] = useParam('tab', fallback); const tab = tabs.includes(raw as T) ? (raw as T) : fallback; return [tab, setTab]; } export function GalleryPage({ overline, title, lead, tabs, tabsLabel, tabLabel, views, }: { overline: ReactNode; title: ReactNode; lead: ReactNode; /** The page's tab names, in strip order. The first is the default view. */ tabs: readonly [T, ...T[]]; /** Accessible name for the strip itself. */ tabsLabel: string; tabLabel: (name: T) => ReactNode; /** * One view per tab. A record rather than children, so a tab without a view is * a type error instead of an empty panel. */ views: Record; }) { const [tab, setTab] = useTabParam(tabs); return (

{overline}

{title}

{lead}

{/* The strip is the kit's own `Tabs`, which is where the roving focus, the arrow-key navigation and the `aria-controls` wiring come from. It is not an exception to the rule in `chrome/ui/playground.tsx`: `Tabs` is chrome here, driving *which grid is shown*. The filters inside each view, which drive the components themselves, stay native. Radix leaves an inactive `TabsContent` unmounted, so the view you are not looking at is not a few hundred components sitting in the document. */} {tabs.map((name) => ( {tabLabel(name)} ))} {tabs.map((name) => ( {views[name]} ))}
); }