import { Button } from '@/components/button'; import { RefreshIcon } from '@/icons'; import { useMessages } from '~/i18n'; import type { AxisValue, Entry, PropBag } from '~/registry/types'; import { snippetOf } from '../lib/codegen'; import { CopyBlock } from './copy'; import { ErrorBoundary } from './error-boundary'; import { useResetProps } from '../lib/prop-params'; import { SectionHeading } from './section'; import { ShareButton } from './share'; /* * THE RULE, for this file and every other: * * The site's chrome uses kit components. A playground control never uses the * component it drives. * * Everything around a preview — the rail, the toolbar, the copy buttons, the * props tables, the badges — is the kit, because a doc site that will not eat * its own cooking is not evidence of anything. The controls in this file are the * exception, and they are native `` on * purpose: the playground has to stay usable while you are actively breaking a * kit component, and driving `Select` with `Select` means the first bug you * introduce hides itself. * * The other half of the same rule is `chrome/ui/error-boundary.tsx`: every rendered * preview is walled off, so a component that throws degrades to a red panel * instead of unmounting the page from the root. * * `showcase/src/pages/tokens.tsx` is the one other file that stays outside the * kit — it reads raw `--ui-*` values out of the document and has to show them * literally. */ function Control({ name, values, value, onChange, }: { name: string; values: readonly AxisValue[]; value: unknown; onChange: (next: AxisValue) => void; }) { const isBoolean = values.every((v) => typeof v === 'boolean'); if (isBoolean) { return ( ); } return ( ); } /** * Live single instance plus one control per axis. * * The props are owned by the page rather than by this component: the Figma * panel further down resolves the same values against CBAR's component set, so * both views describe one selection instead of drifting apart. */ export function Playground({ entry, props, onChange, }: { entry: Entry; props: PropBag; onChange: (name: string, value: AxisValue) => void; }) { const m = useMessages(); /* Before the early return — the hook order cannot depend on whether this particular entry happens to be drivable. */ const [dirty, reset] = useResetProps(); const axes = entry.axes; if (!entry.render || !axes) return null; /* Rendered once and used twice: the live instance and the snippet are the same element, so the code cannot describe something else. */ const instance = entry.render(props); /* The boundary latches on a caught error; this is what un-latches it when the controls move off the value that threw. */ const propsKey = JSON.stringify(props); return (
{/* `h2`, like every other top-level section of a page whose `h1` is the component name. These were all `h3` until the axe panel reported the `h1 → h3` jump on every component page; the classes carry the size, so the level is free to be the correct one. */} snippetOf(instance)} />} />
{instance}
{/* Its own panel rather than a bare stack beside the preview: the controls are a different kind of thing from the thing they drive, and a shared surface with no line between them read as one wide card with some selects loose in the right half. */}

{m.playground.controls}

{/* Absent, not disabled, while the playground sits at its defaults: there is nothing to undo, and the URL says so — see `useResetProps`. */} {dirty ? ( ) : null}
{Object.entries(axes).map(([name, values]) => ( onChange(name, next)} /> ))}
); }