'use client'; import { useId, type CSSProperties, type ReactNode } from 'react'; export type CardOption = { value: string; label: string; desc: string }; /** * Builds the option list for a picker from its value list and a label map, so * the values a setting accepts and the words shown for them stay one thing. */ export function toOptions( values: readonly string[], info: Record, ): CardOption[] { return values.map((value) => ({ value, label: info[value]?.label ?? value, desc: info[value]?.desc ?? '', })); } /** * A labelled grid of picker cards โ€” theme preset, layout, frame, hero style, * aspect ratio. * * All five were written out separately: each had its own `.map()`, its own * `preset-card` button, its own `active` handling, and its label text buried in * an object literal a thousand lines from the value list it described (#533). * The differences between them are the preview drawn on the card and, for the * theme preset, a few custom properties โ€” so those are the props, and the rest * is stated once. * * The class names are unchanged, so `admin.css` styles these exactly as it * styled the copies. */ export default function OptionGrid({ label, options, value, onSelect, cardClassName, renderPreview, renderSpecs, cardStyle, }: { label: string; options: CardOption[]; /** The resolved value โ€” the caller applies its own default before passing it. */ value: string; onSelect: (value: string) => void; /** Second class on the card, e.g. `frame-card`; `preset-card` is always set. */ cardClassName: string; renderPreview?: (option: CardOption) => ReactNode; /** Extra line under the description, used by the theme preset for its specs. */ renderSpecs?: (option: CardOption) => ReactNode; cardStyle?: (option: CardOption) => CSSProperties | undefined; }) { // A group of buttons, not one control, so it is named as a group (#694 ยง4). const labelId = useId(); return (
{label}
{options.map((option) => { const isActive = value === option.value; return ( ); })}
); }