@vitavision/ui 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/index.d.ts +129 -9
- package/dist/index.js +282 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/ui/Input.tsx +67 -10
- package/src/components/ui/PoseInput.tsx +181 -0
- package/src/components/ui/Select.tsx +5 -1
- package/src/components/ui/VectorInput.tsx +183 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/ui/numberText.ts +36 -0
- package/src/fonts/IBMPlexMono-Medium-Latin1.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Medium-Latin2.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Medium-Latin3.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Medium-Pi.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Regular-Latin1.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Regular-Latin2.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Regular-Latin3.woff2 +0 -0
- package/src/fonts/IBMPlexMono-Regular-Pi.woff2 +0 -0
- package/src/fonts/LICENSE-IBM-Plex-Mono.txt +93 -0
- package/src/fonts/LICENSE-IBM-Plex-Sans.txt +93 -0
- package/src/fonts/fonts.css +100 -0
- package/src/fonts/ibm-plex-sans-latin-ext-wght-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- package/src/styles.css +13 -12
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/theme.ts","../src/tone.ts","../src/components/ui/cn.ts","../src/components/ui/Tooltip.tsx","../src/components/ThemeToggle.tsx","../src/components/ui/Badge.tsx","../src/components/ui/Density.tsx","../src/components/ui/Button.tsx","../src/components/ui/Dialog.tsx","../src/components/ui/Disclosure.tsx","../src/components/ui/Feedback.tsx","../src/components/ui/Field.tsx","../src/components/ui/Input.tsx","../src/components/ui/Panel.tsx","../src/components/ui/SegmentedControl.tsx","../src/components/ui/Select.tsx","../src/components/ui/Slider.tsx","../src/components/ui/Table.tsx","../src/components/ui/Toggle.tsx","../src/components/ui/ToggleChip.tsx","../src/components/Tabs.tsx"],"sourcesContent":["/*\n * Which of the two palettes is on screen.\n *\n * Judging an anomaly map, or a measurement overlay drawn over a low-contrast part, against\n * a bright room and against a dark one are different tasks, and a researcher switches\n * between them within a session -- so dark mode is a real, storable choice here rather than\n * only `prefers-color-scheme`.\n *\n * Three states, not two. \"system\" is a real choice and has to survive a reload as itself,\n * otherwise the first time the OS flips at sunset the app disagrees with everything else on\n * the machine and there is no way back to following it.\n *\n * The class is applied to <html> rather than <body> so the page background painted before\n * React mounts is already correct. `applyTheme` (below, as `paint`) has to be duplicated as\n * an inline script in the consumer's `index.html` for exactly that reason -- see the\n * package README for the exact snippet, and keep it in agreement with this module if either\n * changes.\n */\n\n/**\n * A stored theme preference: a palette, or `system` to follow the OS\n * (`prefers-color-scheme`) and keep following it.\n */\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\n/**\n * The `localStorage` key a consumer's theme choice is kept under.\n *\n * Every function here takes it as an optional last argument and defaults to this constant,\n * so a single app needs to pass nothing, while two apps sharing a browser profile -- or one\n * app that wants a namespaced key -- can each choose their own without stepping on the\n * other's stored preference.\n */\nexport const DEFAULT_THEME_STORAGE_KEY = \"vitavision-theme\";\n\n// Every DOM access here is inside a function, and guarded: this module is imported on the\n// server too.\nconst query = () =>\n typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n ? window.matchMedia(\"(prefers-color-scheme: dark)\")\n : null;\n\n/**\n * The stored preference.\n *\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n * @returns The stored choice, or `system` when nothing valid is stored or storage cannot be\n * read (private browsing, the server).\n */\nexport function readThemeChoice(storageKey: string = DEFAULT_THEME_STORAGE_KEY): ThemeChoice {\n try {\n const stored = window.localStorage.getItem(storageKey);\n if (stored === \"light\" || stored === \"dark\" || stored === \"system\") return stored;\n } catch {\n // Private browsing, or a WebView with storage disabled. Following the OS is a fine\n // answer to \"I could not read your preference\", and better than refusing to render.\n }\n return \"system\";\n}\n\n/**\n * What `choice` actually resolves to right now.\n *\n * @param choice - The preference.\n * @returns `choice` itself for `light`/`dark`; for `system`, the OS palette (`light` where\n * it cannot be queried).\n */\nexport function resolveTheme(choice: ThemeChoice): \"light\" | \"dark\" {\n if (choice !== \"system\") return choice;\n return query()?.matches ? \"dark\" : \"light\";\n}\n\nfunction paint(choice: ThemeChoice): void {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"dark\", resolveTheme(choice) === \"dark\");\n}\n\n/**\n * Store a preference and paint it (the `dark` class on `<html>`).\n *\n * @param choice - The new preference.\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n */\nexport function setThemeChoice(\n choice: ThemeChoice,\n storageKey: string = DEFAULT_THEME_STORAGE_KEY,\n): void {\n try {\n window.localStorage.setItem(storageKey, choice);\n } catch {\n // Not worth failing the click over; the choice still applies for this session.\n }\n paint(choice);\n}\n\n/**\n * Paint the stored choice and keep following the OS while the choice is \"system\".\n *\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n * @returns An unsubscribe, so a test can tear the listener down; a long-lived app never does.\n */\nexport function initTheme(storageKey: string = DEFAULT_THEME_STORAGE_KEY): () => void {\n paint(readThemeChoice(storageKey));\n\n const media = query();\n if (media === null) return () => {};\n\n const onSystemChange = () => {\n if (readThemeChoice(storageKey) === \"system\") paint(\"system\");\n };\n media.addEventListener(\"change\", onSystemChange);\n return () => media.removeEventListener(\"change\", onSystemChange);\n}\n","/*\n * The verdict vocabulary, for the components that paint outside of `className`.\n *\n * Every Tailwind-driven primitive under `components/ui` reaches the palette through a\n * class name, which only works where Tailwind's own build sees the class. `MeasureOverlay`\n * and `LineProfile` paint through raw SVG `stroke`/`fill` attributes instead — a caliper\n * box or an edge mark is drawn from data the consumer supplies at runtime, not from a\n * fixed set of class names a bundler can scan for. So these two read the same five tokens\n * `styles.css` defines (`--signal`, `--normal`, `--defect`, `--warn`, `--fg-subtle`)\n * directly as CSS custom properties, which resolves correctly in both themes without\n * needing Tailwind involved in the overlay's own render path at all.\n */\n\n/**\n * A paint for SVG overlays and charts: the accent (`signal`), a verdict colour, or `muted`.\n *\n * Named `MeasureTone` rather than `Tone` — `components/ui/Badge.tsx` already owns that name\n * for the verdict-badge vocabulary (`neutral`/`normal`/`defect`/`unlabeled`/`warning`/\n * `info`), and both are re-exported from the package root, where two types of the same name\n * would collide.\n */\nexport type MeasureTone = \"signal\" | \"normal\" | \"defect\" | \"warn\" | \"muted\";\n\nconst TONE_VAR: Record<MeasureTone, string> = {\n signal: \"var(--signal)\",\n normal: \"var(--normal)\",\n defect: \"var(--defect)\",\n warn: \"var(--warn)\",\n muted: \"var(--fg-subtle)\",\n};\n\n/**\n * The CSS paint value for a tone — usable directly as a `stroke`, `fill` or `color`. It is a\n * `var(--token)` reference, so it follows the theme without a re-render.\n *\n * @param tone - The tone; `undefined` falls back.\n * @param fallback - The tone used when `tone` is `undefined`. Defaults to `signal`.\n * @returns A `var(--…)` CSS value.\n */\nexport function toneColor(tone: MeasureTone | undefined, fallback: MeasureTone = \"signal\"): string {\n return TONE_VAR[tone ?? fallback];\n}\n","/**\n * Class composition, and the shared focus treatment.\n *\n * A naive primitive *appends* the caller's `className` to its own base string, so\n * `<Button className=\"px-6\">` produces `px-3 … px-6` and wins or loses on source order\n * rather than on intent. `twMerge` resolves that by conflict group: the caller's `px-6`\n * replaces the base `px-3` and leaves everything else alone.\n */\n\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Compose class names: `clsx` for the conditionals, then `tailwind-merge` so a later class\n * replaces an earlier one of the same conflict group (the caller's `px-6` beats the base\n * `px-3`). Every primitive merges its caller's `className` through this.\n *\n * @param inputs - Class values: strings, arrays, and `{ class: condition }` records.\n * @returns One class string with conflicts resolved.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n\n/**\n * One focus treatment for every interactive element in the design system.\n *\n * `outline` rather than `ring` because an outline follows the element's own border-radius\n * and needs no offset *colour* -- a ring offset has to know which surface it sits on, and\n * these primitives are used on `ground`, `surface` and `raised` alike.\n *\n * `focus-visible` rather than `focus`, so a mouse click does not leave a ring behind.\n */\nexport const focusRing =\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal\";\n\n/**\n * The focus treatment for controls whose own box is the hit target, where an offset ring\n * would clip: the same outline as {@link focusRing}, drawn with no offset.\n */\nexport const focusRingInset = \"focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-signal\";\n","/*\n * A short explanation, on demand.\n *\n * Replaces the native `title` attribute, which appears after an unpredictable delay, in the\n * OS palette, at a size nobody chose, and never at all for a keyboard user.\n *\n * A tooltip is for text that helps but is not needed to operate the control. Anything a\n * reader must have in order to answer the question belongs in the field's description,\n * where it is always visible.\n */\n\nimport * as RadixTooltip from \"@radix-ui/react-tooltip\";\nimport { HelpCircle, type LucideIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * Shares one open delay (200 ms) across the tooltips below it. Mount it once near the root:\n * `Tooltip` and `InfoHint` need it above them.\n */\nexport function TooltipProvider({\n children,\n}: {\n /** The app, or the region that uses tooltips. */\n children: ReactNode;\n}) {\n return <RadixTooltip.Provider delayDuration={200}>{children}</RadixTooltip.Provider>;\n}\n\n/**\n * A short explanation on hover or focus of its child, for text that helps but is not needed\n * to operate the control (that belongs in a field's description).\n *\n * The child must be one focusable element that accepts a ref (Radix `asChild`). Radix\n * exposes `data-state` (`closed`/`delayed-open`/`instant-open`) on it.\n */\nexport function Tooltip({\n content,\n children,\n}: {\n /** The explanation. */\n content: ReactNode;\n /** The one element it explains. */\n children: ReactNode;\n}) {\n return (\n <RadixTooltip.Root>\n <RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>\n <RadixTooltip.Portal>\n <RadixTooltip.Content\n sideOffset={6}\n collisionPadding={8}\n className={cn(\n \"z-50 max-w-xs rounded-control border border-line bg-overlay px-2.5 py-1.5\",\n \"text-xs leading-snug text-fg shadow-lg shadow-black/25\",\n )}\n >\n {content}\n <RadixTooltip.Arrow className=\"fill-overlay\" />\n </RadixTooltip.Content>\n </RadixTooltip.Portal>\n </RadixTooltip.Root>\n );\n}\n\n/**\n * The standard affordance: a quiet mark that yields the explanation on hover or focus.\n *\n * `icon` exists because the mark answers two different questions. The default `?` offers\n * help with a control; `Info` offers *facts about the thing on screen* -- where a dataset\n * came from, which method produced a measurement -- which the tone demotes here rather\n * than spending a permanent line on. Reading them as the same affordance would be a small\n * lie.\n */\nexport function InfoHint({\n children,\n label = \"More information\",\n icon: Icon = HelpCircle,\n}: {\n /** The explanation shown on hover or focus. */\n children: ReactNode;\n /** The button's accessible name. Defaults to \"More information\". */\n label?: string | undefined;\n /** The mark: `HelpCircle` (the default) for help, `Info` for facts about the thing. */\n icon?: LucideIcon | undefined;\n}) {\n return (\n <Tooltip content={children}>\n <button\n type=\"button\"\n aria-label={label}\n className={cn(\"rounded-full text-fg-subtle transition-colors hover:text-fg\", focusRing)}\n >\n <Icon className=\"size-3.5\" />\n </button>\n </Tooltip>\n );\n}\n","/*\n * The palette switch, shared by every lab app.\n *\n * Three states, not two, and cycled by one button rather than spread across a\n * segmented control: this sits in a header next to the thing the app is\n * actually about, and a control that costs one glyph is the right size for a\n * preference that is set once a session. \"System\" is a real state — an app that\n * silently stops following the OS the first time you touch the switch has taken\n * something away.\n *\n * Lived in visual-anomaly-lab's own `App.tsx` first. It moved here the moment a\n * second app needed it, rather than being copied: two copies of a control that\n * writes to `localStorage` is two chances to disagree about the key, the cycle\n * order, or which icon means which state.\n *\n * `storageKey` is per app on purpose — `theme.ts` takes the key as an argument\n * for the same reason. Two apps sharing a browser profile each keep their own\n * preference, and an app that already had one under its own key does not lose\n * it by adopting this component.\n */\n\nimport { Moon, Sun, SunMoon } from \"lucide-react\";\nimport { useState, useSyncExternalStore } from \"react\";\n\nimport { DEFAULT_THEME_STORAGE_KEY, readThemeChoice, setThemeChoice, type ThemeChoice } from \"../theme\";\nimport { Tooltip } from \"./ui/Tooltip\";\nimport { cn, focusRing } from \"./ui/cn\";\n\nconst ORDER: ThemeChoice[] = [\"system\", \"light\", \"dark\"];\nconst ICON = { system: SunMoon, light: Sun, dark: Moon };\nconst LABEL: Record<ThemeChoice, string> = {\n system: \"Theme: following the system\",\n light: \"Theme: light\",\n dark: \"Theme: dark\",\n};\n\n/** Another tab (or window) changing the stored choice re-renders the toggle. */\nfunction subscribe(onChange: () => void): () => void {\n window.addEventListener(\"storage\", onChange);\n return () => window.removeEventListener(\"storage\", onChange);\n}\n\n/** What the server, and a hydrating client's first render, show: no stored choice read yet. */\nconst serverChoice = (): ThemeChoice => \"system\";\n\n/**\n * The palette switch: one button cycling system → light → dark, stored in `localStorage`\n * under `storageKey` and painted as the `dark` class on `<html>`.\n *\n * Its `aria-label` (repeated in a tooltip) names the current state; it needs a\n * `TooltipProvider` above it. The stored choice is read as an external store, so the server\n * render and the hydrating first client render agree (\"system\") and the stored choice\n * follows without a mismatch. The state is exposed as `data-theme-choice`.\n */\nexport function ThemeToggle({\n storageKey = DEFAULT_THEME_STORAGE_KEY,\n className,\n}: {\n /** The `localStorage` key; per app, so two apps sharing a profile keep their own choice. */\n storageKey?: string | undefined;\n /** Merged with the button's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const stored = useSyncExternalStore(subscribe, () => readThemeChoice(storageKey), serverChoice);\n // What this toggle last set, so it keeps cycling even where storage is unavailable and the\n // stored value cannot follow.\n const [chosen, setChosen] = useState<{ key: string; choice: ThemeChoice } | null>(null);\n const choice = chosen?.key === storageKey ? chosen.choice : stored;\n\n const advance = () => {\n const next = ORDER[(ORDER.indexOf(choice) + 1) % ORDER.length]!;\n setChosen({ key: storageKey, choice: next });\n setThemeChoice(next, storageKey);\n };\n\n const Icon = ICON[choice];\n\n return (\n <Tooltip content={LABEL[choice]}>\n <button\n type=\"button\"\n onClick={advance}\n aria-label={LABEL[choice]}\n data-theme-choice={choice}\n className={cn(\n \"rounded-control p-1.5 text-fg-muted transition-colors hover:bg-raised hover:text-fg\",\n focusRing,\n className,\n )}\n >\n <Icon className=\"size-4\" />\n </button>\n </Tooltip>\n );\n}\n","/*\n * Status colour, in one place.\n *\n * The tone vocabulary is deliberately small and deliberately *about verdicts*. The chrome\n * carries no saturation, so a colour on screen means something: green is a normal sample\n * (or an in-tolerance measurement), red is a defect (or an out-of-tolerance one), amber is\n * a caveat. `info` is the one tone that borrows the interaction accent, and it is reserved\n * for statements of fact about a method -- \"produces anomaly maps\", \"caliper axis fixed\" --\n * rather than for anything a reader must act on.\n */\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/**\n * The badge and status-dot vocabulary: the verdicts (`normal`, `defect`, `warning`),\n * `unlabeled` for a sample with no verdict yet, `info` for a statement of fact about a\n * method, and `neutral` for everything else.\n */\nexport type Tone = \"neutral\" | \"normal\" | \"defect\" | \"unlabeled\" | \"warning\" | \"info\";\n\nconst TONE_CLASSES: Record<Tone, string> = {\n neutral: \"bg-raised text-fg-muted ring-line\",\n normal: \"bg-normal/12 text-normal ring-normal/25\",\n defect: \"bg-defect/12 text-defect ring-defect/25\",\n unlabeled: \"bg-raised text-fg-subtle ring-line\",\n warning: \"bg-warn/12 text-warn ring-warn/25\",\n info: \"bg-signal/12 text-signal ring-signal/25\",\n};\n\nconst DOT_CLASSES: Record<Tone, string> = {\n neutral: \"bg-fg-subtle\",\n normal: \"bg-normal\",\n defect: \"bg-defect\",\n unlabeled: \"bg-line-strong\",\n warning: \"bg-warn\",\n info: \"bg-signal\",\n};\n\n/**\n * A short label in a tone: a verdict, a status, a count.\n *\n * The tone is exposed as `data-tone`, so a stylesheet or a test can read it without parsing\n * class names.\n */\nexport function Badge({\n tone = \"neutral\",\n className,\n children,\n}: {\n /** Which colour, by meaning. Defaults to `neutral`. */\n tone?: Tone | undefined;\n /** Merged with the badge's own classes through `cn`. */\n className?: string | undefined;\n /** The label. */\n children: ReactNode;\n}) {\n return (\n <span\n data-tone={tone}\n className={cn(\n \"inline-flex items-center gap-1 rounded-control px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset\",\n TONE_CLASSES[tone],\n className,\n )}\n >\n {children}\n </span>\n );\n}\n\n/**\n * A status as a dot and a word.\n *\n * The mapping from a domain status to a tone belongs to each caller -- only it knows\n * whether \"running\" is good news -- but the *rendering* of the answer does not.\n */\nexport function StatusDot({\n tone,\n className,\n children,\n}: {\n /** The status, as a tone; also exposed as `data-tone`. */\n tone: Tone;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n /** The word beside the dot. The dot alone is decoration: say the status in words. */\n children?: ReactNode;\n}) {\n return (\n <span\n data-tone={tone}\n className={cn(\"inline-flex items-center gap-1.5 text-xs text-fg-muted\", className)}\n >\n <span className={cn(\"size-1.5 shrink-0 rounded-full\", DOT_CLASSES[tone])} aria-hidden />\n {children}\n </span>\n );\n}\n\n/** Counts as a compact `12 normal · 9 defect` run of badges, skipping the zeroes. */\nexport function CountRun({\n counts,\n className,\n}: {\n /** `[name, count, tone]` triples, in display order. Zero counts are dropped. */\n counts: [string, number, Tone][];\n /** Merged with the run's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const shown = counts.filter(([, value]) => value > 0);\n if (shown.length === 0) {\n return <span className={cn(\"text-xs text-fg-subtle\", className)}>empty</span>;\n }\n return (\n <span className={cn(\"flex flex-wrap items-center gap-1.5\", className)}>\n {shown.map(([name, value, tone]) => (\n <Badge key={name} tone={tone}>\n <span className=\"font-mono\">{value}</span> {name}\n </Badge>\n ))}\n </span>\n );\n}\n","/*\n * How much room the chrome takes, as one decision per region rather than per component.\n *\n * The default spacing is written for a page: a form you read through once, where generous\n * padding is what makes it legible. An inspector column beside a canvas is the opposite\n * problem — it is a permanent surface that accumulates tools, and there the same padding is\n * simply less of the instrument on screen. Nesting compounds it: a panel body's `p-4`\n * inside a section's `gap-3` inside a column's own padding is three margins deep before any\n * control appears.\n *\n * `compact` is not \"the same design, smaller\". It drops the leading and the padding a\n * reading surface needs and keeps the type sizes and hit targets a control needs, so a\n * dense column stays clickable. Below this there is no honest saving left — the next step\n * down is a control too small to hit.\n *\n * The default is `comfortable`, so a consumer that never mentions density gets exactly what\n * it had.\n */\n\nimport { createContext, use } from \"react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * How much room the chrome takes: `comfortable` (the default, for a page you read through)\n * or `compact` (for a permanent tool surface such as an inspector column).\n */\nexport type Density = \"comfortable\" | \"compact\";\n\nconst DensityContext = createContext<Density>(\"comfortable\");\n\n/**\n * The density in force here. Every primitive that has two spacings reads this.\n *\n * @returns The value of the nearest `DensityProvider`, or `comfortable` outside one.\n */\nexport function useDensity(): Density {\n return use(DensityContext);\n}\n\n/**\n * Pick one of two values by the density in force — the primitives' own idiom.\n *\n * @param density - The density in force, from `useDensity()`.\n * @param comfortable - The value for `comfortable`.\n * @param compact - The value for `compact`.\n * @returns `compact` when the density is compact, else `comfortable`.\n */\nexport function byDensity<T>(density: Density, comfortable: T, compact: T): T {\n return density === \"compact\" ? compact : comfortable;\n}\n\n/**\n * Sets the density for a region: every primitive below it reads the value through\n * `useDensity`. Nest providers to switch a sub-region back.\n */\nexport function DensityProvider({\n value,\n children,\n}: {\n /** The density for everything below. */\n value: Density;\n /** The region. */\n children: ReactNode;\n}) {\n return <DensityContext value={value}>{children}</DensityContext>;\n}\n","import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { Loader2 } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A button's look, by what the action is: `primary` for the one action a screen is for,\n * `secondary` for the rest, `ghost` for toolbar actions, `danger` for an action that\n * destroys something.\n */\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n\n/** A button's height and padding. `md` is the comfortable density's size, `sm` the compact one's. */\nexport type ButtonSize = \"sm\" | \"md\";\n\nconst VARIANT_CLASSES: Record<ButtonVariant, string> = {\n primary: \"bg-signal text-signal-fg hover:bg-signal-strong\",\n secondary: \"bg-surface text-fg ring-1 ring-inset ring-line-strong hover:bg-raised\",\n ghost: \"text-fg-muted hover:bg-raised hover:text-fg\",\n // Soft rather than a solid red block. The verdict red is also the colour of a defect\n // badge, and a filled button of it reads as an alarm on a screen that is mostly grey.\n // Genuinely destructive actions are behind a confirmation dialog regardless. The hover\n // tint stops at /15 so the red label keeps 4.5:1 on it.\n danger: \"bg-defect/10 text-defect ring-1 ring-inset ring-defect/30 hover:bg-defect/15\",\n};\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n sm: \"h-7 gap-1.5 px-2.5 text-xs\",\n md: \"h-8 gap-2 px-3 text-sm\",\n};\n\n/**\n * The classes that make a control look like a button, for the one element that has to be\n * something else. Prefer `Button` and `ButtonLink`; this exists so a third shape — a\n * `<label>` wrapping a file input, say — does not have to copy the variant table.\n *\n * @param options - The variant (default `secondary`), the size (default `md`) and the\n * caller's own classes, merged last.\n * @returns The class string.\n */\nexport function buttonClasses({\n variant = \"secondary\",\n size = \"md\",\n className,\n}: {\n variant?: ButtonVariant | undefined;\n size?: ButtonSize | undefined;\n className?: string | undefined;\n} = {}): string {\n return cn(\n \"inline-flex shrink-0 items-center justify-center rounded-control font-medium transition-colors\",\n \"disabled:cursor-not-allowed disabled:opacity-45\",\n SIZE_CLASSES[size],\n VARIANT_CLASSES[variant],\n focusRing,\n className,\n );\n}\n\nfunction ButtonIcon({ icon }: { icon: ReactNode }) {\n return (\n <span className=\"shrink-0 [&>svg]:size-3.5\" aria-hidden>\n {icon}\n </span>\n );\n}\n\n/**\n * An action. `asChild` renders the button's look onto the single child element instead of a\n * `<button>` — how a router's link becomes a button-shaped navigation without this package\n * knowing about routers: `<Button asChild><Link to=\"/runs\">Runs</Link></Button>`.\n * For a plain URL, `ButtonLink` is the shorter spelling.\n *\n * Takes every `<button>` prop, `ref` included. State is exposed as `data-variant` and, while\n * `loading`, `data-loading` (plus `aria-busy`).\n */\nexport function Button({\n variant = \"secondary\",\n size,\n type = \"button\",\n loading = false,\n icon,\n asChild = false,\n className,\n children,\n disabled,\n ...rest\n}: ComponentProps<\"button\"> & {\n /** What kind of action this is. Defaults to `secondary`. */\n variant?: ButtonVariant | undefined;\n /** Defaults to the density in force — `md` comfortable, `sm` compact. */\n size?: ButtonSize | undefined;\n /** Swaps the icon for a spinner and blocks the click, without the label changing. */\n loading?: boolean | undefined;\n /** A leading icon, decorative (`aria-hidden`): the label carries the meaning. */\n icon?: ReactNode;\n /**\n * Render onto the one child element (Radix `Slot`) rather than a `<button>`. The child\n * keeps its own semantics — a link stays a link — so `type`, `disabled` and `loading` do\n * not apply.\n */\n asChild?: boolean | undefined;\n}) {\n const density = useDensity();\n const resolved = size ?? byDensity(density, \"md\", \"sm\");\n\n if (asChild) {\n return (\n <Slot\n {...rest}\n data-variant={variant}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {icon && <ButtonIcon icon={icon} />}\n <Slottable>{children}</Slottable>\n </Slot>\n );\n }\n\n return (\n <button\n type={type}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n data-variant={variant}\n data-loading={loading ? \"\" : undefined}\n {...rest}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : (\n icon && <ButtonIcon icon={icon} />\n )}\n {children}\n </button>\n );\n}\n\n/**\n * A navigation that looks like a button: one `<a>`, styled by the same variants and density.\n *\n * The alternative every consumer reached for was `<Link><Button/></Link>` — a button inside\n * an anchor, which is invalid HTML, gives a keyboard two tab stops for one action, and makes\n * a screen reader announce a button that navigates. There is no `disabled` or `loading`:\n * a link that cannot be followed should not be rendered as one, and a navigation has no\n * pending state of its own.\n *\n * It renders a plain `<a href>`, and takes every `<a>` prop, `ref` included. For a router's\n * link, pass it as the only child with `asChild` —\n * `<ButtonLink asChild><Link to=\"/runs\">Runs</Link></ButtonLink>` — so client-side\n * navigation keeps working and this package still never imports a router.\n */\nexport function ButtonLink({\n variant = \"secondary\",\n size,\n icon,\n asChild = false,\n className,\n children,\n ...rest\n}: ComponentProps<\"a\"> & {\n /** What kind of action this is. Defaults to `secondary`. */\n variant?: ButtonVariant | undefined;\n /** Defaults to the density in force — `md` comfortable, `sm` compact. */\n size?: ButtonSize | undefined;\n /** A leading icon, decorative (`aria-hidden`): the label carries the meaning. */\n icon?: ReactNode;\n /** Render onto the one child element — a router's `<Link>` — instead of an `<a>`. */\n asChild?: boolean | undefined;\n}) {\n const density = useDensity();\n const resolved = size ?? byDensity(density, \"md\", \"sm\");\n const Comp = asChild ? Slot : \"a\";\n\n return (\n <Comp\n {...rest}\n data-variant={variant}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {icon && <ButtonIcon icon={icon} />}\n <Slottable>{children}</Slottable>\n </Comp>\n );\n}\n","/*\n * Confirmation for the things that cannot be undone, and a shell for the things that need\n * a form.\n *\n * `Dialog` is a modal with no verdict of its own: a title, a body the caller fills, and a\n * footer it chooses. `ConfirmDialog` is one arrangement of it -- the destructive-action\n * confirmation every app in this family needs at least once (deleting a dataset, discarding\n * an in-progress measurement session) -- rather than a second copy of the overlay, the\n * centring and the width cap.\n */\n\nimport * as RadixDialog from \"@radix-ui/react-dialog\";\nimport { useCallback, useState, type ReactNode } from \"react\";\n\nimport { Button } from \"./Button\";\nimport { cn, focusRingInset } from \"./cn\";\n\n/**\n * Whether the body overflows its height cap, kept current as the dialog or its content\n * resizes. A scrolling body has to be reachable by keyboard (so it can be scrolled without\n * a pointer); a body that fits must not add a tab stop.\n */\nfunction useOverflowing(): [\n (node: HTMLDivElement | null) => (() => void) | undefined,\n boolean,\n] {\n const [overflowing, setOverflowing] = useState(false);\n const ref = useCallback((node: HTMLDivElement | null) => {\n if (node === null) return undefined;\n const measure = () => setOverflowing(node.scrollHeight > node.clientHeight + 1);\n measure();\n if (typeof ResizeObserver === \"undefined\") return undefined;\n const observer = new ResizeObserver(measure);\n observer.observe(node);\n // The body is capped, so once it hits the cap only the content keeps changing size.\n if (node.firstElementChild) observer.observe(node.firstElementChild);\n return () => observer.disconnect();\n }, []);\n return [ref, overflowing];\n}\n\n/**\n * A modal: a title, a body the caller fills, and a footer it chooses.\n *\n * Controlled — the caller owns `open`. The body scrolls under a height cap so the footer\n * stays on screen; while it overflows it is a keyboard tab stop (with the focus ring) and\n * carries `data-overflowing`, so it can be scrolled without a pointer.\n */\nexport function Dialog({\n open,\n onOpenChange,\n title,\n description,\n footer,\n className,\n children,\n}: {\n /** Whether the dialog is shown. */\n open: boolean;\n /** Called with `false` on Escape, an overlay click or a `DialogClose`. */\n onOpenChange: (open: boolean) => void;\n /** Names the dialog (`aria-labelledby`). */\n title: string;\n /** Optional: what the reader needs before they can answer, not decoration. */\n description?: ReactNode;\n /** The action row, right-aligned below the body. */\n footer?: ReactNode;\n /** Merged with the dialog panel's own classes through `cn`. */\n className?: string | undefined;\n /** The body. */\n children?: ReactNode;\n}) {\n const [bodyRef, overflowing] = useOverflowing();\n\n return (\n <RadixDialog.Root open={open} onOpenChange={onOpenChange}>\n <RadixDialog.Portal>\n <RadixDialog.Overlay className=\"fixed inset-0 z-50 bg-black/50 backdrop-blur-[1px]\" />\n <RadixDialog.Content\n className={cn(\n \"fixed top-1/2 left-1/2 z-50 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2\",\n // A ceiling and a column, so a dialog that lists things keeps its footer on\n // screen and scrolls its body instead of growing past the viewport. Every\n // short dialog is far below the cap and is unaffected. This is a scroller\n // inside an overlay, not a second page scroller.\n \"flex max-h-[calc(100vh-4rem)] flex-col\",\n \"rounded-panel border border-line bg-overlay p-5 shadow-xl shadow-black/40\",\n \"focus:outline-none\",\n className,\n )}\n >\n <RadixDialog.Title className=\"text-sm font-semibold tracking-tight text-fg\">\n {title}\n </RadixDialog.Title>\n {/* Radix warns when a dialog has no description; render an empty one rather than\n inventing prose for a form whose fields already carry their own labels. */}\n <RadixDialog.Description\n className={cn(\n \"mt-2 text-sm leading-relaxed text-fg-muted\",\n description === undefined && \"sr-only\",\n )}\n >\n {description ?? title}\n </RadixDialog.Description>\n <div\n ref={bodyRef}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing ? \"\" : undefined}\n className={cn(\"min-h-0 overflow-y-auto\", focusRingInset)}\n >\n <div>{children}</div>\n </div>\n {footer && <div className=\"mt-5 flex shrink-0 justify-end gap-2\">{footer}</div>}\n </RadixDialog.Content>\n </RadixDialog.Portal>\n </RadixDialog.Root>\n );\n}\n\n/**\n * The destructive-action confirmation: a `Dialog` whose footer is Cancel and one confirm\n * button. Say in `description` what will actually happen.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n confirmLabel,\n onConfirm,\n loading = false,\n disabled = false,\n destructive = false,\n className,\n}: {\n /** Whether the dialog is shown. */\n open: boolean;\n /** Called with `false` on Cancel, Escape or an overlay click. */\n onOpenChange: (open: boolean) => void;\n /** The question, e.g. \"Delete dataset?\". */\n title: string;\n /** Say what will actually happen, naming the thing by the name the reader gave it. */\n description: ReactNode;\n /** The confirm button's label: the verb, e.g. \"Delete\". */\n confirmLabel: string;\n /** Called on confirm. The dialog stays open; close it (or set `loading`) yourself. */\n onConfirm: () => void;\n /** Shows the confirm button's spinner and blocks it while the action runs. */\n loading?: boolean;\n /** Blocks the confirm button. */\n disabled?: boolean;\n /** Renders the confirm button as `danger` rather than `primary`. */\n destructive?: boolean;\n /** Merged with the dialog panel's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <Dialog\n open={open}\n onOpenChange={onOpenChange}\n title={title}\n description={description}\n className={className}\n footer={\n <>\n <RadixDialog.Close asChild>\n <Button variant=\"ghost\">Cancel</Button>\n </RadixDialog.Close>\n <Button\n variant={destructive ? \"danger\" : \"primary\"}\n loading={loading}\n disabled={disabled}\n onClick={onConfirm}\n >\n {confirmLabel}\n </Button>\n </>\n }\n />\n );\n}\n\n/**\n * The dialog's own dismiss, for a footer the caller builds: wrap a button in it with\n * `asChild` — `<DialogClose asChild><Button>Done</Button></DialogClose>`.\n */\nexport const DialogClose = RadixDialog.Close;\n","/*\n * Content folded away, with a caret we draw ourselves.\n *\n * The base layer (`styles.css`) removes the native `<details>` marker; this supplies one\n * that rotates with the element's own open state, so no JavaScript is involved in the\n * animation.\n *\n * `<details>` rather than a headless package on purpose: the element already has the\n * semantics, the keyboard behaviour and the find-in-page integration.\n */\n\nimport { ChevronRight } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/**\n * Content folded away behind a summary line, on a native `<details>`, with a caret that\n * rotates with the element's own `open` state (no JavaScript involved).\n *\n * Uncontrolled: `defaultOpen` sets the initial state and the element owns it after that.\n */\nexport function Disclosure({\n summary,\n count,\n defaultOpen = false,\n className,\n children,\n}: {\n /** The always-visible line that toggles the content. */\n summary: ReactNode;\n /** How many things are inside, where the number is the reason to open it. */\n count?: number;\n /** Whether it starts open. */\n defaultOpen?: boolean;\n /** Merged with the `<details>` element's own classes through `cn`. */\n className?: string | undefined;\n /** The folded content. */\n children: ReactNode;\n}) {\n return (\n <details open={defaultOpen} className={cn(\"group\", className)}>\n <summary\n className={cn(\n \"flex cursor-pointer items-center gap-1.5 rounded-control py-1 text-xs font-medium text-fg-muted\",\n \"transition-colors hover:text-fg\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal\",\n )}\n >\n <ChevronRight\n className=\"size-3.5 shrink-0 transition-transform group-open:rotate-90\"\n aria-hidden\n />\n {summary}\n {count !== undefined && (\n <span className=\"font-mono text-fg-subtle tabular-nums\">{count}</span>\n )}\n </summary>\n <div className=\"pt-3\">{children}</div>\n </details>\n );\n}\n","/*\n * What the screen says when it has something other than data to show.\n *\n * An error explains what happened and what to do about it; it does not apologise and it is\n * never vague. An empty state is an invitation to act, so it carries the action rather than\n * describing it. And loading is a shape, not the word \"Loading…\" -- a skeleton or a\n * progress bar keeps a slow request from looking identical to a broken one.\n */\n\nimport { AlertTriangle, CheckCircle2, Info, XCircle } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/** A callout's kind: a fact (`info`), a caveat (`warning`), a failure (`error`) or `success`. */\nexport type CalloutTone = \"info\" | \"warning\" | \"error\" | \"success\";\n\nconst CALLOUT: Record<CalloutTone, { classes: string; Icon: typeof Info }> = {\n info: { classes: \"border-signal/30 bg-signal/8 text-fg\", Icon: Info },\n warning: { classes: \"border-warn/30 bg-warn/8 text-fg\", Icon: AlertTriangle },\n error: { classes: \"border-defect/30 bg-defect/8 text-fg\", Icon: XCircle },\n success: { classes: \"border-normal/30 bg-normal/8 text-fg\", Icon: CheckCircle2 },\n};\n\nconst ICON_TONE: Record<CalloutTone, string> = {\n info: \"text-signal\",\n warning: \"text-warn\",\n error: \"text-defect\",\n success: \"text-normal\",\n};\n\n/**\n * A boxed message with a tone icon, an optional title and optional actions.\n *\n * An `error` callout is a `role=\"alert\"` (announced at once); the others are\n * `role=\"status\"`. The tone is exposed as `data-tone`.\n */\nexport function Callout({\n tone = \"info\",\n title,\n actions,\n className,\n children,\n}: {\n /** Defaults to `info`. */\n tone?: CalloutTone | undefined;\n /** A one-line summary above the message. */\n title?: ReactNode;\n /** Buttons for what to do about it, below the message. */\n actions?: ReactNode;\n /** Merged with the callout's own classes through `cn`. */\n className?: string | undefined;\n /** The message: what happened and what to do about it. */\n children: ReactNode;\n}) {\n const { classes, Icon } = CALLOUT[tone];\n return (\n <div\n role={tone === \"error\" ? \"alert\" : \"status\"}\n data-tone={tone}\n className={cn(\n \"flex items-start gap-2.5 rounded-control border px-3 py-2.5 text-sm leading-snug\",\n classes,\n className,\n )}\n >\n <Icon className={cn(\"mt-0.5 size-4 shrink-0\", ICON_TONE[tone])} aria-hidden />\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {title && <p className=\"font-medium\">{title}</p>}\n <div className=\"min-w-0 text-fg-muted\">{children}</div>\n {actions && <div className=\"mt-1 flex flex-wrap items-center gap-2\">{actions}</div>}\n </div>\n </div>\n );\n}\n\n/** The common error shape: an `error` `Callout` with only a message. */\nexport function ErrorBox({\n className,\n children,\n}: {\n /** Merged with the callout's own classes through `cn`. */\n className?: string | undefined;\n /** What went wrong and what to do about it. */\n children: ReactNode;\n}) {\n return (\n <Callout tone=\"error\" className={className}>\n {children}\n </Callout>\n );\n}\n\n/** An empty state: a sentence saying why there is nothing here, and the action that fixes it. */\nexport function Empty({\n action,\n className,\n children,\n}: {\n /** The action — usually a primary `Button` — rather than a description of it. */\n action?: ReactNode;\n /** Merged with the empty state's own classes through `cn`. */\n className?: string | undefined;\n /** Why there is nothing to show. */\n children: ReactNode;\n}) {\n return (\n <div className={cn(\"flex flex-col items-center gap-3 py-10 text-center\", className)}>\n <p className=\"text-sm text-fg-muted\">{children}</p>\n {action}\n </div>\n );\n}\n\n/** One placeholder bar for content on its way (`aria-hidden`; size it with `className`). */\nexport function Skeleton({\n className,\n}: {\n /** Sets the bar's size; merged with its own classes through `cn`. */\n className?: string | undefined;\n}) {\n return <div aria-hidden className={cn(\"animate-pulse rounded-control bg-line\", className)} />;\n}\n\n/** A stand-in for a panel's worth of content, so the layout does not jump when it lands. */\nexport function SkeletonRows({\n rows = 3,\n className,\n}: {\n /** How many bars. Defaults to 3; the last is shorter. */\n rows?: number | undefined;\n /** Merged with the column's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <div role=\"status\" aria-label=\"Loading\" className={cn(\"flex flex-col gap-2\", className)}>\n {Array.from({ length: rows }, (_, index) => (\n <Skeleton key={index} className={cn(\"h-4\", index === rows - 1 ? \"w-2/3\" : \"w-full\")} />\n ))}\n </div>\n );\n}\n\n/**\n * A known fraction done, as a bar and (with `label`) a percentage line.\n *\n * A `role=\"progressbar\"` with `aria-valuenow` in percent. Its accessible name is\n * `aria-label` if given, else `label`, else \"Progress\" — pass one that names the job when\n * a screen shows more than one bar.\n */\nexport function ProgressBar({\n fraction,\n label,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** Done, from 0 to 1. Clamped. */\n fraction: number;\n /** What is progressing, printed after the percentage: \"42 of 100 images\". */\n label?: string | undefined;\n /** The bar's accessible name. Defaults to `label`, then to \"Progress\". */\n \"aria-label\"?: string | undefined;\n /** Merged with the wrapper's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const percent = Math.round(Math.min(Math.max(fraction, 0), 1) * 100);\n return (\n <div className={cn(\"flex flex-col gap-1.5\", className)}>\n <div\n role=\"progressbar\"\n aria-label={ariaLabel ?? label ?? \"Progress\"}\n aria-valuenow={percent}\n aria-valuemin={0}\n aria-valuemax={100}\n data-complete={percent === 100 ? \"\" : undefined}\n className=\"h-1.5 overflow-hidden rounded-full bg-line\"\n >\n <div\n className=\"h-full rounded-full bg-signal transition-[width] duration-300\"\n style={{ width: `${percent}%` }}\n />\n </div>\n {label && (\n <span className=\"font-mono text-xs text-fg-muted tabular-nums\">\n {percent}% · {label}\n </span>\n )}\n </div>\n );\n}\n","/*\n * A labelled control.\n *\n * Two things a naive label cannot express, both of which the schema form needs. A required\n * field's marker is a separate span rather than text concatenated onto the label string, so\n * it can be styled, read out as a marker, or dropped. And a field's description (and its\n * error) is wired to the control through `aria-describedby`, so a screen reader connects\n * the two.\n *\n * `as=\"group\"` exists because a `<label>` cannot label a set of radios or a segmented\n * control; that case needs a labelled group instead, and getting it wrong is the difference\n * between a control that announces itself and one that announces nothing.\n */\n\nimport { createContext, use, useId, useMemo, type ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/** What a `Field` tells the control inside it: the ids that describe it, and whether it is invalid. */\ninterface FieldControl {\n describedBy: string | undefined;\n invalid: boolean;\n}\n\nconst FieldContext = createContext<FieldControl>({ describedBy: undefined, invalid: false });\n\n/**\n * The `aria-describedby` and `aria-invalid` a control inside a `Field` should carry, merged\n * with the control's own `aria-describedby`. Used by this package's controls; not exported\n * from the package.\n */\nexport function useFieldDescription(own?: string): {\n \"aria-describedby\": string | undefined;\n \"aria-invalid\": true | undefined;\n} {\n const { describedBy, invalid } = use(FieldContext);\n const ids = [own, describedBy].filter(Boolean).join(\" \");\n return { \"aria-describedby\": ids === \"\" ? undefined : ids, \"aria-invalid\": invalid || undefined };\n}\n\n/**\n * A label, the control, and optionally a description, an error and a compact annotation.\n *\n * The label text and the annotation name the control (the `<label>` wraps them and the\n * control); the description and the error are outside the label and wired into the\n * control's `aria-describedby` instead (the error also sets `aria-invalid`): directly for\n * this package's controls — `Input`, `NumberInput`, `Textarea`, `Select`, `Slider`,\n * `SegmentedControl`, `Checkbox` — and, with `as=\"group\"`, on the group itself. A control of your own inside a `label` field does not\n * pick them up; give it `aria-describedby` yourself. State is exposed as `data-invalid` and\n * `data-required`.\n */\nexport function Field({\n label,\n description,\n error,\n required = false,\n annotation,\n as = \"label\",\n className,\n children,\n}: {\n /** The field's name, shown above the control. */\n label: string;\n /** What the value means or does. Always visible, and read with the control. */\n description?: ReactNode;\n /** What is wrong with the current value. Shown in the verdict red, announced as an alert. */\n error?: ReactNode;\n /** Marks the field required (a `*` after the label). */\n required?: boolean | undefined;\n /** A compact fact about the accepted values -- a range, a unit -- kept out of the prose. */\n annotation?: ReactNode;\n /**\n * `label` (the default) wraps the control in a `<label>`; `group` renders a\n * `role=\"group\"` named by `label`, for a set of radios or a segmented control.\n */\n as?: \"label\" | \"group\";\n /** Merged with the field's own classes through `cn`. */\n className?: string | undefined;\n /** The control. */\n children: ReactNode;\n}) {\n const descriptionId = useId();\n const errorId = useId();\n const density = useDensity();\n\n const hasError = error !== undefined && error !== null && error !== false;\n const describedBy =\n [description ? descriptionId : undefined, hasError ? errorId : undefined]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n // A group carries the description itself; its controls would only repeat it.\n const control = useMemo(\n () => ({ describedBy: as === \"label\" ? describedBy : undefined, invalid: hasError }),\n [as, describedBy, hasError],\n );\n\n const gap = byDensity(density, \"gap-1.5\", \"gap-1\");\n\n const labelled = (\n <>\n <span className=\"flex items-baseline gap-2\">\n <span className={cn(\"font-medium text-fg\", byDensity(density, \"text-xs\", \"text-[11px]\"))}>\n {label}\n {required && (\n <span className=\"ml-0.5 text-defect\" title=\"Required\">\n *\n </span>\n )}\n </span>\n {annotation && (\n <span className=\"ml-auto font-mono text-[11px] text-fg-subtle\">{annotation}</span>\n )}\n </span>\n <FieldContext value={control}>{children}</FieldContext>\n </>\n );\n\n return (\n <div\n className={cn(\"flex min-w-0 flex-col\", gap, className)}\n data-invalid={hasError ? \"\" : undefined}\n data-required={required ? \"\" : undefined}\n {...(as === \"group\"\n ? { role: \"group\", \"aria-label\": label, \"aria-describedby\": describedBy }\n : {})}\n >\n {/* The description and the error sit outside the <label>: inside it they would be read\n as part of the control's name as well as its description. */}\n {as === \"label\" ? (\n <label className={cn(\"flex min-w-0 flex-col\", gap)}>{labelled}</label>\n ) : (\n labelled\n )}\n\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n {hasError && (\n <span id={errorId} role=\"alert\" className=\"text-xs leading-snug text-defect\">\n {error}\n </span>\n )}\n </div>\n );\n}\n","/*\n * Text entry.\n *\n * `controlClasses` is exported so a native control that isn't one of the wrapped ones below\n * can still pick up the shared palette and focus treatment.\n */\n\nimport type { ComponentProps } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRingInset } from \"./cn\";\n\n/**\n * The shared look of a text-like control — border, palette, placeholder, hover, disabled and\n * focus — for a native control that is not one of `Input`, `NumberInput` or `Textarea`.\n * Add the height with `useControlHeight()`.\n */\nexport const controlClasses = cn(\n \"w-full rounded-control border border-line-strong bg-raised px-2.5 text-sm text-fg\",\n \"placeholder:text-fg-subtle\",\n \"transition-colors hover:border-fg-subtle\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRingInset,\n);\n\n/**\n * The former name of `controlClasses`, kept as an alias for existing consumers.\n *\n * @deprecated Use `Input`, `NumberInput`, `Textarea` or `Select`, or `controlClasses`.\n */\nexport const inputClasses = controlClasses;\n\n/**\n * The control height in force — exported so an app's own native control can match.\n *\n * @returns A Tailwind height class: `h-8` comfortable, `h-7` compact.\n */\nexport function useControlHeight(): string {\n return byDensity(useDensity(), \"h-8\", \"h-7\");\n}\n\n/**\n * A single-line text input. Takes every `<input>` prop, `ref` included; inside a `Field` it\n * is described by the field's description and error.\n */\nexport function Input({ className, \"aria-describedby\": describedBy, ...rest }: ComponentProps<\"input\">) {\n return (\n <input\n {...useFieldDescription(describedBy)}\n {...rest}\n className={cn(controlClasses, useControlHeight(), className)}\n />\n );\n}\n\n/**\n * A number, with the schema's own bounds attached.\n *\n * `font-mono` because these are read as quantities and compared down a column. Takes every\n * `<input>` prop, `ref` included (`type` is always `number`).\n */\nexport function NumberInput({\n className,\n \"aria-describedby\": describedBy,\n ...rest\n}: Omit<ComponentProps<\"input\">, \"min\" | \"max\"> & {\n /** The smallest accepted value. */\n min?: number | undefined;\n /** The largest accepted value. */\n max?: number | undefined;\n}) {\n return (\n <input\n {...useFieldDescription(describedBy)}\n {...rest}\n type=\"number\"\n inputMode=\"decimal\"\n className={cn(controlClasses, useControlHeight(), \"font-mono tabular-nums\", className)}\n />\n );\n}\n\n/**\n * Multi-line text — set in mono, for JSON and lists. Takes every `<textarea>` prop, `ref`\n * included.\n */\nexport function Textarea({\n className,\n \"aria-describedby\": describedBy,\n ...rest\n}: ComponentProps<\"textarea\">) {\n return (\n <textarea\n {...useFieldDescription(describedBy)}\n {...rest}\n className={cn(controlClasses, \"py-1.5 font-mono\", className)}\n />\n );\n}\n","/*\n * The page furniture: what holds a screen together above the level of a single control.\n *\n * One heading size and one gap rule, defined once so every route in every consuming app\n * agrees with the others by construction rather than by luck.\n */\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { isValidElement, type ReactElement, type ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/**\n * A bordered surface with an optional header (a title and actions) above a padded body.\n * The header and the padding follow the density in force.\n */\nexport function Panel({\n title,\n actions,\n className,\n bodyClassName,\n children,\n}: {\n /** The header's heading (an `<h2>`). */\n title?: ReactNode;\n /** Controls at the header's right end. */\n actions?: ReactNode;\n /** Merged with the panel's own classes through `cn`. */\n className?: string | undefined;\n /** Merged with the body's own classes (its padding) through `cn`. */\n bodyClassName?: string | undefined;\n /** The body. */\n children: ReactNode;\n}) {\n const density = useDensity();\n\n return (\n <section className={cn(\"rounded-panel border border-line bg-surface\", className)}>\n {(title || actions) && (\n <header\n className={cn(\n \"flex items-center justify-between gap-4 border-b border-line\",\n byDensity(density, \"min-h-11 px-4 py-2.5\", \"min-h-8 px-2.5 py-1\"),\n )}\n >\n <h2\n className={cn(\n \"truncate font-semibold text-fg\",\n byDensity(\n density,\n \"text-sm tracking-tight\",\n \"text-[11px] tracking-wider uppercase text-fg-muted\",\n ),\n )}\n >\n {title}\n </h2>\n {actions && <div className=\"flex shrink-0 items-center gap-1.5\">{actions}</div>}\n </header>\n )}\n <div className={cn(byDensity(density, \"p-4\", \"p-2.5\"), bodyClassName)}>{children}</div>\n </section>\n );\n}\n\n/**\n * A numbered step inside a form.\n *\n * The number is not decoration: it marks a genuine ordering, where each choice narrows the\n * next. Do not reach for this where the parts are merely adjacent.\n */\nexport function Section({\n step,\n title,\n hint,\n actions,\n className,\n children,\n}: {\n /** The step's number, shown zero-padded before the title (decorative, `aria-hidden`). */\n step?: number | undefined;\n /** The step's heading (an `<h3>`). */\n title: string;\n /** A short line after the title. */\n hint?: ReactNode;\n /** Controls at the heading row's right end. */\n actions?: ReactNode;\n /** Merged with the section's own classes through `cn`. */\n className?: string | undefined;\n /** The step's content. */\n children: ReactNode;\n}) {\n const density = useDensity();\n\n return (\n <section className={cn(\"flex flex-col\", byDensity(density, \"gap-3\", \"gap-1.5\"), className)}>\n <div className={cn(\"flex items-baseline\", byDensity(density, \"gap-2.5\", \"gap-2\"))}>\n {step !== undefined && (\n <span\n className=\"font-mono text-xs text-fg-subtle tabular-nums\"\n aria-hidden\n >\n {String(step).padStart(2, \"0\")}\n </span>\n )}\n <h3 className=\"text-sm font-semibold tracking-tight text-fg\">{title}</h3>\n {hint && <p className=\"text-xs text-fg-muted\">{hint}</p>}\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n {children}\n </section>\n );\n}\n\n/**\n * Where `PageHeader`'s back link goes: `{ href, label }` for a plain URL, or the app's own\n * link element (a router's `<Link>`).\n */\nexport type BackLink =\n | { href: string; label: string }\n /** A link element — typically a router's `<Link to=\"…\">Label</Link>` — styled and prefixed with an arrow. */\n | ReactElement;\n\nconst BACK_CLASSES = \"w-fit text-xs text-fg-muted transition-colors hover:text-signal\";\n\n/**\n * The top of a screen: an optional back link, the `<h1>` title with actions beside it, and\n * a line of meta facts below. Links go through `back`, never a router import.\n */\nexport function PageHeader({\n title,\n meta,\n actions,\n back,\n className,\n}: {\n /** The screen's heading (the `<h1>`). */\n title: ReactNode;\n /** Small facts under the title — an id, a date, a status badge. */\n meta?: ReactNode;\n /** The screen's actions, beside the title. */\n actions?: ReactNode;\n /**\n * The way up. `{ href, label }` renders a plain `<a>`; for client-side navigation pass the\n * router's own link — `back={<Link to=\"/experiments\">Experiments</Link>}` — which is\n * rendered with the back-link style and arrow, so this package never imports a router.\n */\n back?: BackLink | undefined;\n /** Merged with the header's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <header className={cn(\"flex flex-col gap-1.5\", className)}>\n {back &&\n (isHrefLink(back) ? (\n <a href={back.href} className={BACK_CLASSES}>\n ← {back.label}\n </a>\n ) : (\n <Slot className={BACK_CLASSES}>\n {\"← \"}\n <Slottable>{back}</Slottable>\n </Slot>\n ))}\n <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2\">\n <h1 className=\"text-xl font-semibold tracking-tight text-fg\">{title}</h1>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n {meta && <div className=\"flex flex-wrap items-center gap-2 text-xs text-fg-muted\">{meta}</div>}\n </header>\n );\n}\n\nfunction isHrefLink(back: BackLink): back is { href: string; label: string } {\n return !isValidElement(back);\n}\n\n/** One fact on a `ReadoutStrip`: a value, optionally labelled and optionally a link. */\nexport type ReadoutItem = {\n /** A quiet prefix naming what the value is, where the value alone is ambiguous. */\n label?: string | undefined;\n /** The fact. An item whose value is `null` or `undefined` is skipped. */\n value: ReactNode;\n /** Makes the value a plain `<a href>`. */\n href?: string | undefined;\n /**\n * Makes the value a link through the app's own element — a router's `<Link to=\"…\" />`,\n * given without children: the value becomes its content. Takes precedence over `href`.\n */\n link?: ReactElement | undefined;\n};\n\nconst READOUT_LINK = \"text-fg transition-colors hover:text-signal\";\n\n/**\n * The instrument's display line: the facts about what is on screen, in one fixed slot.\n *\n * Stated in the same place on every screen, in mono, the way a piece of measuring equipment\n * has one readout rather than a label per panel. Rendered as a list because it is one.\n */\nexport function ReadoutStrip({\n items,\n className,\n}: {\n /** The facts, in order. Items with no value are skipped. */\n items: ReadoutItem[];\n /** Merged with the list's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const shown = items.filter((item) => item.value !== null && item.value !== undefined);\n if (shown.length === 0) return null;\n\n return (\n <ol\n className={cn(\n \"flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-xs text-fg-muted\",\n className,\n )}\n >\n {shown.map((item, index) => (\n // The items are positional facts with no identity of their own (a label is optional\n // and values repeat), so their place in the strip is the key.\n // eslint-disable-next-line @eslint-react/no-array-index-key -- positional list, see above\n <li key={index} className=\"flex items-center gap-2\">\n {index > 0 && (\n <span className=\"text-fg-subtle\" aria-hidden>\n ·\n </span>\n )}\n {item.label && <span className=\"text-fg-subtle\">{item.label}</span>}\n {item.link ? (\n <Slot className={READOUT_LINK}>\n <Slottable>{item.link}</Slottable>\n {item.value}\n </Slot>\n ) : item.href ? (\n <a href={item.href} className={READOUT_LINK}>\n {item.value}\n </a>\n ) : (\n <span className=\"text-fg\">{item.value}</span>\n )}\n </li>\n ))}\n </ol>\n );\n}\n","/*\n * A choice among two or three, shown as two or three things.\n *\n * The control for a closed set the app can enumerate up front -- `Literal[\"small\", \"medium\"]`, an axis choice -- shown all at once, no popup, no typing, and no way to enter\n * a value the backend or algorithm would reject.\n *\n * Built on native radios rather than a headless package: a radio group in one `name` gets\n * roving focus and arrow-key navigation from the platform, which is the entire reason to\n * reach for a library here.\n *\n * **`\"\"` means unset**, and this control keeps that true without showing it. The segment\n * highlighted is always the *effective* value, so the reader sees what will happen;\n * choosing the segment that matches the default stores `\"\"` rather than the value, so the\n * default is still sent by nobody and defined only in one place. See `api/schemaForm.ts`\n * for why that rule is worth this much care.\n */\n\nimport { useId } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn } from \"./cn\";\n\n/**\n * A choice among two or three, shown as a strip of segments (native radios in one group, so\n * arrow keys move between them).\n *\n * **`\"\"` means unset.** The highlighted segment is always the *effective* value — `value`,\n * or `defaultValue` while `value` is `\"\"` — and choosing the segment that matches\n * `defaultValue` reports `\"\"`, so the default stays defined in one place. Each segment\n * carries `data-state=\"checked\"` or `\"unchecked\"`; the group carries `data-disabled`.\n */\nexport function SegmentedControl({\n value,\n defaultValue,\n options,\n onValueChange,\n disabled = false,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** `\"\"` when the field is untouched. */\n value: string;\n /** What applies when nothing is sent. Highlighted while `value` is `\"\"`. */\n defaultValue?: string | undefined;\n /** The segments, in order. */\n options: { value: string; label: string }[];\n /** Called with the chosen value, or `\"\"` when the default's segment is chosen. */\n onValueChange: (value: string) => void;\n /** Blocks every segment. */\n disabled?: boolean | undefined;\n /** Names the radio group. */\n \"aria-label\"?: string | undefined;\n /** Merged with the strip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const nameId = useId();\n const described = useFieldDescription();\n const effective = value === \"\" ? defaultValue : value;\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n aria-describedby={described[\"aria-describedby\"]}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n // `shrink-0` because this is a control, not filler: in a crowded toolbar a flex item at\n // its default `1 1 auto` gives up width first, and the segment labels are what pay for\n // it. Let the row scroll instead of squeezing the choice.\n \"inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-control border border-line-strong bg-raised p-0.5\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {options.map((option) => {\n const checked = option.value === effective;\n return (\n <label\n key={option.value}\n data-state={checked ? \"checked\" : \"unchecked\"}\n className={cn(\n // `whitespace-nowrap` or a two-word label's min-content width is its longest\n // *word*, and \"Side by side\" is free to stack onto three lines inside a\n // fixed-height strip that then clips it.\n \"relative cursor-pointer whitespace-nowrap rounded-[0.3rem] px-2.5 py-1 text-xs font-medium transition-colors\",\n \"has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-signal\",\n checked\n ? \"bg-surface text-fg shadow-sm\"\n : \"text-fg-muted hover:text-fg\",\n disabled && \"cursor-not-allowed\",\n )}\n >\n <input\n type=\"radio\"\n name={nameId}\n value={option.value}\n checked={checked}\n disabled={disabled}\n onChange={() =>\n // Selecting the default returns the field to unset: same effective value,\n // nothing sent, and the default keeps living in exactly one place.\n onValueChange(option.value === defaultValue ? \"\" : option.value)\n }\n className=\"absolute inset-0 cursor-pointer opacity-0\"\n />\n {option.label}\n </label>\n );\n })}\n </div>\n );\n}\n","/*\n * A picker with a list you can actually see.\n *\n * A native `<select>` cannot style its option list on macOS, so the popup arrives in the OS\n * palette in the middle of an otherwise deliberate instrument panel, and it cannot render a\n * two-line option or a disabled one with a reason.\n *\n * The one contract worth reading before changing this: **`\"\"` means unset.** Radix reserves\n * the empty string internally -- an `Item` may not carry it -- so an explicit \"use the\n * default\" entry travels under a sentinel and is mapped back at the boundary. Callers only\n * ever see `\"\"`.\n */\n\nimport * as RadixSelect from \"@radix-ui/react-select\";\nimport { Check, ChevronDown } from \"lucide-react\";\nimport { useState } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRingInset } from \"./cn\";\n\nconst UNSET_SENTINEL = \"__unset__\";\n\n/** One entry of a `Select`'s list. */\nexport type SelectOption = {\n /** What `onValueChange` reports. Must not be `\"\"`, which means unset. */\n value: string;\n /** What the list and the trigger show. */\n label: string;\n /** Shown quietly after the label -- a strategy, a count, a reason it is unavailable. */\n note?: string;\n /** Shown but not choosable; say why in `note`. */\n disabled?: boolean;\n};\n\n/**\n * A picker whose option list is styled with the app, with room for a quiet note per option.\n *\n * Controlled, and **`\"\"` means unset**: the trigger then shows `placeholder`, and the\n * `unsetLabel` entry (when given) reports `\"\"`. The trigger carries Radix's `data-state`\n * (`open`/`closed`), `data-placeholder` while unset and `data-disabled`; inside a `Field` it\n * is described by the field's description and error.\n */\nexport function Select({\n value,\n onValueChange,\n options,\n placeholder = \"Choose…\",\n unsetLabel,\n disabled = false,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ownDescribedBy,\n className,\n}: {\n /** The chosen option's value, or `\"\"` for unset. */\n value: string;\n /** Called with the chosen value, or `\"\"` for the unset entry. */\n onValueChange: (value: string) => void;\n /** The list, in order. */\n options: SelectOption[];\n /** Shown on the trigger while the value is unset. Defaults to \"Choose…\". */\n placeholder?: string | undefined;\n /** When given, adds a leading entry that returns the field to unset. */\n unsetLabel?: string | undefined;\n /** Blocks the trigger. */\n disabled?: boolean | undefined;\n /** Names the trigger, where no `<label>` does. */\n \"aria-label\"?: string | undefined;\n /** Ids of elements describing the trigger, merged with a surrounding `Field`'s. */\n \"aria-describedby\"?: string | undefined;\n /** Merged with the trigger's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const [open, setOpen] = useState(false);\n const described = useFieldDescription(ownDescribedBy);\n\n return (\n <RadixSelect.Root\n // Controlled for its whole lifetime: Radix treats `value=\"\"` as \"no selection\" and shows\n // the placeholder, which is exactly this component's unset.\n value={value}\n onValueChange={(next) => onValueChange(next === UNSET_SENTINEL ? \"\" : next)}\n open={open}\n onOpenChange={setOpen}\n disabled={disabled}\n >\n <RadixSelect.Trigger\n aria-label={ariaLabel}\n {...described}\n // While the list is open Radix hides everything outside it from assistive technology\n // (`aria-hidden`), this trigger included; a hidden element must not stay in the tab\n // order. Focus returns to it on close all the same.\n tabIndex={open ? -1 : undefined}\n className={cn(\n \"flex h-8 w-full items-center justify-between gap-2 rounded-control border border-line-strong bg-raised px-2.5 text-sm text-fg\",\n \"transition-colors hover:border-fg-subtle\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"data-[placeholder]:text-fg-subtle\",\n focusRingInset,\n className,\n )}\n >\n <RadixSelect.Value placeholder={placeholder} />\n <RadixSelect.Icon className=\"shrink-0 text-fg-subtle\">\n <ChevronDown className=\"size-3.5\" />\n </RadixSelect.Icon>\n </RadixSelect.Trigger>\n\n <RadixSelect.Portal>\n <RadixSelect.Content\n position=\"popper\"\n sideOffset={4}\n className={cn(\n \"z-50 max-h-72 min-w-(--radix-select-trigger-width) overflow-hidden\",\n \"rounded-panel border border-line bg-overlay shadow-lg shadow-black/25\",\n )}\n >\n <RadixSelect.Viewport className=\"p-1\">\n {unsetLabel !== undefined && (\n <SelectItem value={UNSET_SENTINEL} label={unsetLabel} muted />\n )}\n {options.map((option) => (\n <SelectItem\n key={option.value}\n value={option.value}\n label={option.label}\n note={option.note}\n disabled={option.disabled}\n />\n ))}\n </RadixSelect.Viewport>\n </RadixSelect.Content>\n </RadixSelect.Portal>\n </RadixSelect.Root>\n );\n}\n\nfunction SelectItem({\n value,\n label,\n note,\n disabled,\n muted = false,\n}: {\n value: string;\n label: string;\n note?: string | undefined;\n disabled?: boolean | undefined;\n muted?: boolean;\n}) {\n return (\n <RadixSelect.Item\n value={value}\n disabled={disabled ?? false}\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-control py-1.5 pr-2 pl-7 text-sm outline-none select-none\",\n \"data-highlighted:bg-raised data-highlighted:text-fg\",\n \"data-disabled:pointer-events-none data-disabled:opacity-45\",\n muted ? \"text-fg-muted\" : \"text-fg\",\n )}\n >\n <RadixSelect.ItemIndicator className=\"absolute left-2 text-signal\">\n <Check className=\"size-3.5\" />\n </RadixSelect.ItemIndicator>\n <RadixSelect.ItemText>{label}</RadixSelect.ItemText>\n {note && <span className=\"ml-auto font-mono text-xs text-fg-subtle\">{note}</span>}\n </RadixSelect.Item>\n );\n}\n","/*\n * A continuous value, with its number always on screen.\n *\n * A slider that drives what you are looking at -- a results threshold, an overlay opacity,\n * a caliper search range -- is a measurement instrument: the number matters as much as the\n * position, so it is rendered beside the track rather than left to a separate line of prose.\n */\n\nimport * as RadixSlider from \"@radix-ui/react-slider\";\nimport type { ReactNode } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A continuous value, with its number rendered beside the track.\n *\n * Controlled. The name (`aria-label`) and a surrounding `Field`'s description go on the\n * thumb — the element with `role=\"slider\"` that a keyboard and a screen reader operate.\n * Radix exposes `data-disabled` and `data-orientation` on the parts.\n */\nexport function Slider({\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 1,\n step = 0.01,\n readout,\n disabled = false,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ownDescribedBy,\n className,\n}: {\n /** The current value. */\n value: number;\n /** Called on every move. */\n onValueChange: (value: number) => void;\n /** Called once when a drag or a key press ends — for updates too expensive to run per move. */\n onValueCommit?: ((value: number) => void) | undefined;\n /** Defaults to 0. */\n min?: number | undefined;\n /** Defaults to 1. */\n max?: number | undefined;\n /** Defaults to 0.01. */\n step?: number | undefined;\n /** Rendered to the right of the track. Give it a fixed-width mono span for stability. */\n readout?: ReactNode;\n /** Blocks the thumb and dims the track. */\n disabled?: boolean | undefined;\n /** Names the slider (set on the thumb). */\n \"aria-label\"?: string | undefined;\n /** Ids of elements describing the slider, merged with a surrounding `Field`'s. */\n \"aria-describedby\"?: string | undefined;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const described = useFieldDescription(ownDescribedBy);\n\n return (\n // Grows by default: a slider that does not fill the space it is given collapses to a\n // few pixels of track, which is unusable and reads as a rendering fault.\n <div className={cn(\"flex min-w-0 flex-1 items-center gap-3\", className)}>\n <RadixSlider.Root\n value={[value]}\n onValueChange={([next]) => next !== undefined && onValueChange(next)}\n onValueCommit={([next]) => next !== undefined && onValueCommit?.(next)}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n className={cn(\n \"relative flex h-5 min-w-0 flex-1 touch-none items-center select-none\",\n \"data-disabled:opacity-50\",\n )}\n >\n <RadixSlider.Track className=\"relative h-1 grow overflow-hidden rounded-full bg-line\">\n <RadixSlider.Range className=\"absolute h-full bg-signal\" />\n </RadixSlider.Track>\n <RadixSlider.Thumb\n aria-label={ariaLabel}\n {...described}\n className={cn(\n \"block size-3.5 rounded-full border-2 border-signal bg-surface transition-shadow\",\n \"hover:shadow-[0_0_0_4px] hover:shadow-signal/20\",\n \"disabled:cursor-not-allowed\",\n focusRing,\n )}\n />\n </RadixSlider.Root>\n {readout && (\n <span className=\"shrink-0 font-mono text-xs text-fg tabular-nums\">{readout}</span>\n )}\n </div>\n );\n}\n","/*\n * A table.\n *\n * `numeric` is the column property that matters here. Quantities are set in mono with\n * tabular figures and aligned right, so a column of scores or measurements can be compared\n * by eye down its decimal point rather than read one row at a time.\n */\n\nimport type { KeyboardEvent, MouseEvent, ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/** One column of a `Table`: its header and how to render a row's cell. */\nexport type Column<Row> = {\n /** Unique among the columns; the React key of its header and cells. */\n key: string;\n /** The `<th>` content. */\n header: ReactNode;\n /** Right-aligned, mono, tabular. Use for anything that is a quantity. */\n numeric?: boolean;\n /** A CSS width for the column, e.g. `\"6rem\"`. */\n width?: string;\n /** The cell's content for a row. */\n cell: (row: Row) => ReactNode;\n};\n\n/**\n * A table of rows, with numeric columns set in mono and right-aligned.\n *\n * With `onRowClick` the rows are keyboard-reachable (Tab, then Enter or Space) while keeping\n * their table semantics. The active row carries `aria-current` and `data-state=\"active\"`;\n * padding follows the density in force. With no rows it renders the `empty` message instead.\n */\nexport function Table<Row>({\n columns,\n rows,\n rowKey,\n empty = \"Nothing here.\",\n caption,\n className,\n onRowClick,\n isRowActive,\n onRowHover,\n}: {\n /** The columns, in order. */\n columns: Column<Row>[];\n /** The rows, in order. */\n rows: Row[];\n /** A stable key per row. */\n rowKey: (row: Row, index: number) => string | number;\n /** Shown instead of the table when there are no rows. Defaults to \"Nothing here.\". */\n empty?: ReactNode;\n /** Names the table for assistive technology (a visually hidden `<caption>`). */\n caption?: string | undefined;\n /** Merged with the scroll wrapper's (or the empty message's) own classes through `cn`. */\n className?: string | undefined;\n /** Makes rows activatable. Keyboard-reachable, so a table used as a list of\n * destinations is not a mouse-only control. The event is passed so a caller can read\n * modifiers — a list kept in step with a canvas needs shift-range and meta-toggle to mean\n * the same thing in both places. */\n onRowClick?: (\n row: Row,\n index: number,\n event: MouseEvent<HTMLTableRowElement> | KeyboardEvent<HTMLTableRowElement>,\n ) => void;\n /** Marks the row that is current — the selected match, the open frame. Rendered as\n * `aria-current`, so it is announced rather than only tinted. */\n isRowActive?: (row: Row, index: number) => boolean;\n /** Pointer enter/leave, for tables kept in step with a canvas overlay.\n * `null` on leave. */\n onRowHover?: (row: Row | null, index: number | null) => void;\n}) {\n const density = useDensity();\n\n if (rows.length === 0) {\n return (\n <p\n className={cn(\n \"text-center text-fg-muted\",\n byDensity(density, \"py-6 text-sm\", \"py-3 text-xs\"),\n className,\n )}\n >\n {empty}\n </p>\n );\n }\n\n return (\n <div className={cn(\"w-full overflow-x-auto\", className)}>\n <table className={cn(\"w-full text-left\", byDensity(density, \"text-sm\", \"text-[11px]\"))}>\n {caption && <caption className=\"sr-only\">{caption}</caption>}\n <thead>\n <tr className=\"border-b border-line\">\n {columns.map((column) => (\n <th\n key={column.key}\n scope=\"col\"\n style={column.width ? { width: column.width } : undefined}\n className={cn(\n \"font-medium text-fg-muted\",\n byDensity(density, \"pb-2 pr-3 text-xs\", \"pb-1 pr-2 text-[10px]\"),\n column.numeric && \"text-right\",\n )}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr\n key={rowKey(row, index)}\n data-state={isRowActive?.(row, index) ? \"active\" : undefined}\n className={cn(\n \"border-b border-line/60 last:border-0\",\n onRowClick && \"cursor-pointer hover:bg-raised\",\n isRowActive?.(row, index) && \"bg-signal/10\",\n )}\n tabIndex={onRowClick ? 0 : undefined}\n // Deliberately *not* `role=\"button\"`. Overriding a `<tr>`'s implicit `row`\n // makes its `<td>`s' implicit `cell` invalid too — the cells have no row to\n // belong to — so a screen reader stops announcing the table as a table and a\n // reader loses the column each value came from, which on a table of\n // quantities is the whole content. `tabIndex` plus the Enter/Space handler\n // below is what makes the row reachable; the role stays what the element is.\n aria-current={isRowActive?.(row, index) ? true : undefined}\n onClick={onRowClick ? (event) => onRowClick(row, index, event) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index, event);\n }\n }\n : undefined\n }\n onPointerEnter={onRowHover ? () => onRowHover(row, index) : undefined}\n onPointerLeave={onRowHover ? () => onRowHover(null, null) : undefined}\n >\n {columns.map((column) => (\n <td\n key={column.key}\n className={cn(\n \"align-middle text-fg last:pr-0\",\n byDensity(density, \"py-2 pr-3\", \"py-0.5 pr-2\"),\n column.numeric && \"text-right font-mono tabular-nums\",\n )}\n >\n {column.cell(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n","/*\n * The two binary controls, and the difference between them.\n *\n * A `Switch` changes how the thing behaves from now on -- \"allow downloads\", \"show the\n * mask\", \"lock the caliper axis\" -- and reads as a setting. A `Checkbox` asserts something\n * about a state you are about to commit -- \"I have read the warnings\", \"keep this channel\"\n * -- and reads as a statement.\n */\n\nimport * as RadixCheckbox from \"@radix-ui/react-checkbox\";\nimport * as RadixSwitch from \"@radix-ui/react-switch\";\nimport { Check, Minus } from \"lucide-react\";\nimport { useId, type MouseEvent, type ReactNode } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A setting: changes how the thing behaves from now on, and reads as one (\"show the mask\").\n * Always labelled; the label and the description sit beside the switch.\n *\n * Controlled. Radix exposes `data-state` (`checked`/`unchecked`) and `data-disabled`.\n */\nexport function Switch({\n checked,\n onCheckedChange,\n label,\n description,\n disabled = false,\n className,\n}: {\n /** Whether it is on. */\n checked: boolean;\n /** Called with the new state. */\n onCheckedChange: (checked: boolean) => void;\n /** What the setting is. */\n label: ReactNode;\n /** What turning it on does. */\n description?: ReactNode;\n /** Blocks the switch. */\n disabled?: boolean | undefined;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const descriptionId = useId();\n return (\n <label className={cn(\"flex min-w-0 items-start gap-2.5\", className)}>\n <RadixSwitch.Root\n checked={checked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n aria-describedby={description ? descriptionId : undefined}\n className={cn(\n \"mt-0.5 h-4 w-7 shrink-0 rounded-full border border-line-strong bg-raised transition-colors\",\n \"data-[state=checked]:border-signal data-[state=checked]:bg-signal\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRing,\n )}\n >\n <RadixSwitch.Thumb\n className={cn(\n \"block size-3 rounded-full bg-fg-subtle transition-transform will-change-transform\",\n \"translate-x-0.5 data-[state=checked]:translate-x-3.5 data-[state=checked]:bg-signal-fg\",\n )}\n />\n </RadixSwitch.Root>\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm leading-tight text-fg\">{label}</span>\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n </span>\n </label>\n );\n}\n\n/**\n * A statement about a state you are about to commit (\"keep this channel\"). With a `label`\n * it renders its own row; without one it is a bare box (give it `aria-label`, e.g. in a\n * table cell).\n *\n * Controlled. Radix exposes `data-state` (`checked`/`unchecked`/`indeterminate`) and\n * `data-disabled`; inside a `Field` it is described by the field's description and error.\n */\nexport function Checkbox({\n checked,\n onCheckedChange,\n onClick,\n label,\n description,\n disabled = false,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** `\"indeterminate\"` for a partial selection over a set. */\n checked: boolean | \"indeterminate\";\n /** Called with the new state (never `\"indeterminate\"`: a click resolves it). */\n onCheckedChange: (checked: boolean) => void;\n /**\n * For a checkbox whose meaning depends on the modifier keys — extending a range with\n * shift, toggling one out of a selection with the platform key. `onCheckedChange` gets a\n * boolean and cannot carry them. Keyboard activation synthesises a click with its\n * modifiers intact, so this sees both input paths.\n */\n onClick?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;\n /** What checking it asserts. Omit for a bare box. */\n label?: ReactNode;\n /** More about the statement, under the label. */\n description?: ReactNode;\n /** Blocks the box. */\n disabled?: boolean | undefined;\n /** For a checkbox in a table cell, where the row is the label. */\n \"aria-label\"?: string | undefined;\n /** Merged with the row's (or, without a label, the box's) own classes through `cn`. */\n className?: string | undefined;\n}) {\n const descriptionId = useId();\n const described = useFieldDescription(description ? descriptionId : undefined);\n const box = (\n <RadixCheckbox.Root\n checked={checked}\n onCheckedChange={(next) => onCheckedChange(next === true)}\n onClick={onClick}\n disabled={disabled}\n aria-label={ariaLabel}\n {...described}\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded-[0.25rem] border border-line-strong bg-raised transition-colors\",\n \"data-[state=checked]:border-signal data-[state=checked]:bg-signal\",\n \"data-[state=indeterminate]:border-signal data-[state=indeterminate]:bg-signal\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRing,\n label === undefined && className,\n )}\n >\n <RadixCheckbox.Indicator className=\"text-signal-fg\">\n {checked === \"indeterminate\" ? (\n <Minus className=\"size-3\" strokeWidth={3} />\n ) : (\n <Check className=\"size-3\" strokeWidth={3} />\n )}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n );\n\n if (label === undefined) return box;\n\n return (\n <label className={cn(\"flex min-w-0 items-start gap-2.5\", className)}>\n <span className=\"mt-0.5\">{box}</span>\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm leading-tight text-fg\">{label}</span>\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n </span>\n </label>\n );\n}\n","/*\n * A dense on/off control for a thing that is drawn on screen.\n *\n * `Switch` is a settings row — a stacked label, a description, generous padding — and\n * three of them side by side over an image canvas is a panel, not a toolbar. This is the\n * same binary decision in the space a toolbar has: one line, one target, and an optional\n * swatch carrying the colour the layer is actually drawn in — an overlay's caliper boxes,\n * a chart's series — so the legend and the control are the same object rather than two\n * things to reconcile.\n *\n * Hand-built on a native `button` with `role=\"switch\"`: this is not one of the four Radix\n * primitives the rest of the toggle vocabulary is built on, and a chip does not need one.\n */\n\nimport type { ReactNode } from \"react\";\n\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A dense on/off chip for a layer drawn on screen, with an optional swatch in the layer's\n * colour: a toolbar-sized `Switch` (`role=\"switch\"`, `aria-checked`).\n *\n * Controlled. State is exposed as `data-state` (`checked`/`unchecked`).\n */\nexport function ToggleChip({\n checked,\n onCheckedChange,\n children,\n swatch,\n disabled = false,\n title,\n className,\n}: {\n /** Whether the layer is shown. */\n checked: boolean;\n /** Called with the new state. */\n onCheckedChange: (checked: boolean) => void;\n /** The layer's name. */\n children: ReactNode;\n /** A CSS colour: the colour this layer is drawn in, shown as a dot. */\n swatch?: string;\n /** Blocks the chip. */\n disabled?: boolean | undefined;\n /** Why the control is disabled, or what the layer is. Never the only explanation. */\n title?: string | undefined;\n /** Merged with the chip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n data-state={checked ? \"checked\" : \"unchecked\"}\n disabled={disabled}\n title={title}\n onClick={() => onCheckedChange(!checked)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-control border px-2 py-1 text-xs whitespace-nowrap transition-colors\",\n checked\n ? \"border-line-strong bg-raised text-fg\"\n : \"border-line bg-transparent text-fg-muted hover:text-fg\",\n \"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-fg-muted\",\n focusRing,\n className,\n )}\n >\n {swatch !== undefined && (\n <span\n aria-hidden\n className={cn(\n \"size-2 shrink-0 rounded-full border transition-opacity\",\n checked ? \"opacity-100\" : \"opacity-30\",\n )}\n style={{ backgroundColor: swatch, borderColor: swatch }}\n />\n )}\n {children}\n </button>\n );\n}\n","/*\n * A tab strip.\n *\n * The count is set in mono beside the label rather than in parentheses inside it: it is a\n * quantity -- how many overrides sit behind an options tab, how many samples a filter chip\n * covers -- and mono makes it read as one at a glance.\n */\n\nimport type { KeyboardEvent } from \"react\";\n\nimport { cn, focusRing } from \"./ui/cn\";\n\n/** One tab of a `Tabs` strip. */\nexport interface TabItem<Id extends string> {\n /** Identifies the tab to `active` and `onSelect`. Unique within the strip. */\n id: Id;\n /** The tab's text, and its accessible name. */\n label: string;\n /** Shown after the label, for the chips that carry a row or override count. */\n count?: number;\n /** Shown but not selectable, and skipped by the arrow keys. */\n disabled?: boolean;\n /** Why this tab has nothing to show, for a reader wondering what they missed. */\n title?: string;\n}\n\nconst NEXT_KEYS: Record<string, (index: number, length: number) => number> = {\n ArrowRight: (index, length) => (index + 1) % length,\n ArrowLeft: (index, length) => (index - 1 + length) % length,\n Home: () => 0,\n End: (_, length) => length - 1,\n};\n\n/**\n * A tab strip (`role=\"tablist\"`): the caller renders the panel for the `active` tab.\n *\n * Keyboard: one tab stop for the whole strip (the active tab); ←/→ move to the previous or\n * next enabled tab and select it, Home/End to the first and last. Each tab carries\n * `data-state` (`active`/`inactive`). Pass `idPrefix` to wire the tabs to the panel: the tab\n * for `id` gets the element id `<idPrefix>-tab-<id>` and the active tab gets\n * `aria-controls=\"<idPrefix>-panel-<id>\"`, so render the panel as a `role=\"tabpanel\"` element\n * with the id `<idPrefix>-panel-<active>` and `aria-labelledby=\"<idPrefix>-tab-<active>\"`.\n */\nexport function Tabs<Id extends string>({\n items,\n active,\n onSelect,\n label,\n idPrefix,\n className,\n}: {\n /** The tabs, in order. */\n items: TabItem<Id>[];\n /** The selected tab's id. */\n active: Id;\n /** Called with a tab's id when it is clicked or reached with the arrow keys. */\n onSelect: (id: Id) => void;\n /** Names the tab list. */\n label: string;\n /** Prefix for the tab and panel element ids; enables `aria-controls` (see above). */\n idPrefix?: string | undefined;\n /** Merged with the strip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const enabled = items.filter((item) => !item.disabled);\n // The one tab stop: the active tab, or the first enabled one when the active tab is\n // disabled or not in the list.\n const focusable = enabled.some((item) => item.id === active) ? active : enabled[0]?.id;\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const move = NEXT_KEYS[event.key];\n const target = event.target as HTMLElement;\n const current = enabled.findIndex((item) => item.id === target.dataset[\"tabId\"]);\n if (move === undefined || current === -1) return;\n event.preventDefault();\n const next = enabled[move(current, enabled.length)];\n if (next === undefined) return;\n const tabs = event.currentTarget.querySelectorAll<HTMLElement>('[role=\"tab\"]');\n for (const tab of tabs) if (tab.dataset[\"tabId\"] === next.id) tab.focus();\n if (next.id !== active) onSelect(next.id);\n };\n\n return (\n <div\n role=\"tablist\"\n aria-label={label}\n aria-orientation=\"horizontal\"\n onKeyDown={onKeyDown}\n className={cn(\n \"inline-flex max-w-full flex-wrap items-center gap-0.5 rounded-control border border-line bg-raised p-0.5\",\n className,\n )}\n >\n {items.map((item) => {\n const selected = item.id === active;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n id={idPrefix === undefined ? undefined : `${idPrefix}-tab-${item.id}`}\n aria-selected={selected}\n aria-controls={\n idPrefix !== undefined && selected ? `${idPrefix}-panel-${item.id}` : undefined\n }\n tabIndex={item.id === focusable ? 0 : -1}\n data-tab-id={item.id}\n data-state={selected ? \"active\" : \"inactive\"}\n disabled={item.disabled}\n title={item.title}\n onClick={() => onSelect(item.id)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-[0.3rem] px-2.5 py-1 text-xs font-medium transition-colors\",\n \"disabled:cursor-not-allowed disabled:opacity-40\",\n selected\n ? \"bg-surface text-fg shadow-sm\"\n : \"text-fg-muted hover:text-fg\",\n focusRing,\n )}\n >\n {item.label}\n {item.count !== undefined && item.count > 0 && (\n <span\n className={cn(\n \"rounded-full px-1.5 font-mono text-[10px] tabular-nums\",\n selected ? \"bg-signal/15 text-signal\" : \"bg-line text-fg-muted\",\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,MAAa,4BAA4B;AAIzC,MAAM,cACJ,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aAC1D,OAAO,WAAW,8BAA8B,IAChD;;;;;;;;AASN,SAAgB,gBAAgB,aAAqB,2BAAwC;CAC3F,IAAI;EACF,MAAM,SAAS,OAAO,aAAa,QAAQ,UAAU;EACrD,IAAI,WAAW,WAAW,WAAW,UAAU,WAAW,UAAU,OAAO;CAC7E,QAAQ,CAGR;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,aAAa,QAAuC;CAClE,IAAI,WAAW,UAAU,OAAO;CAChC,OAAO,MAAM,CAAC,EAAE,UAAU,SAAS;AACrC;AAEA,SAAS,MAAM,QAA2B;CACxC,IAAI,OAAO,aAAa,aAAa;CACrC,SAAS,gBAAgB,UAAU,OAAO,QAAQ,aAAa,MAAM,MAAM,MAAM;AACnF;;;;;;;AAQA,SAAgB,eACd,QACA,aAAqB,2BACf;CACN,IAAI;EACF,OAAO,aAAa,QAAQ,YAAY,MAAM;CAChD,QAAQ,CAER;CACA,MAAM,MAAM;AACd;;;;;;;AAQA,SAAgB,UAAU,aAAqB,2BAAuC;CACpF,MAAM,gBAAgB,UAAU,CAAC;CAEjC,MAAM,QAAQ,MAAM;CACpB,IAAI,UAAU,MAAM,aAAa,CAAC;CAElC,MAAM,uBAAuB;EAC3B,IAAI,gBAAgB,UAAU,MAAM,UAAU,MAAM,QAAQ;CAC9D;CACA,MAAM,iBAAiB,UAAU,cAAc;CAC/C,aAAa,MAAM,oBAAoB,UAAU,cAAc;AACjE;;;ACzFA,MAAM,WAAwC;CAC5C,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,MAAM;CACN,OAAO;AACT;;;;;;;;;AAUA,SAAgB,UAAU,MAA+B,WAAwB,UAAkB;CACjG,OAAO,SAAS,QAAQ;AAC1B;;;;;;;;;;;;;;;;;;;ACrBA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;;;;;;;;AAWA,MAAa,YACX;;;;;AAMF,MAAa,iBAAiB;;;;;;;ACnB9B,SAAgB,gBAAgB,EAC9B,YAIC;CACD,OAAO,oBAAC,aAAa,UAAd;EAAuB,eAAe;EAAM;CAAgC,CAAA;AACrF;;;;;;;;AASA,SAAgB,QAAQ,EACtB,SACA,YAMC;CACD,OACE,qBAAC,aAAa,MAAd,EAAA,UAAA,CACE,oBAAC,aAAa,SAAd;EAAsB,SAAA;EAAS;CAA+B,CAAA,GAC9D,oBAAC,aAAa,QAAd,EAAA,UACE,qBAAC,aAAa,SAAd;EACE,YAAY;EACZ,kBAAkB;EAClB,WAAW,GACT,6EACA,wDACF;EANF,UAAA,CAQG,SACD,oBAAC,aAAa,OAAd,EAAoB,WAAU,eAAgB,CAAA,CAC1B;CACH,CAAA,EAAA,CAAA,CACJ,EAAA,CAAA;AAEvB;;;;;;;;;;AAWA,SAAgB,SAAS,EACvB,UACA,QAAQ,oBACR,MAAM,OAAO,cAQZ;CACD,OACE,oBAAC,SAAD;EAAS,SAAS;EAChB,UAAA,oBAAC,UAAD;GACE,MAAK;GACL,cAAY;GACZ,WAAW,GAAG,+DAA+D,SAAS;GAEtF,UAAA,oBAAC,MAAD,EAAM,WAAU,WAAY,CAAA;EACtB,CAAA;CACD,CAAA;AAEb;;;ACtEA,MAAM,QAAuB;CAAC;CAAU;CAAS;AAAM;AACvD,MAAM,OAAO;CAAE,QAAQ;CAAS,OAAO;CAAK,MAAM;AAAK;AACvD,MAAM,QAAqC;CACzC,QAAQ;CACR,OAAO;CACP,MAAM;AACR;;AAGA,SAAS,UAAU,UAAkC;CACnD,OAAO,iBAAiB,WAAW,QAAQ;CAC3C,aAAa,OAAO,oBAAoB,WAAW,QAAQ;AAC7D;;AAGA,MAAM,qBAAkC;;;;;;;;;;AAWxC,SAAgB,YAAY,EAC1B,aAAa,2BACb,aAMC;CACD,MAAM,SAAS,qBAAqB,iBAAiB,gBAAgB,UAAU,GAAG,YAAY;CAG9F,MAAM,CAAC,QAAQ,aAAa,SAAsD,IAAI;CACtF,MAAM,SAAS,QAAQ,QAAQ,aAAa,OAAO,SAAS;CAE5D,MAAM,gBAAgB;EACpB,MAAM,OAAO,OAAO,MAAM,QAAQ,MAAM,IAAI,KAAK,MAAM;EACvD,UAAU;GAAE,KAAK;GAAY,QAAQ;EAAK,CAAC;EAC3C,eAAe,MAAM,UAAU;CACjC;CAEA,MAAM,OAAO,KAAK;CAElB,OACE,oBAAC,SAAD;EAAS,SAAS,MAAM;EACtB,UAAA,oBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,cAAY,MAAM;GAClB,qBAAmB;GACnB,WAAW,GACT,uFACA,WACA,SACF;GAEA,UAAA,oBAAC,MAAD,EAAM,WAAU,SAAU,CAAA;EACpB,CAAA;CACD,CAAA;AAEb;;;ACxEA,MAAM,eAAqC;CACzC,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,SAAS;CACT,MAAM;AACR;AAEA,MAAM,cAAoC;CACxC,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,SAAS;CACT,MAAM;AACR;;;;;;;AAQA,SAAgB,MAAM,EACpB,OAAO,WACP,WACA,YAQC;CACD,OACE,oBAAC,QAAD;EACE,aAAW;EACX,WAAW,GACT,sGACA,aAAa,OACb,SACF;EAEC;CACG,CAAA;AAEV;;;;;;;AAQA,SAAgB,UAAU,EACxB,MACA,WACA,YAQC;CACD,OACE,qBAAC,QAAD;EACE,aAAW;EACX,WAAW,GAAG,0DAA0D,SAAS;EAFnF,UAAA,CAIE,oBAAC,QAAD;GAAM,WAAW,GAAG,kCAAkC,YAAY,KAAK;GAAG,eAAA;EAAa,CAAA,GACtF,QACG;;AAEV;;AAGA,SAAgB,SAAS,EACvB,QACA,aAMC;CACD,MAAM,QAAQ,OAAO,QAAQ,GAAG,WAAW,QAAQ,CAAC;CACpD,IAAI,MAAM,WAAW,GACnB,OAAO,oBAAC,QAAD;EAAM,WAAW,GAAG,0BAA0B,SAAS;EAAG,UAAA;CAAW,CAAA;CAE9E,OACE,oBAAC,QAAD;EAAM,WAAW,GAAG,uCAAuC,SAAS;EACjE,UAAA,MAAM,KAAK,CAAC,MAAM,OAAO,UACxB,qBAAC,OAAD;GAAwB;GAAxB,UAAA;IACE,oBAAC,QAAD;KAAM,WAAU;KAAa,UAAA;IAAY,CAAA;IAAC;IAAE;GACvC;EAFK,GAAA,IAEL,CACR;CACG,CAAA;AAEV;;;AChGA,MAAM,iBAAiB,cAAuB,aAAa;;;;;;AAO3D,SAAgB,aAAsB;CACpC,OAAO,IAAI,cAAc;AAC3B;;;;;;;;;AAUA,SAAgB,UAAa,SAAkB,aAAgB,SAAe;CAC5E,OAAO,YAAY,YAAY,UAAU;AAC3C;;;;;AAMA,SAAgB,gBAAgB,EAC9B,OACA,YAMC;CACD,OAAO,oBAAC,gBAAD;EAAuB;EAAQ;CAAyB,CAAA;AACjE;;;AChDA,MAAM,kBAAiD;CACrD,SAAS;CACT,WAAW;CACX,OAAO;CAKP,QAAQ;AACV;AAEA,MAAM,eAA2C;CAC/C,IAAI;CACJ,IAAI;AACN;;;;;;;;;;AAWA,SAAgB,cAAc,EAC5B,UAAU,aACV,OAAO,MACP,cAKE,CAAC,GAAW;CACd,OAAO,GACL,kGACA,mDACA,aAAa,OACb,gBAAgB,UAChB,WACA,SACF;AACF;AAEA,SAAS,WAAW,EAAE,QAA6B;CACjD,OACE,oBAAC,QAAD;EAAM,WAAU;EAA4B,eAAA;EACzC,UAAA;CACG,CAAA;AAEV;;;;;;;;;;AAWA,SAAgB,OAAO,EACrB,UAAU,aACV,MACA,OAAO,UACP,UAAU,OACV,MACA,UAAU,OACV,WACA,UACA,UACA,GAAG,QAgBF;CACD,MAAM,UAAU,WAAW;CAC3B,MAAM,WAAW,QAAQ,UAAU,SAAS,MAAM,IAAI;CAEtD,IAAI,SACF,OACE,qBAAC,MAAD;EACE,GAAI;EACJ,gBAAc;EACd,WAAW,cAAc;GAAE;GAAS,MAAM;GAAU;EAAU,CAAC;EAHjE,UAAA,CAKG,QAAQ,oBAAC,YAAD,EAAkB,KAAO,CAAA,GAClC,oBAAC,WAAD,EAAY,SAAoB,CAAA,CAC5B;;CAIV,OACE,qBAAC,UAAD;EACQ;EACN,UAAU,YAAY;EACtB,aAAW,WAAW,KAAA;EACtB,gBAAc;EACd,gBAAc,UAAU,KAAK,KAAA;EAC7B,GAAI;EACJ,WAAW,cAAc;GAAE;GAAS,MAAM;GAAU;EAAU,CAAC;EAPjE,UAAA,CASG,UACC,oBAAC,SAAD;GAAS,WAAU;GAAiC,eAAA;EAAa,CAAA,IAEjE,QAAQ,oBAAC,YAAD,EAAkB,KAAO,CAAA,GAElC,QACK;;AAEZ;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,EACzB,UAAU,aACV,MACA,MACA,UAAU,OACV,WACA,UACA,GAAG,QAUF;CACD,MAAM,UAAU,WAAW;CAC3B,MAAM,WAAW,QAAQ,UAAU,SAAS,MAAM,IAAI;CAGtD,OACE,qBAHW,UAAU,OAAO,KAG5B;EACE,GAAI;EACJ,gBAAc;EACd,WAAW,cAAc;GAAE;GAAS,MAAM;GAAU;EAAU,CAAC;EAHjE,UAAA,CAKG,QAAQ,oBAAC,YAAD,EAAkB,KAAO,CAAA,GAClC,oBAAC,WAAD,EAAY,SAAoB,CAAA,CAC5B;;AAEV;;;;;;;;ACrKA,SAAS,iBAGP;CACA,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CAYpD,OAAO,CAXK,aAAa,SAAgC;EACvD,IAAI,SAAS,MAAM,OAAO,KAAA;EAC1B,MAAM,gBAAgB,eAAe,KAAK,eAAe,KAAK,eAAe,CAAC;EAC9E,QAAQ;EACR,IAAI,OAAO,mBAAmB,aAAa,OAAO,KAAA;EAClD,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,IAAI;EAErB,IAAI,KAAK,mBAAmB,SAAS,QAAQ,KAAK,iBAAiB;EACnE,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,CACM,GAAG,WAAW;AAC1B;;;;;;;;AASA,SAAgB,OAAO,EACrB,MACA,cACA,OACA,aACA,QACA,WACA,YAgBC;CACD,MAAM,CAAC,SAAS,eAAe,eAAe;CAE9C,OACE,oBAAC,YAAY,MAAb;EAAwB;EAAoB;EAC1C,UAAA,qBAAC,YAAY,QAAb,EAAA,UAAA,CACE,oBAAC,YAAY,SAAb,EAAqB,WAAU,qDAAsD,CAAA,GACrF,qBAAC,YAAY,SAAb;GACE,WAAW,GACT,iGAKA,0CACA,6EACA,sBACA,SACF;GAXF,UAAA;IAaE,oBAAC,YAAY,OAAb;KAAmB,WAAU;KAC1B,UAAA;IACgB,CAAA;IAGnB,oBAAC,YAAY,aAAb;KACE,WAAW,GACT,8CACA,gBAAgB,KAAA,KAAa,SAC/B;KAEC,UAAA,eAAe;IACO,CAAA;IACzB,oBAAC,OAAD;KACE,KAAK;KACL,UAAU,cAAc,IAAI,KAAA;KAC5B,oBAAkB,cAAc,KAAK,KAAA;KACrC,WAAW,GAAG,2BAA2B,cAAc;KAEvD,UAAA,oBAAC,OAAD,EAAM,SAAc,CAAA;IACjB,CAAA;IACJ,UAAU,oBAAC,OAAD;KAAK,WAAU;KAAwC,UAAA;IAAY,CAAA;GAC3D;EACH,CAAA,CAAA,EAAA,CAAA;CACJ,CAAA;AAEtB;;;;;AAMA,SAAgB,cAAc,EAC5B,MACA,cACA,OACA,aACA,cACA,WACA,UAAU,OACV,WAAW,OACX,cAAc,OACd,aAsBC;CACD,OACE,oBAAC,QAAD;EACQ;EACQ;EACP;EACM;EACF;EACX,QACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,YAAY,OAAb;GAAmB,SAAA;GACjB,UAAA,oBAAC,QAAD;IAAQ,SAAQ;IAAQ,UAAA;GAAc,CAAA;EACrB,CAAA,GACnB,oBAAC,QAAD;GACE,SAAS,cAAc,WAAW;GACzB;GACC;GACV,SAAS;GAER,UAAA;EACK,CAAA,CACR,EAAA,CAAA;CAEL,CAAA;AAEL;;;;;AAMA,MAAa,cAAc,YAAY;;;;;;;;;ACpKvC,SAAgB,WAAW,EACzB,SACA,OACA,cAAc,OACd,WACA,YAYC;CACD,OACE,qBAAC,WAAD;EAAS,MAAM;EAAa,WAAW,GAAG,SAAS,SAAS;EAA5D,UAAA,CACE,qBAAC,WAAD;GACE,WAAW,GACT,mGACA,mCACA,qFACF;GALF,UAAA;IAOE,oBAAC,cAAD;KACE,WAAU;KACV,eAAA;IACD,CAAA;IACA;IACA,UAAU,KAAA,KACT,oBAAC,QAAD;KAAM,WAAU;KAAyC,UAAA;IAAY,CAAA;GAEhE;EACT,CAAA,GAAA,oBAAC,OAAD;GAAK,WAAU;GAAQ;EAAc,CAAA,CAC9B;;AAEb;;;AC5CA,MAAM,UAAuE;CAC3E,MAAM;EAAE,SAAS;EAAwC,MAAM;CAAK;CACpE,SAAS;EAAE,SAAS;EAAoC,MAAM;CAAc;CAC5E,OAAO;EAAE,SAAS;EAAwC,MAAM;CAAQ;CACxE,SAAS;EAAE,SAAS;EAAwC,MAAM;CAAa;AACjF;AAEA,MAAM,YAAyC;CAC7C,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;AACX;;;;;;;AAQA,SAAgB,QAAQ,EACtB,OAAO,QACP,OACA,SACA,WACA,YAYC;CACD,MAAM,EAAE,SAAS,SAAS,QAAQ;CAClC,OACE,qBAAC,OAAD;EACE,MAAM,SAAS,UAAU,UAAU;EACnC,aAAW;EACX,WAAW,GACT,oFACA,SACA,SACF;EAPF,UAAA,CASE,oBAAC,MAAD;GAAM,WAAW,GAAG,0BAA0B,UAAU,KAAK;GAAG,eAAA;EAAa,CAAA,GAC7E,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACG,SAAS,oBAAC,KAAD;KAAG,WAAU;KAAe,UAAA;IAAS,CAAA;IAC/C,oBAAC,OAAD;KAAK,WAAU;KAAyB;IAAc,CAAA;IACrD,WAAW,oBAAC,OAAD;KAAK,WAAU;KAA0C,UAAA;IAAa,CAAA;GAC/E;EACF,CAAA,CAAA;;AAET;;AAGA,SAAgB,SAAS,EACvB,WACA,YAMC;CACD,OACE,oBAAC,SAAD;EAAS,MAAK;EAAmB;EAC9B;CACM,CAAA;AAEb;;AAGA,SAAgB,MAAM,EACpB,QACA,WACA,YAQC;CACD,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,sDAAsD,SAAS;EAAlF,UAAA,CACE,oBAAC,KAAD;GAAG,WAAU;GAAyB;EAAY,CAAA,GACjD,MACE;;AAET;;AAGA,SAAgB,SAAS,EACvB,aAIC;CACD,OAAO,oBAAC,OAAD;EAAK,eAAA;EAAY,WAAW,GAAG,yCAAyC,SAAS;CAAI,CAAA;AAC9F;;AAGA,SAAgB,aAAa,EAC3B,OAAO,GACP,aAMC;CACD,OACE,oBAAC,OAAD;EAAK,MAAK;EAAS,cAAW;EAAU,WAAW,GAAG,uBAAuB,SAAS;EACnF,UAAA,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,UAChC,oBAAC,UAAD,EAAsB,WAAW,GAAG,OAAO,UAAU,OAAO,IAAI,UAAU,QAAQ,EAAI,GAAvE,KAAuE,CACvF;CACE,CAAA;AAET;;;;;;;;AASA,SAAgB,YAAY,EAC1B,UACA,OACA,cAAc,WACd,aAUC;CACD,MAAM,UAAU,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG;CACnE,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,yBAAyB,SAAS;EAArD,UAAA,CACE,oBAAC,OAAD;GACE,MAAK;GACL,cAAY,aAAa,SAAS;GAClC,iBAAe;GACf,iBAAe;GACf,iBAAe;GACf,iBAAe,YAAY,MAAM,KAAK,KAAA;GACtC,WAAU;GAEV,UAAA,oBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG;GAC/B,CAAA;EACE,CAAA,GACJ,SACC,qBAAC,QAAD;GAAM,WAAU;GAAhB,UAAA;IACG;IAAQ;IAAK;GACV;EAEL,CAAA,CAAA;;AAET;;;ACpKA,MAAM,eAAe,cAA4B;CAAE,aAAa,KAAA;CAAW,SAAS;AAAM,CAAC;;;;;;AAO3F,SAAgB,oBAAoB,KAGlC;CACA,MAAM,EAAE,aAAa,YAAY,IAAI,YAAY;CACjD,MAAM,MAAM,CAAC,KAAK,WAAW,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CACvD,OAAO;EAAE,oBAAoB,QAAQ,KAAK,KAAA,IAAY;EAAK,gBAAgB,WAAW,KAAA;CAAU;AAClG;;;;;;;;;;;;AAaA,SAAgB,MAAM,EACpB,OACA,aACA,OACA,WAAW,OACX,YACA,KAAK,SACL,WACA,YAqBC;CACD,MAAM,gBAAgB,MAAM;CAC5B,MAAM,UAAU,MAAM;CACtB,MAAM,UAAU,WAAW;CAE3B,MAAM,WAAW,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU;CACpE,MAAM,cACJ,CAAC,cAAc,gBAAgB,KAAA,GAAW,WAAW,UAAU,KAAA,CAAS,CAAC,CACtE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG,KAAK,KAAA;CAGlB,MAAM,UAAU,eACP;EAAE,aAAa,OAAO,UAAU,cAAc,KAAA;EAAW,SAAS;CAAS,IAClF;EAAC;EAAI;EAAa;CAAQ,CAC5B;CAEA,MAAM,MAAM,UAAU,SAAS,WAAW,OAAO;CAEjD,MAAM,WACJ,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,QAAD;EAAM,WAAU;EAAhB,UAAA,CACE,qBAAC,QAAD;GAAM,WAAW,GAAG,uBAAuB,UAAU,SAAS,WAAW,aAAa,CAAC;GAAvF,UAAA,CACG,OACA,YACC,oBAAC,QAAD;IAAM,WAAU;IAAqB,OAAM;IAAW,UAAA;GAEhD,CAAA,CAEJ;EACL,CAAA,GAAA,cACC,oBAAC,QAAD;GAAM,WAAU;GAAgD,UAAA;EAAiB,CAAA,CAE/E;CACN,CAAA,GAAA,oBAAC,cAAD;EAAc,OAAO;EAAU;CAAuB,CAAA,CACtD,EAAA,CAAA;CAGJ,OACE,qBAAC,OAAD;EACE,WAAW,GAAG,yBAAyB,KAAK,SAAS;EACrD,gBAAc,WAAW,KAAK,KAAA;EAC9B,iBAAe,WAAW,KAAK,KAAA;EAC/B,GAAK,OAAO,UACR;GAAE,MAAM;GAAS,cAAc;GAAO,oBAAoB;EAAY,IACtE,CAAC;EANP,UAAA;GAUG,OAAO,UACN,oBAAC,SAAD;IAAO,WAAW,GAAG,yBAAyB,GAAG;IAAI,UAAA;GAAgB,CAAA,IAErE;GAGD,eACC,oBAAC,QAAD;IAAM,IAAI;IAAe,WAAU;IAChC,UAAA;GACG,CAAA;GAEP,YACC,oBAAC,QAAD;IAAM,IAAI;IAAS,MAAK;IAAQ,WAAU;IACvC,UAAA;GACG,CAAA;EAEL;;AAET;;;;;;;;AClIA,MAAa,iBAAiB,GAC5B,qFACA,8BACA,4CACA,mDACA,cACF;;;;;;AAOA,MAAa,eAAe;;;;;;AAO5B,SAAgB,mBAA2B;CACzC,OAAO,UAAU,WAAW,GAAG,OAAO,KAAK;AAC7C;;;;;AAMA,SAAgB,MAAM,EAAE,WAAW,oBAAoB,aAAa,GAAG,QAAiC;CACtG,OACE,oBAAC,SAAD;EACE,GAAI,oBAAoB,WAAW;EACnC,GAAI;EACJ,WAAW,GAAG,gBAAgB,iBAAiB,GAAG,SAAS;CAC5D,CAAA;AAEL;;;;;;;AAQA,SAAgB,YAAY,EAC1B,WACA,oBAAoB,aACpB,GAAG,QAMF;CACD,OACE,oBAAC,SAAD;EACE,GAAI,oBAAoB,WAAW;EACnC,GAAI;EACJ,MAAK;EACL,WAAU;EACV,WAAW,GAAG,gBAAgB,iBAAiB,GAAG,0BAA0B,SAAS;CACtF,CAAA;AAEL;;;;;AAMA,SAAgB,SAAS,EACvB,WACA,oBAAoB,aACpB,GAAG,QAC0B;CAC7B,OACE,oBAAC,YAAD;EACE,GAAI,oBAAoB,WAAW;EACnC,GAAI;EACJ,WAAW,GAAG,gBAAgB,oBAAoB,SAAS;CAC5D,CAAA;AAEL;;;;;;;AClFA,SAAgB,MAAM,EACpB,OACA,SACA,WACA,eACA,YAYC;CACD,MAAM,UAAU,WAAW;CAE3B,OACE,qBAAC,WAAD;EAAS,WAAW,GAAG,+CAA+C,SAAS;EAA/E,UAAA,EACI,SAAS,YACT,qBAAC,UAAD;GACE,WAAW,GACT,gEACA,UAAU,SAAS,wBAAwB,qBAAqB,CAClE;GAJF,UAAA,CAME,oBAAC,MAAD;IACE,WAAW,GACT,kCACA,UACE,SACA,0BACA,oDACF,CACF;IAEC,UAAA;GACC,CAAA,GACH,WAAW,oBAAC,OAAD;IAAK,WAAU;IAAsC,UAAA;GAAa,CAAA,CACxE;EAEV,CAAA,GAAA,oBAAC,OAAD;GAAK,WAAW,GAAG,UAAU,SAAS,OAAO,OAAO,GAAG,aAAa;GAAI;EAAc,CAAA,CAC/E;;AAEb;;;;;;;AAQA,SAAgB,QAAQ,EACtB,MACA,OACA,MACA,SACA,WACA,YAcC;CACD,MAAM,UAAU,WAAW;CAE3B,OACE,qBAAC,WAAD;EAAS,WAAW,GAAG,iBAAiB,UAAU,SAAS,SAAS,SAAS,GAAG,SAAS;EAAzF,UAAA,CACE,qBAAC,OAAD;GAAK,WAAW,GAAG,uBAAuB,UAAU,SAAS,WAAW,OAAO,CAAC;GAAhF,UAAA;IACG,SAAS,KAAA,KACR,oBAAC,QAAD;KACE,WAAU;KACV,eAAA;KAEC,UAAA,OAAO,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;IACzB,CAAA;IAER,oBAAC,MAAD;KAAI,WAAU;KAAgD,UAAA;IAAU,CAAA;IACvE,QAAQ,oBAAC,KAAD;KAAG,WAAU;KAAyB,UAAA;IAAQ,CAAA;IACtD,WAAW,oBAAC,OAAD;KAAK,WAAU;KAAmC,UAAA;IAAa,CAAA;GACxE;EACJ,CAAA,GAAA,QACM;;AAEb;AAWA,MAAM,eAAe;;;;;AAMrB,SAAgB,WAAW,EACzB,OACA,MACA,SACA,MACA,aAgBC;CACD,OACE,qBAAC,UAAD;EAAQ,WAAW,GAAG,yBAAyB,SAAS;EAAxD,UAAA;GACG,SACE,WAAW,IAAI,IACd,qBAAC,KAAD;IAAG,MAAM,KAAK;IAAM,WAAW;IAA/B,UAAA,CAA6C,MACxC,KAAK,KACP;GAEH,CAAA,IAAA,qBAAC,MAAD;IAAM,WAAW;IAAjB,UAAA,CACG,MACD,oBAAC,WAAD,EAAA,UAAY,KAAgB,CAAA,CACxB;;GAEV,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,MAAD;KAAI,WAAU;KAAgD,UAAA;IAAU,CAAA,GACvE,WAAW,oBAAC,OAAD;KAAK,WAAU;KAAoC,UAAA;IAAa,CAAA,CACzE;;GACJ,QAAQ,oBAAC,OAAD;IAAK,WAAU;IAA2D,UAAA;GAAU,CAAA;EACvF;;AAEZ;AAEA,SAAS,WAAW,MAAyD;CAC3E,OAAO,CAAC,eAAe,IAAI;AAC7B;AAiBA,MAAM,eAAe;;;;;;;AAQrB,SAAgB,aAAa,EAC3B,OACA,aAMC;CACD,MAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK,UAAU,QAAQ,KAAK,UAAU,KAAA,CAAS;CACpF,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,OACE,oBAAC,MAAD;EACE,WAAW,GACT,+EACA,SACF;EAEC,UAAA,MAAM,KAAK,MAAM,UAIhB,qBAAC,MAAD;GAAgB,WAAU;GAA1B,UAAA;IACG,QAAQ,KACP,oBAAC,QAAD;KAAM,WAAU;KAAiB,eAAA;KAAY,UAAA;IAEvC,CAAA;IAEP,KAAK,SAAS,oBAAC,QAAD;KAAM,WAAU;KAAkB,UAAA,KAAK;IAAY,CAAA;IACjE,KAAK,OACJ,qBAAC,MAAD;KAAM,WAAW;KAAjB,UAAA,CACE,oBAAC,WAAD,EAAA,UAAY,KAAK,KAAgB,CAAA,GAChC,KAAK,KACF;IACJ,CAAA,IAAA,KAAK,OACP,oBAAC,KAAD;KAAG,MAAM,KAAK;KAAM,WAAW;KAC5B,UAAA,KAAK;IACL,CAAA,IAEH,oBAAC,QAAD;KAAM,WAAU;KAAW,UAAA,KAAK;IAAY,CAAA;GAE5C;EAnBK,GAAA,KAmBL,CACL;CACC,CAAA;AAER;;;;;;;;;;;;ACxNA,SAAgB,iBAAiB,EAC/B,OACA,cACA,SACA,eACA,WAAW,OACX,cAAc,WACd,aAgBC;CACD,MAAM,SAAS,MAAM;CACrB,MAAM,YAAY,oBAAoB;CACtC,MAAM,YAAY,UAAU,KAAK,eAAe;CAEhD,OACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,oBAAkB,UAAU;EAC5B,iBAAe,WAAW,KAAK,KAAA;EAC/B,WAAW,GAIT,wHACA,YAAY,cACZ,SACF;EAEC,UAAA,QAAQ,KAAK,WAAW;GACvB,MAAM,UAAU,OAAO,UAAU;GACjC,OACE,qBAAC,SAAD;IAEE,cAAY,UAAU,YAAY;IAClC,WAAW,GAIT,gHACA,mGACA,UACI,iCACA,+BACJ,YAAY,oBACd;IAbF,UAAA,CAeE,oBAAC,SAAD;KACE,MAAK;KACL,MAAM;KACN,OAAO,OAAO;KACL;KACC;KACV,gBAGE,cAAc,OAAO,UAAU,eAAe,KAAK,OAAO,KAAK;KAEjE,WAAU;IACX,CAAA,GACA,OAAO,KACH;GA5BA,GAAA,OAAO,KA4BP;EAEX,CAAC;CACE,CAAA;AAET;;;AC3FA,MAAM,iBAAiB;;;;;;;;;AAsBvB,SAAgB,OAAO,EACrB,OACA,eACA,SACA,cAAc,WACd,YACA,WAAW,OACX,cAAc,WACd,oBAAoB,gBACpB,aAoBC;CACD,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CACtC,MAAM,YAAY,oBAAoB,cAAc;CAEpD,OACE,qBAAC,YAAY,MAAb;EAGS;EACP,gBAAgB,SAAS,cAAc,SAAS,iBAAiB,KAAK,IAAI;EACpE;EACN,cAAc;EACJ;EAPZ,UAAA,CASE,qBAAC,YAAY,SAAb;GACE,cAAY;GACZ,GAAI;GAIJ,UAAU,OAAO,KAAK,KAAA;GACtB,WAAW,GACT,iIACA,4CACA,mDACA,qCACA,gBACA,SACF;GAdF,UAAA,CAgBE,oBAAC,YAAY,OAAb,EAAgC,YAAc,CAAA,GAC9C,oBAAC,YAAY,MAAb;IAAkB,WAAU;IAC1B,UAAA,oBAAC,aAAD,EAAa,WAAU,WAAY,CAAA;GACnB,CAAA,CACC;EAErB,CAAA,GAAA,oBAAC,YAAY,QAAb,EAAA,UACE,oBAAC,YAAY,SAAb;GACE,UAAS;GACT,YAAY;GACZ,WAAW,GACT,sEACA,uEACF;GAEA,UAAA,qBAAC,YAAY,UAAb;IAAsB,WAAU;IAAhC,UAAA,CACG,eAAe,KAAA,KACd,oBAAC,YAAD;KAAY,OAAO;KAAgB,OAAO;KAAY,OAAA;IAAO,CAAA,GAE9D,QAAQ,KAAK,WACZ,oBAAC,YAAD;KAEE,OAAO,OAAO;KACd,OAAO,OAAO;KACd,MAAM,OAAO;KACb,UAAU,OAAO;IAClB,GALM,OAAO,KAKb,CACF,CACmB;;EACH,CAAA,EACH,CAAA,CACJ;;AAEtB;AAEA,SAAS,WAAW,EAClB,OACA,OACA,MACA,UACA,QAAQ,SAOP;CACD,OACE,qBAAC,YAAY,MAAb;EACS;EACP,UAAU,YAAY;EACtB,WAAW,GACT,qHACA,uDACA,8DACA,QAAQ,kBAAkB,SAC5B;EARF,UAAA;GAUE,oBAAC,YAAY,eAAb;IAA2B,WAAU;IACnC,UAAA,oBAAC,OAAD,EAAO,WAAU,WAAY,CAAA;GACJ,CAAA;GAC3B,oBAAC,YAAY,UAAb,EAAA,UAAuB,MAA4B,CAAA;GAClD,QAAQ,oBAAC,QAAD;IAAM,WAAU;IAA4C,UAAA;GAAW,CAAA;EAChE;;AAEtB;;;;;;;;;;AClJA,SAAgB,OAAO,EACrB,OACA,eACA,eACA,MAAM,GACN,MAAM,GACN,OAAO,KACP,SACA,WAAW,OACX,cAAc,WACd,oBAAoB,gBACpB,aAwBC;CACD,MAAM,YAAY,oBAAoB,cAAc;CAEpD,OAGE,qBAAC,OAAD;EAAK,WAAW,GAAG,0CAA0C,SAAS;EAAtE,UAAA,CACE,qBAAC,YAAY,MAAb;GACE,OAAO,CAAC,KAAK;GACb,gBAAgB,CAAC,UAAU,SAAS,KAAA,KAAa,cAAc,IAAI;GACnE,gBAAgB,CAAC,UAAU,SAAS,KAAA,KAAa,gBAAgB,IAAI;GAChE;GACA;GACC;GACI;GACV,WAAW,GACT,wEACA,0BACF;GAXF,UAAA,CAaE,oBAAC,YAAY,OAAb;IAAmB,WAAU;IAC3B,UAAA,oBAAC,YAAY,OAAb,EAAmB,WAAU,4BAA6B,CAAA;GACzC,CAAA,GACnB,oBAAC,YAAY,OAAb;IACE,cAAY;IACZ,GAAI;IACJ,WAAW,GACT,mFACA,mDACA,+BACA,SACF;GACD,CAAA,CACe;EACjB,CAAA,GAAA,WACC,oBAAC,QAAD;GAAM,WAAU;GAAmD,UAAA;EAAc,CAAA,CAEhF;;AAET;;;;;;;;;;AC7DA,SAAgB,MAAW,EACzB,SACA,MACA,QACA,QAAQ,iBACR,SACA,WACA,YACA,aACA,cA6BC;CACD,MAAM,UAAU,WAAW;CAE3B,IAAI,KAAK,WAAW,GAClB,OACE,oBAAC,KAAD;EACE,WAAW,GACT,6BACA,UAAU,SAAS,gBAAgB,cAAc,GACjD,SACF;EAEC,UAAA;CACA,CAAA;CAIP,OACE,oBAAC,OAAD;EAAK,WAAW,GAAG,0BAA0B,SAAS;EACpD,UAAA,qBAAC,SAAD;GAAO,WAAW,GAAG,oBAAoB,UAAU,SAAS,WAAW,aAAa,CAAC;GAArF,UAAA;IACG,WAAW,oBAAC,WAAD;KAAS,WAAU;KAAW,UAAA;IAAiB,CAAA;IAC3D,oBAAC,SAAD,EAAA,UACE,oBAAC,MAAD;KAAI,WAAU;KACX,UAAA,QAAQ,KAAK,WACZ,oBAAC,MAAD;MAEE,OAAM;MACN,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,KAAA;MAChD,WAAW,GACT,6BACA,UAAU,SAAS,qBAAqB,uBAAuB,GAC/D,OAAO,WAAW,YACpB;MAEC,UAAA,OAAO;KACN,GAVG,OAAO,GAUV,CACL;IACC,CAAA,EACC,CAAA;IACP,oBAAC,SAAD,EAAA,UACG,KAAK,KAAK,KAAK,UACd,oBAAC,MAAD;KAEE,cAAY,cAAc,KAAK,KAAK,IAAI,WAAW,KAAA;KACnD,WAAW,GACT,yCACA,cAAc,kCACd,cAAc,KAAK,KAAK,KAAK,cAC/B;KACA,UAAU,aAAa,IAAI,KAAA;KAO3B,gBAAc,cAAc,KAAK,KAAK,IAAI,OAAO,KAAA;KACjD,SAAS,cAAc,UAAU,WAAW,KAAK,OAAO,KAAK,IAAI,KAAA;KACjE,WACE,cACK,UAAU;MACT,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;OAC9C,MAAM,eAAe;OACrB,WAAW,KAAK,OAAO,KAAK;MAC9B;KACF,IACA,KAAA;KAEN,gBAAgB,mBAAmB,WAAW,KAAK,KAAK,IAAI,KAAA;KAC5D,gBAAgB,mBAAmB,WAAW,MAAM,IAAI,IAAI,KAAA;KAE3D,UAAA,QAAQ,KAAK,WACZ,oBAAC,MAAD;MAEE,WAAW,GACT,kCACA,UAAU,SAAS,aAAa,aAAa,GAC7C,OAAO,WAAW,mCACpB;MAEC,UAAA,OAAO,KAAK,GAAG;KACd,GARG,OAAO,GAQV,CACL;IACC,GAzCG,OAAO,KAAK,KAAK,CAyCpB,CACL,EACI,CAAA;GACF;;CACJ,CAAA;AAET;;;;;;;;;AC1IA,SAAgB,OAAO,EACrB,SACA,iBACA,OACA,aACA,WAAW,OACX,aAcC;CACD,MAAM,gBAAgB,MAAM;CAC5B,OACE,qBAAC,SAAD;EAAO,WAAW,GAAG,oCAAoC,SAAS;EAAlE,UAAA,CACE,oBAAC,YAAY,MAAb;GACW;GACQ;GACP;GACV,oBAAkB,cAAc,gBAAgB,KAAA;GAChD,WAAW,GACT,8FACA,qEACA,mDACA,SACF;GAEA,UAAA,oBAAC,YAAY,OAAb,EACE,WAAW,GACT,qFACA,wFACF,EACD,CAAA;EACe,CAAA,GAClB,qBAAC,QAAD;GAAM,WAAU;GAAhB,UAAA,CACE,oBAAC,QAAD;IAAM,WAAU;IAAiC,UAAA;GAAY,CAAA,GAC5D,eACC,oBAAC,QAAD;IAAM,IAAI;IAAe,WAAU;IAChC,UAAA;GACG,CAAA,CAEJ;EACD,CAAA,CAAA;;AAEX;;;;;;;;;AAUA,SAAgB,SAAS,EACvB,SACA,iBACA,SACA,OACA,aACA,WAAW,OACX,cAAc,WACd,aAuBC;CACD,MAAM,gBAAgB,MAAM;CAC5B,MAAM,YAAY,oBAAoB,cAAc,gBAAgB,KAAA,CAAS;CAC7E,MAAM,MACJ,oBAAC,cAAc,MAAf;EACW;EACT,kBAAkB,SAAS,gBAAgB,SAAS,IAAI;EAC/C;EACC;EACV,cAAY;EACZ,GAAI;EACJ,WAAW,GACT,4HACA,qEACA,iFACA,mDACA,WACA,UAAU,KAAA,KAAa,SACzB;EAEA,UAAA,oBAAC,cAAc,WAAf;GAAyB,WAAU;GAChC,UAAA,YAAY,kBACX,oBAAC,OAAD;IAAO,WAAU;IAAS,aAAa;GAAI,CAAA,IAE3C,oBAAC,OAAD;IAAO,WAAU;IAAS,aAAa;GAAI,CAAA;EAEtB,CAAA;CACP,CAAA;CAGtB,IAAI,UAAU,KAAA,GAAW,OAAO;CAEhC,OACE,qBAAC,SAAD;EAAO,WAAW,GAAG,oCAAoC,SAAS;EAAlE,UAAA,CACE,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAU,CAAA,GACpC,qBAAC,QAAD;GAAM,WAAU;GAAhB,UAAA,CACE,oBAAC,QAAD;IAAM,WAAU;IAAiC,UAAA;GAAY,CAAA,GAC5D,eACC,oBAAC,QAAD;IAAM,IAAI;IAAe,WAAU;IAChC,UAAA;GACG,CAAA,CAEJ;EACD,CAAA,CAAA;;AAEX;;;;;;;;;AC1IA,SAAgB,WAAW,EACzB,SACA,iBACA,UACA,QACA,WAAW,OACX,OACA,aAgBC;CACD,OACE,qBAAC,UAAD;EACE,MAAK;EACL,MAAK;EACL,gBAAc;EACd,cAAY,UAAU,YAAY;EACxB;EACH;EACP,eAAe,gBAAgB,CAAC,OAAO;EACvC,WAAW,GACT,iHACA,UACI,yCACA,0DACJ,gFACA,WACA,SACF;EAhBF,UAAA,CAkBG,WAAW,KAAA,KACV,oBAAC,QAAD;GACE,eAAA;GACA,WAAW,GACT,0DACA,UAAU,gBAAgB,YAC5B;GACA,OAAO;IAAE,iBAAiB;IAAQ,aAAa;GAAO;EACvD,CAAA,GAEF,QACK;;AAEZ;;;ACtDA,MAAM,YAAuE;CAC3E,aAAa,OAAO,YAAY,QAAQ,KAAK;CAC7C,YAAY,OAAO,YAAY,QAAQ,IAAI,UAAU;CACrD,YAAY;CACZ,MAAM,GAAG,WAAW,SAAS;AAC/B;;;;;;;;;;;AAYA,SAAgB,KAAwB,EACtC,OACA,QACA,UACA,OACA,UACA,aAcC;CACD,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,KAAK,QAAQ;CAGrD,MAAM,YAAY,QAAQ,MAAM,SAAS,KAAK,OAAO,MAAM,IAAI,SAAS,QAAQ,EAAE,EAAE;CAEpF,MAAM,aAAa,UAAyC;EAC1D,MAAM,OAAO,UAAU,MAAM;EAC7B,MAAM,SAAS,MAAM;EACrB,MAAM,UAAU,QAAQ,WAAW,SAAS,KAAK,OAAO,OAAO,QAAQ,QAAQ;EAC/E,IAAI,SAAS,KAAA,KAAa,YAAY,IAAI;EAC1C,MAAM,eAAe;EACrB,MAAM,OAAO,QAAQ,KAAK,SAAS,QAAQ,MAAM;EACjD,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,OAAO,MAAM,cAAc,iBAA8B,gBAAc;EAC7E,KAAK,MAAM,OAAO,MAAM,IAAI,IAAI,QAAQ,aAAa,KAAK,IAAI,IAAI,MAAM;EACxE,IAAI,KAAK,OAAO,QAAQ,SAAS,KAAK,EAAE;CAC1C;CAEA,OACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,oBAAiB;EACN;EACX,WAAW,GACT,4GACA,SACF;EAEC,UAAA,MAAM,KAAK,SAAS;GACnB,MAAM,WAAW,KAAK,OAAO;GAC7B,OACE,qBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,IAAI,aAAa,KAAA,IAAY,KAAA,IAAY,GAAG,SAAS,OAAO,KAAK;IACjE,iBAAe;IACf,iBACE,aAAa,KAAA,KAAa,WAAW,GAAG,SAAS,SAAS,KAAK,OAAO,KAAA;IAExE,UAAU,KAAK,OAAO,YAAY,IAAI;IACtC,eAAa,KAAK;IAClB,cAAY,WAAW,WAAW;IAClC,UAAU,KAAK;IACf,OAAO,KAAK;IACZ,eAAe,SAAS,KAAK,EAAE;IAC/B,WAAW,GACT,uGACA,mDACA,WACI,iCACA,+BACJ,SACF;IAtBF,UAAA,CAwBG,KAAK,OACL,KAAK,UAAU,KAAA,KAAa,KAAK,QAAQ,KACxC,oBAAC,QAAD;KACE,WAAW,GACT,0DACA,WAAW,6BAA6B,uBAC1C;KAEC,UAAA,KAAK;IACF,CAAA,CAEF;GAlCD,GAAA,KAAK,EAkCJ;EAEZ,CAAC;CACE,CAAA;AAET"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/theme.ts","../src/tone.ts","../src/components/ui/cn.ts","../src/components/ui/Tooltip.tsx","../src/components/ThemeToggle.tsx","../src/components/ui/Badge.tsx","../src/components/ui/Density.tsx","../src/components/ui/Button.tsx","../src/components/ui/Dialog.tsx","../src/components/ui/Disclosure.tsx","../src/components/ui/Feedback.tsx","../src/components/ui/Field.tsx","../src/components/ui/Input.tsx","../src/components/ui/Panel.tsx","../src/components/ui/numberText.ts","../src/components/ui/VectorInput.tsx","../src/components/ui/PoseInput.tsx","../src/components/ui/SegmentedControl.tsx","../src/components/ui/Select.tsx","../src/components/ui/Slider.tsx","../src/components/ui/Table.tsx","../src/components/ui/Toggle.tsx","../src/components/ui/ToggleChip.tsx","../src/components/Tabs.tsx"],"sourcesContent":["/*\n * Which of the two palettes is on screen.\n *\n * Judging an anomaly map, or a measurement overlay drawn over a low-contrast part, against\n * a bright room and against a dark one are different tasks, and a researcher switches\n * between them within a session -- so dark mode is a real, storable choice here rather than\n * only `prefers-color-scheme`.\n *\n * Three states, not two. \"system\" is a real choice and has to survive a reload as itself,\n * otherwise the first time the OS flips at sunset the app disagrees with everything else on\n * the machine and there is no way back to following it.\n *\n * The class is applied to <html> rather than <body> so the page background painted before\n * React mounts is already correct. `applyTheme` (below, as `paint`) has to be duplicated as\n * an inline script in the consumer's `index.html` for exactly that reason -- see the\n * package README for the exact snippet, and keep it in agreement with this module if either\n * changes.\n */\n\n/**\n * A stored theme preference: a palette, or `system` to follow the OS\n * (`prefers-color-scheme`) and keep following it.\n */\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\n/**\n * The `localStorage` key a consumer's theme choice is kept under.\n *\n * Every function here takes it as an optional last argument and defaults to this constant,\n * so a single app needs to pass nothing, while two apps sharing a browser profile -- or one\n * app that wants a namespaced key -- can each choose their own without stepping on the\n * other's stored preference.\n */\nexport const DEFAULT_THEME_STORAGE_KEY = \"vitavision-theme\";\n\n// Every DOM access here is inside a function, and guarded: this module is imported on the\n// server too.\nconst query = () =>\n typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n ? window.matchMedia(\"(prefers-color-scheme: dark)\")\n : null;\n\n/**\n * The stored preference.\n *\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n * @returns The stored choice, or `system` when nothing valid is stored or storage cannot be\n * read (private browsing, the server).\n */\nexport function readThemeChoice(storageKey: string = DEFAULT_THEME_STORAGE_KEY): ThemeChoice {\n try {\n const stored = window.localStorage.getItem(storageKey);\n if (stored === \"light\" || stored === \"dark\" || stored === \"system\") return stored;\n } catch {\n // Private browsing, or a WebView with storage disabled. Following the OS is a fine\n // answer to \"I could not read your preference\", and better than refusing to render.\n }\n return \"system\";\n}\n\n/**\n * What `choice` actually resolves to right now.\n *\n * @param choice - The preference.\n * @returns `choice` itself for `light`/`dark`; for `system`, the OS palette (`light` where\n * it cannot be queried).\n */\nexport function resolveTheme(choice: ThemeChoice): \"light\" | \"dark\" {\n if (choice !== \"system\") return choice;\n return query()?.matches ? \"dark\" : \"light\";\n}\n\nfunction paint(choice: ThemeChoice): void {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"dark\", resolveTheme(choice) === \"dark\");\n}\n\n/**\n * Store a preference and paint it (the `dark` class on `<html>`).\n *\n * @param choice - The new preference.\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n */\nexport function setThemeChoice(\n choice: ThemeChoice,\n storageKey: string = DEFAULT_THEME_STORAGE_KEY,\n): void {\n try {\n window.localStorage.setItem(storageKey, choice);\n } catch {\n // Not worth failing the click over; the choice still applies for this session.\n }\n paint(choice);\n}\n\n/**\n * Paint the stored choice and keep following the OS while the choice is \"system\".\n *\n * @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.\n * @returns An unsubscribe, so a test can tear the listener down; a long-lived app never does.\n */\nexport function initTheme(storageKey: string = DEFAULT_THEME_STORAGE_KEY): () => void {\n paint(readThemeChoice(storageKey));\n\n const media = query();\n if (media === null) return () => {};\n\n const onSystemChange = () => {\n if (readThemeChoice(storageKey) === \"system\") paint(\"system\");\n };\n media.addEventListener(\"change\", onSystemChange);\n return () => media.removeEventListener(\"change\", onSystemChange);\n}\n","/*\n * The verdict vocabulary, for the components that paint outside of `className`.\n *\n * Every Tailwind-driven primitive under `components/ui` reaches the palette through a\n * class name, which only works where Tailwind's own build sees the class. `MeasureOverlay`\n * and `LineProfile` paint through raw SVG `stroke`/`fill` attributes instead — a caliper\n * box or an edge mark is drawn from data the consumer supplies at runtime, not from a\n * fixed set of class names a bundler can scan for. So these two read the same five tokens\n * `styles.css` defines (`--signal`, `--normal`, `--defect`, `--warn`, `--fg-subtle`)\n * directly as CSS custom properties, which resolves correctly in both themes without\n * needing Tailwind involved in the overlay's own render path at all.\n */\n\n/**\n * A paint for SVG overlays and charts: the accent (`signal`), a verdict colour, or `muted`.\n *\n * Named `MeasureTone` rather than `Tone` — `components/ui/Badge.tsx` already owns that name\n * for the verdict-badge vocabulary (`neutral`/`normal`/`defect`/`unlabeled`/`warning`/\n * `info`), and both are re-exported from the package root, where two types of the same name\n * would collide.\n */\nexport type MeasureTone = \"signal\" | \"normal\" | \"defect\" | \"warn\" | \"muted\";\n\nconst TONE_VAR: Record<MeasureTone, string> = {\n signal: \"var(--signal)\",\n normal: \"var(--normal)\",\n defect: \"var(--defect)\",\n warn: \"var(--warn)\",\n muted: \"var(--fg-subtle)\",\n};\n\n/**\n * The CSS paint value for a tone — usable directly as a `stroke`, `fill` or `color`. It is a\n * `var(--token)` reference, so it follows the theme without a re-render.\n *\n * @param tone - The tone; `undefined` falls back.\n * @param fallback - The tone used when `tone` is `undefined`. Defaults to `signal`.\n * @returns A `var(--…)` CSS value.\n */\nexport function toneColor(tone: MeasureTone | undefined, fallback: MeasureTone = \"signal\"): string {\n return TONE_VAR[tone ?? fallback];\n}\n","/**\n * Class composition, and the shared focus treatment.\n *\n * A naive primitive *appends* the caller's `className` to its own base string, so\n * `<Button className=\"px-6\">` produces `px-3 … px-6` and wins or loses on source order\n * rather than on intent. `twMerge` resolves that by conflict group: the caller's `px-6`\n * replaces the base `px-3` and leaves everything else alone.\n */\n\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Compose class names: `clsx` for the conditionals, then `tailwind-merge` so a later class\n * replaces an earlier one of the same conflict group (the caller's `px-6` beats the base\n * `px-3`). Every primitive merges its caller's `className` through this.\n *\n * @param inputs - Class values: strings, arrays, and `{ class: condition }` records.\n * @returns One class string with conflicts resolved.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n\n/**\n * One focus treatment for every interactive element in the design system.\n *\n * `outline` rather than `ring` because an outline follows the element's own border-radius\n * and needs no offset *colour* -- a ring offset has to know which surface it sits on, and\n * these primitives are used on `ground`, `surface` and `raised` alike.\n *\n * `focus-visible` rather than `focus`, so a mouse click does not leave a ring behind.\n */\nexport const focusRing =\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal\";\n\n/**\n * The focus treatment for controls whose own box is the hit target, where an offset ring\n * would clip: the same outline as {@link focusRing}, drawn with no offset.\n */\nexport const focusRingInset = \"focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-signal\";\n","/*\n * A short explanation, on demand.\n *\n * Replaces the native `title` attribute, which appears after an unpredictable delay, in the\n * OS palette, at a size nobody chose, and never at all for a keyboard user.\n *\n * A tooltip is for text that helps but is not needed to operate the control. Anything a\n * reader must have in order to answer the question belongs in the field's description,\n * where it is always visible.\n */\n\nimport * as RadixTooltip from \"@radix-ui/react-tooltip\";\nimport { HelpCircle, type LucideIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * Shares one open delay (200 ms) across the tooltips below it. Mount it once near the root:\n * `Tooltip` and `InfoHint` need it above them.\n */\nexport function TooltipProvider({\n children,\n}: {\n /** The app, or the region that uses tooltips. */\n children: ReactNode;\n}) {\n return <RadixTooltip.Provider delayDuration={200}>{children}</RadixTooltip.Provider>;\n}\n\n/**\n * A short explanation on hover or focus of its child, for text that helps but is not needed\n * to operate the control (that belongs in a field's description).\n *\n * The child must be one focusable element that accepts a ref (Radix `asChild`). Radix\n * exposes `data-state` (`closed`/`delayed-open`/`instant-open`) on it.\n */\nexport function Tooltip({\n content,\n children,\n}: {\n /** The explanation. */\n content: ReactNode;\n /** The one element it explains. */\n children: ReactNode;\n}) {\n return (\n <RadixTooltip.Root>\n <RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>\n <RadixTooltip.Portal>\n <RadixTooltip.Content\n sideOffset={6}\n collisionPadding={8}\n className={cn(\n \"z-50 max-w-xs rounded-control border border-line bg-overlay px-2.5 py-1.5\",\n \"text-xs leading-snug text-fg shadow-lg shadow-black/25\",\n )}\n >\n {content}\n <RadixTooltip.Arrow className=\"fill-overlay\" />\n </RadixTooltip.Content>\n </RadixTooltip.Portal>\n </RadixTooltip.Root>\n );\n}\n\n/**\n * The standard affordance: a quiet mark that yields the explanation on hover or focus.\n *\n * `icon` exists because the mark answers two different questions. The default `?` offers\n * help with a control; `Info` offers *facts about the thing on screen* -- where a dataset\n * came from, which method produced a measurement -- which the tone demotes here rather\n * than spending a permanent line on. Reading them as the same affordance would be a small\n * lie.\n */\nexport function InfoHint({\n children,\n label = \"More information\",\n icon: Icon = HelpCircle,\n}: {\n /** The explanation shown on hover or focus. */\n children: ReactNode;\n /** The button's accessible name. Defaults to \"More information\". */\n label?: string | undefined;\n /** The mark: `HelpCircle` (the default) for help, `Info` for facts about the thing. */\n icon?: LucideIcon | undefined;\n}) {\n return (\n <Tooltip content={children}>\n <button\n type=\"button\"\n aria-label={label}\n className={cn(\"rounded-full text-fg-subtle transition-colors hover:text-fg\", focusRing)}\n >\n <Icon className=\"size-3.5\" />\n </button>\n </Tooltip>\n );\n}\n","/*\n * The palette switch, shared by every lab app.\n *\n * Three states, not two, and cycled by one button rather than spread across a\n * segmented control: this sits in a header next to the thing the app is\n * actually about, and a control that costs one glyph is the right size for a\n * preference that is set once a session. \"System\" is a real state — an app that\n * silently stops following the OS the first time you touch the switch has taken\n * something away.\n *\n * Lived in visual-anomaly-lab's own `App.tsx` first. It moved here the moment a\n * second app needed it, rather than being copied: two copies of a control that\n * writes to `localStorage` is two chances to disagree about the key, the cycle\n * order, or which icon means which state.\n *\n * `storageKey` is per app on purpose — `theme.ts` takes the key as an argument\n * for the same reason. Two apps sharing a browser profile each keep their own\n * preference, and an app that already had one under its own key does not lose\n * it by adopting this component.\n */\n\nimport { Moon, Sun, SunMoon } from \"lucide-react\";\nimport { useState, useSyncExternalStore } from \"react\";\n\nimport { DEFAULT_THEME_STORAGE_KEY, readThemeChoice, setThemeChoice, type ThemeChoice } from \"../theme\";\nimport { Tooltip } from \"./ui/Tooltip\";\nimport { cn, focusRing } from \"./ui/cn\";\n\nconst ORDER: ThemeChoice[] = [\"system\", \"light\", \"dark\"];\nconst ICON = { system: SunMoon, light: Sun, dark: Moon };\nconst LABEL: Record<ThemeChoice, string> = {\n system: \"Theme: following the system\",\n light: \"Theme: light\",\n dark: \"Theme: dark\",\n};\n\n/** Another tab (or window) changing the stored choice re-renders the toggle. */\nfunction subscribe(onChange: () => void): () => void {\n window.addEventListener(\"storage\", onChange);\n return () => window.removeEventListener(\"storage\", onChange);\n}\n\n/** What the server, and a hydrating client's first render, show: no stored choice read yet. */\nconst serverChoice = (): ThemeChoice => \"system\";\n\n/**\n * The palette switch: one button cycling system → light → dark, stored in `localStorage`\n * under `storageKey` and painted as the `dark` class on `<html>`.\n *\n * Its `aria-label` (repeated in a tooltip) names the current state; it needs a\n * `TooltipProvider` above it. The stored choice is read as an external store, so the server\n * render and the hydrating first client render agree (\"system\") and the stored choice\n * follows without a mismatch. The state is exposed as `data-theme-choice`.\n */\nexport function ThemeToggle({\n storageKey = DEFAULT_THEME_STORAGE_KEY,\n className,\n}: {\n /** The `localStorage` key; per app, so two apps sharing a profile keep their own choice. */\n storageKey?: string | undefined;\n /** Merged with the button's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const stored = useSyncExternalStore(subscribe, () => readThemeChoice(storageKey), serverChoice);\n // What this toggle last set, so it keeps cycling even where storage is unavailable and the\n // stored value cannot follow.\n const [chosen, setChosen] = useState<{ key: string; choice: ThemeChoice } | null>(null);\n const choice = chosen?.key === storageKey ? chosen.choice : stored;\n\n const advance = () => {\n const next = ORDER[(ORDER.indexOf(choice) + 1) % ORDER.length]!;\n setChosen({ key: storageKey, choice: next });\n setThemeChoice(next, storageKey);\n };\n\n const Icon = ICON[choice];\n\n return (\n <Tooltip content={LABEL[choice]}>\n <button\n type=\"button\"\n onClick={advance}\n aria-label={LABEL[choice]}\n data-theme-choice={choice}\n className={cn(\n \"rounded-control p-1.5 text-fg-muted transition-colors hover:bg-raised hover:text-fg\",\n focusRing,\n className,\n )}\n >\n <Icon className=\"size-4\" />\n </button>\n </Tooltip>\n );\n}\n","/*\n * Status colour, in one place.\n *\n * The tone vocabulary is deliberately small and deliberately *about verdicts*. The chrome\n * carries no saturation, so a colour on screen means something: green is a normal sample\n * (or an in-tolerance measurement), red is a defect (or an out-of-tolerance one), amber is\n * a caveat. `info` is the one tone that borrows the interaction accent, and it is reserved\n * for statements of fact about a method -- \"produces anomaly maps\", \"caliper axis fixed\" --\n * rather than for anything a reader must act on.\n */\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/**\n * The badge and status-dot vocabulary: the verdicts (`normal`, `defect`, `warning`),\n * `unlabeled` for a sample with no verdict yet, `info` for a statement of fact about a\n * method, and `neutral` for everything else.\n */\nexport type Tone = \"neutral\" | \"normal\" | \"defect\" | \"unlabeled\" | \"warning\" | \"info\";\n\nconst TONE_CLASSES: Record<Tone, string> = {\n neutral: \"bg-raised text-fg-muted ring-line\",\n normal: \"bg-normal/12 text-normal ring-normal/25\",\n defect: \"bg-defect/12 text-defect ring-defect/25\",\n unlabeled: \"bg-raised text-fg-subtle ring-line\",\n warning: \"bg-warn/12 text-warn ring-warn/25\",\n info: \"bg-signal/12 text-signal ring-signal/25\",\n};\n\nconst DOT_CLASSES: Record<Tone, string> = {\n neutral: \"bg-fg-subtle\",\n normal: \"bg-normal\",\n defect: \"bg-defect\",\n unlabeled: \"bg-line-strong\",\n warning: \"bg-warn\",\n info: \"bg-signal\",\n};\n\n/**\n * A short label in a tone: a verdict, a status, a count.\n *\n * The tone is exposed as `data-tone`, so a stylesheet or a test can read it without parsing\n * class names.\n */\nexport function Badge({\n tone = \"neutral\",\n className,\n children,\n}: {\n /** Which colour, by meaning. Defaults to `neutral`. */\n tone?: Tone | undefined;\n /** Merged with the badge's own classes through `cn`. */\n className?: string | undefined;\n /** The label. */\n children: ReactNode;\n}) {\n return (\n <span\n data-tone={tone}\n className={cn(\n \"inline-flex items-center gap-1 rounded-control px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset\",\n TONE_CLASSES[tone],\n className,\n )}\n >\n {children}\n </span>\n );\n}\n\n/**\n * A status as a dot and a word.\n *\n * The mapping from a domain status to a tone belongs to each caller -- only it knows\n * whether \"running\" is good news -- but the *rendering* of the answer does not.\n */\nexport function StatusDot({\n tone,\n className,\n children,\n}: {\n /** The status, as a tone; also exposed as `data-tone`. */\n tone: Tone;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n /** The word beside the dot. The dot alone is decoration: say the status in words. */\n children?: ReactNode;\n}) {\n return (\n <span\n data-tone={tone}\n className={cn(\"inline-flex items-center gap-1.5 text-xs text-fg-muted\", className)}\n >\n <span className={cn(\"size-1.5 shrink-0 rounded-full\", DOT_CLASSES[tone])} aria-hidden />\n {children}\n </span>\n );\n}\n\n/** Counts as a compact `12 normal · 9 defect` run of badges, skipping the zeroes. */\nexport function CountRun({\n counts,\n className,\n}: {\n /** `[name, count, tone]` triples, in display order. Zero counts are dropped. */\n counts: [string, number, Tone][];\n /** Merged with the run's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const shown = counts.filter(([, value]) => value > 0);\n if (shown.length === 0) {\n return <span className={cn(\"text-xs text-fg-subtle\", className)}>empty</span>;\n }\n return (\n <span className={cn(\"flex flex-wrap items-center gap-1.5\", className)}>\n {shown.map(([name, value, tone]) => (\n <Badge key={name} tone={tone}>\n <span className=\"font-mono\">{value}</span> {name}\n </Badge>\n ))}\n </span>\n );\n}\n","/*\n * How much room the chrome takes, as one decision per region rather than per component.\n *\n * The default spacing is written for a page: a form you read through once, where generous\n * padding is what makes it legible. An inspector column beside a canvas is the opposite\n * problem — it is a permanent surface that accumulates tools, and there the same padding is\n * simply less of the instrument on screen. Nesting compounds it: a panel body's `p-4`\n * inside a section's `gap-3` inside a column's own padding is three margins deep before any\n * control appears.\n *\n * `compact` is not \"the same design, smaller\". It drops the leading and the padding a\n * reading surface needs and keeps the type sizes and hit targets a control needs, so a\n * dense column stays clickable. Below this there is no honest saving left — the next step\n * down is a control too small to hit.\n *\n * The default is `comfortable`, so a consumer that never mentions density gets exactly what\n * it had.\n */\n\nimport { createContext, use } from \"react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * How much room the chrome takes: `comfortable` (the default, for a page you read through)\n * or `compact` (for a permanent tool surface such as an inspector column).\n */\nexport type Density = \"comfortable\" | \"compact\";\n\nconst DensityContext = createContext<Density>(\"comfortable\");\n\n/**\n * The density in force here. Every primitive that has two spacings reads this.\n *\n * @returns The value of the nearest `DensityProvider`, or `comfortable` outside one.\n */\nexport function useDensity(): Density {\n return use(DensityContext);\n}\n\n/**\n * Pick one of two values by the density in force — the primitives' own idiom.\n *\n * @param density - The density in force, from `useDensity()`.\n * @param comfortable - The value for `comfortable`.\n * @param compact - The value for `compact`.\n * @returns `compact` when the density is compact, else `comfortable`.\n */\nexport function byDensity<T>(density: Density, comfortable: T, compact: T): T {\n return density === \"compact\" ? compact : comfortable;\n}\n\n/**\n * Sets the density for a region: every primitive below it reads the value through\n * `useDensity`. Nest providers to switch a sub-region back.\n */\nexport function DensityProvider({\n value,\n children,\n}: {\n /** The density for everything below. */\n value: Density;\n /** The region. */\n children: ReactNode;\n}) {\n return <DensityContext value={value}>{children}</DensityContext>;\n}\n","import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { Loader2 } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A button's look, by what the action is: `primary` for the one action a screen is for,\n * `secondary` for the rest, `ghost` for toolbar actions, `danger` for an action that\n * destroys something.\n */\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n\n/** A button's height and padding. `md` is the comfortable density's size, `sm` the compact one's. */\nexport type ButtonSize = \"sm\" | \"md\";\n\nconst VARIANT_CLASSES: Record<ButtonVariant, string> = {\n primary: \"bg-signal text-signal-fg hover:bg-signal-strong\",\n secondary: \"bg-surface text-fg ring-1 ring-inset ring-line-strong hover:bg-raised\",\n ghost: \"text-fg-muted hover:bg-raised hover:text-fg\",\n // Soft rather than a solid red block. The verdict red is also the colour of a defect\n // badge, and a filled button of it reads as an alarm on a screen that is mostly grey.\n // Genuinely destructive actions are behind a confirmation dialog regardless. The hover\n // tint stops at /15 so the red label keeps 4.5:1 on it.\n danger: \"bg-defect/10 text-defect ring-1 ring-inset ring-defect/30 hover:bg-defect/15\",\n};\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n sm: \"h-7 gap-1.5 px-2.5 text-xs\",\n md: \"h-8 gap-2 px-3 text-sm\",\n};\n\n/**\n * The classes that make a control look like a button, for the one element that has to be\n * something else. Prefer `Button` and `ButtonLink`; this exists so a third shape — a\n * `<label>` wrapping a file input, say — does not have to copy the variant table.\n *\n * @param options - The variant (default `secondary`), the size (default `md`) and the\n * caller's own classes, merged last.\n * @returns The class string.\n */\nexport function buttonClasses({\n variant = \"secondary\",\n size = \"md\",\n className,\n}: {\n variant?: ButtonVariant | undefined;\n size?: ButtonSize | undefined;\n className?: string | undefined;\n} = {}): string {\n return cn(\n \"inline-flex shrink-0 items-center justify-center rounded-control font-medium transition-colors\",\n \"disabled:cursor-not-allowed disabled:opacity-45\",\n SIZE_CLASSES[size],\n VARIANT_CLASSES[variant],\n focusRing,\n className,\n );\n}\n\nfunction ButtonIcon({ icon }: { icon: ReactNode }) {\n return (\n <span className=\"shrink-0 [&>svg]:size-3.5\" aria-hidden>\n {icon}\n </span>\n );\n}\n\n/**\n * An action. `asChild` renders the button's look onto the single child element instead of a\n * `<button>` — how a router's link becomes a button-shaped navigation without this package\n * knowing about routers: `<Button asChild><Link to=\"/runs\">Runs</Link></Button>`.\n * For a plain URL, `ButtonLink` is the shorter spelling.\n *\n * Takes every `<button>` prop, `ref` included. State is exposed as `data-variant` and, while\n * `loading`, `data-loading` (plus `aria-busy`).\n */\nexport function Button({\n variant = \"secondary\",\n size,\n type = \"button\",\n loading = false,\n icon,\n asChild = false,\n className,\n children,\n disabled,\n ...rest\n}: ComponentProps<\"button\"> & {\n /** What kind of action this is. Defaults to `secondary`. */\n variant?: ButtonVariant | undefined;\n /** Defaults to the density in force — `md` comfortable, `sm` compact. */\n size?: ButtonSize | undefined;\n /** Swaps the icon for a spinner and blocks the click, without the label changing. */\n loading?: boolean | undefined;\n /** A leading icon, decorative (`aria-hidden`): the label carries the meaning. */\n icon?: ReactNode;\n /**\n * Render onto the one child element (Radix `Slot`) rather than a `<button>`. The child\n * keeps its own semantics — a link stays a link — so `type`, `disabled` and `loading` do\n * not apply.\n */\n asChild?: boolean | undefined;\n}) {\n const density = useDensity();\n const resolved = size ?? byDensity(density, \"md\", \"sm\");\n\n if (asChild) {\n return (\n <Slot\n {...rest}\n data-variant={variant}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {icon && <ButtonIcon icon={icon} />}\n <Slottable>{children}</Slottable>\n </Slot>\n );\n }\n\n return (\n <button\n type={type}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n data-variant={variant}\n data-loading={loading ? \"\" : undefined}\n {...rest}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : (\n icon && <ButtonIcon icon={icon} />\n )}\n {children}\n </button>\n );\n}\n\n/**\n * A navigation that looks like a button: one `<a>`, styled by the same variants and density.\n *\n * The alternative every consumer reached for was `<Link><Button/></Link>` — a button inside\n * an anchor, which is invalid HTML, gives a keyboard two tab stops for one action, and makes\n * a screen reader announce a button that navigates. There is no `disabled` or `loading`:\n * a link that cannot be followed should not be rendered as one, and a navigation has no\n * pending state of its own.\n *\n * It renders a plain `<a href>`, and takes every `<a>` prop, `ref` included. For a router's\n * link, pass it as the only child with `asChild` —\n * `<ButtonLink asChild><Link to=\"/runs\">Runs</Link></ButtonLink>` — so client-side\n * navigation keeps working and this package still never imports a router.\n */\nexport function ButtonLink({\n variant = \"secondary\",\n size,\n icon,\n asChild = false,\n className,\n children,\n ...rest\n}: ComponentProps<\"a\"> & {\n /** What kind of action this is. Defaults to `secondary`. */\n variant?: ButtonVariant | undefined;\n /** Defaults to the density in force — `md` comfortable, `sm` compact. */\n size?: ButtonSize | undefined;\n /** A leading icon, decorative (`aria-hidden`): the label carries the meaning. */\n icon?: ReactNode;\n /** Render onto the one child element — a router's `<Link>` — instead of an `<a>`. */\n asChild?: boolean | undefined;\n}) {\n const density = useDensity();\n const resolved = size ?? byDensity(density, \"md\", \"sm\");\n const Comp = asChild ? Slot : \"a\";\n\n return (\n <Comp\n {...rest}\n data-variant={variant}\n className={buttonClasses({ variant, size: resolved, className })}\n >\n {icon && <ButtonIcon icon={icon} />}\n <Slottable>{children}</Slottable>\n </Comp>\n );\n}\n","/*\n * Confirmation for the things that cannot be undone, and a shell for the things that need\n * a form.\n *\n * `Dialog` is a modal with no verdict of its own: a title, a body the caller fills, and a\n * footer it chooses. `ConfirmDialog` is one arrangement of it -- the destructive-action\n * confirmation every app in this family needs at least once (deleting a dataset, discarding\n * an in-progress measurement session) -- rather than a second copy of the overlay, the\n * centring and the width cap.\n */\n\nimport * as RadixDialog from \"@radix-ui/react-dialog\";\nimport { useCallback, useState, type ReactNode } from \"react\";\n\nimport { Button } from \"./Button\";\nimport { cn, focusRingInset } from \"./cn\";\n\n/**\n * Whether the body overflows its height cap, kept current as the dialog or its content\n * resizes. A scrolling body has to be reachable by keyboard (so it can be scrolled without\n * a pointer); a body that fits must not add a tab stop.\n */\nfunction useOverflowing(): [\n (node: HTMLDivElement | null) => (() => void) | undefined,\n boolean,\n] {\n const [overflowing, setOverflowing] = useState(false);\n const ref = useCallback((node: HTMLDivElement | null) => {\n if (node === null) return undefined;\n const measure = () => setOverflowing(node.scrollHeight > node.clientHeight + 1);\n measure();\n if (typeof ResizeObserver === \"undefined\") return undefined;\n const observer = new ResizeObserver(measure);\n observer.observe(node);\n // The body is capped, so once it hits the cap only the content keeps changing size.\n if (node.firstElementChild) observer.observe(node.firstElementChild);\n return () => observer.disconnect();\n }, []);\n return [ref, overflowing];\n}\n\n/**\n * A modal: a title, a body the caller fills, and a footer it chooses.\n *\n * Controlled — the caller owns `open`. The body scrolls under a height cap so the footer\n * stays on screen; while it overflows it is a keyboard tab stop (with the focus ring) and\n * carries `data-overflowing`, so it can be scrolled without a pointer.\n */\nexport function Dialog({\n open,\n onOpenChange,\n title,\n description,\n footer,\n className,\n children,\n}: {\n /** Whether the dialog is shown. */\n open: boolean;\n /** Called with `false` on Escape, an overlay click or a `DialogClose`. */\n onOpenChange: (open: boolean) => void;\n /** Names the dialog (`aria-labelledby`). */\n title: string;\n /** Optional: what the reader needs before they can answer, not decoration. */\n description?: ReactNode;\n /** The action row, right-aligned below the body. */\n footer?: ReactNode;\n /** Merged with the dialog panel's own classes through `cn`. */\n className?: string | undefined;\n /** The body. */\n children?: ReactNode;\n}) {\n const [bodyRef, overflowing] = useOverflowing();\n\n return (\n <RadixDialog.Root open={open} onOpenChange={onOpenChange}>\n <RadixDialog.Portal>\n <RadixDialog.Overlay className=\"fixed inset-0 z-50 bg-black/50 backdrop-blur-[1px]\" />\n <RadixDialog.Content\n className={cn(\n \"fixed top-1/2 left-1/2 z-50 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2\",\n // A ceiling and a column, so a dialog that lists things keeps its footer on\n // screen and scrolls its body instead of growing past the viewport. Every\n // short dialog is far below the cap and is unaffected. This is a scroller\n // inside an overlay, not a second page scroller.\n \"flex max-h-[calc(100vh-4rem)] flex-col\",\n \"rounded-panel border border-line bg-overlay p-5 shadow-xl shadow-black/40\",\n \"focus:outline-none\",\n className,\n )}\n >\n <RadixDialog.Title className=\"text-sm font-semibold tracking-tight text-fg\">\n {title}\n </RadixDialog.Title>\n {/* Radix warns when a dialog has no description; render an empty one rather than\n inventing prose for a form whose fields already carry their own labels. */}\n <RadixDialog.Description\n className={cn(\n \"mt-2 text-sm leading-relaxed text-fg-muted\",\n description === undefined && \"sr-only\",\n )}\n >\n {description ?? title}\n </RadixDialog.Description>\n <div\n ref={bodyRef}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing ? \"\" : undefined}\n className={cn(\"min-h-0 overflow-y-auto\", focusRingInset)}\n >\n <div>{children}</div>\n </div>\n {footer && <div className=\"mt-5 flex shrink-0 justify-end gap-2\">{footer}</div>}\n </RadixDialog.Content>\n </RadixDialog.Portal>\n </RadixDialog.Root>\n );\n}\n\n/**\n * The destructive-action confirmation: a `Dialog` whose footer is Cancel and one confirm\n * button. Say in `description` what will actually happen.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n confirmLabel,\n onConfirm,\n loading = false,\n disabled = false,\n destructive = false,\n className,\n}: {\n /** Whether the dialog is shown. */\n open: boolean;\n /** Called with `false` on Cancel, Escape or an overlay click. */\n onOpenChange: (open: boolean) => void;\n /** The question, e.g. \"Delete dataset?\". */\n title: string;\n /** Say what will actually happen, naming the thing by the name the reader gave it. */\n description: ReactNode;\n /** The confirm button's label: the verb, e.g. \"Delete\". */\n confirmLabel: string;\n /** Called on confirm. The dialog stays open; close it (or set `loading`) yourself. */\n onConfirm: () => void;\n /** Shows the confirm button's spinner and blocks it while the action runs. */\n loading?: boolean;\n /** Blocks the confirm button. */\n disabled?: boolean;\n /** Renders the confirm button as `danger` rather than `primary`. */\n destructive?: boolean;\n /** Merged with the dialog panel's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <Dialog\n open={open}\n onOpenChange={onOpenChange}\n title={title}\n description={description}\n className={className}\n footer={\n <>\n <RadixDialog.Close asChild>\n <Button variant=\"ghost\">Cancel</Button>\n </RadixDialog.Close>\n <Button\n variant={destructive ? \"danger\" : \"primary\"}\n loading={loading}\n disabled={disabled}\n onClick={onConfirm}\n >\n {confirmLabel}\n </Button>\n </>\n }\n />\n );\n}\n\n/**\n * The dialog's own dismiss, for a footer the caller builds: wrap a button in it with\n * `asChild` — `<DialogClose asChild><Button>Done</Button></DialogClose>`.\n */\nexport const DialogClose = RadixDialog.Close;\n","/*\n * Content folded away, with a caret we draw ourselves.\n *\n * The base layer (`styles.css`) removes the native `<details>` marker; this supplies one\n * that rotates with the element's own open state, so no JavaScript is involved in the\n * animation.\n *\n * `<details>` rather than a headless package on purpose: the element already has the\n * semantics, the keyboard behaviour and the find-in-page integration.\n */\n\nimport { ChevronRight } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/**\n * Content folded away behind a summary line, on a native `<details>`, with a caret that\n * rotates with the element's own `open` state (no JavaScript involved).\n *\n * Uncontrolled: `defaultOpen` sets the initial state and the element owns it after that.\n */\nexport function Disclosure({\n summary,\n count,\n defaultOpen = false,\n className,\n children,\n}: {\n /** The always-visible line that toggles the content. */\n summary: ReactNode;\n /** How many things are inside, where the number is the reason to open it. */\n count?: number;\n /** Whether it starts open. */\n defaultOpen?: boolean;\n /** Merged with the `<details>` element's own classes through `cn`. */\n className?: string | undefined;\n /** The folded content. */\n children: ReactNode;\n}) {\n return (\n <details open={defaultOpen} className={cn(\"group\", className)}>\n <summary\n className={cn(\n \"flex cursor-pointer items-center gap-1.5 rounded-control py-1 text-xs font-medium text-fg-muted\",\n \"transition-colors hover:text-fg\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal\",\n )}\n >\n <ChevronRight\n className=\"size-3.5 shrink-0 transition-transform group-open:rotate-90\"\n aria-hidden\n />\n {summary}\n {count !== undefined && (\n <span className=\"font-mono text-fg-subtle tabular-nums\">{count}</span>\n )}\n </summary>\n <div className=\"pt-3\">{children}</div>\n </details>\n );\n}\n","/*\n * What the screen says when it has something other than data to show.\n *\n * An error explains what happened and what to do about it; it does not apologise and it is\n * never vague. An empty state is an invitation to act, so it carries the action rather than\n * describing it. And loading is a shape, not the word \"Loading…\" -- a skeleton or a\n * progress bar keeps a slow request from looking identical to a broken one.\n */\n\nimport { AlertTriangle, CheckCircle2, Info, XCircle } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"./cn\";\n\n/** A callout's kind: a fact (`info`), a caveat (`warning`), a failure (`error`) or `success`. */\nexport type CalloutTone = \"info\" | \"warning\" | \"error\" | \"success\";\n\nconst CALLOUT: Record<CalloutTone, { classes: string; Icon: typeof Info }> = {\n info: { classes: \"border-signal/30 bg-signal/8 text-fg\", Icon: Info },\n warning: { classes: \"border-warn/30 bg-warn/8 text-fg\", Icon: AlertTriangle },\n error: { classes: \"border-defect/30 bg-defect/8 text-fg\", Icon: XCircle },\n success: { classes: \"border-normal/30 bg-normal/8 text-fg\", Icon: CheckCircle2 },\n};\n\nconst ICON_TONE: Record<CalloutTone, string> = {\n info: \"text-signal\",\n warning: \"text-warn\",\n error: \"text-defect\",\n success: \"text-normal\",\n};\n\n/**\n * A boxed message with a tone icon, an optional title and optional actions.\n *\n * An `error` callout is a `role=\"alert\"` (announced at once); the others are\n * `role=\"status\"`. The tone is exposed as `data-tone`.\n */\nexport function Callout({\n tone = \"info\",\n title,\n actions,\n className,\n children,\n}: {\n /** Defaults to `info`. */\n tone?: CalloutTone | undefined;\n /** A one-line summary above the message. */\n title?: ReactNode;\n /** Buttons for what to do about it, below the message. */\n actions?: ReactNode;\n /** Merged with the callout's own classes through `cn`. */\n className?: string | undefined;\n /** The message: what happened and what to do about it. */\n children: ReactNode;\n}) {\n const { classes, Icon } = CALLOUT[tone];\n return (\n <div\n role={tone === \"error\" ? \"alert\" : \"status\"}\n data-tone={tone}\n className={cn(\n \"flex items-start gap-2.5 rounded-control border px-3 py-2.5 text-sm leading-snug\",\n classes,\n className,\n )}\n >\n <Icon className={cn(\"mt-0.5 size-4 shrink-0\", ICON_TONE[tone])} aria-hidden />\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {title && <p className=\"font-medium\">{title}</p>}\n <div className=\"min-w-0 text-fg-muted\">{children}</div>\n {actions && <div className=\"mt-1 flex flex-wrap items-center gap-2\">{actions}</div>}\n </div>\n </div>\n );\n}\n\n/** The common error shape: an `error` `Callout` with only a message. */\nexport function ErrorBox({\n className,\n children,\n}: {\n /** Merged with the callout's own classes through `cn`. */\n className?: string | undefined;\n /** What went wrong and what to do about it. */\n children: ReactNode;\n}) {\n return (\n <Callout tone=\"error\" className={className}>\n {children}\n </Callout>\n );\n}\n\n/** An empty state: a sentence saying why there is nothing here, and the action that fixes it. */\nexport function Empty({\n action,\n className,\n children,\n}: {\n /** The action — usually a primary `Button` — rather than a description of it. */\n action?: ReactNode;\n /** Merged with the empty state's own classes through `cn`. */\n className?: string | undefined;\n /** Why there is nothing to show. */\n children: ReactNode;\n}) {\n return (\n <div className={cn(\"flex flex-col items-center gap-3 py-10 text-center\", className)}>\n <p className=\"text-sm text-fg-muted\">{children}</p>\n {action}\n </div>\n );\n}\n\n/** One placeholder bar for content on its way (`aria-hidden`; size it with `className`). */\nexport function Skeleton({\n className,\n}: {\n /** Sets the bar's size; merged with its own classes through `cn`. */\n className?: string | undefined;\n}) {\n return <div aria-hidden className={cn(\"animate-pulse rounded-control bg-line\", className)} />;\n}\n\n/** A stand-in for a panel's worth of content, so the layout does not jump when it lands. */\nexport function SkeletonRows({\n rows = 3,\n className,\n}: {\n /** How many bars. Defaults to 3; the last is shorter. */\n rows?: number | undefined;\n /** Merged with the column's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <div role=\"status\" aria-label=\"Loading\" className={cn(\"flex flex-col gap-2\", className)}>\n {Array.from({ length: rows }, (_, index) => (\n <Skeleton key={index} className={cn(\"h-4\", index === rows - 1 ? \"w-2/3\" : \"w-full\")} />\n ))}\n </div>\n );\n}\n\n/**\n * A known fraction done, as a bar and (with `label`) a percentage line.\n *\n * A `role=\"progressbar\"` with `aria-valuenow` in percent. Its accessible name is\n * `aria-label` if given, else `label`, else \"Progress\" — pass one that names the job when\n * a screen shows more than one bar.\n */\nexport function ProgressBar({\n fraction,\n label,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** Done, from 0 to 1. Clamped. */\n fraction: number;\n /** What is progressing, printed after the percentage: \"42 of 100 images\". */\n label?: string | undefined;\n /** The bar's accessible name. Defaults to `label`, then to \"Progress\". */\n \"aria-label\"?: string | undefined;\n /** Merged with the wrapper's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const percent = Math.round(Math.min(Math.max(fraction, 0), 1) * 100);\n return (\n <div className={cn(\"flex flex-col gap-1.5\", className)}>\n <div\n role=\"progressbar\"\n aria-label={ariaLabel ?? label ?? \"Progress\"}\n aria-valuenow={percent}\n aria-valuemin={0}\n aria-valuemax={100}\n data-complete={percent === 100 ? \"\" : undefined}\n className=\"h-1.5 overflow-hidden rounded-full bg-line\"\n >\n <div\n className=\"h-full rounded-full bg-signal transition-[width] duration-300\"\n style={{ width: `${percent}%` }}\n />\n </div>\n {label && (\n <span className=\"font-mono text-xs text-fg-muted tabular-nums\">\n {percent}% · {label}\n </span>\n )}\n </div>\n );\n}\n","/*\n * A labelled control.\n *\n * Two things a naive label cannot express, both of which the schema form needs. A required\n * field's marker is a separate span rather than text concatenated onto the label string, so\n * it can be styled, read out as a marker, or dropped. And a field's description (and its\n * error) is wired to the control through `aria-describedby`, so a screen reader connects\n * the two.\n *\n * `as=\"group\"` exists because a `<label>` cannot label a set of radios or a segmented\n * control; that case needs a labelled group instead, and getting it wrong is the difference\n * between a control that announces itself and one that announces nothing.\n */\n\nimport { createContext, use, useId, useMemo, type ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/** What a `Field` tells the control inside it: the ids that describe it, and whether it is invalid. */\ninterface FieldControl {\n describedBy: string | undefined;\n invalid: boolean;\n}\n\nconst FieldContext = createContext<FieldControl>({ describedBy: undefined, invalid: false });\n\n/**\n * The `aria-describedby` and `aria-invalid` a control inside a `Field` should carry, merged\n * with the control's own `aria-describedby`. Used by this package's controls; not exported\n * from the package.\n */\nexport function useFieldDescription(own?: string): {\n \"aria-describedby\": string | undefined;\n \"aria-invalid\": true | undefined;\n} {\n const { describedBy, invalid } = use(FieldContext);\n const ids = [own, describedBy].filter(Boolean).join(\" \");\n return { \"aria-describedby\": ids === \"\" ? undefined : ids, \"aria-invalid\": invalid || undefined };\n}\n\n/**\n * A label, the control, and optionally a description, an error and a compact annotation.\n *\n * The label text and the annotation name the control (the `<label>` wraps them and the\n * control); the description and the error are outside the label and wired into the\n * control's `aria-describedby` instead (the error also sets `aria-invalid`): directly for\n * this package's controls — `Input`, `NumberInput`, `Textarea`, `Select`, `Slider`,\n * `SegmentedControl`, `Checkbox` — and, with `as=\"group\"`, on the group itself. A control of your own inside a `label` field does not\n * pick them up; give it `aria-describedby` yourself. State is exposed as `data-invalid` and\n * `data-required`.\n */\nexport function Field({\n label,\n description,\n error,\n required = false,\n annotation,\n as = \"label\",\n className,\n children,\n}: {\n /** The field's name, shown above the control. */\n label: string;\n /** What the value means or does. Always visible, and read with the control. */\n description?: ReactNode;\n /** What is wrong with the current value. Shown in the verdict red, announced as an alert. */\n error?: ReactNode;\n /** Marks the field required (a `*` after the label). */\n required?: boolean | undefined;\n /** A compact fact about the accepted values -- a range, a unit -- kept out of the prose. */\n annotation?: ReactNode;\n /**\n * `label` (the default) wraps the control in a `<label>`; `group` renders a\n * `role=\"group\"` named by `label`, for a set of radios or a segmented control.\n */\n as?: \"label\" | \"group\";\n /** Merged with the field's own classes through `cn`. */\n className?: string | undefined;\n /** The control. */\n children: ReactNode;\n}) {\n const descriptionId = useId();\n const errorId = useId();\n const density = useDensity();\n\n const hasError = error !== undefined && error !== null && error !== false;\n const describedBy =\n [description ? descriptionId : undefined, hasError ? errorId : undefined]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n // A group carries the description itself; its controls would only repeat it.\n const control = useMemo(\n () => ({ describedBy: as === \"label\" ? describedBy : undefined, invalid: hasError }),\n [as, describedBy, hasError],\n );\n\n const gap = byDensity(density, \"gap-1.5\", \"gap-1\");\n\n const labelled = (\n <>\n <span className=\"flex items-baseline gap-2\">\n <span className={cn(\"font-medium text-fg\", byDensity(density, \"text-xs\", \"text-[11px]\"))}>\n {label}\n {required && (\n <span className=\"ml-0.5 text-defect\" title=\"Required\">\n *\n </span>\n )}\n </span>\n {annotation && (\n <span className=\"ml-auto font-mono text-[11px] text-fg-subtle\">{annotation}</span>\n )}\n </span>\n <FieldContext value={control}>{children}</FieldContext>\n </>\n );\n\n return (\n <div\n className={cn(\"flex min-w-0 flex-col\", gap, className)}\n data-invalid={hasError ? \"\" : undefined}\n data-required={required ? \"\" : undefined}\n {...(as === \"group\"\n ? { role: \"group\", \"aria-label\": label, \"aria-describedby\": describedBy }\n : {})}\n >\n {/* The description and the error sit outside the <label>: inside it they would be read\n as part of the control's name as well as its description. */}\n {as === \"label\" ? (\n <label className={cn(\"flex min-w-0 flex-col\", gap)}>{labelled}</label>\n ) : (\n labelled\n )}\n\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n {hasError && (\n <span id={errorId} role=\"alert\" className=\"text-xs leading-snug text-defect\">\n {error}\n </span>\n )}\n </div>\n );\n}\n","/*\n * Text entry.\n *\n * `controlClasses` is exported so a native control that isn't one of the wrapped ones below\n * can still pick up the shared palette and focus treatment.\n */\n\nimport { useId, type ComponentProps } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRingInset } from \"./cn\";\n\n/**\n * The shared look of a text-like control — border, palette, placeholder, hover, disabled and\n * focus — for a native control that is not one of `Input`, `NumberInput` or `Textarea`.\n * Add the height with `useControlHeight()`.\n */\nexport const controlClasses = cn(\n \"w-full rounded-control border border-line-strong bg-raised px-2.5 text-sm text-fg\",\n \"placeholder:text-fg-subtle\",\n \"transition-colors hover:border-fg-subtle\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRingInset,\n);\n\n/**\n * The former name of `controlClasses`, kept as an alias for existing consumers.\n *\n * @deprecated Use `Input`, `NumberInput`, `Textarea` or `Select`, or `controlClasses`.\n */\nexport const inputClasses = controlClasses;\n\n/**\n * The control height in force — exported so an app's own native control can match.\n *\n * @returns A Tailwind height class: `h-8` comfortable, `h-7` compact.\n */\nexport function useControlHeight(): string {\n return byDensity(useDensity(), \"h-8\", \"h-7\");\n}\n\n/**\n * A single-line text input. Takes every `<input>` prop, `ref` included; inside a `Field` it\n * is described by the field's description and error.\n */\nexport function Input({ className, \"aria-describedby\": describedBy, ...rest }: ComponentProps<\"input\">) {\n return (\n <input\n {...useFieldDescription(describedBy)}\n {...rest}\n className={cn(controlClasses, useControlHeight(), className)}\n />\n );\n}\n\n/** Props of `NumberInput`: every `<input>` prop, `ref` included, with numeric bounds and a `unit`. */\nexport type NumberInputProps = Omit<ComponentProps<\"input\">, \"min\" | \"max\"> & {\n /** The smallest accepted value. */\n min?: number | undefined;\n /** The largest accepted value. */\n max?: number | undefined;\n /**\n * The quantity's unit (`mm`, `m`, `°`, `px`), written inside the field after the number, in\n * mono and muted. It is not part of the value. It is announced as the field's description\n * (`aria-describedby`): after the caller's own `aria-describedby`, before a surrounding\n * `Field`'s description and error.\n */\n unit?: string | undefined;\n};\n\n/**\n * A number, with the schema's own bounds attached and, optionally, its unit written in the\n * field.\n *\n * `font-mono` because these are read as quantities and compared down a column. Takes every\n * `<input>` prop, `ref` included (`type` is always `number`); `className`, `style` and `ref`\n * always go to the `<input>`.\n *\n * With a `unit`, the input sits in a `relative` full-width wrapper carrying `data-unit`, keeps\n * room on its right for the unit, and is described by it; size such a field through its\n * container. Without one — or with `\"\"` — the markup is exactly the plain input.\n */\nexport function NumberInput({ unit, ...props }: NumberInputProps) {\n return unit === undefined || unit === \"\" ? (\n <PlainNumberInput {...props} />\n ) : (\n <UnitNumberInput unit={unit} {...props} />\n );\n}\n\nfunction PlainNumberInput({\n className,\n \"aria-describedby\": describedBy,\n ...rest\n}: Omit<NumberInputProps, \"unit\">) {\n return (\n <input\n {...useFieldDescription(describedBy)}\n {...rest}\n type=\"number\"\n inputMode=\"decimal\"\n className={cn(controlClasses, useControlHeight(), \"font-mono tabular-nums\", className)}\n />\n );\n}\n\n/*\n * A component of its own so that only a field with a unit calls `useId`: a plain\n * `NumberInput` stays byte-identical to the one before `unit` existed, down to the ids React\n * hands the elements rendered after it.\n */\nfunction UnitNumberInput({\n unit,\n style,\n \"aria-describedby\": describedBy,\n ...rest\n}: Omit<NumberInputProps, \"unit\"> & { unit: string }) {\n const unitId = useId();\n return (\n <span data-unit={unit} className=\"relative flex w-full min-w-0 items-center\">\n <PlainNumberInput\n {...rest}\n aria-describedby={describedBy ? `${describedBy} ${unitId}` : unitId}\n // Room for the unit: its width in the unit's own characters, plus the field's padding.\n style={{ paddingInlineEnd: `calc(${unit.length}ch + 1rem)`, ...style }}\n />\n {/* `aria-hidden` keeps the unit out of the field's *name* when a `<label>` wraps it;\n `aria-describedby` still reads it, as the description. */}\n <span\n id={unitId}\n aria-hidden\n className=\"pointer-events-none absolute inset-y-0 right-2.5 flex items-center font-mono text-xs text-fg-subtle select-none\"\n >\n {unit}\n </span>\n </span>\n );\n}\n\n/**\n * Multi-line text — set in mono, for JSON and lists. Takes every `<textarea>` prop, `ref`\n * included.\n */\nexport function Textarea({\n className,\n \"aria-describedby\": describedBy,\n ...rest\n}: ComponentProps<\"textarea\">) {\n return (\n <textarea\n {...useFieldDescription(describedBy)}\n {...rest}\n className={cn(controlClasses, \"py-1.5 font-mono\", className)}\n />\n );\n}\n","/*\n * The page furniture: what holds a screen together above the level of a single control.\n *\n * One heading size and one gap rule, defined once so every route in every consuming app\n * agrees with the others by construction rather than by luck.\n */\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { isValidElement, type ReactElement, type ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/**\n * A bordered surface with an optional header (a title and actions) above a padded body.\n * The header and the padding follow the density in force.\n */\nexport function Panel({\n title,\n actions,\n className,\n bodyClassName,\n children,\n}: {\n /** The header's heading (an `<h2>`). */\n title?: ReactNode;\n /** Controls at the header's right end. */\n actions?: ReactNode;\n /** Merged with the panel's own classes through `cn`. */\n className?: string | undefined;\n /** Merged with the body's own classes (its padding) through `cn`. */\n bodyClassName?: string | undefined;\n /** The body. */\n children: ReactNode;\n}) {\n const density = useDensity();\n\n return (\n <section className={cn(\"rounded-panel border border-line bg-surface\", className)}>\n {(title || actions) && (\n <header\n className={cn(\n \"flex items-center justify-between gap-4 border-b border-line\",\n byDensity(density, \"min-h-11 px-4 py-2.5\", \"min-h-8 px-2.5 py-1\"),\n )}\n >\n <h2\n className={cn(\n \"truncate font-semibold text-fg\",\n byDensity(\n density,\n \"text-sm tracking-tight\",\n \"text-[11px] tracking-wider uppercase text-fg-muted\",\n ),\n )}\n >\n {title}\n </h2>\n {actions && <div className=\"flex shrink-0 items-center gap-1.5\">{actions}</div>}\n </header>\n )}\n <div className={cn(byDensity(density, \"p-4\", \"p-2.5\"), bodyClassName)}>{children}</div>\n </section>\n );\n}\n\n/**\n * A numbered step inside a form.\n *\n * The number is not decoration: it marks a genuine ordering, where each choice narrows the\n * next. Do not reach for this where the parts are merely adjacent.\n */\nexport function Section({\n step,\n title,\n hint,\n actions,\n className,\n children,\n}: {\n /** The step's number, shown zero-padded before the title (decorative, `aria-hidden`). */\n step?: number | undefined;\n /** The step's heading (an `<h3>`). */\n title: string;\n /** A short line after the title. */\n hint?: ReactNode;\n /** Controls at the heading row's right end. */\n actions?: ReactNode;\n /** Merged with the section's own classes through `cn`. */\n className?: string | undefined;\n /** The step's content. */\n children: ReactNode;\n}) {\n const density = useDensity();\n\n return (\n <section className={cn(\"flex flex-col\", byDensity(density, \"gap-3\", \"gap-1.5\"), className)}>\n <div className={cn(\"flex items-baseline\", byDensity(density, \"gap-2.5\", \"gap-2\"))}>\n {step !== undefined && (\n <span\n className=\"font-mono text-xs text-fg-subtle tabular-nums\"\n aria-hidden\n >\n {String(step).padStart(2, \"0\")}\n </span>\n )}\n <h3 className=\"text-sm font-semibold tracking-tight text-fg\">{title}</h3>\n {hint && <p className=\"text-xs text-fg-muted\">{hint}</p>}\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n {children}\n </section>\n );\n}\n\n/**\n * Where `PageHeader`'s back link goes: `{ href, label }` for a plain URL, or the app's own\n * link element (a router's `<Link>`).\n */\nexport type BackLink =\n | { href: string; label: string }\n /** A link element — typically a router's `<Link to=\"…\">Label</Link>` — styled and prefixed with an arrow. */\n | ReactElement;\n\nconst BACK_CLASSES = \"w-fit text-xs text-fg-muted transition-colors hover:text-signal\";\n\n/**\n * The top of a screen: an optional back link, the `<h1>` title with actions beside it, and\n * a line of meta facts below. Links go through `back`, never a router import.\n */\nexport function PageHeader({\n title,\n meta,\n actions,\n back,\n className,\n}: {\n /** The screen's heading (the `<h1>`). */\n title: ReactNode;\n /** Small facts under the title — an id, a date, a status badge. */\n meta?: ReactNode;\n /** The screen's actions, beside the title. */\n actions?: ReactNode;\n /**\n * The way up. `{ href, label }` renders a plain `<a>`; for client-side navigation pass the\n * router's own link — `back={<Link to=\"/experiments\">Experiments</Link>}` — which is\n * rendered with the back-link style and arrow, so this package never imports a router.\n */\n back?: BackLink | undefined;\n /** Merged with the header's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <header className={cn(\"flex flex-col gap-1.5\", className)}>\n {back &&\n (isHrefLink(back) ? (\n <a href={back.href} className={BACK_CLASSES}>\n ← {back.label}\n </a>\n ) : (\n <Slot className={BACK_CLASSES}>\n {\"← \"}\n <Slottable>{back}</Slottable>\n </Slot>\n ))}\n <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2\">\n <h1 className=\"text-xl font-semibold tracking-tight text-fg\">{title}</h1>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n {meta && <div className=\"flex flex-wrap items-center gap-2 text-xs text-fg-muted\">{meta}</div>}\n </header>\n );\n}\n\nfunction isHrefLink(back: BackLink): back is { href: string; label: string } {\n return !isValidElement(back);\n}\n\n/** One fact on a `ReadoutStrip`: a value, optionally labelled and optionally a link. */\nexport type ReadoutItem = {\n /** A quiet prefix naming what the value is, where the value alone is ambiguous. */\n label?: string | undefined;\n /** The fact. An item whose value is `null` or `undefined` is skipped. */\n value: ReactNode;\n /** Makes the value a plain `<a href>`. */\n href?: string | undefined;\n /**\n * Makes the value a link through the app's own element — a router's `<Link to=\"…\" />`,\n * given without children: the value becomes its content. Takes precedence over `href`.\n */\n link?: ReactElement | undefined;\n};\n\nconst READOUT_LINK = \"text-fg transition-colors hover:text-signal\";\n\n/**\n * The instrument's display line: the facts about what is on screen, in one fixed slot.\n *\n * Stated in the same place on every screen, in mono, the way a piece of measuring equipment\n * has one readout rather than a label per panel. Rendered as a list because it is one.\n */\nexport function ReadoutStrip({\n items,\n className,\n}: {\n /** The facts, in order. Items with no value are skipped. */\n items: ReadoutItem[];\n /** Merged with the list's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const shown = items.filter((item) => item.value !== null && item.value !== undefined);\n if (shown.length === 0) return null;\n\n return (\n <ol\n className={cn(\n \"flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-xs text-fg-muted\",\n className,\n )}\n >\n {shown.map((item, index) => (\n // The items are positional facts with no identity of their own (a label is optional\n // and values repeat), so their place in the strip is the key.\n // eslint-disable-next-line @eslint-react/no-array-index-key -- positional list, see above\n <li key={index} className=\"flex items-center gap-2\">\n {index > 0 && (\n <span className=\"text-fg-subtle\" aria-hidden>\n ·\n </span>\n )}\n {item.label && <span className=\"text-fg-subtle\">{item.label}</span>}\n {item.link ? (\n <Slot className={READOUT_LINK}>\n <Slottable>{item.link}</Slottable>\n {item.value}\n </Slot>\n ) : item.href ? (\n <a href={item.href} className={READOUT_LINK}>\n {item.value}\n </a>\n ) : (\n <span className=\"text-fg\">{item.value}</span>\n )}\n </li>\n ))}\n </ol>\n );\n}\n","/*\n * Numbers as a field shows them and as a person types them. Internal to this package (used by\n * `VectorInput` and `PoseInput`); not exported.\n *\n * A field bound straight to a number re-formats under the cursor: typing `0.` into a field\n * showing `0.000` becomes `0` before the next digit arrives. So the multi-number fields keep\n * the typed text while they have focus and show the formatted value otherwise; these are the\n * two conversions between the two.\n */\n\n/**\n * A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign\n * error in a coordinate readout).\n *\n * @param value - The number. Non-finite values print as `NaN`, `Infinity`, `-Infinity`.\n * @param precision - Decimals, 0–20.\n * @returns The text.\n */\nexport function formatNumber(value: number, precision: number): string {\n if (!Number.isFinite(value)) return String(value);\n const text = value.toFixed(precision);\n return /^-0(?:\\.0*)?$/.test(text) ? text.slice(1) : text;\n}\n\n/**\n * The number a field's text means, if it means one.\n *\n * @param text - What is in the field.\n * @returns The number, or `null` for empty, partial (`-`, `1e`) or non-numeric text.\n */\nexport function parseNumber(text: string): number | null {\n const trimmed = text.trim();\n if (trimmed === \"\") return null;\n const value = Number(trimmed);\n return Number.isFinite(value) ? value : null;\n}\n","/*\n * A small vector — a position, a set of angles, a scale — edited as one row.\n *\n * Three separate `Field`s for x, y and z spend three labels and three rows on what a reader\n * thinks of as one quantity. This is one row: a quiet axis letter before each number, the\n * unit once at the end, and one name for the whole group.\n *\n * Each field keeps the text being typed while it has focus and shows the formatted value\n * otherwise (see `numberText.ts`), so `precision` never fights the cursor. Every valid\n * keystroke is reported, so whatever the vector drives follows the typing.\n */\n\nimport { useId, useState } from \"react\";\nimport type { KeyboardEvent } from \"react\";\n\nimport { NumberInput } from \"./Input\";\nimport { ReadoutStrip } from \"./Panel\";\nimport { cn } from \"./cn\";\nimport { formatNumber, parseNumber } from \"./numberText\";\n\nconst DEFAULT_LABELS = [\"x\", \"y\", \"z\", \"w\"];\n\n/** Props of `VectorInput`. */\nexport interface VectorInputProps {\n /** The components. Its length is the number of fields. */\n value: readonly number[];\n /** Called with the whole vector (a new array) whenever one component changes to a valid number. */\n onValueChange?: ((value: number[]) => void) | undefined;\n /** Each component's label, shown before its field and naming it. Defaults to x, y, z, w. */\n labels?: readonly string[] | undefined;\n /** The unit of every component, shown once after the row (`m`, `mm`, `°`). */\n unit?: string | undefined;\n /** The arrow-key step. Defaults to `any` (the browser then steps by 1). */\n step?: number | undefined;\n /** Decimals shown when a field is not being edited. Defaults to 3. */\n precision?: number | undefined;\n /** The smallest accepted component. */\n min?: number | undefined;\n /** The largest accepted component. */\n max?: number | undefined;\n /** Show the values as a compact mono readout instead of fields. */\n readOnly?: boolean | undefined;\n /** Blocks every field. */\n disabled?: boolean | undefined;\n /** Names the group (`role=\"group\"`). */\n \"aria-label\": string;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n}\n\n/**\n * A row of number fields editing one small vector, with the axis labels before the fields\n * and the unit once after them.\n *\n * Controlled. A `role=\"group\"` named by `aria-label`, each field named by its label and\n * described by the unit. While a field has focus it shows what is typed; on blur or Enter it\n * shows the value at `precision`; Escape restores the value it had on focus. With `readOnly`\n * it renders a `ReadoutStrip` (`x 0.100 m · y …`) instead. The row carries `data-readonly`\n * and `data-disabled`.\n */\nexport function VectorInput({\n value,\n onValueChange,\n labels = DEFAULT_LABELS,\n unit,\n step,\n precision = 3,\n min,\n max,\n readOnly = false,\n disabled = false,\n \"aria-label\": ariaLabel,\n className,\n}: VectorInputProps) {\n const unitId = useId();\n const labelOf = (index: number) => labels[index] ?? String(index);\n\n if (readOnly) {\n return (\n <div role=\"group\" aria-label={ariaLabel} data-readonly=\"\" className={cn(\"min-w-0\", className)}>\n <ReadoutStrip\n items={value.map((component, index) => ({\n label: labelOf(index),\n value: unit ? `${formatNumber(component, precision)} ${unit}` : formatNumber(component, precision),\n }))}\n />\n </div>\n );\n }\n\n const change = (index: number, component: number) => {\n if (component === value[index]) return;\n onValueChange?.(value.map((current, i) => (i === index ? component : current)));\n };\n\n return (\n <div\n role=\"group\"\n aria-label={ariaLabel}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"flex min-w-0 items-center gap-1.5\", className)}\n >\n {value.map((component, index) => (\n // The components are positional: index is their identity.\n // eslint-disable-next-line @eslint-react/no-array-index-key -- see above\n <label key={index} className=\"flex min-w-0 flex-1 items-center gap-1\">\n <span className=\"shrink-0 font-mono text-[11px] text-fg-subtle\">{labelOf(index)}</span>\n <ComponentField\n value={component}\n precision={precision}\n step={step}\n min={min}\n max={max}\n disabled={disabled}\n describedBy={unit ? unitId : undefined}\n onCommit={(next) => change(index, next)}\n />\n </label>\n ))}\n {unit && (\n <span id={unitId} className=\"shrink-0 font-mono text-xs text-fg-subtle\">\n {unit}\n </span>\n )}\n </div>\n );\n}\n\n/** One component: formatted at rest, the typed text while focused. */\nfunction ComponentField({\n value,\n precision,\n step,\n min,\n max,\n disabled,\n describedBy,\n onCommit,\n}: {\n value: number;\n precision: number;\n step: number | undefined;\n min: number | undefined;\n max: number | undefined;\n disabled: boolean;\n describedBy: string | undefined;\n onCommit: (value: number) => void;\n}) {\n // Non-null while the field is being edited: the text as typed, and the value on focus.\n const [draft, setDraft] = useState<{ text: string; initial: number } | null>(null);\n const formatted = formatNumber(value, precision);\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"Enter\") {\n setDraft((current) => current && { ...current, text: formatted });\n } else if (event.key === \"Escape\" && draft) {\n event.preventDefault();\n onCommit(draft.initial);\n setDraft({ ...draft, text: formatNumber(draft.initial, precision) });\n }\n };\n\n return (\n <NumberInput\n value={draft?.text ?? formatted}\n step={step ?? \"any\"}\n min={min}\n max={max}\n disabled={disabled}\n aria-describedby={describedBy}\n onFocus={() => setDraft({ text: formatted, initial: value })}\n onBlur={() => setDraft(null)}\n onKeyDown={onKeyDown}\n onChange={(event) => {\n const text = event.currentTarget.value;\n setDraft((current) => ({ text, initial: current?.initial ?? value }));\n const parsed = parseNumber(text);\n if (parsed !== null) onCommit(parsed);\n }}\n className=\"min-w-0 px-1.5\"\n />\n );\n}\n","/*\n * A rigid transform, edited the way people think about one.\n *\n * Poses travel as `{ rotation: [qx, qy, qz, qw], translation: [tx, ty, tz] }` — the SE(3) wire\n * form — because a quaternion is the honest representation. Nobody edits one by hand,\n * though: a rotation is read and typed as roll, pitch and yaw in degrees. This component\n * shows that view and writes back the wire form.\n *\n * It owns no rotation mathematics. Which Euler convention \"roll, pitch, yaw\" means — the\n * axis order, intrinsic or extrinsic — is a decision of the 3D package that defines the\n * frames, so the conversion is passed in as a `RotationView`. This package stays pure UI.\n */\n\nimport { useState } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { ReadoutStrip } from \"./Panel\";\nimport { cn } from \"./cn\";\nimport { formatNumber } from \"./numberText\";\nimport { VectorInput } from \"./VectorInput\";\n\n/** Three numbers: a translation `[tx, ty, tz]`, or Euler angles. */\nexport type Vec3 = readonly [number, number, number];\n\n/** A unit quaternion in the wire order `[qx, qy, qz, qw]` (scalar last). */\nexport type Quaternion = readonly [number, number, number, number];\n\n/**\n * An SE(3) pose in the wire form (`a_se3_b` as nalgebra's `Isometry3` serialises it):\n * translation in metres, rotation as a unit quaternion `[qx, qy, qz, qw]`.\n */\nexport interface PoseValue {\n /** The rotation, `[qx, qy, qz, qw]`. */\n rotation: Quaternion;\n /** The translation `[tx, ty, tz]`, in metres. */\n translation: Vec3;\n}\n\n/**\n * How a rotation is shown as three angles: the conversion both ways, and the angles' names.\n * Supplied by the package that owns the frame conventions; `PoseInput` only calls it.\n */\nexport interface RotationView {\n /** The quaternion as three angles, in radians, in the order of `labels`. */\n toEuler: (rotation: Quaternion) => Vec3;\n /** Three angles in radians, in the order of `labels`, as a unit quaternion `[qx, qy, qz, qw]`. */\n fromEuler: (angles: Vec3) => Quaternion;\n /** The angles' names, shown before each field. Defaults to roll, pitch, yaw. */\n labels?: readonly [string, string, string] | undefined;\n}\n\n/** Props of `PoseInput`. */\nexport interface PoseInputProps {\n /** The pose, in the wire form. */\n value: PoseValue;\n /** Called with a new pose (new arrays) whenever a component changes. */\n onValueChange?: ((value: PoseValue) => void) | undefined;\n /** The quaternion ↔ angles conversion and the angles' names. Required: see `RotationView`. */\n rotationView: RotationView;\n /** Show and edit the translation in metres (the default) or millimetres. The value is always metres. */\n translationUnit?: \"m\" | \"mm\" | undefined;\n /** Decimals of the translation. Defaults to 4 for m, 1 for mm. */\n translationPrecision?: number | undefined;\n /** Decimals of the angles, in degrees. Defaults to 2. */\n rotationPrecision?: number | undefined;\n /** Show a compact mono readout instead of fields. */\n readOnly?: boolean | undefined;\n /** Blocks every field. */\n disabled?: boolean | undefined;\n /** Names the pose (`role=\"group\"`). Defaults to \"Pose\". */\n \"aria-label\"?: string | undefined;\n /** Merged with the component's own classes through `cn`. */\n className?: string | undefined;\n}\n\nconst DEGREES = 180 / Math.PI;\nconst DEFAULT_ANGLE_LABELS = [\"roll\", \"pitch\", \"yaw\"] as const;\n\nconst sameQuaternion = (a: Quaternion, b: Quaternion) => a.every((component, index) => component === b[index]);\n\n/**\n * An SE(3) pose shown and edited as a translation (m or mm) and three angles in degrees,\n * reading and writing the wire form `{ rotation: [qx, qy, qz, qw], translation: [tx, ty, tz] }`.\n *\n * The angles come from `rotationView.toEuler` and go back through `rotationView.fromEuler`.\n * The angles as typed are kept while the pose they produced is the one shown, so a yaw of\n * 190° is not redisplayed as −170° and a gimbal-locked pitch does not scramble the other two\n * angles mid-edit.\n *\n * A `role=\"group\"` named by `aria-label`, holding two `VectorInput` groups (\"Translation\",\n * \"Rotation\"). With `readOnly` it renders a `ReadoutStrip` instead\n * (`t 0.1000 0.2000 0.3000 m · rpy 0.00 90.00 0.00 °`). The root carries `data-readonly`.\n */\nexport function PoseInput({\n value,\n onValueChange,\n rotationView,\n translationUnit = \"m\",\n translationPrecision,\n rotationPrecision = 2,\n readOnly = false,\n disabled = false,\n \"aria-label\": ariaLabel = \"Pose\",\n className,\n}: PoseInputProps) {\n const density = useDensity();\n // The angles last typed, and the quaternion they became.\n const [typed, setTyped] = useState<{ rotation: Quaternion; angles: Vec3 } | null>(null);\n\n const scale = translationUnit === \"mm\" ? 1000 : 1;\n const tPrecision = translationPrecision ?? (translationUnit === \"mm\" ? 1 : 4);\n const translation = value.translation.map((component) => component * scale);\n const radians =\n typed !== null && sameQuaternion(typed.rotation, value.rotation) ? typed.angles : rotationView.toEuler(value.rotation);\n const degrees = radians.map((angle) => angle * DEGREES);\n const angleLabels = rotationView.labels ?? DEFAULT_ANGLE_LABELS;\n\n if (readOnly) {\n const join = (values: readonly number[], precision: number) =>\n values.map((component) => formatNumber(component, precision)).join(\" \");\n return (\n <div role=\"group\" aria-label={ariaLabel} data-readonly=\"\" className={cn(\"min-w-0\", className)}>\n <ReadoutStrip\n items={[\n { label: \"t\", value: `${join(translation, tPrecision)} ${translationUnit}` },\n { label: \"rpy\", value: `${join(degrees, rotationPrecision)} °` },\n ]}\n />\n </div>\n );\n }\n\n const onTranslation = (next: number[]) => {\n const [x = 0, y = 0, z = 0] = next.map((component) => component / scale);\n onValueChange?.({ rotation: value.rotation, translation: [x, y, z] });\n };\n\n const onRotation = (next: number[]) => {\n const [a = 0, b = 0, c = 0] = next.map((angle) => angle / DEGREES);\n const angles: Vec3 = [a, b, c];\n const rotation = rotationView.fromEuler(angles);\n setTyped({ rotation, angles });\n onValueChange?.({ rotation, translation: value.translation });\n };\n\n const rowLabel = cn(\"text-fg-muted\", byDensity(density, \"text-xs\", \"text-[11px]\"));\n\n return (\n <div\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\"grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2\", byDensity(density, \"gap-y-2\", \"gap-y-1\"), className)}\n >\n <span aria-hidden className={rowLabel}>\n Translation\n </span>\n <VectorInput\n aria-label=\"Translation\"\n value={translation}\n onValueChange={onTranslation}\n unit={translationUnit}\n precision={tPrecision}\n step={translationUnit === \"mm\" ? 1 : 0.001}\n disabled={disabled}\n />\n <span aria-hidden className={rowLabel}>\n Rotation\n </span>\n <VectorInput\n aria-label=\"Rotation\"\n value={degrees}\n onValueChange={onRotation}\n labels={angleLabels}\n unit=\"°\"\n precision={rotationPrecision}\n step={1}\n disabled={disabled}\n />\n </div>\n );\n}\n","/*\n * A choice among two or three, shown as two or three things.\n *\n * The control for a closed set the app can enumerate up front -- `Literal[\"small\", \"medium\"]`, an axis choice -- shown all at once, no popup, no typing, and no way to enter\n * a value the backend or algorithm would reject.\n *\n * Built on native radios rather than a headless package: a radio group in one `name` gets\n * roving focus and arrow-key navigation from the platform, which is the entire reason to\n * reach for a library here.\n *\n * **`\"\"` means unset**, and this control keeps that true without showing it. The segment\n * highlighted is always the *effective* value, so the reader sees what will happen;\n * choosing the segment that matches the default stores `\"\"` rather than the value, so the\n * default is still sent by nobody and defined only in one place. See `api/schemaForm.ts`\n * for why that rule is worth this much care.\n */\n\nimport { useId } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn } from \"./cn\";\n\n/**\n * A choice among two or three, shown as a strip of segments (native radios in one group, so\n * arrow keys move between them).\n *\n * **`\"\"` means unset.** The highlighted segment is always the *effective* value — `value`,\n * or `defaultValue` while `value` is `\"\"` — and choosing the segment that matches\n * `defaultValue` reports `\"\"`, so the default stays defined in one place. Each segment\n * carries `data-state=\"checked\"` or `\"unchecked\"`; the group carries `data-disabled`.\n */\nexport function SegmentedControl({\n value,\n defaultValue,\n options,\n onValueChange,\n disabled = false,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** `\"\"` when the field is untouched. */\n value: string;\n /** What applies when nothing is sent. Highlighted while `value` is `\"\"`. */\n defaultValue?: string | undefined;\n /** The segments, in order. */\n options: { value: string; label: string }[];\n /** Called with the chosen value, or `\"\"` when the default's segment is chosen. */\n onValueChange: (value: string) => void;\n /** Blocks every segment. */\n disabled?: boolean | undefined;\n /** Names the radio group. */\n \"aria-label\"?: string | undefined;\n /** Merged with the strip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const nameId = useId();\n const described = useFieldDescription();\n const effective = value === \"\" ? defaultValue : value;\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n aria-describedby={described[\"aria-describedby\"]}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n // `shrink-0` because this is a control, not filler: in a crowded toolbar a flex item at\n // its default `1 1 auto` gives up width first, and the segment labels are what pay for\n // it. Let the row scroll instead of squeezing the choice.\n \"inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-control border border-line-strong bg-raised p-0.5\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {options.map((option) => {\n const checked = option.value === effective;\n return (\n <label\n key={option.value}\n data-state={checked ? \"checked\" : \"unchecked\"}\n className={cn(\n // `whitespace-nowrap` or a two-word label's min-content width is its longest\n // *word*, and \"Side by side\" is free to stack onto three lines inside a\n // fixed-height strip that then clips it.\n \"relative cursor-pointer whitespace-nowrap rounded-[0.3rem] px-2.5 py-1 text-xs font-medium transition-colors\",\n \"has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-signal\",\n checked\n ? \"bg-surface text-fg shadow-sm\"\n : \"text-fg-muted hover:text-fg\",\n disabled && \"cursor-not-allowed\",\n )}\n >\n <input\n type=\"radio\"\n name={nameId}\n value={option.value}\n checked={checked}\n disabled={disabled}\n onChange={() =>\n // Selecting the default returns the field to unset: same effective value,\n // nothing sent, and the default keeps living in exactly one place.\n onValueChange(option.value === defaultValue ? \"\" : option.value)\n }\n className=\"absolute inset-0 cursor-pointer opacity-0\"\n />\n {option.label}\n </label>\n );\n })}\n </div>\n );\n}\n","/*\n * A picker with a list you can actually see.\n *\n * A native `<select>` cannot style its option list on macOS, so the popup arrives in the OS\n * palette in the middle of an otherwise deliberate instrument panel, and it cannot render a\n * two-line option or a disabled one with a reason.\n *\n * The one contract worth reading before changing this: **`\"\"` means unset.** Radix reserves\n * the empty string internally -- an `Item` may not carry it -- so an explicit \"use the\n * default\" entry travels under a sentinel and is mapped back at the boundary. Callers only\n * ever see `\"\"`.\n */\n\nimport * as RadixSelect from \"@radix-ui/react-select\";\nimport { Check, ChevronDown } from \"lucide-react\";\nimport { useState } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { useControlHeight } from \"./Input\";\nimport { cn, focusRingInset } from \"./cn\";\n\nconst UNSET_SENTINEL = \"__unset__\";\n\n/** One entry of a `Select`'s list. */\nexport type SelectOption = {\n /** What `onValueChange` reports. Must not be `\"\"`, which means unset. */\n value: string;\n /** What the list and the trigger show. */\n label: string;\n /** Shown quietly after the label -- a strategy, a count, a reason it is unavailable. */\n note?: string;\n /** Shown but not choosable; say why in `note`. */\n disabled?: boolean;\n};\n\n/**\n * A picker whose option list is styled with the app, with room for a quiet note per option.\n *\n * Controlled, and **`\"\"` means unset**: the trigger then shows `placeholder`, and the\n * `unsetLabel` entry (when given) reports `\"\"`. The trigger carries Radix's `data-state`\n * (`open`/`closed`), `data-placeholder` while unset and `data-disabled`; inside a `Field` it\n * is described by the field's description and error.\n */\nexport function Select({\n value,\n onValueChange,\n options,\n placeholder = \"Choose…\",\n unsetLabel,\n disabled = false,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ownDescribedBy,\n className,\n}: {\n /** The chosen option's value, or `\"\"` for unset. */\n value: string;\n /** Called with the chosen value, or `\"\"` for the unset entry. */\n onValueChange: (value: string) => void;\n /** The list, in order. */\n options: SelectOption[];\n /** Shown on the trigger while the value is unset. Defaults to \"Choose…\". */\n placeholder?: string | undefined;\n /** When given, adds a leading entry that returns the field to unset. */\n unsetLabel?: string | undefined;\n /** Blocks the trigger. */\n disabled?: boolean | undefined;\n /** Names the trigger, where no `<label>` does. */\n \"aria-label\"?: string | undefined;\n /** Ids of elements describing the trigger, merged with a surrounding `Field`'s. */\n \"aria-describedby\"?: string | undefined;\n /** Merged with the trigger's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const [open, setOpen] = useState(false);\n const described = useFieldDescription(ownDescribedBy);\n // The trigger is a control like any input: 32 px comfortable, 28 px compact.\n const height = useControlHeight();\n\n return (\n <RadixSelect.Root\n // Controlled for its whole lifetime: Radix treats `value=\"\"` as \"no selection\" and shows\n // the placeholder, which is exactly this component's unset.\n value={value}\n onValueChange={(next) => onValueChange(next === UNSET_SENTINEL ? \"\" : next)}\n open={open}\n onOpenChange={setOpen}\n disabled={disabled}\n >\n <RadixSelect.Trigger\n aria-label={ariaLabel}\n {...described}\n // While the list is open Radix hides everything outside it from assistive technology\n // (`aria-hidden`), this trigger included; a hidden element must not stay in the tab\n // order. Focus returns to it on close all the same.\n tabIndex={open ? -1 : undefined}\n className={cn(\n \"flex w-full items-center justify-between gap-2 rounded-control border border-line-strong bg-raised px-2.5 text-sm text-fg\",\n height,\n \"transition-colors hover:border-fg-subtle\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"data-[placeholder]:text-fg-subtle\",\n focusRingInset,\n className,\n )}\n >\n <RadixSelect.Value placeholder={placeholder} />\n <RadixSelect.Icon className=\"shrink-0 text-fg-subtle\">\n <ChevronDown className=\"size-3.5\" />\n </RadixSelect.Icon>\n </RadixSelect.Trigger>\n\n <RadixSelect.Portal>\n <RadixSelect.Content\n position=\"popper\"\n sideOffset={4}\n className={cn(\n \"z-50 max-h-72 min-w-(--radix-select-trigger-width) overflow-hidden\",\n \"rounded-panel border border-line bg-overlay shadow-lg shadow-black/25\",\n )}\n >\n <RadixSelect.Viewport className=\"p-1\">\n {unsetLabel !== undefined && (\n <SelectItem value={UNSET_SENTINEL} label={unsetLabel} muted />\n )}\n {options.map((option) => (\n <SelectItem\n key={option.value}\n value={option.value}\n label={option.label}\n note={option.note}\n disabled={option.disabled}\n />\n ))}\n </RadixSelect.Viewport>\n </RadixSelect.Content>\n </RadixSelect.Portal>\n </RadixSelect.Root>\n );\n}\n\nfunction SelectItem({\n value,\n label,\n note,\n disabled,\n muted = false,\n}: {\n value: string;\n label: string;\n note?: string | undefined;\n disabled?: boolean | undefined;\n muted?: boolean;\n}) {\n return (\n <RadixSelect.Item\n value={value}\n disabled={disabled ?? false}\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-control py-1.5 pr-2 pl-7 text-sm outline-none select-none\",\n \"data-highlighted:bg-raised data-highlighted:text-fg\",\n \"data-disabled:pointer-events-none data-disabled:opacity-45\",\n muted ? \"text-fg-muted\" : \"text-fg\",\n )}\n >\n <RadixSelect.ItemIndicator className=\"absolute left-2 text-signal\">\n <Check className=\"size-3.5\" />\n </RadixSelect.ItemIndicator>\n <RadixSelect.ItemText>{label}</RadixSelect.ItemText>\n {note && <span className=\"ml-auto font-mono text-xs text-fg-subtle\">{note}</span>}\n </RadixSelect.Item>\n );\n}\n","/*\n * A continuous value, with its number always on screen.\n *\n * A slider that drives what you are looking at -- a results threshold, an overlay opacity,\n * a caliper search range -- is a measurement instrument: the number matters as much as the\n * position, so it is rendered beside the track rather than left to a separate line of prose.\n */\n\nimport * as RadixSlider from \"@radix-ui/react-slider\";\nimport type { ReactNode } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A continuous value, with its number rendered beside the track.\n *\n * Controlled. The name (`aria-label`) and a surrounding `Field`'s description go on the\n * thumb — the element with `role=\"slider\"` that a keyboard and a screen reader operate.\n * Radix exposes `data-disabled` and `data-orientation` on the parts.\n */\nexport function Slider({\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 1,\n step = 0.01,\n readout,\n disabled = false,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ownDescribedBy,\n className,\n}: {\n /** The current value. */\n value: number;\n /** Called on every move. */\n onValueChange: (value: number) => void;\n /** Called once when a drag or a key press ends — for updates too expensive to run per move. */\n onValueCommit?: ((value: number) => void) | undefined;\n /** Defaults to 0. */\n min?: number | undefined;\n /** Defaults to 1. */\n max?: number | undefined;\n /** Defaults to 0.01. */\n step?: number | undefined;\n /** Rendered to the right of the track. Give it a fixed-width mono span for stability. */\n readout?: ReactNode;\n /** Blocks the thumb and dims the track. */\n disabled?: boolean | undefined;\n /** Names the slider (set on the thumb). */\n \"aria-label\"?: string | undefined;\n /** Ids of elements describing the slider, merged with a surrounding `Field`'s. */\n \"aria-describedby\"?: string | undefined;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const described = useFieldDescription(ownDescribedBy);\n\n return (\n // Grows by default: a slider that does not fill the space it is given collapses to a\n // few pixels of track, which is unusable and reads as a rendering fault.\n <div className={cn(\"flex min-w-0 flex-1 items-center gap-3\", className)}>\n <RadixSlider.Root\n value={[value]}\n onValueChange={([next]) => next !== undefined && onValueChange(next)}\n onValueCommit={([next]) => next !== undefined && onValueCommit?.(next)}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n className={cn(\n \"relative flex h-5 min-w-0 flex-1 touch-none items-center select-none\",\n \"data-disabled:opacity-50\",\n )}\n >\n <RadixSlider.Track className=\"relative h-1 grow overflow-hidden rounded-full bg-line\">\n <RadixSlider.Range className=\"absolute h-full bg-signal\" />\n </RadixSlider.Track>\n <RadixSlider.Thumb\n aria-label={ariaLabel}\n {...described}\n className={cn(\n \"block size-3.5 rounded-full border-2 border-signal bg-surface transition-shadow\",\n \"hover:shadow-[0_0_0_4px] hover:shadow-signal/20\",\n \"disabled:cursor-not-allowed\",\n focusRing,\n )}\n />\n </RadixSlider.Root>\n {readout && (\n <span className=\"shrink-0 font-mono text-xs text-fg tabular-nums\">{readout}</span>\n )}\n </div>\n );\n}\n","/*\n * A table.\n *\n * `numeric` is the column property that matters here. Quantities are set in mono with\n * tabular figures and aligned right, so a column of scores or measurements can be compared\n * by eye down its decimal point rather than read one row at a time.\n */\n\nimport type { KeyboardEvent, MouseEvent, ReactNode } from \"react\";\n\nimport { byDensity, useDensity } from \"./Density\";\nimport { cn } from \"./cn\";\n\n/** One column of a `Table`: its header and how to render a row's cell. */\nexport type Column<Row> = {\n /** Unique among the columns; the React key of its header and cells. */\n key: string;\n /** The `<th>` content. */\n header: ReactNode;\n /** Right-aligned, mono, tabular. Use for anything that is a quantity. */\n numeric?: boolean;\n /** A CSS width for the column, e.g. `\"6rem\"`. */\n width?: string;\n /** The cell's content for a row. */\n cell: (row: Row) => ReactNode;\n};\n\n/**\n * A table of rows, with numeric columns set in mono and right-aligned.\n *\n * With `onRowClick` the rows are keyboard-reachable (Tab, then Enter or Space) while keeping\n * their table semantics. The active row carries `aria-current` and `data-state=\"active\"`;\n * padding follows the density in force. With no rows it renders the `empty` message instead.\n */\nexport function Table<Row>({\n columns,\n rows,\n rowKey,\n empty = \"Nothing here.\",\n caption,\n className,\n onRowClick,\n isRowActive,\n onRowHover,\n}: {\n /** The columns, in order. */\n columns: Column<Row>[];\n /** The rows, in order. */\n rows: Row[];\n /** A stable key per row. */\n rowKey: (row: Row, index: number) => string | number;\n /** Shown instead of the table when there are no rows. Defaults to \"Nothing here.\". */\n empty?: ReactNode;\n /** Names the table for assistive technology (a visually hidden `<caption>`). */\n caption?: string | undefined;\n /** Merged with the scroll wrapper's (or the empty message's) own classes through `cn`. */\n className?: string | undefined;\n /** Makes rows activatable. Keyboard-reachable, so a table used as a list of\n * destinations is not a mouse-only control. The event is passed so a caller can read\n * modifiers — a list kept in step with a canvas needs shift-range and meta-toggle to mean\n * the same thing in both places. */\n onRowClick?: (\n row: Row,\n index: number,\n event: MouseEvent<HTMLTableRowElement> | KeyboardEvent<HTMLTableRowElement>,\n ) => void;\n /** Marks the row that is current — the selected match, the open frame. Rendered as\n * `aria-current`, so it is announced rather than only tinted. */\n isRowActive?: (row: Row, index: number) => boolean;\n /** Pointer enter/leave, for tables kept in step with a canvas overlay.\n * `null` on leave. */\n onRowHover?: (row: Row | null, index: number | null) => void;\n}) {\n const density = useDensity();\n\n if (rows.length === 0) {\n return (\n <p\n className={cn(\n \"text-center text-fg-muted\",\n byDensity(density, \"py-6 text-sm\", \"py-3 text-xs\"),\n className,\n )}\n >\n {empty}\n </p>\n );\n }\n\n return (\n <div className={cn(\"w-full overflow-x-auto\", className)}>\n <table className={cn(\"w-full text-left\", byDensity(density, \"text-sm\", \"text-[11px]\"))}>\n {caption && <caption className=\"sr-only\">{caption}</caption>}\n <thead>\n <tr className=\"border-b border-line\">\n {columns.map((column) => (\n <th\n key={column.key}\n scope=\"col\"\n style={column.width ? { width: column.width } : undefined}\n className={cn(\n \"font-medium text-fg-muted\",\n byDensity(density, \"pb-2 pr-3 text-xs\", \"pb-1 pr-2 text-[10px]\"),\n column.numeric && \"text-right\",\n )}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr\n key={rowKey(row, index)}\n data-state={isRowActive?.(row, index) ? \"active\" : undefined}\n className={cn(\n \"border-b border-line/60 last:border-0\",\n onRowClick && \"cursor-pointer hover:bg-raised\",\n isRowActive?.(row, index) && \"bg-signal/10\",\n )}\n tabIndex={onRowClick ? 0 : undefined}\n // Deliberately *not* `role=\"button\"`. Overriding a `<tr>`'s implicit `row`\n // makes its `<td>`s' implicit `cell` invalid too — the cells have no row to\n // belong to — so a screen reader stops announcing the table as a table and a\n // reader loses the column each value came from, which on a table of\n // quantities is the whole content. `tabIndex` plus the Enter/Space handler\n // below is what makes the row reachable; the role stays what the element is.\n aria-current={isRowActive?.(row, index) ? true : undefined}\n onClick={onRowClick ? (event) => onRowClick(row, index, event) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index, event);\n }\n }\n : undefined\n }\n onPointerEnter={onRowHover ? () => onRowHover(row, index) : undefined}\n onPointerLeave={onRowHover ? () => onRowHover(null, null) : undefined}\n >\n {columns.map((column) => (\n <td\n key={column.key}\n className={cn(\n \"align-middle text-fg last:pr-0\",\n byDensity(density, \"py-2 pr-3\", \"py-0.5 pr-2\"),\n column.numeric && \"text-right font-mono tabular-nums\",\n )}\n >\n {column.cell(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n","/*\n * The two binary controls, and the difference between them.\n *\n * A `Switch` changes how the thing behaves from now on -- \"allow downloads\", \"show the\n * mask\", \"lock the caliper axis\" -- and reads as a setting. A `Checkbox` asserts something\n * about a state you are about to commit -- \"I have read the warnings\", \"keep this channel\"\n * -- and reads as a statement.\n */\n\nimport * as RadixCheckbox from \"@radix-ui/react-checkbox\";\nimport * as RadixSwitch from \"@radix-ui/react-switch\";\nimport { Check, Minus } from \"lucide-react\";\nimport { useId, type MouseEvent, type ReactNode } from \"react\";\n\nimport { useFieldDescription } from \"./Field\";\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A setting: changes how the thing behaves from now on, and reads as one (\"show the mask\").\n * Always labelled; the label and the description sit beside the switch.\n *\n * Controlled. Radix exposes `data-state` (`checked`/`unchecked`) and `data-disabled`.\n */\nexport function Switch({\n checked,\n onCheckedChange,\n label,\n description,\n disabled = false,\n className,\n}: {\n /** Whether it is on. */\n checked: boolean;\n /** Called with the new state. */\n onCheckedChange: (checked: boolean) => void;\n /** What the setting is. */\n label: ReactNode;\n /** What turning it on does. */\n description?: ReactNode;\n /** Blocks the switch. */\n disabled?: boolean | undefined;\n /** Merged with the row's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const descriptionId = useId();\n return (\n <label className={cn(\"flex min-w-0 items-start gap-2.5\", className)}>\n <RadixSwitch.Root\n checked={checked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n aria-describedby={description ? descriptionId : undefined}\n className={cn(\n \"mt-0.5 h-4 w-7 shrink-0 rounded-full border border-line-strong bg-raised transition-colors\",\n \"data-[state=checked]:border-signal data-[state=checked]:bg-signal\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRing,\n )}\n >\n <RadixSwitch.Thumb\n className={cn(\n \"block size-3 rounded-full bg-fg-subtle transition-transform will-change-transform\",\n \"translate-x-0.5 data-[state=checked]:translate-x-3.5 data-[state=checked]:bg-signal-fg\",\n )}\n />\n </RadixSwitch.Root>\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm leading-tight text-fg\">{label}</span>\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n </span>\n </label>\n );\n}\n\n/**\n * A statement about a state you are about to commit (\"keep this channel\"). With a `label`\n * it renders its own row; without one it is a bare box (give it `aria-label`, e.g. in a\n * table cell).\n *\n * Controlled. Radix exposes `data-state` (`checked`/`unchecked`/`indeterminate`) and\n * `data-disabled`; inside a `Field` it is described by the field's description and error.\n */\nexport function Checkbox({\n checked,\n onCheckedChange,\n onClick,\n label,\n description,\n disabled = false,\n \"aria-label\": ariaLabel,\n className,\n}: {\n /** `\"indeterminate\"` for a partial selection over a set. */\n checked: boolean | \"indeterminate\";\n /** Called with the new state (never `\"indeterminate\"`: a click resolves it). */\n onCheckedChange: (checked: boolean) => void;\n /**\n * For a checkbox whose meaning depends on the modifier keys — extending a range with\n * shift, toggling one out of a selection with the platform key. `onCheckedChange` gets a\n * boolean and cannot carry them. Keyboard activation synthesises a click with its\n * modifiers intact, so this sees both input paths.\n */\n onClick?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;\n /** What checking it asserts. Omit for a bare box. */\n label?: ReactNode;\n /** More about the statement, under the label. */\n description?: ReactNode;\n /** Blocks the box. */\n disabled?: boolean | undefined;\n /** For a checkbox in a table cell, where the row is the label. */\n \"aria-label\"?: string | undefined;\n /** Merged with the row's (or, without a label, the box's) own classes through `cn`. */\n className?: string | undefined;\n}) {\n const descriptionId = useId();\n const described = useFieldDescription(description ? descriptionId : undefined);\n const box = (\n <RadixCheckbox.Root\n checked={checked}\n onCheckedChange={(next) => onCheckedChange(next === true)}\n onClick={onClick}\n disabled={disabled}\n aria-label={ariaLabel}\n {...described}\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded-[0.25rem] border border-line-strong bg-raised transition-colors\",\n \"data-[state=checked]:border-signal data-[state=checked]:bg-signal\",\n \"data-[state=indeterminate]:border-signal data-[state=indeterminate]:bg-signal\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n focusRing,\n label === undefined && className,\n )}\n >\n <RadixCheckbox.Indicator className=\"text-signal-fg\">\n {checked === \"indeterminate\" ? (\n <Minus className=\"size-3\" strokeWidth={3} />\n ) : (\n <Check className=\"size-3\" strokeWidth={3} />\n )}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n );\n\n if (label === undefined) return box;\n\n return (\n <label className={cn(\"flex min-w-0 items-start gap-2.5\", className)}>\n <span className=\"mt-0.5\">{box}</span>\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm leading-tight text-fg\">{label}</span>\n {description && (\n <span id={descriptionId} className=\"text-xs leading-snug text-fg-muted\">\n {description}\n </span>\n )}\n </span>\n </label>\n );\n}\n","/*\n * A dense on/off control for a thing that is drawn on screen.\n *\n * `Switch` is a settings row — a stacked label, a description, generous padding — and\n * three of them side by side over an image canvas is a panel, not a toolbar. This is the\n * same binary decision in the space a toolbar has: one line, one target, and an optional\n * swatch carrying the colour the layer is actually drawn in — an overlay's caliper boxes,\n * a chart's series — so the legend and the control are the same object rather than two\n * things to reconcile.\n *\n * Hand-built on a native `button` with `role=\"switch\"`: this is not one of the four Radix\n * primitives the rest of the toggle vocabulary is built on, and a chip does not need one.\n */\n\nimport type { ReactNode } from \"react\";\n\nimport { cn, focusRing } from \"./cn\";\n\n/**\n * A dense on/off chip for a layer drawn on screen, with an optional swatch in the layer's\n * colour: a toolbar-sized `Switch` (`role=\"switch\"`, `aria-checked`).\n *\n * Controlled. State is exposed as `data-state` (`checked`/`unchecked`).\n */\nexport function ToggleChip({\n checked,\n onCheckedChange,\n children,\n swatch,\n disabled = false,\n title,\n className,\n}: {\n /** Whether the layer is shown. */\n checked: boolean;\n /** Called with the new state. */\n onCheckedChange: (checked: boolean) => void;\n /** The layer's name. */\n children: ReactNode;\n /** A CSS colour: the colour this layer is drawn in, shown as a dot. */\n swatch?: string;\n /** Blocks the chip. */\n disabled?: boolean | undefined;\n /** Why the control is disabled, or what the layer is. Never the only explanation. */\n title?: string | undefined;\n /** Merged with the chip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n data-state={checked ? \"checked\" : \"unchecked\"}\n disabled={disabled}\n title={title}\n onClick={() => onCheckedChange(!checked)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-control border px-2 py-1 text-xs whitespace-nowrap transition-colors\",\n checked\n ? \"border-line-strong bg-raised text-fg\"\n : \"border-line bg-transparent text-fg-muted hover:text-fg\",\n \"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-fg-muted\",\n focusRing,\n className,\n )}\n >\n {swatch !== undefined && (\n <span\n aria-hidden\n className={cn(\n \"size-2 shrink-0 rounded-full border transition-opacity\",\n checked ? \"opacity-100\" : \"opacity-30\",\n )}\n style={{ backgroundColor: swatch, borderColor: swatch }}\n />\n )}\n {children}\n </button>\n );\n}\n","/*\n * A tab strip.\n *\n * The count is set in mono beside the label rather than in parentheses inside it: it is a\n * quantity -- how many overrides sit behind an options tab, how many samples a filter chip\n * covers -- and mono makes it read as one at a glance.\n */\n\nimport type { KeyboardEvent } from \"react\";\n\nimport { cn, focusRing } from \"./ui/cn\";\n\n/** One tab of a `Tabs` strip. */\nexport interface TabItem<Id extends string> {\n /** Identifies the tab to `active` and `onSelect`. Unique within the strip. */\n id: Id;\n /** The tab's text, and its accessible name. */\n label: string;\n /** Shown after the label, for the chips that carry a row or override count. */\n count?: number;\n /** Shown but not selectable, and skipped by the arrow keys. */\n disabled?: boolean;\n /** Why this tab has nothing to show, for a reader wondering what they missed. */\n title?: string;\n}\n\nconst NEXT_KEYS: Record<string, (index: number, length: number) => number> = {\n ArrowRight: (index, length) => (index + 1) % length,\n ArrowLeft: (index, length) => (index - 1 + length) % length,\n Home: () => 0,\n End: (_, length) => length - 1,\n};\n\n/**\n * A tab strip (`role=\"tablist\"`): the caller renders the panel for the `active` tab.\n *\n * Keyboard: one tab stop for the whole strip (the active tab); ←/→ move to the previous or\n * next enabled tab and select it, Home/End to the first and last. Each tab carries\n * `data-state` (`active`/`inactive`). Pass `idPrefix` to wire the tabs to the panel: the tab\n * for `id` gets the element id `<idPrefix>-tab-<id>` and the active tab gets\n * `aria-controls=\"<idPrefix>-panel-<id>\"`, so render the panel as a `role=\"tabpanel\"` element\n * with the id `<idPrefix>-panel-<active>` and `aria-labelledby=\"<idPrefix>-tab-<active>\"`.\n */\nexport function Tabs<Id extends string>({\n items,\n active,\n onSelect,\n label,\n idPrefix,\n className,\n}: {\n /** The tabs, in order. */\n items: TabItem<Id>[];\n /** The selected tab's id. */\n active: Id;\n /** Called with a tab's id when it is clicked or reached with the arrow keys. */\n onSelect: (id: Id) => void;\n /** Names the tab list. */\n label: string;\n /** Prefix for the tab and panel element ids; enables `aria-controls` (see above). */\n idPrefix?: string | undefined;\n /** Merged with the strip's own classes through `cn`. */\n className?: string | undefined;\n}) {\n const enabled = items.filter((item) => !item.disabled);\n // The one tab stop: the active tab, or the first enabled one when the active tab is\n // disabled or not in the list.\n const focusable = enabled.some((item) => item.id === active) ? active : enabled[0]?.id;\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const move = NEXT_KEYS[event.key];\n const target = event.target as HTMLElement;\n const current = enabled.findIndex((item) => item.id === target.dataset[\"tabId\"]);\n if (move === undefined || current === -1) return;\n event.preventDefault();\n const next = enabled[move(current, enabled.length)];\n if (next === undefined) return;\n const tabs = event.currentTarget.querySelectorAll<HTMLElement>('[role=\"tab\"]');\n for (const tab of tabs) if (tab.dataset[\"tabId\"] === next.id) tab.focus();\n if (next.id !== active) onSelect(next.id);\n };\n\n return (\n <div\n role=\"tablist\"\n aria-label={label}\n aria-orientation=\"horizontal\"\n onKeyDown={onKeyDown}\n className={cn(\n \"inline-flex max-w-full flex-wrap items-center gap-0.5 rounded-control border border-line bg-raised p-0.5\",\n className,\n )}\n >\n {items.map((item) => {\n const selected = item.id === active;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n id={idPrefix === undefined ? undefined : `${idPrefix}-tab-${item.id}`}\n aria-selected={selected}\n aria-controls={\n idPrefix !== undefined && selected ? `${idPrefix}-panel-${item.id}` : undefined\n }\n tabIndex={item.id === focusable ? 0 : -1}\n data-tab-id={item.id}\n data-state={selected ? \"active\" : \"inactive\"}\n disabled={item.disabled}\n title={item.title}\n onClick={() => onSelect(item.id)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-[0.3rem] px-2.5 py-1 text-xs font-medium transition-colors\",\n \"disabled:cursor-not-allowed disabled:opacity-40\",\n selected\n ? \"bg-surface text-fg shadow-sm\"\n : \"text-fg-muted hover:text-fg\",\n focusRing,\n )}\n >\n {item.label}\n {item.count !== undefined && item.count > 0 && (\n <span\n className={cn(\n \"rounded-full px-1.5 font-mono text-[10px] tabular-nums\",\n selected ? \"bg-signal/15 text-signal\" : \"bg-line text-fg-muted\",\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
|