import * as React from 'react'; import { Button } from '@/components/button'; import { ArrowLeftIcon, HomeIcon, SearchIcon } from '@/icons'; import { FilterField, narrow } from '~/chrome/ui/axis-filter'; import { GalleryPage } from '~/chrome/ui/gallery-page'; import { PACKAGE } from '~/chrome/lib/package-name'; import { MatrixGrid } from '~/chrome/ui/matrix'; import { useMessages } from '../i18n'; import { CONTROL_SIZES as SIZES, PALETTES, VARIANTS, type ControlSizeValue as Size, type Palette, type Variant, } from '../registry/axis-values'; import { useParam } from '../router'; import { FigmaLayoutView } from './buttons-figma'; /** * Every button the kit can draw, on one page, in two shapes. * * **Gallery** is the kit's own shape: a grid per treatment, palettes across, * sizes down. The component page shows three hand-declared matrices; four of the * nine variants and all four icon-only sizes never appear in any of them, so * they could be read about but not copied. This is the whole surface instead, * narrowed by filters that live in the URL, with the same per-cell copy the * matrices already have. Copying is one cell at a time on both tabs on purpose — * a grid here is a whole sweep, and nobody pastes a sweep. * * **Figma layout** is CBAR's shape — states down, treatments across, banded by * palette — so the canvas and the live components can be compared cell for cell. * It lives in `./buttons-figma`. * * The three composable axes come from `registry/axis-values.ts`, which derives * them from the kit's own `colorPalettes` / `controlVariants` / `controlHeights` * — so adding a palette adds a column here without anyone remembering to. Only * the two axes that are Button's alone stay local, below. */ /** * The four that pin their own semantic colour and therefore ignore * `colorPalette` rather than racing it on specificity. */ const PINNED_VARIANTS = ['default', 'destructive', 'secondary', 'link'] as const; /** Square sizes. Icon-only is a size here, not a separate component. */ const ICON_SIZES = ['icon-xs', 'icon-sm', 'icon', 'icon-lg'] as const; /** * CBAR draws its buttons with icons, and the design file means it: the `Button` * set (`2446:6959`) carries `iconLeft?` and `iconRight?` as booleans that both * default to **true**, so all 560 variants on the canvas have a glyph on each * side. The kit takes icons as children rather than as props, which means the * arrangement is child order and there is nothing to compare on the parity page * — so it is a control here instead, defaulting to what Figma defaults to. * * The two glyphs are Figma's own INSTANCE_SWAP defaults, looked up through the * bridge: `iconLeft` = `Solid/Interface/Arrow left` (2163:605), `iconRight` = * `Outline/General/Home` (2163:164). Neither needs a `variant` prop — * `ArrowLeftIcon` is one of the 23 icons CBAR draws once, so it ignores the * prop, and `HomeIcon` already defaults to outline. */ const ICON_SLOTS = ['both', 'left', 'right', 'none'] as const; type IconSlots = (typeof ICON_SLOTS)[number]; const EVERY_TOKEN: readonly string[] = [ ...VARIANTS, ...PALETTES, ...SIZES, ...PINNED_VARIANTS, ...ICON_SIZES, ]; const TABS = ['gallery', 'figma'] as const; /** * The two views, with the choice in the URL like every other control on this * page — `#/buttons?tab=figma&palette=green` is a link someone else opens on the * same cell. The frame itself, including what an unknown `?tab=` falls back to, * is `chrome/ui/gallery-page.tsx`. */ export function ButtonsPage() { const m = useMessages(); return ( m.buttons.tabs[name]} views={{ gallery: , figma: }} /> ); } function GalleryView() { /* Same mechanism as the icons page: one `useParam` per control, everything a string, and writing the fallback drops the key so a default view has a clean URL to copy out of the address bar. */ const m = useMessages(); const [query, setQuery] = useParam('q', ''); const [palette, setPalette] = useParam('palette', 'all'); const [size, setSize] = useParam('size', 'all'); const [disabledParam, setDisabled] = useParam('disabled', ''); const [slots, setSlots] = useParam('icons', 'both'); const disabled = disabledParam === '1'; /* A hand-edited `?icons=` would otherwise read as "neither", which looks like the page ignoring the design rather than the URL being wrong. */ const iconSlots: IconSlots = ICON_SLOTS.includes(slots as IconSlots) ? (slots as IconSlots) : 'both'; const showLeft = iconSlots === 'both' || iconSlots === 'left'; const showRight = iconSlots === 'both' || iconSlots === 'right'; const needle = query.trim().toLowerCase(); const nothingMatches = needle.length > 0 && !EVERY_TOKEN.some((token) => token.includes(needle)); const variants = React.useMemo(() => narrow(VARIANTS, needle), [needle]); const pinnedVariants = React.useMemo(() => narrow(PINNED_VARIANTS, needle), [needle]); const iconSizes = React.useMemo(() => narrow(ICON_SIZES, needle), [needle]); const palettes = React.useMemo(() => { const chosen = palette === 'all' ? PALETTES : PALETTES.filter((p) => p === palette); return narrow(chosen, needle); }, [palette, needle]); const sizes = React.useMemo(() => { const chosen = size === 'all' ? SIZES : SIZES.filter((s) => s === size); return narrow(chosen, needle); }, [size, needle]); /* One cell builder per section, so a snippet cannot describe a button other than the one on screen. */ const composable = (variant: Variant, colorPalette: Palette, cellSize: Size) => ( ); const pinned = (variant: (typeof PINNED_VARIANTS)[number], cellSize: Size) => ( ); /* An icon button still needs a palette; with the filter on `all` the grid would otherwise have to pick one silently, so it says which one it picked. */ const iconPalette: Palette = palette === 'all' ? 'primary' : (palette as Palette); const iconOnly = (variant: Variant, cellSize: (typeof ICON_SIZES)[number]) => ( ); return (
{/* Native controls on purpose: a page documenting the kit must not document it with itself, or a broken component hides its own bug. */} setQuery(e.target.value)} placeholder={m.buttons.searchPlaceholder} className="h-9 min-w-52 flex-1 rounded-md border border-input bg-background px-2 text-sm" /> setDisabled(e.target.checked ? '1' : '')} className="size-4 rounded border-input" />
{nothingMatches ? (

{m.buttons.empty(query)}

) : ( <>

{m.buttons.composableTitle}

{m.buttons.composableLead}

{variants.map((variant) => ( composable(variant, col as Palette, row as Size)} cellTitle={(row, col) => `variant=${variant}, size=${String(row)}, colorPalette=${String(col)}` } copyAll={false} /> ))} pinned(row as (typeof PINNED_VARIANTS)[number], col as Size)} cellTitle={(row, col) => `variant=${String(row)}, size=${String(col)}`} copyAll={false} /> iconOnly(row as Variant, col as (typeof ICON_SIZES)[number])} cellTitle={(row, col) => `variant=${String(row)}, size=${String(col)}`} copyAll={false} /> )}
); }