39 lines
2.3 KiB
TypeScript
39 lines
2.3 KiB
TypeScript
import { useContext, type CSSProperties, type ReactNode } from "react"
|
|
import { StorySectionContext } from "./story-page"
|
|
|
|
// Representative-Story layout used by component review pages (ui_viz StateBoard): labeled sections on one page.
|
|
// It only arranges examples; it never styles the component being reviewed.
|
|
// `note` is a Story note, not Product UI. It sits under the title by default; `noteAfter` places it below the specimen
|
|
// (Title → actual UI → Story note) for specimens whose Label/Control must stay directly under the title.
|
|
export type BoardState = { id: string; label: string; note?: string; noteAfter?: boolean; wide?: boolean; children: ReactNode }
|
|
|
|
// `fixed` uses columns of exactly `min` px instead of stretching them (1fr). Specimens whose popup is anchored to a Radix
|
|
// trigger need whole-pixel columns: a fractional column (e.g. 358.4px) puts the trigger edge between device pixels while the
|
|
// popper rounds the popup to whole pixels, so the two outlines end up 1px apart.
|
|
export function StateBoard({ states, min = 320, fixed = false }: { states: BoardState[]; min?: number; fixed?: boolean }) {
|
|
// Inside a StorySection the labels sit one heading level below the section title.
|
|
const Label = useContext(StorySectionContext) ? "h3" : "h2"
|
|
return (
|
|
<div
|
|
className={`grid items-start gap-x-6 gap-y-10 ${fixed ? "[grid-template-columns:repeat(auto-fill,min(100%,var(--sb-state-min,320px)))]" : "[grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-state-min,320px)),1fr))]"}`}
|
|
style={{ "--sb-state-min": `${min}px` } as CSSProperties}
|
|
>
|
|
{states.map(({ id, label, note, noteAfter, wide, children }) => (
|
|
<section
|
|
className={`grid min-w-0 content-start gap-3${wide ? " col-span-full" : ""}`}
|
|
key={id}
|
|
data-overview-state={id}
|
|
aria-label={label}
|
|
>
|
|
<header className="grid gap-2 border-b border-black/10 pb-2">
|
|
<Label className="text-sm font-medium text-muted-foreground">{label}</Label>
|
|
{note && !noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
|
</header>
|
|
{children}
|
|
{note && noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
|
</section>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|