import * as React from 'react'; import { Button } from '@/components/button'; import { ArrowLeftIcon, HomeIcon } from '@/icons'; import type { ControlSize, ControlVariant } from '@/lib'; import { MatrixGrid } from '~/chrome/ui/matrix'; import { useMessages } from '../i18n'; import { PALETTES, type Assert, type Complete, type Palette } from '../registry/axis-values'; import { useParam } from '../router'; /** * The Button set as CBAR's canvas lays it out. * * The Gallery tab is organised the way the *kit* models buttons — a grid per * treatment, palettes across, sizes down. CBAR's file is organised the other way * round, and the point of this view is that the two can be put side by side: * open `Button` (`2446:6959`) in Figma, pick the same size and palette here, and * the grids line up cell for cell. * * The layout below is read off the canvas geometry rather than guessed. On the * component set every variant's `absoluteBoundingBox` says: * * x size block xl 2466 · lg 3694 · md 4875 · xs 6037 * x variant solid · subtle · outline · ghost · surface (216px apart) * y state default · hover · active · disabled (100px apart) * y palette band 464px apart * * So: one grid per (palette, size), states down, treatments across — and with * more than one size selected the blocks sit side by side, which is the canvas. */ /** * CBAR's column order, left to right. * * Ordering is the whole point of this file, so the list is written out rather * than imported from `registry/axis-values.ts` — but `satisfies` and the * coverage check below make it the *same membership*, so a treatment added to * the kit fails to compile here instead of quietly missing a column. */ const VARIANTS = ['solid', 'subtle', 'outline', 'ghost', 'surface'] as const satisfies readonly ControlVariant[]; /* Exported for the same reason its twin in `registry/axis-values.ts` is: a type alias nobody reads is an unused local, and `noUnusedLocals` is on. */ export type _VariantsCovered = Assert>; /** CBAR's row order, top to bottom. */ const STATES = ['default', 'hover', 'active', 'disabled'] as const; /** * The four blocks across the canvas, in canvas order. * * `sm` is missing on purpose — CBAR does not draw that rung for Button, it jumps * `xs` → `md`. The kit fills it in at 36px and the Gallery tab shows it. So this * one gets `satisfies` but no coverage check: it is a subset by design. */ const SIZES = ['xl', 'lg', 'md', 'xs'] as const satisfies readonly ControlSize[]; /** * `PALETTES` is the shared list and its order already matches the canvas bands, * so it is imported. Note the names are the *kit's*, not the set's labels: CBAR's * Button set has `primary` and `secondary` the other way round — its * `colorPalette=primary` is bound to `surface-colored-secondary` (teal). The * Figma variables, IconButton and ProgressCircle all agree with the kit, so the * set is treated as mis-wired and the two bands here carry a note instead. */ /** The two bands whose Figma counterpart is painted the other way round. */ const SWAPPED = new Set(['primary', 'secondary']); /** * Figma's own INSTANCE_SWAP defaults on the set: `iconLeft` is * `Solid/Interface/Arrow left` (2163:605) and `iconRight` is * `Outline/General/Home` (2163:164). Both `iconLeft?` and `iconRight?` default * to true, so every one of the 560 variants on the canvas carries both. */ const ICON_SLOTS = ['both', 'left', 'right', 'none'] as const; type Variant = (typeof VARIANTS)[number]; type State = (typeof STATES)[number]; type Size = (typeof SIZES)[number]; type IconSlots = (typeof ICON_SLOTS)[number]; /** * What `controlVariants` (src/lib/cva-presets.ts) paints behind `:hover`, as a * class that can be forced on. * * `hover` and `active` are CSS states here, not props, so a static grid has to * assert them. Restating the class rather than inventing one keeps the row * truthful: it is the same declaration the kit ships, applied unconditionally. * * `active` reuses the hover class for two reasons that happen to agree — the kit * has no `:active` treatment at all, and CBAR draws the two identically on every * treatment (checked on 2446:9576 solid, 2446:7384 subtle, 2446:7464 surface, * 2446:7544 outline, 2446:7624 ghost: each `state=active` fill equals its * `state=hover` fill). */ const FORCED: Record = { solid: 'bg-(--ctl-solid-hover)', subtle: 'bg-(--ctl-subtle-hover)', surface: 'bg-(--ctl-subtle-hover)', outline: 'bg-(--ctl-subtle)', ghost: 'bg-(--ctl-subtle)', }; function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( ); } export function FigmaLayoutView() { const m = useMessages(); /* The same three URL keys the Gallery tab uses, so switching tabs keeps the palette and the icon slots you were looking at. */ const [palette, setPalette] = useParam('palette', 'all'); const [sizeParam, setSize] = useParam('size', 'md'); const [slots, setSlots] = useParam('icons', 'both'); /* `sm` is a legal value on the other tab and has no block here, so it falls back rather than emptying the page; `sizeNote` below says why. */ const size = sizeParam === 'all' || SIZES.includes(sizeParam as Size) ? sizeParam : 'md'; 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 palettes = React.useMemo( () => (palette === 'all' ? PALETTES : PALETTES.filter((p) => p === palette)), [palette] ); const sizes = React.useMemo( () => (size === 'all' ? SIZES : SIZES.filter((s) => s === size)), [size] ); /* One builder, two callers: `cell` is what the grid shows and `code` is what its copy buttons serialise. They differ only by the forced class, which nobody should paste — hover is a state, not a variant. */ const button = ( colorPalette: Palette, cellSize: Size, state: State, variant: Variant, forced: boolean ) => ( ); return (

{m.buttons.figma.lead}

{m.buttons.figma.stateNote}

{m.buttons.figma.copyNote}

{/* Native controls, same rule as the Gallery tab: a page documenting the kit must not document it with itself, or a broken component hides its own bug. */}

{m.buttons.figma.sizeNote}

{palettes.map((colorPalette) => (

{colorPalette}

{SWAPPED.has(colorPalette) ? (

{m.buttons.figma.swapNote}

) : null}
{/* One block per size, laid out across the way the canvas lays them out. With a single size selected this is a plain column and the flex row never scrolls. */}
{sizes.map((cellSize) => (
button(colorPalette, cellSize, row as State, col as Variant, true) } code={(row, col) => button(colorPalette, cellSize, row as State, col as Variant, false) } cellTitle={(row, col) => `variant=${String(col)}, state=${String(row)}, colorPalette=${colorPalette}, size=${cellSize}` } copyAll={false} />
))}
))}
); }