@loworbitstudio/visor 1.30.0 → 1.31.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.
@@ -100,6 +100,32 @@
100
100
  }
101
101
  ]
102
102
  },
103
+ {
104
+ "name": "time-picker",
105
+ "type": "registry:ui",
106
+ "description": "A time picker with a typeable field and a popover of hour and minute columns, in a 12- or 24-hour cycle with a configurable minute step.",
107
+ "category": "form",
108
+ "dependencies": [
109
+ "@radix-ui/react-popover",
110
+ "@phosphor-icons/react",
111
+ "@loworbitstudio/visor-core"
112
+ ],
113
+ "registryDependencies": [
114
+ "utils"
115
+ ],
116
+ "files": [
117
+ {
118
+ "path": "components/ui/time-picker/time-picker.tsx",
119
+ "type": "registry:ui",
120
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport { Clock } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./time-picker.module.css\"\n\nexport type HourCycle = 12 | 24\n\nexport interface TimePickerProps {\n /** Selected time as a 24-hour \"HH:MM\" string, whatever the display cycle */\n value?: string\n /** Initial value when uncontrolled */\n defaultValue?: string\n /** Called with a 24-hour \"HH:MM\" string, or undefined when the field is cleared */\n onChange?: (value: string | undefined) => void\n /** Display and typing cycle. Defaults from the locale. */\n hourCycle?: HourCycle\n /** BCP 47 locale that decides the default hour cycle. Defaults to the document language. */\n locale?: string\n /** Minutes between choices in the popover; typed times snap to it */\n minuteStep?: number\n /** Placeholder text when no time is selected */\n placeholder?: string\n /** Whether the picker is disabled */\n disabled?: boolean\n /** Whether the popover is open (controlled) */\n open?: boolean\n /** Called when the popover opens or closes */\n onOpenChange?: (open: boolean) => void\n /** Element the popover portals into. Defaults to document.body. */\n container?: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Portal>[\"container\"]\n /** Accessible label for the clock button */\n triggerLabel?: string\n /** Additional class name for the field wrapper */\n className?: string\n id?: string\n name?: string\n \"aria-label\"?: string\n \"aria-labelledby\"?: string\n \"aria-invalid\"?: boolean | \"true\" | \"false\"\n}\n\nconst MINUTES_PER_DAY = 24 * 60\n\nconst VALUE_PATTERN = /^([01]\\d|2[0-3]):([0-5]\\d)$/\n\n/** Split a \"HH:MM\" value into minutes since midnight, or undefined when malformed. */\nexport function toMinutes(value: string | undefined): number | undefined {\n const match = value ? VALUE_PATTERN.exec(value) : null\n return match ? Number(match[1]) * 60 + Number(match[2]) : undefined\n}\n\n/** Minutes since midnight to a 24-hour \"HH:MM\" string. */\nexport function fromMinutes(total: number): string {\n const wrapped = ((Math.round(total) % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY\n const h = Math.floor(wrapped / 60)\n const m = wrapped % 60\n return `${String(h).padStart(2, \"0\")}:${String(m).padStart(2, \"0\")}`\n}\n\n/** Round to the nearest step, carrying into the next hour (and past midnight). */\nexport function snapToStep(total: number, step: number): number {\n const safe = Number.isFinite(step) && step >= 1 ? Math.floor(step) : 1\n return (Math.round(total / safe) * safe) % MINUTES_PER_DAY\n}\n\n/** Display text for a \"HH:MM\" value in the given cycle. */\nexport function formatTime(value: string | undefined, cycle: HourCycle): string {\n const total = toMinutes(value)\n if (total === undefined) return \"\"\n const h = Math.floor(total / 60)\n const mm = String(total % 60).padStart(2, \"0\")\n if (cycle === 24) return `${String(h).padStart(2, \"0\")}:${mm}`\n return `${h % 12 === 0 ? 12 : h % 12}:${mm} ${h < 12 ? \"AM\" : \"PM\"}`\n}\n\n/**\n * Parse what a person types into minutes since midnight. Accepts \"16:30\",\n * \"1630\", \"4:30pm\", \"4 pm\", \"4p\". In the 12-hour cycle a bare 1-12 hour has\n * no period, so it keeps `fallbackPeriod` (the period of the current value).\n */\nexport function parseTime(\n text: string,\n cycle: HourCycle,\n fallbackPeriod: \"am\" | \"pm\" = \"am\"\n): number | undefined {\n const match = /^(\\d{1,2}):?(\\d{2})?\\s*(?:([ap])\\.?m?\\.?)?$/.exec(text.trim().toLowerCase())\n if (!match) return undefined\n let h = Number(match[1])\n const m = match[2] === undefined ? 0 : Number(match[2])\n if (m > 59) return undefined\n const period = match[3] ? (match[3] === \"a\" ? \"am\" : \"pm\") : undefined\n if (period) {\n if (h < 1 || h > 12) return undefined\n h = (h % 12) + (period === \"pm\" ? 12 : 0)\n } else if (h > 23) {\n return undefined\n } else if (cycle === 12 && h >= 1 && h <= 12) {\n h = (h % 12) + (fallbackPeriod === \"pm\" ? 12 : 0)\n }\n return h * 60 + m\n}\n\n/** 12 for locales that default to a 12-hour clock, otherwise 24. */\nexport function resolveHourCycle(locale?: string): HourCycle {\n try {\n const resolved = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions()\n if (resolved.hourCycle) return resolved.hourCycle === \"h11\" || resolved.hourCycle === \"h12\" ? 12 : 24\n return resolved.hour12 ? 12 : 24\n } catch {\n return 24\n }\n}\n\nfunction documentLang(): string | undefined {\n if (typeof document === \"undefined\") return undefined\n return document.documentElement.lang || undefined\n}\n\nconst pad2 = (n: number) => String(n).padStart(2, \"0\")\n\ninterface ColumnProps {\n label: string\n items: { key: string; label: string; selected: boolean; onSelect: () => void }[]\n slot: string\n}\n\n/** One scrolling listbox column. Arrow keys move focus; Enter or Space selects. */\nfunction Column({ label, items, slot }: ColumnProps) {\n const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n const keys = [\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"]\n if (!keys.includes(event.key)) return\n const options = Array.from(\n event.currentTarget.querySelectorAll<HTMLButtonElement>(\"[role='option']\")\n )\n const at = options.indexOf(document.activeElement as HTMLButtonElement)\n const next =\n event.key === \"Home\" ? 0\n : event.key === \"End\" ? options.length - 1\n : Math.min(options.length - 1, Math.max(0, at + (event.key === \"ArrowDown\" ? 1 : -1)))\n event.preventDefault()\n options[next]?.focus()\n }\n\n const tabbable = Math.max(0, items.findIndex((item) => item.selected))\n return (\n <div\n role=\"listbox\"\n aria-label={label}\n data-slot={slot}\n className={styles.column}\n onKeyDown={onKeyDown}\n >\n {items.map((item, i) => (\n <button\n key={item.key}\n type=\"button\"\n role=\"option\"\n aria-selected={item.selected}\n tabIndex={i === tabbable ? 0 : -1}\n data-selected={item.selected || undefined}\n className={cn(styles.option, item.selected && styles.optionSelected)}\n onClick={item.onSelect}\n >\n {item.label}\n </button>\n ))}\n </div>\n )\n}\n\nconst TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(\n (\n {\n value: valueProp,\n defaultValue,\n onChange,\n hourCycle,\n locale,\n minuteStep = 1,\n placeholder,\n disabled = false,\n open: openProp,\n onOpenChange,\n container,\n triggerLabel = \"Choose time\",\n className,\n id,\n name,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-invalid\": ariaInvalid,\n },\n ref\n ) => {\n const [innerValue, setInnerValue] = React.useState(defaultValue)\n const value = valueProp !== undefined ? valueProp : innerValue\n const [innerOpen, setInnerOpen] = React.useState(false)\n const open = openProp !== undefined ? openProp : innerOpen\n const contentRef = React.useRef<HTMLDivElement>(null)\n\n const cycle: HourCycle = hourCycle ?? resolveHourCycle(locale ?? documentLang())\n const step = Number.isFinite(minuteStep) && minuteStep >= 1 ? Math.floor(minuteStep) : 1\n const total = toMinutes(value)\n\n const [draft, setDraft] = React.useState<string | null>(null)\n const display = draft ?? formatTime(value, cycle)\n\n const setOpen = (next: boolean) => {\n setInnerOpen(next)\n onOpenChange?.(next)\n }\n\n const emit = (next: string | undefined) => {\n setInnerValue(next)\n onChange?.(next)\n }\n\n const commit = (text: string) => {\n setDraft(null)\n if (text.trim() === \"\") {\n if (value !== undefined) emit(undefined)\n return\n }\n const parsed = parseTime(text, cycle, total !== undefined && total >= 720 ? \"pm\" : \"am\")\n // Unparseable text reverts to the last good value.\n if (parsed === undefined) return\n const next = fromMinutes(snapToStep(parsed, step))\n if (next !== value) emit(next)\n }\n\n const stepBy = (direction: 1 | -1) => {\n const base = total ?? (direction === 1 ? -step : MINUTES_PER_DAY)\n emit(fromMinutes(snapToStep(base, step) + direction * step))\n setDraft(null)\n }\n\n const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"ArrowDown\" && event.altKey) {\n event.preventDefault()\n setOpen(true)\n } else if (event.key === \"Enter\") {\n commit(event.currentTarget.value)\n } else if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n event.preventDefault()\n stepBy(event.key === \"ArrowUp\" ? 1 : -1)\n }\n }\n\n // Setting one part keeps the other; an empty field starts from 00.\n const h24 = total === undefined ? undefined : Math.floor(total / 60)\n const minute = total === undefined ? undefined : total % 60\n const setParts = (hour: number | undefined, min: number | undefined) =>\n emit(fromMinutes((hour ?? 0) * 60 + (min ?? 0)))\n\n const hourItems =\n cycle === 24\n ? Array.from({ length: 24 }, (_, h) => ({\n key: String(h),\n label: pad2(h),\n selected: h24 === h,\n onSelect: () => setParts(h, minute),\n }))\n : Array.from({ length: 12 }, (_, i) => {\n const h12 = i + 1\n const isPm = h24 !== undefined && h24 >= 12\n return {\n key: String(h12),\n label: String(h12),\n selected: h24 !== undefined && (h24 % 12 === 0 ? 12 : h24 % 12) === h12,\n onSelect: () => setParts((h12 % 12) + (isPm ? 12 : 0), minute),\n }\n })\n\n const minuteItems = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).map(\n (m) => ({\n key: String(m),\n label: pad2(m),\n selected: minute === m,\n onSelect: () => setParts(h24, m),\n })\n )\n\n const periodItems = ([\"AM\", \"PM\"] as const).map((p) => {\n const isPm = p === \"PM\"\n return {\n key: p,\n label: p,\n selected: h24 !== undefined && (h24 >= 12) === isPm,\n onSelect: () => {\n const base = h24 ?? 0\n setParts((base % 12) + (isPm ? 12 : 0), minute)\n },\n }\n })\n\n return (\n <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>\n <PopoverPrimitive.Anchor asChild>\n <div\n data-slot=\"time-picker\"\n data-disabled={disabled || undefined}\n className={cn(styles.field, disabled && styles.disabled, className)}\n >\n <input\n ref={ref}\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n spellCheck={false}\n data-slot=\"time-picker-input\"\n className={styles.input}\n value={display}\n placeholder={placeholder ?? (cycle === 24 ? \"HH:MM\" : \"H:MM AM\")}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-invalid={ariaInvalid}\n onChange={(e) => setDraft(e.target.value)}\n onBlur={(e) => {\n if (draft !== null) commit(e.target.value)\n }}\n onKeyDown={onInputKeyDown}\n />\n {name && <input type=\"hidden\" name={name} value={value ?? \"\"} />}\n <PopoverPrimitive.Trigger\n type=\"button\"\n data-slot=\"time-picker-trigger\"\n className={styles.trigger}\n disabled={disabled}\n aria-label={triggerLabel}\n >\n <Clock size={16} aria-hidden=\"true\" />\n </PopoverPrimitive.Trigger>\n </div>\n </PopoverPrimitive.Anchor>\n <PopoverPrimitive.Portal container={container}>\n <PopoverPrimitive.Content\n ref={contentRef}\n data-slot=\"time-picker-content\"\n data-hour-cycle={cycle}\n className={styles.content}\n align=\"start\"\n sideOffset={4}\n onOpenAutoFocus={(event) => {\n // Land on the chosen hour (or the first) so arrow keys work at once.\n event.preventDefault()\n const target =\n contentRef.current?.querySelector<HTMLElement>(\"[role='option'][tabindex='0']\")\n target?.focus()\n target?.scrollIntoView?.({ block: \"nearest\" })\n }}\n >\n <Column label=\"Hour\" slot=\"time-picker-hours\" items={hourItems} />\n <Column label=\"Minute\" slot=\"time-picker-minutes\" items={minuteItems} />\n {cycle === 12 && (\n <Column label=\"AM or PM\" slot=\"time-picker-period\" items={periodItems} />\n )}\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n )\n }\n)\nTimePicker.displayName = \"TimePicker\"\n\nexport { TimePicker }\n"
121
+ },
122
+ {
123
+ "path": "components/ui/time-picker/time-picker.module.css",
124
+ "type": "registry:ui",
125
+ "content": "/* Field wrapper: the date-picker trigger anatomy, carrying a typeable input.\n Geometry matches Input at size sm so the two share a baseline in a row. */\n.field {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n height: 2.25rem;\n /* VI-655: the border only holds box geometry. The visible resting edge is an\n inset outline from the shared --control-edge-* tokens, so\n --control-edge-width: 0 removes it with no layout shift. */\n border: 1px solid transparent;\n border-radius: var(--radius-sm, 0.25rem);\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1.5;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .field {\n background-color: var(--field-control-bg, var(--surface-card));\n height: 33.5px;\n border-radius: var(--radius-md);\n}\n\n@media (hover: hover) {\n .field:hover:not(:focus-within):not(.disabled):not(:has(.input[aria-invalid=\"true\"])) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch, so both stay visible with edges off. */\n.field:focus-within {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.field:has(.input[aria-invalid=\"true\"]) {\n outline-color: transparent;\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n.field:has(.input[aria-invalid=\"true\"]):focus-within {\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n.disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.input {\n flex: 1;\n min-width: 0;\n height: 100%;\n padding: 0 var(--spacing-3, 0.75rem);\n border: 0;\n border-radius: inherit;\n outline: none;\n background: transparent;\n color: inherit;\n line-height: inherit;\n}\n\n.input::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n.input:disabled {\n pointer-events: none;\n}\n\n/* Clock button: the popover trigger. Keeps the field's own focus ring quiet\n until the button itself takes focus. */\n.trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n height: 100%;\n padding: 0 var(--spacing-3, 0.75rem);\n border: 0;\n border-radius: inherit;\n background: transparent;\n color: var(--text-secondary, #9ca3af);\n cursor: pointer;\n}\n\n.trigger:disabled {\n pointer-events: none;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(:disabled) {\n color: var(--text-primary, #111827);\n }\n}\n\n.trigger:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 var(--focus-ring-width, 2px) var(--border-focus, #3b82f6);\n}\n\n/* Popover content */\n.content {\n z-index: 50;\n display: flex;\n font-family: var(--font-body, inherit);\n gap: var(--spacing-1, 0.25rem);\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-md);\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline-strong, #d1d5db);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n}\n\n.column {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n max-height: 14rem;\n min-width: 3.25rem;\n overflow-y: auto;\n scrollbar-width: thin;\n}\n\n.option {\n flex-shrink: 0;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n border: 0;\n border-radius: var(--radius-sm, 0.25rem);\n background: transparent;\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n font-variant-numeric: tabular-nums;\n text-align: center;\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .option:hover:not(.optionSelected) {\n background-color: var(--surface-subtle, #f3f4f6);\n }\n}\n\n.option:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 var(--focus-ring-width, 2px) var(--border-focus, #3b82f6);\n}\n\n.optionSelected {\n background-color: var(--primary, #6366f1);\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Label centring (VI-684): trim the input's inner line box to the capital band,\n as Input does. Without text-box support rendering is unchanged. */\n@supports (text-box: trim-both cap alphabetic) {\n .input {\n text-box: trim-both cap alphabetic;\n }\n}\n"
126
+ }
127
+ ]
128
+ },
103
129
  {
104
130
  "name": "timeline",
105
131
  "type": "registry:ui",
@@ -147,7 +173,7 @@
147
173
  {
148
174
  "path": "components/ui/button/button.module.css",
149
175
  "type": "registry:ui",
150
- "content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n /* VI-655: the border only holds box geometry and is always transparent. The\n faces that draw an edge (outline, gated, dlg ghost) draw it on ::after —\n see \"Edged faces\" below — so the button's own outline stays free for the\n focus ring. */\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Label (VI-682). Bare text is wrapped in a block span whose line box is trimmed\n to the capital-height band, so the flex container centres the ink and not the\n font's ascent/descent (which put Product Sans ~1px high in a 40px button).\n Without text-box (Firefox) the span is a plain block and rendering is exactly\n what it was before. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n\n /* The compact dlg size is height:auto, so trimming would shrink its box. */\n .sizeDlg .label {\n text-box: normal;\n }\n}\n\n/* Blink only. Blink snaps the text baseline to a whole CSS px, which leaves the\n trimmed ink up to half a pixel off for fractional cap heights (12px type). The\n baseline lands at (box + cap) / 2; this hands back what the snap took. WebKit\n positions the baseline exactly, so correcting there would overshoot. The\n -apple-pay-button-style probe is the WebKit-only property that tells them apart. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n\n .sizeDlg .label {\n transform: none;\n }\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Edged faces (VI-655). The resting edge is an inset outline over the border\n box, on the shared --control-edge-* tokens, so --control-edge-width: 0 on any\n ancestor removes it with no layout shift. Each face sets only the colour, in\n the same source position its border-color used to hold. */\n.variantOutline,\n.base[data-gated=\"true\"],\n.sizeDlg.variantGhost {\n position: relative;\n}\n\n.variantOutline::after,\n.base[data-gated=\"true\"]::after,\n.sizeDlg.variantGhost::after {\n content: \"\";\n position: absolute;\n inset: -1px;\n border-radius: inherit;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) transparent;\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.variantOutline::after {\n outline-color: var(--control-edge-color, var(--border-strong, #e5e7eb));\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n.base[data-gated=\"true\"]::after {\n outline-color: var(--control-edge-color, var(--border-default, #e5e7eb));\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values.\n\n Type and rounding are theme hooks (VI-656): --button-<size>-font-size,\n -text-transform, -letter-spacing and -radius set one size, and the\n unprefixed --button-text-transform / -letter-spacing / -radius set every\n size. Unset, each size keeps its shipped type size and rounding; case and\n tracking live in visor-base, after the dlg size. */\n.sizeSm {\n --label-box-height: 2rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--button-sm-font-size, var(--font-size-xs, 0.75rem));\n border-radius: var(--button-sm-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeMd {\n --label-box-height: 2.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n font-size: var(--button-md-font-size, var(--font-size-sm, 0.875rem));\n border-radius: var(--button-md-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: var(--button-sm-font-size, 0.8125rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n --label-box-height: 2.125rem;\n height: var(--label-box-height);\n line-height: normal;\n}\n\n.sizeLg {\n --label-box-height: 3rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--button-lg-font-size, var(--font-size-base, 1rem));\n border-radius: var(--button-lg-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n/* Dialog-footer button — dlg-btn (Animal dialog substrate, VI-620).\n Compact modal-action metrics measured from the AN prototype (11.5px / 600,\n ~7px 13px padding, 7px radius), normalized onto Visor tokens: font-size-2xs\n (11px), spacing-2/spacing-3 padding (8/12px), radius-lg (8px). Height is\n auto so the compact padding drives the box. Color faces reuse the existing\n variant axis — primary via variant=\"default\", ghost via variant=\"ghost\"\n (refined below to a hairline-outlined, secondary-ink face), danger via\n variant=\"destructive\" (refined below to a subtle-danger face). */\n.sizeDlg {\n height: auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--button-dlg-font-size, var(--font-size-2xs, 0.6875rem));\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n border-radius: var(--button-dlg-radius, var(--button-radius, var(--radius-lg, 0.5rem)));\n}\n\n/* Icon-only button — a square mark around one glyph (VI-659). The box is the\n glyph plus a hit-target pad on every side, so width always equals height;\n --button-icon-size sets the glyph and --button-icon-pad the pad. A 1em glyph\n (Phosphor's default) takes the size from font-size; a glyph with its own\n width/height attributes is sized by the svg rule. No edge on ghost; on\n outline the edge is the shared ::after one, so --control-edge-width: 0 turns\n it off. */\n.sizeIcon {\n width: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n height: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n padding: 0;\n flex-shrink: 0;\n font-size: var(--button-icon-size, 1rem);\n border-radius: var(--button-icon-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeIcon > svg {\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n flex-shrink: 0;\n}\n\n/* Ghost icon ink — the mark sits beside what it acts on, so it rests quieter\n than a labelled ghost button and comes up to primary ink on hover. :where()\n keeps it at one-class weight, so the held (open / active) ghost rule above\n still wins. */\n:where(.sizeIcon).variantGhost {\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n}\n\n@media (hover: hover) {\n :where(.sizeIcon).variantGhost:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n/* Case and tracking are inherited properties, so any unlayered fallback would\n override something: `inherit` beats a layered utility, `revert-layer` drops\n an unlayered consumer rule. In visor-base, the lowest Visor layer (the reset's),\n every consumer rule still wins, and when the hook is unset revert-layer\n leaves the element exactly as if these declarations were absent. */\n@layer visor-base {\n .sizeSm {\n text-transform: var(--button-sm-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-sm-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeMd {\n text-transform: var(--button-md-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-md-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeLg {\n text-transform: var(--button-lg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-lg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeDlg {\n text-transform: var(--button-dlg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-dlg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n}\n\n/* dlg ghost — transparent well with a hairline outline + secondary ink\n (AN dlg-btn ghost face: transparent / --ink-60 / 1px --hair). Two-class\n specificity keeps this scoped to the dlg size only. */\n.sizeDlg.variantGhost {\n color: var(--text-secondary, #6b7280);\n}\n\n.sizeDlg.variantGhost::after {\n outline-color: var(--control-edge-color, var(--hairline, var(--border-default, #e5e7eb)));\n}\n\n/* dlg danger — subtle-danger face (AN dlg-btn danger face: --red on a faint\n red fill), distinct from the solid `destructive` variant. */\n.sizeDlg.variantDestructive {\n background-color: var(--surface-error-subtle, color-mix(in srgb, var(--destructive, #ef4444) 8%, transparent));\n color: var(--text-error, var(--destructive, #b91c1c));\n}\n\n@media (hover: hover) {\n .sizeDlg.variantDestructive:hover {\n background-color: color-mix(in srgb, var(--destructive, #ef4444) 14%, transparent);\n }\n}\n\n/* Pending state — orthogonal to variant and size (VI-643).\n The idle children stay in the box at zero opacity so the control keeps its\n width, its height and its accessible name; the busy glyph paints over them\n in an absolutely-positioned overlay. A pending state that reflows has not\n fixed the defect it exists for, which is why nothing here is a leading slot\n that adds width.\n\n Themes tune the two timings via --button-pending-delay and\n --button-pending-min-duration. Neither is declared here on purpose: an\n element-level declaration shadows anything a theme emits on :root (VI-625). */\n.base[data-pending=\"true\"] {\n position: relative;\n cursor: progress;\n}\n\n/* No press dip while the action is in flight — the button is not accepting\n the click, so it must not pretend to. */\n.base[data-pending=\"true\"]:active {\n transform: none;\n transition: none;\n}\n\n/* Geometry holder. Mirrors .base's own flex metrics so wrapping the children\n does not change how they lay out. */\n.pendingLabel {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n opacity: 0;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.pendingOverlay {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n padding: inherit;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n pointer-events: none;\n}\n\n/* Clamped, so a pending label longer than the idle one can never widen the\n box it is being held inside. */\n.pendingText {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n"
176
+ "content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n /* VI-655: the border only holds box geometry and is always transparent. The\n faces that draw an edge (outline, gated, dlg ghost) draw it on ::after —\n see \"Edged faces\" below — so the button's own outline stays free for the\n focus ring. */\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Label (VI-682; shared spec: the label-centering doc under docs/). Bare text is wrapped in a block span whose line box is trimmed\n to the capital-height band, so the flex container centres the ink and not the\n font's ascent/descent (which put Product Sans ~1px high in a 40px button).\n Without text-box (Firefox) the span is a plain block and rendering is exactly\n what it was before. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n\n /* The compact dlg size is height:auto, so trimming would shrink its box. */\n .sizeDlg .label {\n text-box: normal;\n }\n}\n\n/* Blink only. Blink snaps the text baseline to a whole CSS px, which leaves the\n trimmed ink up to half a pixel off for fractional cap heights (12px type). The\n baseline lands at (box + cap) / 2; this hands back what the snap took. WebKit\n positions the baseline exactly, so correcting there would overshoot. The\n -apple-pay-button-style probe is the WebKit-only property that tells them apart. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n\n .sizeDlg .label {\n transform: none;\n }\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Edged faces (VI-655). The resting edge is an inset outline over the border\n box, on the shared --control-edge-* tokens, so --control-edge-width: 0 on any\n ancestor removes it with no layout shift. Each face sets only the colour, in\n the same source position its border-color used to hold. */\n.variantOutline,\n.base[data-gated=\"true\"],\n.sizeDlg.variantGhost {\n position: relative;\n}\n\n.variantOutline::after,\n.base[data-gated=\"true\"]::after,\n.sizeDlg.variantGhost::after {\n content: \"\";\n position: absolute;\n inset: -1px;\n border-radius: inherit;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) transparent;\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.variantOutline::after {\n outline-color: var(--control-edge-color, var(--border-strong, #e5e7eb));\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n.base[data-gated=\"true\"]::after {\n outline-color: var(--control-edge-color, var(--border-default, #e5e7eb));\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values.\n\n Type and rounding are theme hooks (VI-656): --button-<size>-font-size,\n -text-transform, -letter-spacing and -radius set one size, and the\n unprefixed --button-text-transform / -letter-spacing / -radius set every\n size. Unset, each size keeps its shipped type size and rounding; case and\n tracking live in visor-base, after the dlg size. */\n.sizeSm {\n --label-box-height: 2rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--button-sm-font-size, var(--font-size-xs, 0.75rem));\n border-radius: var(--button-sm-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeMd {\n --label-box-height: 2.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n font-size: var(--button-md-font-size, var(--font-size-sm, 0.875rem));\n border-radius: var(--button-md-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: var(--button-sm-font-size, 0.8125rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n --label-box-height: 2.125rem;\n height: var(--label-box-height);\n line-height: normal;\n}\n\n.sizeLg {\n --label-box-height: 3rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--button-lg-font-size, var(--font-size-base, 1rem));\n border-radius: var(--button-lg-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n/* Dialog-footer button — dlg-btn (Animal dialog substrate, VI-620).\n Compact modal-action metrics measured from the AN prototype (11.5px / 600,\n ~7px 13px padding, 7px radius), normalized onto Visor tokens: font-size-2xs\n (11px), spacing-2/spacing-3 padding (8/12px), radius-lg (8px). Height is\n auto so the compact padding drives the box. Color faces reuse the existing\n variant axis — primary via variant=\"default\", ghost via variant=\"ghost\"\n (refined below to a hairline-outlined, secondary-ink face), danger via\n variant=\"destructive\" (refined below to a subtle-danger face). */\n.sizeDlg {\n height: auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--button-dlg-font-size, var(--font-size-2xs, 0.6875rem));\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n border-radius: var(--button-dlg-radius, var(--button-radius, var(--radius-lg, 0.5rem)));\n}\n\n/* Icon-only button — a square mark around one glyph (VI-659). The box is the\n glyph plus a hit-target pad on every side, so width always equals height;\n --button-icon-size sets the glyph and --button-icon-pad the pad. A 1em glyph\n (Phosphor's default) takes the size from font-size; a glyph with its own\n width/height attributes is sized by the svg rule. No edge on ghost; on\n outline the edge is the shared ::after one, so --control-edge-width: 0 turns\n it off. */\n.sizeIcon {\n width: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n height: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n padding: 0;\n flex-shrink: 0;\n font-size: var(--button-icon-size, 1rem);\n border-radius: var(--button-icon-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeIcon > svg {\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n flex-shrink: 0;\n}\n\n/* Ghost icon ink — the mark sits beside what it acts on, so it rests quieter\n than a labelled ghost button and comes up to primary ink on hover. :where()\n keeps it at one-class weight, so the held (open / active) ghost rule above\n still wins. */\n:where(.sizeIcon).variantGhost {\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n}\n\n@media (hover: hover) {\n :where(.sizeIcon).variantGhost:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n/* Case and tracking are inherited properties, so any unlayered fallback would\n override something: `inherit` beats a layered utility, `revert-layer` drops\n an unlayered consumer rule. In visor-base, the lowest Visor layer (the reset's),\n every consumer rule still wins, and when the hook is unset revert-layer\n leaves the element exactly as if these declarations were absent. */\n@layer visor-base {\n .sizeSm {\n text-transform: var(--button-sm-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-sm-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeMd {\n text-transform: var(--button-md-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-md-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeLg {\n text-transform: var(--button-lg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-lg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeDlg {\n text-transform: var(--button-dlg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-dlg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n}\n\n/* dlg ghost — transparent well with a hairline outline + secondary ink\n (AN dlg-btn ghost face: transparent / --ink-60 / 1px --hair). Two-class\n specificity keeps this scoped to the dlg size only. */\n.sizeDlg.variantGhost {\n color: var(--text-secondary, #6b7280);\n}\n\n.sizeDlg.variantGhost::after {\n outline-color: var(--control-edge-color, var(--hairline, var(--border-default, #e5e7eb)));\n}\n\n/* dlg danger — subtle-danger face (AN dlg-btn danger face: --red on a faint\n red fill), distinct from the solid `destructive` variant. */\n.sizeDlg.variantDestructive {\n background-color: var(--surface-error-subtle, color-mix(in srgb, var(--destructive, #ef4444) 8%, transparent));\n color: var(--text-error, var(--destructive, #b91c1c));\n}\n\n@media (hover: hover) {\n .sizeDlg.variantDestructive:hover {\n background-color: color-mix(in srgb, var(--destructive, #ef4444) 14%, transparent);\n }\n}\n\n/* Pending state — orthogonal to variant and size (VI-643).\n The idle children stay in the box at zero opacity so the control keeps its\n width, its height and its accessible name; the busy glyph paints over them\n in an absolutely-positioned overlay. A pending state that reflows has not\n fixed the defect it exists for, which is why nothing here is a leading slot\n that adds width.\n\n Themes tune the two timings via --button-pending-delay and\n --button-pending-min-duration. Neither is declared here on purpose: an\n element-level declaration shadows anything a theme emits on :root (VI-625). */\n.base[data-pending=\"true\"] {\n position: relative;\n cursor: progress;\n}\n\n/* No press dip while the action is in flight — the button is not accepting\n the click, so it must not pretend to. */\n.base[data-pending=\"true\"]:active {\n transform: none;\n transition: none;\n}\n\n/* Geometry holder. Mirrors .base's own flex metrics so wrapping the children\n does not change how they lay out. */\n.pendingLabel {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n opacity: 0;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.pendingOverlay {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n padding: inherit;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n pointer-events: none;\n}\n\n/* Clamped, so a pending label longer than the idle one can never widen the\n box it is being held inside. */\n.pendingText {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n"
151
177
  }
152
178
  ]
153
179
  },
@@ -167,12 +193,12 @@
167
193
  {
168
194
  "path": "components/ui/inline-edit/inline-edit.tsx",
169
195
  "type": "registry:ui",
170
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PencilSimple } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./inline-edit.module.css\"\n\ntype InlineEditElement = \"span\" | \"div\" | \"p\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n\nexport interface InlineEditProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"defaultValue\" | \"children\"> {\n /** The committed text. An empty string means \"no override\": `defaultValue` shows instead. */\n value: string\n /** Shown, muted, while `value` is empty. Committing an empty string brings it back. */\n defaultValue?: string\n /** Called with the trimmed text on Enter, blur or Tab, only when it differs from `value`. */\n onCommit: (value: string) => void\n /** The field's name, e.g. \"Section title\". Names the input, and the pencil as \"Edit {label}\". */\n label: string\n /** Accessible name of the pencil. Defaults to `Edit {label}`. */\n editLabel?: string\n /** The element to render. Typography inherits from it, so a title keeps its heading's size. */\n as?: InlineEditElement\n /** Open in the editing state on mount, without moving focus. */\n defaultEditing?: boolean\n}\n\nconst InlineEdit = React.forwardRef<HTMLElement, InlineEditProps>(\n (\n {\n value,\n defaultValue = \"\",\n onCommit,\n label,\n editLabel,\n as: Comp = \"span\",\n defaultEditing = false,\n className,\n ...props\n },\n ref\n ) => {\n const [editing, setEditing] = React.useState(defaultEditing)\n const [draft, setDraft] = React.useState(defaultEditing ? value : \"\")\n const inputRef = React.useRef<HTMLInputElement>(null)\n const pencilRef = React.useRef<HTMLButtonElement>(null)\n // Set once Enter or Escape has closed the editor, so the blur that follows\n // the input unmounting does not commit a second time.\n const settled = React.useRef(false)\n // Where focus goes after the next render: into the input when editing\n // starts, back to the pencil after Enter or Escape.\n const pendingFocus = React.useRef<\"input\" | \"pencil\" | null>(null)\n\n React.useEffect(() => {\n if (pendingFocus.current === \"input\" && inputRef.current) {\n inputRef.current.focus()\n inputRef.current.select()\n } else if (pendingFocus.current === \"pencil\") {\n pencilRef.current?.focus()\n }\n pendingFocus.current = null\n }, [editing])\n\n const start = () => {\n settled.current = false\n pendingFocus.current = \"input\"\n setDraft(value)\n setEditing(true)\n }\n\n const finish = (commit: boolean, returnFocus: boolean) => {\n if (settled.current) return\n settled.current = true\n if (returnFocus) pendingFocus.current = \"pencil\"\n setEditing(false)\n const next = draft.trim()\n if (commit && next !== value) onCommit(next)\n }\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.nativeEvent.isComposing) return\n if (event.key === \"Enter\") {\n event.preventDefault()\n finish(true, true)\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n finish(false, true)\n }\n }\n\n const isDefault = value === \"\"\n\n return (\n <Comp\n ref={ref as React.Ref<never>}\n data-slot=\"inline-edit\"\n data-state={editing ? \"editing\" : \"rest\"}\n className={cn(styles.root, className)}\n {...props}\n >\n {editing ? (\n // The field is sized by a hidden copy of the draft (CSS ::after), so\n // the input is as wide as the text it replaces, not its container.\n <span data-slot=\"inline-edit-field\" className={styles.field} data-value={draft || defaultValue}>\n <input\n ref={inputRef}\n type=\"text\"\n // size={1} drops the input's own ~20-character width, so only the\n // hidden copy of the draft sizes the field.\n size={1}\n data-slot=\"inline-edit-input\"\n className={styles.input}\n aria-label={label}\n value={draft}\n placeholder={defaultValue}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={onKeyDown}\n onBlur={() => finish(true, false)}\n />\n </span>\n ) : (\n <>\n <span\n data-slot=\"inline-edit-text\"\n data-default={isDefault || undefined}\n className={cn(styles.text, isDefault && styles.textDefault)}\n onClick={start}\n >\n {isDefault ? defaultValue : value}\n </span>\n <button\n ref={pencilRef}\n type=\"button\"\n data-slot=\"inline-edit-pencil\"\n className={styles.pencil}\n aria-label={editLabel ?? `Edit ${label}`}\n onClick={start}\n >\n <PencilSimple aria-hidden=\"true\" />\n </button>\n </>\n )}\n </Comp>\n )\n }\n)\nInlineEdit.displayName = \"InlineEdit\"\n\nexport { InlineEdit }\n"
196
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PencilSimple } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./inline-edit.module.css\"\n\ntype InlineEditElement = \"span\" | \"div\" | \"p\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n\nexport interface InlineEditProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"defaultValue\" | \"children\"> {\n /** The committed text. An empty string means \"no override\": `defaultValue` shows instead. */\n value: string\n /** Shown, muted, while `value` is empty. Committing an empty string brings it back. */\n defaultValue?: string\n /** Called with the trimmed text on Enter, blur or Tab, only when it differs from `value`. */\n onCommit: (value: string) => void\n /** The field's name, e.g. \"Section title\". Names the input, and the pencil as \"Edit {label}\". */\n label: string\n /** Accessible name of the pencil. Defaults to `Edit {label}`. */\n editLabel?: string\n /** The element to render. Typography inherits from it, so a title keeps its heading's size. */\n as?: InlineEditElement\n /**\n * Draw the pencil after the text. Default `true`. With `false` the text is the\n * control: a button named \"Edit {label}, {value}\" that opens the field on click,\n * Enter or Space, and takes focus back after Enter or Escape.\n */\n pencil?: boolean\n /** Open in the editing state on mount, without moving focus. */\n defaultEditing?: boolean\n}\n\nconst InlineEdit = React.forwardRef<HTMLElement, InlineEditProps>(\n (\n {\n value,\n defaultValue = \"\",\n onCommit,\n label,\n editLabel,\n as: Comp = \"span\",\n defaultEditing = false,\n pencil = true,\n className,\n ...props\n },\n ref\n ) => {\n const [editing, setEditing] = React.useState(defaultEditing)\n const [draft, setDraft] = React.useState(defaultEditing ? value : \"\")\n const inputRef = React.useRef<HTMLInputElement>(null)\n // The one focusable trigger at rest: the pencil, or the text without it.\n const triggerRef = React.useRef<HTMLButtonElement>(null)\n // Set once Enter or Escape has closed the editor, so the blur that follows\n // the input unmounting does not commit a second time.\n const settled = React.useRef(false)\n // Where focus goes after the next render: into the input when editing\n // starts, back to the trigger after Enter or Escape.\n const pendingFocus = React.useRef<\"input\" | \"trigger\" | null>(null)\n\n React.useEffect(() => {\n if (pendingFocus.current === \"input\" && inputRef.current) {\n inputRef.current.focus()\n inputRef.current.select()\n } else if (pendingFocus.current === \"trigger\") {\n triggerRef.current?.focus()\n }\n pendingFocus.current = null\n }, [editing])\n\n const start = () => {\n settled.current = false\n pendingFocus.current = \"input\"\n setDraft(value)\n setEditing(true)\n }\n\n const finish = (commit: boolean, returnFocus: boolean) => {\n if (settled.current) return\n settled.current = true\n if (returnFocus) pendingFocus.current = \"trigger\"\n setEditing(false)\n const next = draft.trim()\n if (commit && next !== value) onCommit(next)\n }\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.nativeEvent.isComposing) return\n if (event.key === \"Enter\") {\n event.preventDefault()\n finish(true, true)\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n finish(false, true)\n }\n }\n\n const isDefault = value === \"\"\n const shown = isDefault ? defaultValue : value\n\n return (\n <Comp\n ref={ref as React.Ref<never>}\n data-slot=\"inline-edit\"\n data-state={editing ? \"editing\" : \"rest\"}\n className={cn(styles.root, className)}\n {...props}\n >\n {editing ? (\n // The field is sized by a hidden copy of the draft (CSS ::after), so\n // the input is as wide as the text it replaces, not its container.\n <span data-slot=\"inline-edit-field\" className={styles.field} data-value={draft || defaultValue}>\n <input\n ref={inputRef}\n type=\"text\"\n // size={1} drops the input's own ~20-character width, so only the\n // hidden copy of the draft sizes the field.\n size={1}\n data-slot=\"inline-edit-input\"\n className={styles.input}\n aria-label={label}\n value={draft}\n placeholder={defaultValue}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={onKeyDown}\n onBlur={() => finish(true, false)}\n />\n </span>\n ) : (\n <>\n {pencil ? (\n <span\n data-slot=\"inline-edit-text\"\n data-default={isDefault || undefined}\n className={cn(styles.text, isDefault && styles.textDefault)}\n onClick={start}\n >\n {shown}\n </span>\n ) : (\n <button\n ref={triggerRef}\n type=\"button\"\n data-slot=\"inline-edit-text\"\n data-default={isDefault || undefined}\n data-control=\"\"\n className={cn(styles.text, styles.textControl, isDefault && styles.textDefault)}\n aria-label={`${editLabel ?? `Edit ${label}`}, ${shown}`}\n onClick={start}\n >\n {shown}\n </button>\n )}\n {pencil && (\n <button\n ref={triggerRef}\n type=\"button\"\n data-slot=\"inline-edit-pencil\"\n className={styles.pencil}\n aria-label={editLabel ?? `Edit ${label}`}\n onClick={start}\n >\n <PencilSimple aria-hidden=\"true\" />\n </button>\n )}\n </>\n )}\n </Comp>\n )\n }\n)\nInlineEdit.displayName = \"InlineEdit\"\n\nexport { InlineEdit }\n"
171
197
  },
172
198
  {
173
199
  "path": "components/ui/inline-edit/inline-edit.module.css",
174
200
  "type": "registry:ui",
175
- "content": "/* InlineEdit (VI-658)\n *\n * At rest: the text, then a pencil. No edge. Editing: an input in the same\n * box, so the line box keeps its height. Typography (size, weight, colour,\n * case, tracking) is inherited from the element InlineEdit renders as.\n *\n * The pencil is the editor's mark pattern (VI-659): one size and one ink,\n * read from the same --button-icon-* tokens as an icon-only Button. Its hit\n * target is the Button's too, but drawn on ::before so it adds no height to\n * a 14px line. */\n.root {\n /* A long title wraps; it is never truncated. */\n overflow-wrap: anywhere;\n}\n\n.text {\n cursor: text;\n}\n\n/* The default shows while there is no override, muted. */\n.textDefault {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.pencil {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n vertical-align: middle;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n margin-inline-start: var(--spacing-2, 0.5rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.pencil::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.pencil > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .pencil:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.pencil:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* The editing field is the rest box: a one-cell grid sized by a hidden copy\n * of the draft plus the room the pencil took, so it is as wide as the text\n * and pencil were, grows as the user types, and stops at its container. */\n.field {\n display: inline-grid;\n grid-template-columns: minmax(0, 1fr);\n vertical-align: top;\n max-width: 100%;\n}\n\n.field::after {\n content: attr(data-value);\n grid-area: 1 / 1;\n visibility: hidden;\n overflow: hidden;\n white-space: pre;\n padding-inline-end: calc(var(--spacing-2, 0.5rem) + var(--button-icon-size, 1rem));\n}\n\n/* The input sits in the text's place. Its padding is cancelled by an equal\n * negative margin, so the text does not move and the line box keeps its\n * height; the padding only gives the edge room around the glyphs. */\n.input {\n box-sizing: border-box;\n grid-area: 1 / 1;\n width: calc(100% + 2 * var(--spacing-1, 0.25rem));\n min-width: 0;\n margin: calc(-1 * var(--spacing-1, 0.25rem));\n padding: var(--spacing-1, 0.25rem);\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: transparent;\n /* Size, weight and line-height come from the reset's `font: inherit`;\n case, tracking and colour are not covered there. */\n letter-spacing: inherit;\n text-transform: inherit;\n color: inherit;\n /* VI-655: the resting edge is the shared control edge, so\n --control-edge-width: 0 turns it off with every other control. */\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--control-edge-color, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.input::placeholder {\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Focus draws at --control-state-edge-width, which the edge switch does not\n touch, so it stays visible with edges off. Same ring as Input. */\n.input:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n"
201
+ "content": "/* InlineEdit (VI-658)\n *\n * At rest: the text, then a pencil. No edge. Editing: an input in the same\n * box, so the line box keeps its height. Typography (size, weight, colour,\n * case, tracking) is inherited from the element InlineEdit renders as.\n *\n * The pencil is the editor's mark pattern (VI-659): one size and one ink,\n * read from the same --button-icon-* tokens as an icon-only Button. Its hit\n * target is the Button's too, but drawn on ::before so it adds no height to\n * a 14px line. */\n.root {\n /* A long title wraps; it is never truncated. */\n overflow-wrap: anywhere;\n}\n\n.text {\n cursor: text;\n}\n\n/* The default shows while there is no override, muted. */\n.textDefault {\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* pencil={false}: the text is the control. A native button, reset to read as\n * the text it holds: typography comes from the reset's `font: inherit` and the\n * rest is inherited from the element InlineEdit renders as. No edge at rest. */\n.textControl {\n position: relative;\n display: inline;\n /* Padding is present in every state and cancelled by an equal negative\n * margin, so nothing shifts between rest, hover and focus. It is the room\n * the focus ring clears the glyphs by, on top of --focus-ring-offset. */\n margin: calc(-1 * var(--spacing-1, 0.25rem)) calc(-1 * var(--spacing-2, 0.5rem));\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: inherit;\n text-align: inherit;\n letter-spacing: inherit;\n text-transform: inherit;\n overflow-wrap: inherit;\n cursor: text;\n}\n\n/* Centre the ring on the capitals (label-centering doc). The button's line\n * box is trimmed to the capital-to-baseline band, so the padded box, and the\n * ring drawn around it, is symmetric about the caps and not about the font's\n * ascent and descent. The block padding and its cancelling negative margin\n * are the room the ring clears the glyphs by; the margin box is the cap band,\n * so the line box (set by the surrounding text) does not change. Without\n * text-box (Firefox) the padding above stands and the ring centres on the line box. */\n@supports (text-box: trim-both cap alphabetic) {\n .textControl {\n text-box: trim-both cap alphabetic;\n padding-block: var(--spacing-2, 0.5rem);\n margin-block: calc(-1 * var(--spacing-2, 0.5rem));\n }\n}\n\n.textControl.textDefault {\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* WCAG 2.5.8: the hit target is at least 24px tall. Drawn on ::before so it\n * adds no height to the line; it only grows when the line is shorter. */\n.textControl::before {\n content: \"\";\n position: absolute;\n inset-inline: 0;\n inset-block: min(0px, calc((100% - 1.5rem) / 2));\n}\n\n/* Hover reads as editable without an icon: an underline. Solid by default;\n * a theme that wants a dashed one sets --inline-edit-underline-style. Never dotted. */\n@media (hover: hover) {\n .textControl:hover {\n text-decoration-line: underline;\n text-decoration-style: var(--inline-edit-underline-style, solid);\n text-decoration-thickness: var(--stroke-width-thin, 1px);\n text-underline-offset: var(--spacing-1, 0.25rem);\n }\n}\n\n/* Focus shows the ring alone. The underline is the hover cue; under the ring it\n * read as a double line. */\n.textControl:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n text-decoration-line: none;\n}\n\n.pencil {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n vertical-align: middle;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n margin-inline-start: var(--spacing-2, 0.5rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.pencil::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.pencil > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .pencil:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.pencil:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* The editing field is the rest box: a one-cell grid sized by a hidden copy\n * of the draft plus the room the pencil took, so it is as wide as the text\n * and pencil were, grows as the user types, and stops at its container. */\n.field {\n display: inline-grid;\n grid-template-columns: minmax(0, 1fr);\n vertical-align: top;\n max-width: 100%;\n}\n\n.field::after {\n content: attr(data-value);\n grid-area: 1 / 1;\n visibility: hidden;\n overflow: hidden;\n white-space: pre;\n padding-inline-end: calc(var(--spacing-2, 0.5rem) + var(--button-icon-size, 1rem));\n}\n\n/* The input sits in the text's place. Its padding is cancelled by an equal\n * negative margin, so the text does not move and the line box keeps its\n * height; the padding only gives the edge room around the glyphs. */\n.input {\n box-sizing: border-box;\n grid-area: 1 / 1;\n width: calc(100% + 2 * var(--spacing-1, 0.25rem));\n min-width: 0;\n margin: calc(-1 * var(--spacing-1, 0.25rem));\n padding: var(--spacing-1, 0.25rem);\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: transparent;\n /* Size, weight and line-height come from the reset's `font: inherit`;\n case, tracking and colour are not covered there. */\n letter-spacing: inherit;\n text-transform: inherit;\n color: inherit;\n /* VI-655: the resting edge is the shared control edge, so\n --control-edge-width: 0 turns it off with every other control. */\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--control-edge-color, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.input::placeholder {\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Focus draws at --control-state-edge-width, which the edge switch does not\n touch, so it stays visible with edges off. Same ring as Input. */\n.input:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n"
176
202
  }
177
203
  ]
178
204
  },
@@ -198,7 +224,7 @@
198
224
  {
199
225
  "path": "components/ui/input/input.module.css",
200
226
  "type": "registry:ui",
201
- "content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n /* VI-655: the border only holds box geometry and is always transparent. The\n visible resting edge is an outline pulled inside the box, drawn from the\n shared --control-edge-* tokens, so --control-edge-width: 0 on any ancestor\n removes it with no layout shift. --input-border stays the per-component\n colour override and still wins over the shared colour. */\n border: 1px solid transparent;\n /* --field-control-bg is the shared \"what surface does a form control sit on\"\n role. It defaults to var(--input-bg), so default inputs are unchanged. A\n form context (for example an in-dialog wrapper) can set --field-control-bg:\n var(--surface-subtle) to lift controls onto a different surface without a\n per-component prop. Under data-density=\"editorial\", the fallback chain\n defaults to var(--surface-card) while still honoring any scoped\n --field-control-bg override from a form context. */\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n:global([data-density=\"editorial\"]) .base {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.base::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch, so both stay visible with edges off. */\n.base:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n outline-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--input-bg, #f9fafb)))));\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n:global([data-density=\"editorial\"]) .base[aria-invalid=\"true\"] {\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--surface-card)))));\n}\n\n.base[aria-invalid=\"true\"]:focus-visible {\n outline-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n/* File input styles */\n.base[type=\"file\"] {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Leading-icon wrapper\n *\n * When the consumer passes a leadingIcon node, Input wraps its <input> in a\n * <span> and positions the icon absolutely inside it. The <input> itself\n * picks up .hasLeadingIcon, which adds enough left padding to clear the\n * icon (sized in em so it scales with the field's font-size). */\n.leadingIconWrapper {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n}\n\n.leadingIcon {\n position: absolute;\n left: var(--spacing-3, 0.75rem);\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-secondary, #9ca3af);\n pointer-events: none;\n font-size: 1.125em;\n line-height: 1;\n}\n\n/* Size variants\n *\n * Default rendering uses canonical values directly. Under data-density=\"editorial\"\n * (set on any ancestor), the editorial density axis bakes in the blessed admin\n * values — sm inputs at 33.5px height with --radius-md corners. Padding and the\n * md/lg sizes are not driven by the editorial treatment and remain canonical. */\n.sizeSm {\n height: 2.25rem;\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n height: 33.5px;\n border-radius: var(--radius-md);\n}\n\n.sizeMd {\n height: var(--input-height-md, auto);\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n}\n\n.sizeLg {\n height: var(--input-height-lg, auto);\n padding: var(--input-padding-lg, var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem));\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--input-radius-lg, var(--radius-sm, 0.5rem));\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n"
227
+ "content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n /* VI-655: the border only holds box geometry and is always transparent. The\n visible resting edge is an outline pulled inside the box, drawn from the\n shared --control-edge-* tokens, so --control-edge-width: 0 on any ancestor\n removes it with no layout shift. --input-border stays the per-component\n colour override and still wins over the shared colour. */\n border: 1px solid transparent;\n /* --field-control-bg is the shared \"what surface does a form control sit on\"\n role. It defaults to var(--input-bg), so default inputs are unchanged. A\n form context (for example an in-dialog wrapper) can set --field-control-bg:\n var(--surface-subtle) to lift controls onto a different surface without a\n per-component prop. Under data-density=\"editorial\", the fallback chain\n defaults to var(--surface-card) while still honoring any scoped\n --field-control-bg override from a form context. */\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n:global([data-density=\"editorial\"]) .base {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.base::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch, so both stay visible with edges off. */\n.base:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n outline-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--input-bg, #f9fafb)))));\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n:global([data-density=\"editorial\"]) .base[aria-invalid=\"true\"] {\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--surface-card)))));\n}\n\n.base[aria-invalid=\"true\"]:focus-visible {\n outline-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n/* File input styles */\n.base[type=\"file\"] {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Leading-icon wrapper\n *\n * When the consumer passes a leadingIcon node, Input wraps its <input> in a\n * <span> and positions the icon absolutely inside it. The <input> itself\n * picks up .hasLeadingIcon, which adds enough left padding to clear the\n * icon (sized in em so it scales with the field's font-size). */\n.leadingIconWrapper {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n}\n\n.leadingIcon {\n position: absolute;\n left: var(--spacing-3, 0.75rem);\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-secondary, #9ca3af);\n pointer-events: none;\n font-size: 1.125em;\n line-height: 1;\n}\n\n/* Size variants\n *\n * Default rendering uses canonical values directly. Under data-density=\"editorial\"\n * (set on any ancestor), the editorial density axis bakes in the blessed admin\n * values — sm inputs at 33.5px height with --radius-md corners. Padding and the\n * md/lg sizes are not driven by the editorial treatment and remain canonical. */\n.sizeSm {\n height: 2.25rem;\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n height: 33.5px;\n border-radius: var(--radius-md);\n}\n\n.sizeMd {\n height: var(--input-height-md, auto);\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n}\n\n.sizeLg {\n height: var(--input-height-lg, auto);\n padding: var(--input-padding-lg, var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem));\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--input-radius-lg, var(--radius-sm, 0.5rem));\n}\n\n/* Baseline nudge (VI-684). A native <input> cannot be trimmed to its capital\n band without clipping descenders and accents, so its capitals sit wherever the\n font's line box puts them. Blink snaps that baseline to a whole CSS px, and the\n md and lg boxes (51px, 62px) have an odd-pixel centre, so the capitals land up\n to ~0.9px high. The ideal baseline is (box height + 1cap) / 2; this moves the\n padding from the bottom edge to the top by what rounding that baseline to a\n whole px takes away, so the snapped baseline lands on the nearest px to ideal\n and the capitals are within half a px of centre in any font. padding-top plus\n padding-bottom is unchanged, so the box is exactly as tall as before; a theme\n that sets --input-padding-md / --input-padding-lg replaces it, as before.\n Blink only: WebKit places the baseline exactly, so correcting there overshoots\n (-apple-pay-button-style is the WebKit-only probe). The height is 2px of\n border + both paddings + 1lh (line-height 1.5). */\n@supports (padding-top: round(1.4px, 1px)) and (not (-apple-pay-button-style: black)) {\n .sizeMd {\n --input-nudge: calc(round((2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh + 1cap) / 2, 1px) - (2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh + 1cap) / 2 + 0.5px);\n padding: var(--input-padding-md, calc(var(--spacing-3_5, 0.875rem) + var(--input-nudge)) var(--spacing-4, 1rem) calc(var(--spacing-3_5, 0.875rem) - var(--input-nudge)));\n }\n\n .sizeLg {\n --input-nudge: calc(round((2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh + 1cap) / 2, 1px) - (2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh + 1cap) / 2 + 0.5px);\n padding: var(--input-padding-lg, calc(var(--spacing-4_5, 1.125rem) + var(--input-nudge)) var(--spacing-5, 1.25rem) calc(var(--spacing-4_5, 1.125rem) - var(--input-nudge)));\n }\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n"
202
228
  }
203
229
  ]
204
230
  },
@@ -301,7 +327,7 @@
301
327
  {
302
328
  "path": "components/ui/select/select.module.css",
303
329
  "type": "registry:ui",
304
- "content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n /* VI-655: transparent geometry border; the visible resting edge is an inset\n outline from the shared --control-edge-* tokens (as Input does). */\n border: 1px solid transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--select-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.trigger:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, 1px) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Borderless trigger variant (opt-in via <SelectTrigger variant=\"borderless\">).\n *\n * Closed trigger has no resting edge and sits on --field-control-bg — the same\n * shared form-control surface role Input uses — so a borderless Select matches a\n * borderless Input inside an in-dialog form context that sets\n * --field-control-bg: var(--surface-subtle). Default Select is unchanged. The\n * edge is made transparent (rather than removed) so the trigger's box size\n * stays identical to the default trigger and the focus ring keeps its geometry.\n * Declared after the base .trigger so these overrides win the cascade. */\n.triggerBorderless {\n outline-color: transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n /* In-form borderless selects match the Input they sit beside: full width and\n the same md height/padding (the prototype paired select and input controls inside its field rows). */\n width: 100%;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n}\n\n@media (hover: hover) {\n .triggerBorderless:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: transparent;\n }\n}\n\n.triggerBorderless:focus-visible {\n outline-color: transparent;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n:global([data-density=\"editorial\"]) .trigger {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .triggerBorderless {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.triggerSizeSm {\n height: 2.25rem;\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerSizeLg {\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n"
330
+ "content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n /* VI-655: transparent geometry border; the visible resting edge is an inset\n outline from the shared --control-edge-* tokens (as Input does). */\n border: 1px solid transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--select-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.trigger:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, 1px) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Borderless trigger variant (opt-in via <SelectTrigger variant=\"borderless\">).\n *\n * Closed trigger has no resting edge and sits on --field-control-bg — the same\n * shared form-control surface role Input uses — so a borderless Select matches a\n * borderless Input inside an in-dialog form context that sets\n * --field-control-bg: var(--surface-subtle). Default Select is unchanged. The\n * edge is made transparent (rather than removed) so the trigger's box size\n * stays identical to the default trigger and the focus ring keeps its geometry.\n * Declared after the base .trigger so these overrides win the cascade. */\n.triggerBorderless {\n outline-color: transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n /* In-form borderless selects match the Input they sit beside: full width and\n the same md height/padding (the prototype paired select and input controls inside its field rows). */\n width: 100%;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n}\n\n@media (hover: hover) {\n .triggerBorderless:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: transparent;\n }\n}\n\n.triggerBorderless:focus-visible {\n outline-color: transparent;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n:global([data-density=\"editorial\"]) .trigger {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .triggerBorderless {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.triggerSizeSm {\n --label-box-height: 2.25rem; /* VI-684: outer height, for the label correction */\n height: var(--label-box-height);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n --label-box-height: calc(1lh + var(--spacing-3_5, 0.875rem) * 2 + 2px);\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerSizeLg {\n --label-box-height: calc(1lh + var(--spacing-4_5, 1.125rem) * 2 + 2px);\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The value (or\n placeholder) span's line box is trimmed to the capital-to-baseline band, so\n the flex trigger centres the ink and not the font's ascent/descent.\n padding-block hands back the room the trim took, so the trigger's box is\n unchanged. Without text-box (Firefox) rendering is exactly what it was. */\n@supports (text-box: trim-both cap alphabetic) {\n .trigger > span {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2);\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took, from the trigger's outer height (--label-box-height, per size\n above). WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .trigger > span {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
305
331
  }
306
332
  ]
307
333
  },
@@ -422,12 +448,12 @@
422
448
  {
423
449
  "path": "components/ui/badge/badge.tsx",
424
450
  "type": "registry:ui",
425
- "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./badge.module.css\"\n\nconst badgeVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n destructive: styles.variantDestructive,\n success: styles.variantSuccess,\n warning: styles.variantWarning,\n info: styles.variantInfo,\n neutral: styles.variantNeutral,\n \"filled-destructive\": styles.variantFilledDestructive,\n \"filled-success\": styles.variantFilledSuccess,\n \"filled-warning\": styles.variantFilledWarning,\n \"filled-info\": styles.variantFilledInfo,\n \"filled-neutral\": styles.variantFilledNeutral,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n /* `case=\"sentence\"` opts a sm/md badge OUT of the editorial uppercase +\n tracking treatment (lg already opts out via its size class). Omit for the\n default theme-driven casing. */\n case: {\n sentence: styles.caseSentence,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {\n /** Render the label in uppercase. Also controllable via the\n * `--badge-text-transform` CSS custom property from a theme. */\n uppercase?: boolean\n /** Render as a circular, fixed-square chip carrying a single centered glyph\n * (e.g. a filled-success check). Opt-in; default badges are unaffected. */\n iconOnly?: boolean\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n (\n { className, variant, size, case: textCase, uppercase, iconOnly, ...props },\n ref\n ) => {\n return (\n <span\n ref={ref}\n data-slot=\"badge\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-icon-only={iconOnly ? \"\" : undefined}\n className={cn(\n badgeVariants({ variant, size, case: textCase }),\n uppercase && styles.uppercase,\n iconOnly && styles.iconOnly,\n className\n )}\n {...props}\n />\n )\n }\n)\nBadge.displayName = \"Badge\"\n\nexport { Badge, badgeVariants }\n"
451
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./badge.module.css\"\n\nconst badgeVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n destructive: styles.variantDestructive,\n success: styles.variantSuccess,\n warning: styles.variantWarning,\n info: styles.variantInfo,\n neutral: styles.variantNeutral,\n \"filled-destructive\": styles.variantFilledDestructive,\n \"filled-success\": styles.variantFilledSuccess,\n \"filled-warning\": styles.variantFilledWarning,\n \"filled-info\": styles.variantFilledInfo,\n \"filled-neutral\": styles.variantFilledNeutral,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n /* `case=\"sentence\"` opts a sm/md badge OUT of the editorial uppercase +\n tracking treatment (lg already opts out via its size class). Omit for the\n default theme-driven casing. */\n case: {\n sentence: styles.caseSentence,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {\n /** Render the label in uppercase. Also controllable via the\n * `--badge-text-transform` CSS custom property from a theme. */\n uppercase?: boolean\n /** Render as a circular, fixed-square chip carrying a single centered glyph\n * (e.g. a filled-success check). Opt-in; default badges are unaffected. */\n iconOnly?: boolean\n}\n\n/**\n * Wrap bare text in a block label so the stylesheet can trim its line box to the\n * capital-to-baseline band (see `.label`), centring the capitals in the badge\n * whatever the font (VI-684). Elements (icons) pass through.\n */\nfunction wrapLabel(children: React.ReactNode): React.ReactNode {\n return React.Children.map(children, (child) =>\n typeof child === \"string\" || typeof child === \"number\" ? (\n <span className={styles.label} data-slot=\"badge-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n (\n { className, variant, size, case: textCase, uppercase, iconOnly, children, ...props },\n ref\n ) => {\n return (\n <span\n ref={ref}\n data-slot=\"badge\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-icon-only={iconOnly ? \"\" : undefined}\n className={cn(\n badgeVariants({ variant, size, case: textCase }),\n uppercase && styles.uppercase,\n iconOnly && styles.iconOnly,\n className\n )}\n {...props}\n >\n {wrapLabel(children)}\n </span>\n )\n }\n)\nBadge.displayName = \"Badge\"\n\nexport { Badge, badgeVariants }\n"
426
452
  },
427
453
  {
428
454
  "path": "components/ui/badge/badge.module.css",
429
455
  "type": "registry:ui",
430
- "content": "/* Badge base — sizing (height, padding, font-size, gap) is driven by the\n * size classes below. Height is intrinsic (padding + line-height) so badges\n * stay correct when text wraps or icon slots are introduced. */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: fit-content;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: var(--badge-radius, var(--radius-full, 9999px));\n border: var(--badge-border, 1px solid transparent);\n line-height: 1;\n /* Typography defaults. Under data-density=\"editorial\" on any ancestor,\n the density rules below override to uppercase/heavier/tracked.\n VI-625: the D3 Tier-2 badge treatment (--badge-text-transform /\n -letter-spacing / -font-size / -font-weight) is bound here. */\n font-family: var(--badge-font-family, inherit);\n font-weight: var(--badge-font-weight, var(--font-weight-medium, 500));\n text-transform: var(--badge-text-transform, none);\n letter-spacing: var(--badge-letter-spacing, normal);\n white-space: nowrap;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Editorial density: uppercase + heavier + tracked. Any ancestor with\n * data-density=\"editorial\" (for example the app root) activates this. */\n:global([data-density=\"editorial\"]) .base {\n /* Theme-invariant: the editorial treatment runs 600 even when a theme maps\n --font-weight-semibold heavier (entr: 700). density-literal */\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* Editorial density opt-out: sizeLg is intrinsically a label (never tag),\n * and caseSentence lets sm/md opt out per-usage. Both must win over the\n * editorial .base rule above (same specificity, placed after). */\n:global([data-density=\"editorial\"]) .sizeLg,\n:global([data-density=\"editorial\"]) .caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* uppercase prop — directly sets text-transform so it composes cleanly with\n * the density axis. Specificity (0,2,0), placed after .base, wins over default. */\n.base.uppercase {\n text-transform: uppercase;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Sizes — token-driven padding + font-size + gap on the 4px grid.\n * No --spacing-1.5 token exists, so the 6px step uses\n * calc(--spacing-1 + --spacing-1/2) — mirrors the pattern used in kbd. */\n.sizeSm {\n gap: var(--spacing-1, 0.25rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* md — matches canonical rendering (1.25rem total height). Under\n data-density=\"editorial\", font-size steps down to 11px. A theme binding\n --badge-font-size retunes BOTH steps, which is the point: the badge type\n size becomes one dial instead of two. */\n.sizeMd {\n gap: var(--badge-md-gap, var(--spacing-1, 0.25rem));\n padding: var(--badge-md-padding, calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem));\n font-size: var(--badge-font-size, var(--font-size-xs, 0.75rem));\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: var(--badge-font-size, 11px);\n}\n\n.sizeLg {\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Sentence case — opt OUT of any uppercase/tracking applied by the density\n * axis or the uppercase prop. The large size always opts out (it is\n * intrinsically a label); the .caseSentence class lets sm/md opt out\n * per-usage. The editorial-density opt-out mirror lives above, after the\n * editorial .base rule, ensuring it wins at equal specificity. */\n.sizeLg,\n.caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* Icon scaling — leading glyphs track the per-size font-size step. */\n.sizeSm > svg {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.sizeMd > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n\n.sizeLg > svg {\n width: 1rem;\n height: 1rem;\n}\n\n/* Variants\n * VI-661: every variant reads its ground and ink through --badge-bg /\n * --badge-ink, falling back to the value it shipped with. Set either on a\n * badge or any ancestor to tint one badge (an on-image scrim, a status tint)\n * without a variant per tint; with neither set nothing changes. */\n.variantDefault {\n background-color: var(--badge-bg, var(--interactive-primary-bg, currentColor));\n color: var(--badge-ink, var(--interactive-primary-text, var(--text-inverse, #f9fafb)));\n border-color: transparent;\n}\n\n.variantSecondary {\n background-color: var(--badge-bg, var(--interactive-secondary-bg, var(--surface-subtle, transparent)));\n color: var(--badge-ink, var(--interactive-secondary-text, var(--text-primary, currentColor)));\n border-color: transparent;\n}\n\n/* Editorial density: secondary surfaces sit on the card tier. Direct value —\n * the editorial design language owns control surfaces, so the theme's\n * --interactive-secondary-bg is deliberately bypassed (matches the blessed\n * overlay, which out-cascaded the palette at :root). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--badge-bg, var(--surface-card, #ffffff));\n}\n\n.variantOutline {\n background-color: var(--badge-bg, transparent);\n color: var(--badge-ink, var(--text-primary, #111827));\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Tonal variants — canonical surface tokens as plain values. Under\n * data-density=\"editorial\", backgrounds switch to color-mix tints for\n * higher saturation at editorial admin density. Text colors are unchanged. */\n.variantDestructive {\n background-color: var(--badge-bg, var(--surface-error-subtle, transparent));\n color: var(--badge-ink, var(--text-error, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantDestructive {\n background-color: var(--badge-bg, color-mix(in srgb, var(--destructive) 20%, transparent));\n}\n\n.variantSuccess {\n background-color: var(--badge-bg, var(--surface-success-subtle, transparent));\n color: var(--badge-ink, var(--text-success, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantSuccess {\n background-color: var(--badge-bg, color-mix(in srgb, var(--success) 18%, transparent));\n}\n\n.variantWarning {\n background-color: var(--badge-bg, var(--surface-warning-subtle, transparent));\n color: var(--badge-ink, var(--text-warning, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantWarning {\n background-color: var(--badge-bg, color-mix(in srgb, var(--warning) 20%, transparent));\n}\n\n.variantInfo {\n background-color: var(--badge-bg, var(--surface-info-subtle, transparent));\n color: var(--badge-ink, var(--text-info, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantInfo {\n background-color: var(--badge-bg, color-mix(in srgb, var(--info) 18%, transparent));\n}\n\n.variantNeutral {\n background-color: var(--badge-bg, var(--surface-muted, #f3f4f6));\n color: var(--badge-ink, var(--text-secondary, var(--text-primary, #374151)));\n border-color: transparent;\n}\n\n/* Filled variants — step 9 bg + contrast text */\n.variantFilledDestructive {\n background-color: var(--badge-bg, var(--surface-error-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledSuccess {\n background-color: var(--badge-bg, var(--surface-success-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledWarning {\n background-color: var(--badge-bg, var(--surface-warning-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledInfo {\n background-color: var(--badge-bg, var(--surface-info-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n/* filled-neutral — the saturated counterpart to `neutral`. Neutral has no\n * brand `-default` surface, so it uses a fixed neutral-600 fill. Because that\n * fill is dark in BOTH modes, text must stay white (not `--text-inverse`,\n * which flips to a dark value in dark mode). */\n.variantFilledNeutral {\n background-color: var(--badge-bg, var(--color-neutral-600, #4b5563));\n color: var(--badge-ink, var(--color-white, #ffffff));\n border-color: transparent;\n}\n\n/* Icon-only — a circular, fixed-square chip carrying a single centered glyph\n * (for example the filled-success check used by the Roles matrix active cell +\n * legend). Zero padding and a fixed glyph size keep it crisp at any density.\n * The glyph centers via the base flex; per-size svg rules are overridden here. */\n.iconOnly {\n width: 1.375rem;\n height: 1.375rem;\n min-width: 1.375rem;\n padding: 0;\n gap: 0;\n border-radius: var(--radius-full, 9999px);\n}\n\n.iconOnly > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
456
+ "content": "/* Badge base — sizing (height, padding, font-size, gap) is driven by the\n * size classes below. Height is intrinsic (padding + line-height) so badges\n * stay correct when text wraps or icon slots are introduced. */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: fit-content;\n flex-shrink: 0;\n /* Not hidden (VI-684): a descender or an accent in a short badge is taller than its line box, and a clip cuts it. The badge is fit-content, so there is no overflow to hide. */\n overflow: visible;\n border-radius: var(--badge-radius, var(--radius-full, 9999px));\n border: var(--badge-border, 1px solid transparent);\n line-height: 1;\n /* Typography defaults. Under data-density=\"editorial\" on any ancestor,\n the density rules below override to uppercase/heavier/tracked.\n VI-625: the D3 Tier-2 badge treatment (--badge-text-transform /\n -letter-spacing / -font-size / -font-weight) is bound here. */\n font-family: var(--badge-font-family, inherit);\n font-weight: var(--badge-font-weight, var(--font-weight-medium, 500));\n text-transform: var(--badge-text-transform, none);\n letter-spacing: var(--badge-letter-spacing, normal);\n white-space: nowrap;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Editorial density: uppercase + heavier + tracked. Any ancestor with\n * data-density=\"editorial\" (for example the app root) activates this. */\n:global([data-density=\"editorial\"]) .base {\n /* Theme-invariant: the editorial treatment runs 600 even when a theme maps\n --font-weight-semibold heavier (entr: 700). density-literal */\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* Editorial density opt-out: sizeLg is intrinsically a label (never tag),\n * and caseSentence lets sm/md opt out per-usage. Both must win over the\n * editorial .base rule above (same specificity, placed after). */\n:global([data-density=\"editorial\"]) .sizeLg,\n:global([data-density=\"editorial\"]) .caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* uppercase prop — directly sets text-transform so it composes cleanly with\n * the density axis. Specificity (0,2,0), placed after .base, wins over default. */\n.base.uppercase {\n text-transform: uppercase;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Sizes — token-driven padding + font-size + gap on the 4px grid.\n * No --spacing-1.5 token exists, so the 6px step uses\n * calc(--spacing-1 + --spacing-1/2) — mirrors the pattern used in kbd. */\n.sizeSm {\n --label-box-height: 1lh; /* VI-684: outer height, for the label correction (padding-block 0) */\n gap: var(--spacing-1, 0.25rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* md — matches canonical rendering (1.25rem total height). Under\n data-density=\"editorial\", font-size steps down to 11px. A theme binding\n --badge-font-size retunes BOTH steps, which is the point: the badge type\n size becomes one dial instead of two. */\n.sizeMd {\n --label-box-height: calc(1lh + var(--spacing-1, 0.25rem)); /* default padding-block is --spacing-1 / 2 twice; re-bind with --badge-md-padding */\n gap: var(--badge-md-gap, var(--spacing-1, 0.25rem));\n padding: var(--badge-md-padding, calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem));\n font-size: var(--badge-font-size, var(--font-size-xs, 0.75rem));\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: var(--badge-font-size, 11px);\n}\n\n.sizeLg {\n --label-box-height: calc(1lh + 2 * var(--spacing-1, 0.25rem));\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Sentence case — opt OUT of any uppercase/tracking applied by the density\n * axis or the uppercase prop. The large size always opts out (it is\n * intrinsically a label); the .caseSentence class lets sm/md opt out\n * per-usage. The editorial-density opt-out mirror lives above, after the\n * editorial .base rule, ensuring it wins at equal specificity. */\n.sizeLg,\n.caseSentence {\n text-transform: none;\n letter-spacing: 0;\n}\n\n/* Icon scaling — leading glyphs track the per-size font-size step. */\n.sizeSm > svg {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.sizeMd > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n\n.sizeLg > svg {\n width: 1rem;\n height: 1rem;\n}\n\n/* Variants\n * VI-661: every variant reads its ground and ink through --badge-bg /\n * --badge-ink, falling back to the value it shipped with. Set either on a\n * badge or any ancestor to tint one badge (an on-image scrim, a status tint)\n * without a variant per tint; with neither set nothing changes. */\n.variantDefault {\n background-color: var(--badge-bg, var(--interactive-primary-bg, currentColor));\n color: var(--badge-ink, var(--interactive-primary-text, var(--text-inverse, #f9fafb)));\n border-color: transparent;\n}\n\n.variantSecondary {\n background-color: var(--badge-bg, var(--interactive-secondary-bg, var(--surface-subtle, transparent)));\n color: var(--badge-ink, var(--interactive-secondary-text, var(--text-primary, currentColor)));\n border-color: transparent;\n}\n\n/* Editorial density: secondary surfaces sit on the card tier. Direct value —\n * the editorial design language owns control surfaces, so the theme's\n * --interactive-secondary-bg is deliberately bypassed (matches the blessed\n * overlay, which out-cascaded the palette at :root). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--badge-bg, var(--surface-card, #ffffff));\n}\n\n.variantOutline {\n background-color: var(--badge-bg, transparent);\n color: var(--badge-ink, var(--text-primary, #111827));\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Tonal variants — canonical surface tokens as plain values. Under\n * data-density=\"editorial\", backgrounds switch to color-mix tints for\n * higher saturation at editorial admin density. Text colors are unchanged. */\n.variantDestructive {\n background-color: var(--badge-bg, var(--surface-error-subtle, transparent));\n color: var(--badge-ink, var(--text-error, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantDestructive {\n background-color: var(--badge-bg, color-mix(in srgb, var(--destructive) 20%, transparent));\n}\n\n.variantSuccess {\n background-color: var(--badge-bg, var(--surface-success-subtle, transparent));\n color: var(--badge-ink, var(--text-success, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantSuccess {\n background-color: var(--badge-bg, color-mix(in srgb, var(--success) 18%, transparent));\n}\n\n.variantWarning {\n background-color: var(--badge-bg, var(--surface-warning-subtle, transparent));\n color: var(--badge-ink, var(--text-warning, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantWarning {\n background-color: var(--badge-bg, color-mix(in srgb, var(--warning) 20%, transparent));\n}\n\n.variantInfo {\n background-color: var(--badge-bg, var(--surface-info-subtle, transparent));\n color: var(--badge-ink, var(--text-info, currentColor));\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantInfo {\n background-color: var(--badge-bg, color-mix(in srgb, var(--info) 18%, transparent));\n}\n\n.variantNeutral {\n background-color: var(--badge-bg, var(--surface-muted, #f3f4f6));\n color: var(--badge-ink, var(--text-secondary, var(--text-primary, #374151)));\n border-color: transparent;\n}\n\n/* Filled variants — step 9 bg + contrast text */\n.variantFilledDestructive {\n background-color: var(--badge-bg, var(--surface-error-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledSuccess {\n background-color: var(--badge-bg, var(--surface-success-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledWarning {\n background-color: var(--badge-bg, var(--surface-warning-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n.variantFilledInfo {\n background-color: var(--badge-bg, var(--surface-info-default, transparent));\n color: var(--badge-ink, var(--text-inverse, currentColor));\n border-color: transparent;\n}\n\n/* filled-neutral — the saturated counterpart to `neutral`. Neutral has no\n * brand `-default` surface, so it uses a fixed neutral-600 fill. Because that\n * fill is dark in BOTH modes, text must stay white (not `--text-inverse`,\n * which flips to a dark value in dark mode). */\n.variantFilledNeutral {\n background-color: var(--badge-bg, var(--color-neutral-600, #4b5563));\n color: var(--badge-ink, var(--color-white, #ffffff));\n border-color: transparent;\n}\n\n/* Icon-only — a circular, fixed-square chip carrying a single centered glyph\n * (for example the filled-success check used by the Roles matrix active cell +\n * legend). Zero padding and a fixed glyph size keep it crisp at any density.\n * The glyph centers via the base flex; per-size svg rules are overridden here. */\n.iconOnly {\n width: 1.375rem;\n height: 1.375rem;\n min-width: 1.375rem;\n padding: 0;\n gap: 0;\n border-radius: var(--radius-full, 9999px);\n}\n\n.iconOnly > svg {\n width: 0.875rem;\n height: 0.875rem;\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The label's line\n box is trimmed to the capital-to-baseline band, so the flex container centres\n the ink and not the font's ascent/descent. padding-block hands back the room\n the trim took, so the badge's intrinsic height is unchanged. Without text-box\n (Firefox) the span is a plain block and rendering is exactly what it was. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2);\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took, from the control's outer height (--label-box-height, set per size\n above; left unset, the correction is simply off). WebKit places the baseline\n exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
431
457
  }
432
458
  ]
433
459
  },
@@ -469,12 +495,12 @@
469
495
  {
470
496
  "path": "components/ui/chip/chip.tsx",
471
497
  "type": "registry:ui",
472
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./chip.module.css\"\n\n/* ─── Chip (base) ────────────────────────────────────────────────────── */\n\nconst chipVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outlined: styles.variantOutlined,\n \"filled-primary\": styles.variantFilledPrimary,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ChipProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof chipVariants> {\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label (after avatar). */\n leadingIcon?: React.ReactNode\n /** Label content. Renders as children if omitted. */\n label?: React.ReactNode\n /** Custom delete icon. Defaults to XIcon. */\n deleteIcon?: React.ReactNode\n /** Called when the delete button is activated. When provided the delete button is shown. */\n onDeleted?: () => void\n /** Aria label for the delete button. Defaults to \"Remove\". */\n deleteLabel?: string\n}\n\nconst Chip = React.forwardRef<HTMLDivElement, ChipProps>(\n (\n {\n className,\n variant,\n size,\n avatar,\n leadingIcon,\n label,\n children,\n deleteIcon,\n onDeleted,\n deleteLabel = \"Remove\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <div\n ref={ref}\n data-slot=\"chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(chipVariants({ variant, size }), className)}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {onDeleted ? (\n <button\n type=\"button\"\n aria-label={deleteLabel}\n className={styles.deleteButton}\n onClick={(e) => {\n e.stopPropagation()\n onDeleted()\n }}\n tabIndex={0}\n >\n {deleteIcon ?? (\n <XIcon\n size={12}\n weight=\"bold\"\n aria-hidden=\"true\"\n className={styles.deleteIcon}\n />\n )}\n </button>\n ) : null}\n </div>\n )\n },\n)\nChip.displayName = \"Chip\"\n\n/* ─── ChoiceChip (radio-style single-select) ─────────────────────────── */\n\nexport interface ChoiceChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently selected. */\n selected?: boolean\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is pressed. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst ChoiceChip = React.forwardRef<HTMLButtonElement, ChoiceChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n avatar,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-slot=\"choice-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n </button>\n )\n },\n)\nChoiceChip.displayName = \"ChoiceChip\"\n\n/* ─── FilterChip (multi-select toggle-style) ─────────────────────────── */\n\nexport interface FilterChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently active/selected. */\n selected?: boolean\n /** Optional leading icon. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is toggled. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n /** Optional count pill rendered after the label (e.g. \"47\", 132). */\n count?: React.ReactNode\n /** Tone for the count pill. \"neutral\" uses muted surface; \"primary\" uses accent surface. Defaults to \"neutral\". */\n countTone?: \"primary\" | \"neutral\"\n /**\n * Selected-state visual treatment.\n * - `\"accent\"` (default) — accent-tinted background + accent border + link text colour.\n * - `\"neutral\"` — neutral-elevated surface (--surface-card) + neutral border + primary text;\n * the count pill renders as a solid mint pill (--surface-success-default) when selected.\n * Use for editorial / admin contexts where accent bleed across shared tokens is undesirable.\n */\n selectedTreatment?: \"accent\" | \"neutral\"\n /** Optional trailing icon rendered after the count (e.g. a dropdown caret) for \"filter-as-dropdown\" chips. Muted by default. */\n trailingIcon?: React.ReactNode\n}\n\nconst FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n count,\n countTone = \"neutral\",\n selectedTreatment = \"accent\",\n trailingIcon,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n const isNeutral = selectedTreatment === \"neutral\"\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n data-slot=\"filter-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-selected-treatment={selectedTreatment}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n styles.filterChip,\n selected && (isNeutral ? styles.selectedNeutral : styles.selected),\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label}>{content}</span>\n {count != null ? (\n <span\n data-slot=\"filter-chip-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n className={cn(\n styles.count,\n isNeutral && selected\n ? styles.countSolid\n : countTone === \"primary\"\n ? styles.countPrimary\n : styles.countNeutral,\n )}\n >\n {count}\n </span>\n ) : null}\n {trailingIcon ? (\n <span className={styles.trailing} aria-hidden=\"true\">\n {trailingIcon}\n </span>\n ) : null}\n </button>\n )\n },\n)\nFilterChip.displayName = \"FilterChip\"\n\nexport { Chip, chipVariants, ChoiceChip, FilterChip }\n"
498
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./chip.module.css\"\n\n/* ─── Chip (base) ────────────────────────────────────────────────────── */\n\nconst chipVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outlined: styles.variantOutlined,\n \"filled-primary\": styles.variantFilledPrimary,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ChipProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof chipVariants> {\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label (after avatar). */\n leadingIcon?: React.ReactNode\n /** Label content. Renders as children if omitted. */\n label?: React.ReactNode\n /** Custom delete icon. Defaults to XIcon. */\n deleteIcon?: React.ReactNode\n /** Called when the delete button is activated. When provided the delete button is shown. */\n onDeleted?: () => void\n /** Aria label for the delete button. Defaults to \"Remove\". */\n deleteLabel?: string\n}\n\nconst Chip = React.forwardRef<HTMLDivElement, ChipProps>(\n (\n {\n className,\n variant,\n size,\n avatar,\n leadingIcon,\n label,\n children,\n deleteIcon,\n onDeleted,\n deleteLabel = \"Remove\",\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <div\n ref={ref}\n data-slot=\"chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(chipVariants({ variant, size }), className)}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label} data-slot=\"chip-text\">{content}</span>\n {onDeleted ? (\n <button\n type=\"button\"\n aria-label={deleteLabel}\n className={styles.deleteButton}\n onClick={(e) => {\n e.stopPropagation()\n onDeleted()\n }}\n tabIndex={0}\n >\n {deleteIcon ?? (\n <XIcon\n size={12}\n weight=\"bold\"\n aria-hidden=\"true\"\n className={styles.deleteIcon}\n />\n )}\n </button>\n ) : null}\n </div>\n )\n },\n)\nChip.displayName = \"Chip\"\n\n/* ─── ChoiceChip (radio-style single-select) ─────────────────────────── */\n\nexport interface ChoiceChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently selected. */\n selected?: boolean\n /** Optional avatar/icon rendered before the label. */\n avatar?: React.ReactNode\n /** Optional leading icon rendered before the label. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is pressed. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n}\n\nconst ChoiceChip = React.forwardRef<HTMLButtonElement, ChoiceChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n avatar,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-slot=\"choice-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n selected && styles.selected,\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {avatar ? (\n <span className={styles.avatar} aria-hidden=\"true\">\n {avatar}\n </span>\n ) : null}\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label} data-slot=\"chip-text\">{content}</span>\n </button>\n )\n },\n)\nChoiceChip.displayName = \"ChoiceChip\"\n\n/* ─── FilterChip (multi-select toggle-style) ─────────────────────────── */\n\nexport interface FilterChipProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof chipVariants> {\n /** Whether this chip is currently active/selected. */\n selected?: boolean\n /** Optional leading icon. */\n leadingIcon?: React.ReactNode\n /** Label text or content. */\n label?: React.ReactNode\n /** Called when the chip is toggled. */\n onPressed?: () => void\n /** Value used when in a ChipGroup. */\n value?: string\n /** Optional count pill rendered after the label (e.g. \"47\", 132). */\n count?: React.ReactNode\n /** Tone for the count pill. \"neutral\" uses muted surface; \"primary\" uses accent surface. Defaults to \"neutral\". */\n countTone?: \"primary\" | \"neutral\"\n /**\n * Selected-state visual treatment.\n * - `\"accent\"` (default) — accent-tinted background + accent border + link text colour.\n * - `\"neutral\"` — neutral-elevated surface (--surface-card) + neutral border + primary text;\n * the count pill renders as a solid mint pill (--surface-success-default) when selected.\n * Use for editorial / admin contexts where accent bleed across shared tokens is undesirable.\n */\n selectedTreatment?: \"accent\" | \"neutral\"\n /** Optional trailing icon rendered after the count (e.g. a dropdown caret) for \"filter-as-dropdown\" chips. Muted by default. */\n trailingIcon?: React.ReactNode\n}\n\nconst FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(\n (\n {\n className,\n variant,\n size,\n selected = false,\n leadingIcon,\n label,\n children,\n onPressed,\n value,\n disabled,\n count,\n countTone = \"neutral\",\n selectedTreatment = \"accent\",\n trailingIcon,\n ...props\n },\n ref,\n ) => {\n const content = label ?? children\n const isNeutral = selectedTreatment === \"neutral\"\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n data-slot=\"filter-chip\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-selected={selected ? \"true\" : \"false\"}\n data-selected-treatment={selectedTreatment}\n data-value={value}\n disabled={disabled}\n className={cn(\n chipVariants({ variant, size }),\n styles.interactive,\n styles.filterChip,\n selected && (isNeutral ? styles.selectedNeutral : styles.selected),\n className,\n )}\n onClick={onPressed}\n {...props}\n >\n {leadingIcon ? (\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className={styles.label} data-slot=\"chip-text\">{content}</span>\n {count != null ? (\n <span\n data-slot=\"filter-chip-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n className={cn(\n styles.count,\n isNeutral && selected\n ? styles.countSolid\n : countTone === \"primary\"\n ? styles.countPrimary\n : styles.countNeutral,\n )}\n >\n {count}\n </span>\n ) : null}\n {trailingIcon ? (\n <span className={styles.trailing} aria-hidden=\"true\">\n {trailingIcon}\n </span>\n ) : null}\n </button>\n )\n },\n)\nFilterChip.displayName = \"FilterChip\"\n\nexport { Chip, chipVariants, ChoiceChip, FilterChip }\n"
473
499
  },
474
500
  {
475
501
  "path": "components/ui/chip/chip.module.css",
476
502
  "type": "registry:ui",
477
- "content": "/* Chip base */\n.base {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: fit-content;\n flex-shrink: 0;\n border-radius: var(--chip-radius, var(--radius-full, 9999px));\n /* VI-655: the border only holds box geometry. The visible edge is drawn on\n ::after (below) so the chip's own outline stays free for the focus ring. */\n border: var(--stroke-width-thin, 1px) solid transparent;\n background-color: var(--chip-bg, var(--surface-card, #ffffff));\n color: var(--chip-text-color, var(--text-primary, #111827));\n font-family: var(--chip-font-family, var(--font-body, inherit));\n font-weight: var(--chip-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--chip-letter-spacing, inherit);\n text-transform: var(--chip-text-transform, inherit);\n white-space: nowrap;\n user-select: none;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* The chip's edge: an inset outline over the border box, on the shared\n --control-edge-* tokens, so --control-edge-width: 0 on any ancestor removes\n it with no layout shift. Each variant sets only the colour; the outlined\n variant's colour is the per-component override, --chip-border. */\n.base::after {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--stroke-width-thin, 1px));\n border-radius: inherit;\n outline: var(--control-edge-width, var(--stroke-width-thin, 1px)) var(--control-edge-style, solid) var(--control-edge-color, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, var(--stroke-width-thin, 1px)));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Variant: default (filled/tonal surface) */\n.variantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.variantDefault::after {\n outline-color: transparent;\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n.variantOutlined::after {\n outline-color: var(--chip-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n}\n\n/* Variant: filled-primary — saturated brand-essence chip treatment.\n * Primary background + primary-text foreground + subtle shadow.\n * 700 weight + slight negative tracking assert \"this IS a brand value\".\n * Use when the chip must carry primary brand prominence — essence chips\n * on the Brand Workbench Elicit canvas, strategy surfaces, brand decks. */\n.variantFilledPrimary {\n background-color: var(--primary, #111827);\n color: var(--primary-text, #f9fafb);\n font-weight: 700;\n letter-spacing: -0.01em;\n box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));\n}\n\n.variantFilledPrimary::after {\n outline-color: transparent;\n}\n\n/* Sizes */\n.sizeSm {\n height: var(--chip-sm-height, 1.5rem);\n padding: 0 var(--chip-sm-padding-x, var(--spacing-2, 0.5rem));\n font-size: var(--chip-sm-font-size, var(--font-size-xs, 0.75rem));\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n height: var(--chip-md-height, 2rem);\n padding: 0 var(--chip-md-padding-x, var(--spacing-3, 0.75rem));\n font-size: var(--chip-md-font-size, var(--font-size-sm, 0.875rem));\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Editorial density: tighter font + gap for md chips. */\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: var(--font-size-xs);\n gap: var(--spacing-2);\n}\n\n.sizeLg {\n height: var(--chip-lg-height, 2.5rem);\n padding: 0 var(--chip-lg-padding-x, var(--spacing-4, 1rem));\n font-size: var(--chip-lg-font-size, var(--font-size-base, 1rem));\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Sub-parts */\n.avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.leadingIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.deleteButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n width: 1rem;\n height: 1rem;\n}\n\n@media (hover: hover) {\n .deleteButton:hover {\n color: var(--text-primary, #111827);\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 60%, transparent);\n }\n}\n\n.deleteButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 1px);\n}\n\n.deleteIcon {\n display: block;\n}\n\n/* Count pill (FilterChip only). Under data-density=\"editorial\", it renders\n as a fixed circular semibold pill. */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: auto;\n height: auto;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1;\n align-self: baseline;\n}\n\n:global([data-density=\"editorial\"]) .count {\n min-width: 1.25rem;\n height: 1.25rem;\n padding-inline: var(--spacing-1);\n font-weight: var(--font-weight-semibold);\n align-self: center;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Accent selected state overrides count pill (default behaviour). */\n.selected .count {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-link, #2563eb);\n}\n\n/* Editorial density: selected count uses solid primary pill. */\n:global([data-density=\"editorial\"]) .selected .count {\n background-color: var(--primary);\n color: var(--primary-text);\n}\n\n/* Solid mint count pill — editorial/neutral selected treatment */\n.countSolid {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Trailing icon (FilterChip dropdown caret) — muted, after the count. */\n.trailing {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n font-size: 0.875rem;\n line-height: 1;\n}\n\n/* Interactive chips (ChoiceChip, FilterChip) */\n.interactive {\n cursor: pointer;\n border: var(--stroke-width-thin, 1px) solid transparent;\n}\n\n.interactive::after {\n outline-color: transparent;\n}\n\nbutton.base,\n.interactive {\n /* Inherit base layout; add button-specific resets */\n text-align: left;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.interactive:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n}\n\n.interactive:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .interactive:hover:not(:disabled):not([data-selected=\"true\"]) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n\n .interactive:hover:not(:disabled):not([data-selected=\"true\"])::after {\n outline-color: var(--border-default, #e5e7eb);\n }\n}\n\n.interactive:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* Selected state. Under data-density=\"editorial\", switches to neutral-elevated\n treatment (surface-elev bg + transparent border + primary text). */\n.selected {\n background-color: var(--chip-selected-bg, var(--surface-accent-subtle, #eff6ff));\n color: var(--chip-selected-text-color, var(--text-link, #2563eb));\n}\n\n.selected::after {\n outline-color: var(--chip-selected-border-color, var(--surface-accent-default, #3b82f6));\n}\n\n/* Editorial density: neutral-elevated selected treatment.\n --surface-elev is a theme role; the color-mix fallback matches how the\n retired overlay derived it. */\n:global([data-density=\"editorial\"]) .selected {\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n color: var(--text-primary);\n}\n\n:global([data-density=\"editorial\"]) .selected::after {\n outline-color: transparent;\n}\n\n@media (hover: hover) {\n .interactive.selected:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-accent-subtle, #eff6ff) 80%, var(--surface-interactive-hover, #f3f4f6) 20%);\n }\n}\n\n/* Editorial / neutral-elevated selected treatment\n * Uses --surface-card bg + neutral edge instead of accent tint.\n * Compose with .selected to override the accent colours. */\n.selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n.selectedNeutral::after {\n outline-color: var(--border-strong, #d1d5db);\n}\n\n/* Re-assert selectedNeutral under editorial density: the density .selected rule\n * is (0,2,0) and would otherwise override .selectedNeutral at (0,1,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .selectedNeutral::after {\n outline-color: var(--border-strong, #d1d5db);\n}\n\n.selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Re-assert selectedNeutral count under editorial density: the density\n * .selected .count rule at (0,3,0) would otherwise override at (0,2,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n@media (hover: hover) {\n .interactive.selectedNeutral:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n/* FilterChip resting (unselected) surface. Under editorial density, uses\n --surface-elev so the filter row reads as one consistent elevated set. */\n.filterChip[data-selected=\"false\"] {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n:global([data-density=\"editorial\"]) .filterChip[data-selected=\"false\"] {\n background-color: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n}\n"
503
+ "content": "/* Chip base */\n.base {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: fit-content;\n flex-shrink: 0;\n border-radius: var(--chip-radius, var(--radius-full, 9999px));\n /* VI-655: the border only holds box geometry. The visible edge is drawn on\n ::after (below) so the chip's own outline stays free for the focus ring. */\n border: var(--stroke-width-thin, 1px) solid transparent;\n background-color: var(--chip-bg, var(--surface-card, #ffffff));\n color: var(--chip-text-color, var(--text-primary, #111827));\n font-family: var(--chip-font-family, var(--font-body, inherit));\n font-weight: var(--chip-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--chip-letter-spacing, inherit);\n text-transform: var(--chip-text-transform, inherit);\n white-space: nowrap;\n user-select: none;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* The chip's edge: an inset outline over the border box, on the shared\n --control-edge-* tokens, so --control-edge-width: 0 on any ancestor removes\n it with no layout shift. Each variant sets only the colour; the outlined\n variant's colour is the per-component override, --chip-border. */\n.base::after {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--stroke-width-thin, 1px));\n border-radius: inherit;\n outline: var(--control-edge-width, var(--stroke-width-thin, 1px)) var(--control-edge-style, solid) var(--control-edge-color, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, var(--stroke-width-thin, 1px)));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Variant: default (filled/tonal surface) */\n.variantDefault {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.variantDefault::after {\n outline-color: transparent;\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n.variantOutlined::after {\n outline-color: var(--chip-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n}\n\n/* Variant: filled-primary — saturated brand-essence chip treatment.\n * Primary background + primary-text foreground + subtle shadow.\n * 700 weight + slight negative tracking assert \"this IS a brand value\".\n * Use when the chip must carry primary brand prominence — essence chips\n * on the Brand Workbench Elicit canvas, strategy surfaces, brand decks. */\n.variantFilledPrimary {\n background-color: var(--primary, #111827);\n color: var(--primary-text, #f9fafb);\n font-weight: 700;\n letter-spacing: -0.01em;\n box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));\n}\n\n.variantFilledPrimary::after {\n outline-color: transparent;\n}\n\n/* Sizes */\n.sizeSm {\n --label-box-height: var(--chip-sm-height, 1.5rem);\n height: var(--label-box-height);\n padding: 0 var(--chip-sm-padding-x, var(--spacing-2, 0.5rem));\n font-size: var(--chip-sm-font-size, var(--font-size-xs, 0.75rem));\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n --label-box-height: var(--chip-md-height, 2rem);\n height: var(--label-box-height);\n padding: 0 var(--chip-md-padding-x, var(--spacing-3, 0.75rem));\n font-size: var(--chip-md-font-size, var(--font-size-sm, 0.875rem));\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Editorial density: tighter font + gap for md chips. */\n:global([data-density=\"editorial\"]) .sizeMd {\n font-size: var(--font-size-xs);\n gap: var(--spacing-2);\n}\n\n.sizeLg {\n --label-box-height: var(--chip-lg-height, 2.5rem);\n height: var(--label-box-height);\n padding: 0 var(--chip-lg-padding-x, var(--spacing-4, 1rem));\n font-size: var(--chip-lg-font-size, var(--font-size-base, 1rem));\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Sub-parts */\n.avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.leadingIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.deleteButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n color: var(--text-secondary, #6b7280);\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n width: 1rem;\n height: 1rem;\n}\n\n@media (hover: hover) {\n .deleteButton:hover {\n color: var(--text-primary, #111827);\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 60%, transparent);\n }\n}\n\n.deleteButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 1px);\n}\n\n.deleteIcon {\n display: block;\n}\n\n/* Count pill (FilterChip only). Under data-density=\"editorial\", it renders\n as a fixed circular semibold pill. */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: auto;\n height: auto;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1;\n align-self: baseline;\n}\n\n:global([data-density=\"editorial\"]) .count {\n min-width: 1.25rem;\n height: 1.25rem;\n padding-inline: var(--spacing-1);\n font-weight: var(--font-weight-semibold);\n align-self: center;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Accent selected state overrides count pill (default behaviour). */\n.selected .count {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-link, #2563eb);\n}\n\n/* Editorial density: selected count uses solid primary pill. */\n:global([data-density=\"editorial\"]) .selected .count {\n background-color: var(--primary);\n color: var(--primary-text);\n}\n\n/* Solid mint count pill — editorial/neutral selected treatment */\n.countSolid {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Trailing icon (FilterChip dropdown caret) — muted, after the count. */\n.trailing {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n font-size: 0.875rem;\n line-height: 1;\n}\n\n/* Interactive chips (ChoiceChip, FilterChip) */\n.interactive {\n cursor: pointer;\n border: var(--stroke-width-thin, 1px) solid transparent;\n}\n\n.interactive::after {\n outline-color: transparent;\n}\n\nbutton.base,\n.interactive {\n /* Inherit base layout; add button-specific resets */\n text-align: left;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.interactive:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n}\n\n.interactive:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #6b7280);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .interactive:hover:not(:disabled):not([data-selected=\"true\"]) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n\n .interactive:hover:not(:disabled):not([data-selected=\"true\"])::after {\n outline-color: var(--border-default, #e5e7eb);\n }\n}\n\n.interactive:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* Selected state. Under data-density=\"editorial\", switches to neutral-elevated\n treatment (surface-elev bg + transparent border + primary text). */\n.selected {\n background-color: var(--chip-selected-bg, var(--surface-accent-subtle, #eff6ff));\n color: var(--chip-selected-text-color, var(--text-link, #2563eb));\n}\n\n.selected::after {\n outline-color: var(--chip-selected-border-color, var(--surface-accent-default, #3b82f6));\n}\n\n/* Editorial density: neutral-elevated selected treatment.\n --surface-elev is a theme role; the color-mix fallback matches how the\n retired overlay derived it. */\n:global([data-density=\"editorial\"]) .selected {\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n color: var(--text-primary);\n}\n\n:global([data-density=\"editorial\"]) .selected::after {\n outline-color: transparent;\n}\n\n@media (hover: hover) {\n .interactive.selected:hover:not(:disabled) {\n background-color: color-mix(in srgb, var(--surface-accent-subtle, #eff6ff) 80%, var(--surface-interactive-hover, #f3f4f6) 20%);\n }\n}\n\n/* Editorial / neutral-elevated selected treatment\n * Uses --surface-card bg + neutral edge instead of accent tint.\n * Compose with .selected to override the accent colours. */\n.selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n.selectedNeutral::after {\n outline-color: var(--border-strong, #d1d5db);\n}\n\n/* Re-assert selectedNeutral under editorial density: the density .selected rule\n * is (0,2,0) and would otherwise override .selectedNeutral at (0,1,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .selectedNeutral::after {\n outline-color: var(--border-strong, #d1d5db);\n}\n\n.selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n/* Re-assert selectedNeutral count under editorial density: the density\n * .selected .count rule at (0,3,0) would otherwise override at (0,2,0). */\n:global([data-density=\"editorial\"]) .selectedNeutral .count {\n background-color: var(--surface-success-default, #22c55e);\n color: var(--text-inverse, #ffffff);\n}\n\n@media (hover: hover) {\n .interactive.selectedNeutral:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n/* FilterChip resting (unselected) surface. Under editorial density, uses\n --surface-elev so the filter row reads as one consistent elevated set. */\n.filterChip[data-selected=\"false\"] {\n background-color: var(--surface-muted, #f3f4f6);\n}\n\n:global([data-density=\"editorial\"]) .filterChip[data-selected=\"false\"] {\n background-color: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The label's line\n box is trimmed to the capital-to-baseline band, so the flex container centres\n the ink and not the font's ascent/descent. padding-block hands back the room\n the trim took, so the box is unchanged. The label is overflow: hidden for its\n ellipsis, and that clips against the trimmed line box, so accents above the\n cap height and descenders below the baseline would be cut: the extra\n padding-block, cancelled by the negative margin-block, widens the clip box\n without changing the layout box. Without text-box (Firefox) rendering is\n exactly what it was. */\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2 + 0.3em);\n margin-block: -0.3em;\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took. WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
478
504
  }
479
505
  ]
480
506
  },
@@ -780,12 +806,12 @@
780
806
  {
781
807
  "path": "components/ui/dialog/dialog.tsx",
782
808
  "type": "registry:ui",
783
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./dialog.module.css\"\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\nDialog.displayName = \"Dialog\"\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\nDialogTrigger.displayName = \"DialogTrigger\"\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\nDialogClose.displayName = \"DialogClose\"\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\nDialogPortal.displayName = \"DialogPortal\"\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"dialog-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nDialogOverlay.displayName = \"DialogOverlay\"\n\nconst DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n React.ComponentProps<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"dialog-content\"\n className={cn(styles.content, className)}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n))\nDialogContent.displayName = \"DialogContent\"\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nDialogHeader.displayName = \"DialogHeader\"\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"dialog-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nDialogTitle.displayName = \"DialogTitle\"\n\nconst DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"dialog-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nDialogDescription.displayName = \"DialogDescription\"\n\nexport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"
809
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { ArrowLeftIcon, XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./dialog.module.css\"\n\ntype DialogPresentation = \"dialog\" | \"sheet\" | \"responsive\"\n\ninterface DialogContextValue {\n /** The presentation actually in force: `responsive` has already been resolved. */\n presentation: \"dialog\" | \"sheet\"\n container: HTMLElement | null | undefined\n open: boolean\n}\n\nconst DialogContext = React.createContext<DialogContextValue>({\n presentation: \"dialog\",\n container: undefined,\n open: false,\n})\n\nexport interface DialogProps\n extends React.ComponentProps<typeof DialogPrimitive.Root> {\n /**\n * How the content is presented. `dialog` is a centred modal, `sheet` is a\n * bottom sheet, `responsive` is a dialog from `breakpoint` up and a sheet\n * below it. The content, focus trap and Escape handling are identical.\n */\n presentation?: DialogPresentation\n /**\n * Viewport width at which `responsive` switches from sheet to dialog. A\n * number is px; a string is any CSS length (`\"40rem\"`). Defaults to 640.\n */\n breakpoint?: number | string\n /**\n * Element the overlay and content portal into, covering only that element.\n * A scoped dialog is not modal: it traps focus inside the content and makes\n * the container's other children `inert`, but leaves everything outside the\n * container interactive (Radix modal mode would aria-hide the page and block\n * pointer events on `body`). Pass the element itself (hold it in state), not\n * a ref. The container is given `position: relative` if it is `static`.\n */\n container?: HTMLElement | null\n}\n\nfunction Dialog({\n presentation = \"dialog\",\n breakpoint = 640,\n container,\n modal = true,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n ...props\n}: DialogProps) {\n const minWidth = typeof breakpoint === \"number\" ? `${breakpoint}px` : breakpoint\n const isWide = useMinWidth(minWidth)\n const resolved =\n presentation === \"responsive\" ? (isWide ? \"dialog\" : \"sheet\") : presentation\n\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)\n const open = openProp ?? uncontrolledOpen\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n setUncontrolledOpen(next)\n onOpenChange?.(next)\n },\n [onOpenChange]\n )\n\n const value = React.useMemo(\n () => ({ presentation: resolved, container, open }),\n [resolved, container, open]\n )\n\n return (\n <DialogContext.Provider value={value}>\n <DialogPrimitive.Root\n data-slot=\"dialog\"\n modal={container ? false : modal}\n open={open}\n onOpenChange={handleOpenChange}\n {...props}\n />\n </DialogContext.Provider>\n )\n}\nDialog.displayName = \"Dialog\"\n\n/** True when the viewport is at least `minWidth` wide. Desktop where matchMedia is absent. */\nfunction useMinWidth(minWidth: string): boolean {\n const query = `(min-width: ${minWidth})`\n const [matches, setMatches] = React.useState<boolean>(() =>\n typeof window !== \"undefined\" && typeof window.matchMedia === \"function\"\n ? window.matchMedia(query).matches\n : true\n )\n React.useEffect(() => {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return\n const mq = window.matchMedia(query)\n setMatches(mq.matches)\n const onChange = (e: MediaQueryListEvent) => setMatches(e.matches)\n mq.addEventListener(\"change\", onChange)\n return () => mq.removeEventListener(\"change\", onChange)\n }, [query])\n return matches\n}\n\n/**\n * Scoped dialogs: make every other child of the container inert while open, and\n * make sure the container can anchor absolutely positioned content.\n */\nfunction useScopedContainer(container: HTMLElement | null | undefined, node: HTMLElement | null) {\n React.useEffect(() => {\n if (!container || !node) return\n let branch: HTMLElement = node\n while (branch.parentElement && branch.parentElement !== container) {\n branch = branch.parentElement\n }\n if (branch.parentElement !== container) return\n\n const inerted: Element[] = []\n for (const child of Array.from(container.children)) {\n if (child !== branch && !child.hasAttribute(\"inert\")) {\n child.setAttribute(\"inert\", \"\")\n inerted.push(child)\n }\n }\n const anchored = getComputedStyle(container).position === \"static\"\n if (anchored) container.style.position = \"relative\"\n\n return () => {\n for (const child of inerted) child.removeAttribute(\"inert\")\n if (anchored) container.style.position = \"\"\n }\n }, [container, node])\n}\n\n/**\n * Bottom sheet: keep the panel above the on-screen keyboard. The layout\n * viewport does not shrink on iOS, so size to the visual viewport and lift the\n * panel by the keyboard's height; keep the focused field in view.\n */\nfunction useKeyboardAvoidance(enabled: boolean, node: HTMLElement | null) {\n React.useEffect(() => {\n if (!enabled || !node || typeof window === \"undefined\") return\n const vv = window.visualViewport\n if (!vv) return\n const update = () => {\n const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop)\n node.style.setProperty(\"--dialog-viewport-height\", `${vv.height}px`)\n node.style.setProperty(\"--dialog-keyboard-inset\", `${inset}px`)\n const active = document.activeElement\n if (inset > 0 && active instanceof HTMLElement && node.contains(active)) {\n active.scrollIntoView?.({ block: \"nearest\" })\n }\n }\n update()\n vv.addEventListener(\"resize\", update)\n vv.addEventListener(\"scroll\", update)\n return () => {\n vv.removeEventListener(\"resize\", update)\n vv.removeEventListener(\"scroll\", update)\n node.style.removeProperty(\"--dialog-viewport-height\")\n node.style.removeProperty(\"--dialog-keyboard-inset\")\n }\n }, [enabled, node])\n}\n\nconst DRAG_DISMISS_DISTANCE = 80\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\nDialogTrigger.displayName = \"DialogTrigger\"\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\nDialogClose.displayName = \"DialogClose\"\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\nDialogPortal.displayName = \"DialogPortal\"\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"dialog-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nDialogOverlay.displayName = \"DialogOverlay\"\n\n/** Backdrop for a scoped dialog. Radix renders no overlay outside modal mode. */\nconst DialogScrim = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => {\n const { open } = React.useContext(DialogContext)\n return (\n <div\n ref={ref}\n data-slot=\"dialog-overlay\"\n data-state={open ? \"open\" : \"closed\"}\n aria-hidden=\"true\"\n className={cn(styles.overlay, styles.scrim, className)}\n {...props}\n />\n )\n }\n)\nDialogScrim.displayName = \"DialogScrim\"\n\nexport interface DialogContentProps\n extends React.ComponentProps<typeof DialogPrimitive.Content> {\n /** Show a drag handle (sheet presentation only). Dragging down dismisses. */\n showHandle?: boolean\n}\n\nconst DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(({ className, children, showHandle = false, onInteractOutside, onFocus, ...props }, ref) => {\n const { presentation, container } = React.useContext(DialogContext)\n const scoped = Boolean(container)\n const isSheet = presentation === \"sheet\"\n const [node, setNode] = React.useState<HTMLDivElement | null>(null)\n const closeRef = React.useRef<HTMLButtonElement>(null)\n const dragStart = React.useRef<number | null>(null)\n\n const setRefs = React.useCallback(\n (el: HTMLDivElement | null) => {\n setNode(el)\n if (typeof ref === \"function\") ref(el)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n },\n [ref]\n )\n\n useScopedContainer(container, node)\n useKeyboardAvoidance(isSheet && !scoped, node)\n\n const handleInteractOutside: typeof onInteractOutside = (event) => {\n onInteractOutside?.(event)\n if (!scoped || event.defaultPrevented) return\n // Only the scrim dismisses. A press on the page outside the container, or on\n // the container's inert siblings, must leave the dialog open.\n const target = event.target\n if (!(target instanceof Element && target.closest('[data-slot=\"dialog-overlay\"]'))) {\n event.preventDefault()\n }\n }\n\n const handleFocus = (event: React.FocusEvent<HTMLDivElement>) => {\n onFocus?.(event)\n const target = event.target\n if (isSheet && target instanceof HTMLElement && target.matches(\"input, textarea, select, [contenteditable]\")) {\n requestAnimationFrame(() => target.scrollIntoView?.({ block: \"nearest\" }))\n }\n }\n\n const dragTo = (clientY: number, commit: boolean) => {\n if (dragStart.current === null || !node) return\n const dy = Math.max(0, clientY - dragStart.current)\n if (!commit) {\n node.style.transition = \"none\"\n node.style.transform = `translateY(${dy}px)`\n return\n }\n dragStart.current = null\n node.style.transition = \"\"\n node.style.transform = \"\"\n if (dy > DRAG_DISMISS_DISTANCE) closeRef.current?.click()\n }\n\n return (\n <DialogPortal container={container}>\n {scoped ? <DialogScrim /> : <DialogOverlay />}\n <DialogPrimitive.Content\n ref={setRefs}\n data-slot=\"dialog-content\"\n data-presentation={presentation}\n data-scoped={scoped ? \"\" : undefined}\n className={cn(\n styles.content,\n isSheet && styles.presentationSheet,\n scoped && styles.scoped,\n className\n )}\n onInteractOutside={handleInteractOutside}\n onFocus={handleFocus}\n {...props}\n >\n {isSheet && showHandle && (\n <div\n data-slot=\"dialog-handle\"\n aria-hidden=\"true\"\n className={styles.handle}\n onPointerDown={(e) => {\n dragStart.current = e.clientY\n e.currentTarget.setPointerCapture?.(e.pointerId)\n }}\n onPointerMove={(e) => dragTo(e.clientY, false)}\n onPointerUp={(e) => dragTo(e.clientY, true)}\n onPointerCancel={() => dragTo(dragStart.current ?? 0, true)}\n />\n )}\n {children}\n <DialogPrimitive.Close ref={closeRef} className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n})\nDialogContent.displayName = \"DialogContent\"\n\nexport interface DialogHeaderProps extends React.ComponentProps<\"div\"> {\n /** Leading back action, rendered before the title (see `DialogBack`). */\n back?: React.ReactNode\n}\n\nfunction DialogHeader({ className, back, children, ...props }: DialogHeaderProps) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(styles.header, back ? styles.headerWithBack : undefined, className)}\n {...props}\n >\n {back ? (\n <>\n <div data-slot=\"dialog-back-slot\" className={styles.backSlot}>{back}</div>\n <div className={styles.headerBody}>{children}</div>\n </>\n ) : (\n children\n )}\n </div>\n )\n}\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogBack = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, children, type = \"button\", \"aria-label\": ariaLabel = \"Back\", ...props }, ref) => (\n <button\n ref={ref}\n type={type}\n data-slot=\"dialog-back\"\n aria-label={ariaLabel}\n className={cn(styles.backButton, className)}\n {...props}\n >\n {children ?? <ArrowLeftIcon className={styles.closeIcon} aria-hidden=\"true\" />}\n </button>\n )\n)\nDialogBack.displayName = \"DialogBack\"\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"dialog-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nDialogTitle.displayName = \"DialogTitle\"\n\nconst DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"dialog-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nDialogDescription.displayName = \"DialogDescription\"\n\nexport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogHeader,\n DialogBack,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"
784
810
  },
785
811
  {
786
812
  "path": "components/ui/dialog/dialog.module.css",
787
813
  "type": "registry:ui",
788
- "content": "/* Dialog overlay\n * Default: 4px backdrop blur. Density axis: editorial uses 2px. */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n:global([data-density=\"editorial\"]) .overlay {\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-150, 150ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Dialog content\n * Default: 32rem max-width, radius-lg, surface-page, spacing-6, shadow-xl.\n * Density axis: editorial uses 480px, radius-xl, surface-card, spacing-8, shadow-lg. */\n.content {\n position: fixed;\n top: 50%;\n left: 50%;\n z-index: 50;\n width: 100%;\n max-width: 32rem;\n transform: translate(-50%, -50%);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-page, #ffffff);\n padding: var(--spacing-6, 1.5rem);\n box-shadow: var(--shadow-xl);\n transition: opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .content {\n /* Floating modal — opaque. --surface-card may be translucent glass per theme\n * (blackout: rgba(255,255,255,0.04)); over the backdrop it reads see-through.\n * --surface-popover is the opaque floating token (== --surface-card on solid\n * themes, so no visual change there). VI-623. */\n background-color: var(--surface-popover, var(--surface-page, #ffffff));\n max-width: 480px;\n border-radius: var(--radius-xl);\n padding: var(--spacing-8);\n box-shadow: var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Title\n * Default: font-size-lg / semibold. Density axis: editorial uses font-size-2xl / 700. */\n.title {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .title {\n font-size: var(--font-size-2xl);\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Description\n * Default: text-secondary. Density axis: editorial uses text-tertiary. */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .description {\n color: var(--text-tertiary);\n}\n\n/* Footer */\n.footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding-top: var(--spacing-3, 0.75rem);\n margin-top: var(--spacing-2, 0.5rem);\n border-top: 1px solid transparent;\n border-image: linear-gradient(var(--hairline, var(--border-subtle, rgba(0, 0, 0, 0.06))), var(--hairline, var(--border-subtle, rgba(0, 0, 0, 0.06)))) 1 / var(--hairline-width, 1px) 0 0 0;\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n to {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n}\n"
814
+ "content": "/* Dialog overlay\n * Default: 4px backdrop blur. Density axis: editorial uses 2px. */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n:global([data-density=\"editorial\"]) .overlay {\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-150, 150ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Dialog content\n * Default: 32rem max-width, radius-lg, surface-page, spacing-6, shadow-xl.\n * Density axis: editorial uses 480px, radius-xl, surface-card, spacing-8, shadow-lg. */\n.content {\n position: fixed;\n top: 50%;\n left: 50%;\n z-index: 50;\n width: 100%;\n max-width: 32rem;\n transform: translate(-50%, -50%);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-page, #ffffff);\n padding: var(--spacing-6, 1.5rem);\n box-shadow: var(--shadow-xl);\n transition: opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .content {\n /* Floating modal — opaque. --surface-card may be translucent glass per theme\n * (blackout: rgba(255,255,255,0.04)); over the backdrop it reads see-through.\n * --surface-popover is the opaque floating token (== --surface-card on solid\n * themes, so no visual change there). VI-623. */\n background-color: var(--surface-popover, var(--surface-page, #ffffff));\n max-width: 480px;\n border-radius: var(--radius-xl);\n padding: var(--spacing-8);\n box-shadow: var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Bottom-sheet presentation (VI-667): same content, anchored to the bottom edge.\n * Sized by dvh and the visual viewport so the on-screen keyboard never covers it;\n * the --dialog-* vars are written by DialogContent from the browser's visual viewport API.\n * No border — the sheet reads against the scrim by surface and shadow alone. */\n.presentationSheet {\n top: auto;\n right: 0;\n bottom: var(--dialog-keyboard-inset, 0px);\n left: 0;\n max-width: none;\n max-height: min(85dvh, calc(var(--dialog-viewport-height, 100dvh) - var(--spacing-4, 1rem)));\n overflow-y: auto;\n overscroll-behavior: contain;\n transform: none;\n border-radius: var(--radius-lg, 0.5rem) var(--radius-lg, 0.5rem) 0 0;\n padding-bottom: calc(var(--spacing-6, 1.5rem) + env(safe-area-inset-bottom, 0px));\n}\n\n:global([data-density=\"editorial\"]) .presentationSheet {\n max-width: none;\n border-radius: var(--radius-xl) var(--radius-xl) 0 0;\n padding-bottom: calc(var(--spacing-8) + env(safe-area-inset-bottom, 0px));\n}\n\n.presentationSheet[data-state=\"open\"] {\n animation: sheetShow var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.presentationSheet[data-state=\"closed\"] {\n animation: sheetHide var(--motion-duration-150, 150ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Scoped (container) presentation: covers only the container, not the window. */\n.scrim,\n.scoped {\n position: absolute;\n}\n\n.scoped {\n max-height: calc(100% - var(--spacing-8, 2rem));\n overflow-y: auto;\n}\n\n.scoped.presentationSheet {\n bottom: 0;\n max-height: 85%;\n}\n\n/* Drag handle */\n.handle {\n width: 2.5rem;\n height: var(--spacing-1, 0.25rem);\n margin: calc(var(--spacing-3, 0.75rem) * -1) auto var(--spacing-4, 1rem);\n border-radius: var(--radius-full, 9999px);\n background-color: var(--text-tertiary, #9ca3af);\n opacity: var(--opacity-40, 0.4);\n cursor: grab;\n touch-action: none;\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Back action: leading counterpart of the close button */\n.backButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.backButton:hover {\n opacity: 1;\n}\n\n.backButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n.headerWithBack {\n flex-direction: row;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.backSlot {\n flex: none;\n /* Align the 24px control with the first title line (line-height lg ~1.75rem). */\n margin-block: calc(var(--spacing-1, 0.25rem) * 0.5);\n}\n\n.headerBody {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n min-width: 0;\n}\n\n/* Title\n * Default: font-size-lg / semibold. Density axis: editorial uses font-size-2xl / 700. */\n.title {\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .title {\n font-size: var(--font-size-2xl);\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Description\n * Default: text-secondary. Density axis: editorial uses text-tertiary. */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .description {\n color: var(--text-tertiary);\n}\n\n/* Footer */\n.footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding-top: var(--spacing-3, 0.75rem);\n margin-top: var(--spacing-2, 0.5rem);\n border-top: 1px solid transparent;\n border-image: linear-gradient(var(--hairline, var(--border-subtle, rgba(0, 0, 0, 0.06))), var(--hairline, var(--border-subtle, rgba(0, 0, 0, 0.06)))) 1 / var(--hairline-width, 1px) 0 0 0;\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n to {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 8px));\n }\n}\n\n@keyframes sheetShow {\n from { transform: translateY(100%); }\n to { transform: translateY(0); }\n}\n\n@keyframes sheetHide {\n from { transform: translateY(0); }\n to { transform: translateY(100%); }\n}\n\n/* Reduced motion: the dialog and the sheet appear without moving. */\n@media (prefers-reduced-motion: reduce) {\n .content,\n .presentationSheet,\n .backButton,\n .closeButton {\n transition: none;\n }\n\n .content[data-state=\"open\"],\n .presentationSheet[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n }\n\n .content[data-state=\"closed\"],\n .presentationSheet[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n }\n}\n"
789
815
  }
790
816
  ]
791
817
  },
@@ -807,12 +833,12 @@
807
833
  {
808
834
  "path": "components/ui/sheet/sheet.tsx",
809
835
  "type": "registry:ui",
810
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./sheet.module.css\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"sheet\" {...props} />\n}\nSheet.displayName = \"Sheet\"\n\nfunction SheetTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\nSheetTrigger.displayName = \"SheetTrigger\"\n\nfunction SheetClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\nSheetClose.displayName = \"SheetClose\"\n\nfunction SheetPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\nSheetPortal.displayName = \"SheetPortal\"\n\nconst SheetOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"sheet-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nSheetOverlay.displayName = \"SheetOverlay\"\n\nconst sheetContentVariants = cva(styles.content, {\n variants: {\n side: {\n top: styles.sideTop,\n right: styles.sideRight,\n bottom: styles.sideBottom,\n left: styles.sideLeft,\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n})\n\nexport interface SheetContentProps\n extends React.ComponentProps<typeof DialogPrimitive.Content>,\n VariantProps<typeof sheetContentVariants> {\n showCloseButton?: boolean\n}\n\nconst SheetContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ className, children, side = \"right\", showCloseButton = true, ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"sheet-content\"\n data-side={side}\n className={cn(sheetContentVariants({ side }), className)}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close data-slot=\"sheet-close\" className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = \"SheetContent\"\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nSheetHeader.displayName = \"SheetHeader\"\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nSheetFooter.displayName = \"SheetFooter\"\n\nconst SheetTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"sheet-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nSheetTitle.displayName = \"SheetTitle\"\n\nconst SheetDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"sheet-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nSheetDescription.displayName = \"SheetDescription\"\n\nexport {\n Sheet,\n SheetTrigger,\n SheetClose,\n SheetPortal,\n SheetOverlay,\n SheetContent,\n SheetHeader,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n}\n"
836
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { XIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./sheet.module.css\"\n\ninterface SheetContextValue {\n container: HTMLElement | null | undefined\n open: boolean\n}\n\nconst SheetContext = React.createContext<SheetContextValue>({ container: undefined, open: false })\n\nexport interface SheetProps extends React.ComponentProps<typeof DialogPrimitive.Root> {\n /**\n * Element the overlay and content portal into, covering only that element.\n * A scoped sheet is not modal: it traps focus inside the panel and makes the\n * container's other children `inert`, but leaves everything outside the\n * container interactive (Radix modal mode would aria-hide the page and block\n * pointer events on `body`). Pass the element itself (hold it in state), not\n * a ref. The container is given `position: relative` if it is `static`.\n */\n container?: HTMLElement | null\n}\n\nfunction Sheet({ container, modal = true, open: openProp, defaultOpen = false, onOpenChange, ...props }: SheetProps) {\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)\n const open = openProp ?? uncontrolledOpen\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n setUncontrolledOpen(next)\n onOpenChange?.(next)\n },\n [onOpenChange]\n )\n const value = React.useMemo(() => ({ container, open }), [container, open])\n return (\n <SheetContext.Provider value={value}>\n <DialogPrimitive.Root\n data-slot=\"sheet\"\n modal={container ? false : modal}\n open={open}\n onOpenChange={handleOpenChange}\n {...props}\n />\n </SheetContext.Provider>\n )\n}\nSheet.displayName = \"Sheet\"\n\n/**\n * Scoped sheets: make every other child of the container inert while open, and\n * make sure the container can anchor absolutely positioned content.\n */\nfunction useScopedContainer(container: HTMLElement | null | undefined, node: HTMLElement | null) {\n React.useEffect(() => {\n if (!container || !node) return\n let branch: HTMLElement = node\n while (branch.parentElement && branch.parentElement !== container) {\n branch = branch.parentElement\n }\n if (branch.parentElement !== container) return\n\n const inerted: Element[] = []\n for (const child of Array.from(container.children)) {\n if (child !== branch && !child.hasAttribute(\"inert\")) {\n child.setAttribute(\"inert\", \"\")\n inerted.push(child)\n }\n }\n const anchored = getComputedStyle(container).position === \"static\"\n if (anchored) container.style.position = \"relative\"\n\n return () => {\n for (const child of inerted) child.removeAttribute(\"inert\")\n if (anchored) container.style.position = \"\"\n }\n }, [container, node])\n}\n\nfunction SheetTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\nSheetTrigger.displayName = \"SheetTrigger\"\n\nfunction SheetClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\nSheetClose.displayName = \"SheetClose\"\n\nfunction SheetPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\nSheetPortal.displayName = \"SheetPortal\"\n\nconst SheetOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentProps<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n data-slot=\"sheet-overlay\"\n className={cn(styles.overlay, className)}\n {...props}\n />\n))\nSheetOverlay.displayName = \"SheetOverlay\"\n\nconst sheetContentVariants = cva(styles.content, {\n variants: {\n side: {\n top: styles.sideTop,\n right: styles.sideRight,\n bottom: styles.sideBottom,\n left: styles.sideLeft,\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n})\n\nexport interface SheetContentProps\n extends React.ComponentProps<typeof DialogPrimitive.Content>,\n VariantProps<typeof sheetContentVariants> {\n showCloseButton?: boolean\n}\n\n/** Backdrop for a scoped sheet. Radix renders no overlay outside modal mode. */\nconst SheetScrim = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => {\n const { open } = React.useContext(SheetContext)\n return (\n <div\n ref={ref}\n data-slot=\"sheet-overlay\"\n data-state={open ? \"open\" : \"closed\"}\n aria-hidden=\"true\"\n className={cn(styles.overlay, styles.scrim, className)}\n {...props}\n />\n )\n }\n)\nSheetScrim.displayName = \"SheetScrim\"\n\nconst SheetContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ className, children, side = \"right\", showCloseButton = true, onInteractOutside, ...props }, ref) => {\n const { container } = React.useContext(SheetContext)\n const scoped = Boolean(container)\n const [node, setNode] = React.useState<HTMLDivElement | null>(null)\n\n const setRefs = React.useCallback(\n (el: HTMLDivElement | null) => {\n setNode(el)\n if (typeof ref === \"function\") ref(el)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n },\n [ref]\n )\n\n useScopedContainer(container, node)\n\n const handleInteractOutside: typeof onInteractOutside = (event) => {\n onInteractOutside?.(event)\n if (!scoped || event.defaultPrevented) return\n // Only the scrim dismisses; the page outside the container stays usable.\n const target = event.target\n if (!(target instanceof Element && target.closest('[data-slot=\"sheet-overlay\"]'))) {\n event.preventDefault()\n }\n }\n\n return (\n <SheetPortal container={container}>\n {scoped ? <SheetScrim /> : <SheetOverlay />}\n <DialogPrimitive.Content\n ref={setRefs}\n data-slot=\"sheet-content\"\n data-side={side}\n data-scoped={scoped ? \"\" : undefined}\n className={cn(sheetContentVariants({ side }), scoped && styles.scoped, className)}\n onInteractOutside={handleInteractOutside}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close data-slot=\"sheet-close\" className={styles.closeButton}>\n <XIcon className={styles.closeIcon} />\n <span className={styles.srOnly}>Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </SheetPortal>\n )\n})\nSheetContent.displayName = \"SheetContent\"\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nSheetHeader.displayName = \"SheetHeader\"\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nSheetFooter.displayName = \"SheetFooter\"\n\nconst SheetTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentProps<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n data-slot=\"sheet-title\"\n className={cn(styles.title, className)}\n {...props}\n />\n))\nSheetTitle.displayName = \"SheetTitle\"\n\nconst SheetDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentProps<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n data-slot=\"sheet-description\"\n className={cn(styles.description, className)}\n {...props}\n />\n))\nSheetDescription.displayName = \"SheetDescription\"\n\nexport {\n Sheet,\n SheetTrigger,\n SheetClose,\n SheetPortal,\n SheetOverlay,\n SheetContent,\n SheetHeader,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n}\n"
811
837
  },
812
838
  {
813
839
  "path": "components/ui/sheet/sheet.module.css",
814
840
  "type": "registry:ui",
815
- "content": "/* Sheet overlay */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Sheet content base */\n.content {\n position: fixed;\n z-index: 50;\n display: flex;\n flex-direction: column;\n background-color: var(--surface-page, #ffffff);\n font-size: var(--font-size-sm, 0.875rem);\n box-shadow: var(--shadow-xl);\n transition: transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Side variants */\n.sideTop {\n inset-inline: 0;\n top: 0;\n height: auto;\n max-height: 80vh;\n}\n\n.sideTop[data-state=\"open\"] {\n animation: slideInFromTop var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideTop[data-state=\"closed\"] {\n animation: slideOutToTop var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideRight {\n inset-block: 0;\n right: 0;\n height: 100%;\n width: 75%;\n max-width: 24rem;\n}\n\n.sideRight[data-state=\"open\"] {\n animation: slideInFromRight var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideRight[data-state=\"closed\"] {\n animation: slideOutToRight var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideBottom {\n inset-inline: 0;\n bottom: 0;\n height: auto;\n max-height: 80vh;\n}\n\n.sideBottom[data-state=\"open\"] {\n animation: slideInFromBottom var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideBottom[data-state=\"closed\"] {\n animation: slideOutToBottom var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideLeft {\n inset-block: 0;\n left: 0;\n height: 100%;\n width: 75%;\n max-width: 24rem;\n}\n\n.sideLeft[data-state=\"open\"] {\n animation: slideInFromLeft var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideLeft[data-state=\"closed\"] {\n animation: slideOutToLeft var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n padding: var(--spacing-6, 1.5rem);\n}\n\n/* Footer */\n.footer {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-6, 1.5rem);\n}\n\n/* Title */\n.title {\n font-size: var(--font-size-base, 1rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n/* Description */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes slideInFromTop {\n from { transform: translateY(-100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes slideOutToTop {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(-100%); opacity: 0; }\n}\n\n@keyframes slideInFromRight {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes slideOutToRight {\n from { transform: translateX(0); opacity: 1; }\n to { transform: translateX(100%); opacity: 0; }\n}\n\n@keyframes slideInFromBottom {\n from { transform: translateY(100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes slideOutToBottom {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(100%); opacity: 0; }\n}\n\n@keyframes slideInFromLeft {\n from { transform: translateX(-100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes slideOutToLeft {\n from { transform: translateX(0); opacity: 1; }\n to { transform: translateX(-100%); opacity: 0; }\n}\n"
841
+ "content": "/* Sheet overlay */\n.overlay {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: var(--overlay-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n\n.overlay[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.overlay[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Sheet content base */\n.content {\n position: fixed;\n z-index: 50;\n display: flex;\n flex-direction: column;\n background-color: var(--surface-page, #ffffff);\n font-size: var(--font-size-sm, 0.875rem);\n box-shadow: var(--shadow-xl);\n transition: transform var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Side variants */\n.sideTop {\n inset-inline: 0;\n top: 0;\n height: auto;\n max-height: 80vh;\n max-height: 80dvh;\n}\n\n.sideTop[data-state=\"open\"] {\n animation: slideInFromTop var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideTop[data-state=\"closed\"] {\n animation: slideOutToTop var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideRight {\n inset-block: 0;\n right: 0;\n height: 100%;\n width: 75%;\n max-width: 24rem;\n}\n\n.sideRight[data-state=\"open\"] {\n animation: slideInFromRight var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideRight[data-state=\"closed\"] {\n animation: slideOutToRight var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideBottom {\n inset-inline: 0;\n bottom: 0;\n height: auto;\n max-height: 80vh;\n max-height: 80dvh;\n}\n\n.sideBottom[data-state=\"open\"] {\n animation: slideInFromBottom var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideBottom[data-state=\"closed\"] {\n animation: slideOutToBottom var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n.sideLeft {\n inset-block: 0;\n left: 0;\n height: 100%;\n width: 75%;\n max-width: 24rem;\n}\n\n.sideLeft[data-state=\"open\"] {\n animation: slideInFromLeft var(--motion-duration-normal, 200ms) var(--motion-easing-enter, ease-out);\n}\n\n.sideLeft[data-state=\"closed\"] {\n animation: slideOutToLeft var(--motion-duration-normal, 200ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Scoped (container) presentation: covers only the container, not the window. */\n.scrim,\n.scoped {\n position: absolute;\n}\n\n/* Close button */\n.closeButton {\n position: absolute;\n top: 1rem;\n right: 1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n opacity: 0.7;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n padding: var(--spacing-1, 0.25rem);\n color: var(--text-primary, #111827);\n}\n\n.closeButton:hover {\n opacity: 1;\n}\n\n.closeButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.closeIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n padding: var(--spacing-6, 1.5rem);\n}\n\n/* Footer */\n.footer {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-6, 1.5rem);\n}\n\n/* Title */\n.title {\n font-size: var(--font-size-base, 1rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n margin: 0;\n}\n\n/* Description */\n.description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-secondary, #6b7280);\n margin: 0;\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes slideInFromTop {\n from { transform: translateY(-100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes slideOutToTop {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(-100%); opacity: 0; }\n}\n\n@keyframes slideInFromRight {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes slideOutToRight {\n from { transform: translateX(0); opacity: 1; }\n to { transform: translateX(100%); opacity: 0; }\n}\n\n@keyframes slideInFromBottom {\n from { transform: translateY(100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes slideOutToBottom {\n from { transform: translateY(0); opacity: 1; }\n to { transform: translateY(100%); opacity: 0; }\n}\n\n@keyframes slideInFromLeft {\n from { transform: translateX(-100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes slideOutToLeft {\n from { transform: translateX(0); opacity: 1; }\n to { transform: translateX(-100%); opacity: 0; }\n}\n\n/* Reduced motion: the sheet appears without the slide. */\n@media (prefers-reduced-motion: reduce) {\n .content {\n transition: none;\n }\n\n .sideTop[data-state=\"open\"],\n .sideRight[data-state=\"open\"],\n .sideBottom[data-state=\"open\"],\n .sideLeft[data-state=\"open\"] {\n animation: fadeIn var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n }\n\n .sideTop[data-state=\"closed\"],\n .sideRight[data-state=\"closed\"],\n .sideBottom[data-state=\"closed\"],\n .sideLeft[data-state=\"closed\"] {\n animation: fadeOut var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n }\n}\n"
816
842
  }
817
843
  ]
818
844
  },
@@ -859,12 +885,12 @@
859
885
  {
860
886
  "path": "components/ui/tabs/tabs.tsx",
861
887
  "type": "registry:ui",
862
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tabs.module.css\"\n\nconst Tabs = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Root>,\n React.ComponentProps<typeof TabsPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Root\n ref={ref}\n data-slot=\"tabs\"\n className={cn(styles.root, className)}\n {...props}\n />\n))\nTabs.displayName = \"Tabs\"\n\nconst tabsListVariants = cva(styles.list, {\n variants: {\n variant: {\n default: styles.listVariantDefault,\n line: styles.listVariantLine,\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nexport interface TabsListProps\n extends React.ComponentProps<typeof TabsPrimitive.List>,\n VariantProps<typeof tabsListVariants> {}\n\nconst TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, variant = \"default\", children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDivElement>(null)\n const [indicatorStyle, setIndicatorStyle] = React.useState<{\n left: number\n width: number\n } | null>(null)\n\n // Merge forwarded ref with internal ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n internalRef.current = node\n if (typeof ref === \"function\") {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref]\n )\n\n const updateIndicator = React.useCallback(() => {\n if (variant !== \"line\" || !internalRef.current) return\n const activeTab = internalRef.current.querySelector<HTMLElement>(\n '[data-state=\"active\"]'\n )\n if (activeTab) {\n setIndicatorStyle({\n left: activeTab.offsetLeft,\n width: activeTab.offsetWidth,\n })\n }\n }, [variant])\n\n React.useEffect(() => {\n if (variant !== \"line\" || !internalRef.current) return\n updateIndicator()\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(internalRef.current, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n window.addEventListener(\"resize\", updateIndicator)\n return () => {\n observer.disconnect()\n window.removeEventListener(\"resize\", updateIndicator)\n }\n }, [variant, updateIndicator])\n\n return (\n <TabsPrimitive.List\n ref={mergedRef}\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n >\n {children}\n {variant === \"line\" && indicatorStyle && (\n <span\n className={styles.indicator}\n style={{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n }}\n />\n )}\n </TabsPrimitive.List>\n )\n})\nTabsList.displayName = \"TabsList\"\n\nexport interface TabsTriggerProps\n extends React.ComponentProps<typeof TabsPrimitive.Trigger> {\n /** Inline count pill rendered after the label. Accepts strings, numbers, or elements. */\n count?: React.ReactNode\n /** Visual tone of the count pill. Defaults to \"neutral\". Active state re-tones automatically. */\n countTone?: \"primary\" | \"neutral\"\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, children, count, countTone = \"neutral\", ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n data-slot=\"tabs-trigger\"\n className={cn(styles.trigger, className)}\n {...props}\n >\n {/* The .label wrapper exists to pair the label with the count pill\n (truncation + flex sizing). Without a count, render children directly\n so the trigger's flex gap applies between consumer-provided icon and\n label elements. */}\n {count != null ? <span className={styles.label}>{children}</span> : children}\n {count != null && (\n <span\n className={cn(\n styles.count,\n countTone === \"primary\" ? styles.countPrimary : styles.countNeutral,\n )}\n data-slot=\"tabs-trigger-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n >\n {count}\n </span>\n )}\n </TabsPrimitive.Trigger>\n))\nTabsTrigger.displayName = \"TabsTrigger\"\n\nconst TabsContent = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Content>,\n React.ComponentProps<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n data-slot=\"tabs-content\"\n className={cn(styles.content, className)}\n {...props}\n />\n))\nTabsContent.displayName = \"TabsContent\"\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n"
888
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tabs.module.css\"\n\nconst Tabs = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Root>,\n React.ComponentProps<typeof TabsPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Root\n ref={ref}\n data-slot=\"tabs\"\n className={cn(styles.root, className)}\n {...props}\n />\n))\nTabs.displayName = \"Tabs\"\n\nconst tabsListVariants = cva(styles.list, {\n variants: {\n variant: {\n default: styles.listVariantDefault,\n line: styles.listVariantLine,\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nexport interface TabsListProps\n extends React.ComponentProps<typeof TabsPrimitive.List>,\n VariantProps<typeof tabsListVariants> {}\n\nconst TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, variant = \"default\", children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDivElement>(null)\n const [indicatorStyle, setIndicatorStyle] = React.useState<{\n left: number\n width: number\n } | null>(null)\n\n // Merge forwarded ref with internal ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n internalRef.current = node\n if (typeof ref === \"function\") {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n },\n [ref]\n )\n\n const updateIndicator = React.useCallback(() => {\n if (variant !== \"line\" || !internalRef.current) return\n const activeTab = internalRef.current.querySelector<HTMLElement>(\n '[data-state=\"active\"]'\n )\n if (activeTab) {\n setIndicatorStyle({\n left: activeTab.offsetLeft,\n width: activeTab.offsetWidth,\n })\n }\n }, [variant])\n\n React.useEffect(() => {\n if (variant !== \"line\" || !internalRef.current) return\n updateIndicator()\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(internalRef.current, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n window.addEventListener(\"resize\", updateIndicator)\n return () => {\n observer.disconnect()\n window.removeEventListener(\"resize\", updateIndicator)\n }\n }, [variant, updateIndicator])\n\n return (\n <TabsPrimitive.List\n ref={mergedRef}\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n >\n {children}\n {variant === \"line\" && indicatorStyle && (\n <span\n className={styles.indicator}\n style={{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n }}\n />\n )}\n </TabsPrimitive.List>\n )\n})\nTabsList.displayName = \"TabsList\"\n\nexport interface TabsTriggerProps\n extends React.ComponentProps<typeof TabsPrimitive.Trigger> {\n /** Inline count pill rendered after the label. Accepts strings, numbers, or elements. */\n count?: React.ReactNode\n /** Visual tone of the count pill. Defaults to \"neutral\". Active state re-tones automatically. */\n countTone?: \"primary\" | \"neutral\"\n}\n\n/**\n * Wrap bare text in a block label so the stylesheet can trim its line box to the\n * capital-to-baseline band (see `.text`), centring the capitals in the trigger\n * whatever the font (VI-684). Elements (icons) pass through.\n */\nfunction wrapLabel(children: React.ReactNode): React.ReactNode {\n return React.Children.map(children, (child) =>\n typeof child === \"string\" || typeof child === \"number\" ? (\n <span className={styles.text} data-slot=\"tabs-trigger-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, children, count, countTone = \"neutral\", ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n data-slot=\"tabs-trigger\"\n className={cn(styles.trigger, className)}\n {...props}\n >\n {/* The .label wrapper exists to pair the label with the count pill\n (truncation + flex sizing). Without a count, render children directly\n so the trigger's flex gap applies between consumer-provided icon and\n label elements. */}\n {count != null ? (\n <span className={cn(styles.label, styles.text)} data-slot=\"tabs-trigger-text\">{children}</span>\n ) : (\n wrapLabel(children)\n )}\n {count != null && (\n <span\n className={cn(\n styles.count,\n countTone === \"primary\" ? styles.countPrimary : styles.countNeutral,\n )}\n data-slot=\"tabs-trigger-count\"\n data-tone={countTone}\n aria-hidden=\"false\"\n >\n {count}\n </span>\n )}\n </TabsPrimitive.Trigger>\n))\nTabsTrigger.displayName = \"TabsTrigger\"\n\nconst TabsContent = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.Content>,\n React.ComponentProps<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n data-slot=\"tabs-content\"\n className={cn(styles.content, className)}\n {...props}\n />\n))\nTabsContent.displayName = \"TabsContent\"\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n"
863
889
  },
864
890
  {
865
891
  "path": "components/ui/tabs/tabs.module.css",
866
892
  "type": "registry:ui",
867
- "content": "/* Tabs root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Tabs list */\n.list {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--tabs-list-radius, var(--radius-full, 9999px));\n padding: var(--tabs-list-padding, calc(var(--spacing-1, 0.25rem) * 0.75));\n color: var(--tabs-list-color, var(--text-secondary, #6b7280));\n height: var(--tabs-list-height, 2.25rem);\n width: fit-content;\n}\n\n.listVariantDefault {\n background-color: var(--tabs-list-bg, var(--surface-muted, #f3f4f6));\n}\n\n.listVariantLine {\n position: relative;\n gap: var(--spacing-2, 0.5rem);\n background-color: transparent;\n border-radius: 0;\n /* The border is geometry; the rail rule is painted by border-image at\n --hairline-width, so --hairline-width: 0 removes it with no layout shift.\n A bound --tabs-line-border-bottom (a full border shorthand) is not valid\n as a border-image, so binding it drops the painted rule and draws the\n bound border instead. */\n border-bottom: var(--tabs-line-border-bottom, 1px solid transparent);\n border-image: var(\n --tabs-line-border-bottom,\n linear-gradient(var(--hairline, var(--border-default, #e5e7eb)), var(--hairline, var(--border-default, #e5e7eb))) 1 / 0 0 var(--hairline-width, 1px) 0\n );\n height: auto;\n padding-bottom: 0;\n width: 100%;\n justify-content: flex-start;\n}\n\n/* Line variant: content-width triggers with comfortable padding — the strip\n reads as an underlined rail, not a segmented control. */\n.listVariantLine .trigger {\n flex: 0 0 auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n}\n\n/* Tabs trigger */\n.trigger {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--tabs-trigger-radius, var(--radius-md, 0.375rem));\n border: 1px solid transparent;\n padding: var(--tabs-trigger-padding, var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem));\n font-size: var(--tabs-trigger-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--tabs-trigger-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--tabs-trigger-letter-spacing, inherit);\n text-transform: var(--tabs-trigger-text-transform, inherit);\n white-space: nowrap;\n color: var(--tabs-trigger-color, var(--text-secondary, #6b7280));\n background: transparent;\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n flex: 1;\n height: calc(100% - 2px);\n}\n\n.trigger:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .trigger:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.trigger:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.trigger:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.trigger[data-state=\"active\"] {\n background-color: var(--tabs-trigger-active-bg, var(--surface-page, #ffffff));\n color: var(--tabs-trigger-active-color, var(--text-primary, #111827));\n box-shadow: var(--tabs-trigger-active-shadow, var(--shadow-sm));\n}\n\n/* Trigger label — wraps children to allow count pill alongside */\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Count pill */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1.5;\n vertical-align: baseline;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, #6b7280);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Active-state re-tone: regardless of countTone, re-tone to primary on active */\n.trigger[data-state=\"active\"] .count {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Tabs content */\n.content {\n flex: 1;\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n}\n\n.content:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Line variant: active trigger loses shadow/bg, just text color */\n.listVariantLine .trigger[data-state=\"active\"] {\n background-color: transparent;\n box-shadow: none;\n}\n\n/* Animated sliding indicator for line variant */\n.indicator {\n position: absolute;\n bottom: -1px;\n height: var(--tabs-indicator-height, 2px);\n /* Default: text-primary */\n background-color: var(--tabs-indicator-color, var(--text-primary, #111827));\n border-radius: var(--radius-full, 9999px);\n transition: left var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Density axis — editorial: brand primary underline for dark admin surfaces */\n:global([data-density=\"editorial\"]) .indicator {\n background-color: var(--primary);\n}\n"
893
+ "content": "/* Tabs root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Tabs list */\n.list {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--tabs-list-radius, var(--radius-full, 9999px));\n padding: var(--tabs-list-padding, calc(var(--spacing-1, 0.25rem) * 0.75));\n color: var(--tabs-list-color, var(--text-secondary, #6b7280));\n height: var(--tabs-list-height, 2.25rem);\n width: fit-content;\n}\n\n.listVariantDefault {\n background-color: var(--tabs-list-bg, var(--surface-muted, #f3f4f6));\n}\n\n.listVariantLine {\n position: relative;\n gap: var(--spacing-2, 0.5rem);\n background-color: transparent;\n border-radius: 0;\n /* The border is geometry; the rail rule is painted by border-image at\n --hairline-width, so --hairline-width: 0 removes it with no layout shift.\n A bound --tabs-line-border-bottom (a full border shorthand) is not valid\n as a border-image, so binding it drops the painted rule and draws the\n bound border instead. */\n border-bottom: var(--tabs-line-border-bottom, 1px solid transparent);\n border-image: var(\n --tabs-line-border-bottom,\n linear-gradient(var(--hairline, var(--border-default, #e5e7eb)), var(--hairline, var(--border-default, #e5e7eb))) 1 / 0 0 var(--hairline-width, 1px) 0\n );\n height: auto;\n padding-bottom: 0;\n width: 100%;\n justify-content: flex-start;\n}\n\n/* Line variant: content-width triggers with comfortable padding — the strip\n reads as an underlined rail, not a segmented control. */\n.listVariantLine .trigger {\n flex: 0 0 auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n --label-box-height: calc(1lh + var(--spacing-2, 0.5rem) * 2 + 2px);\n}\n\n/* Tabs trigger */\n.trigger {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--tabs-trigger-radius, var(--radius-md, 0.375rem));\n border: 1px solid transparent;\n padding: var(--tabs-trigger-padding, var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem));\n font-size: var(--tabs-trigger-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--tabs-trigger-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--tabs-trigger-letter-spacing, inherit);\n text-transform: var(--tabs-trigger-text-transform, inherit);\n white-space: nowrap;\n color: var(--tabs-trigger-color, var(--text-secondary, #6b7280));\n background: transparent;\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n flex: 1;\n height: calc(100% - 2px);\n /* VI-684: outer height, for the label correction (list height - list padding - 2px). */\n --label-box-height: calc(var(--tabs-list-height, 2.25rem) - var(--spacing-1, 0.25rem) * 1.5 - 2px);\n}\n\n.trigger:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .trigger:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.trigger:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.trigger:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.trigger[data-state=\"active\"] {\n background-color: var(--tabs-trigger-active-bg, var(--surface-page, #ffffff));\n color: var(--tabs-trigger-active-color, var(--text-primary, #111827));\n box-shadow: var(--tabs-trigger-active-shadow, var(--shadow-sm));\n}\n\n/* Trigger label — wraps children to allow count pill alongside */\n.label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Bare label text (no count pill) — same block, without the flex sizing of .label. */\n.text {\n display: block;\n white-space: nowrap;\n}\n\n/* Count pill */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: 1.5;\n vertical-align: baseline;\n}\n\n.countNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: var(--text-secondary, #6b7280);\n}\n\n.countPrimary {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Active-state re-tone: regardless of countTone, re-tone to primary on active */\n.trigger[data-state=\"active\"] .count {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--text-link, #2563eb);\n}\n\n/* Tabs content */\n.content {\n flex: 1;\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n}\n\n.content:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Line variant: active trigger loses shadow/bg, just text color */\n.listVariantLine .trigger[data-state=\"active\"] {\n background-color: transparent;\n box-shadow: none;\n}\n\n/* Animated sliding indicator for line variant */\n.indicator {\n position: absolute;\n bottom: -1px;\n height: var(--tabs-indicator-height, 2px);\n /* Default: text-primary */\n background-color: var(--tabs-indicator-color, var(--text-primary, #111827));\n border-radius: var(--radius-full, 9999px);\n transition: left var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Density axis — editorial: brand primary underline for dark admin surfaces */\n:global([data-density=\"editorial\"]) .indicator {\n background-color: var(--primary);\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The label's line\n box is trimmed to the capital-to-baseline band, so the flex container centres\n the ink and not the font's ascent/descent. padding-block hands back the room\n the trim took, so the control's intrinsic box is unchanged. Without text-box\n (Firefox) rendering is exactly what it was. */\n@supports (text-box: trim-both cap alphabetic) {\n .text {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2);\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took, from the control's outer height (--label-box-height, set per size\n above; left unset, the correction is simply off). WebKit places the baseline\n exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .text {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
868
894
  }
869
895
  ]
870
896
  },
@@ -909,12 +935,12 @@
909
935
  {
910
936
  "path": "components/ui/save-status/save-status.tsx",
911
937
  "type": "registry:ui",
912
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowClockwise } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./save-status.module.css\"\n\n/** The states `useAutosave` reports. */\nexport type SaveStatusState = \"saved\" | \"saving\" | \"unsaved\" | \"refused\"\n\nconst DEFAULT_LABELS: Record<SaveStatusState, string> = {\n saved: \"Saved\",\n saving: \"Saving\",\n unsaved: \"Unsaved\",\n refused: \"Couldn't save\",\n}\n\nexport interface SaveStatusProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Where the value stands — pass `useAutosave(...).status`. */\n status: SaveStatusState\n /** Shows a retry mark while `status` is `refused`. Pass `useAutosave(...).retry`. */\n onRetry?: () => void\n /** Override any of the four labels (e.g. to translate them). */\n labels?: Partial<Record<SaveStatusState, string>>\n /** Accessible name of the retry mark. Defaults to \"Retry saving\". */\n retryLabel?: string\n}\n\n/**\n * The autosave readout: status text in a fixed-width slot, so the row it\n * sits in never reflows as the state changes. It is status text, not a\n * button; the only control is the retry mark, shown after a failed save.\n * It draws no edge.\n */\nconst SaveStatus = React.forwardRef<HTMLSpanElement, SaveStatusProps>(\n ({ status, onRetry, labels, retryLabel = \"Retry saving\", className, ...props }, ref) => {\n const label = labels?.[status] ?? DEFAULT_LABELS[status]\n return (\n <span\n ref={ref}\n data-slot=\"save-status\"\n data-state={status}\n className={cn(styles.root, className)}\n {...props}\n >\n <span data-slot=\"save-status-text\" className={styles.text} role=\"status\" aria-live=\"polite\">\n {label}\n </span>\n {status === \"refused\" && onRetry ? (\n <button\n type=\"button\"\n data-slot=\"save-status-retry\"\n className={styles.retry}\n aria-label={retryLabel}\n onClick={onRetry}\n >\n <ArrowClockwise aria-hidden=\"true\" />\n </button>\n ) : null}\n </span>\n )\n }\n)\nSaveStatus.displayName = \"SaveStatus\"\n\nexport { SaveStatus }\n"
938
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowClockwise } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./save-status.module.css\"\n\n/**\n * The states `useAutosave` reports, plus `syncing`: a first sync from an outside\n * source is writing rows the user did not type. `useAutosave` never emits it;\n * the consumer sets it.\n */\nexport type SaveStatusState = \"saved\" | \"saving\" | \"syncing\" | \"unsaved\" | \"refused\"\n\nconst STATES: SaveStatusState[] = [\"saved\", \"saving\", \"syncing\", \"unsaved\", \"refused\"]\n\nconst DEFAULT_LABELS: Record<SaveStatusState, string> = {\n saved: \"Saved\",\n saving: \"Saving\",\n syncing: \"Syncing\",\n unsaved: \"Unsaved\",\n refused: \"Couldn't save\",\n}\n\nexport interface SaveStatusProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Where the value stands — pass `useAutosave(...).status`. */\n status: SaveStatusState\n /** Shows a retry mark while `status` is `refused`. Pass `useAutosave(...).retry`. */\n onRetry?: () => void\n /** Override any of the five labels (e.g. to translate them). */\n labels?: Partial<Record<SaveStatusState, string>>\n /**\n * How `unsaved` is drawn. `\"dot\"` draws a dot alone, no word; the label stays\n * as visually hidden text, so the live region still announces it. Defaults to `\"text\"`.\n */\n unsavedAs?: \"text\" | \"dot\"\n /** Accessible name of the retry mark. Defaults to \"Retry saving\". */\n retryLabel?: string\n}\n\n/**\n * The autosave readout: status text in a slot as wide as the longest label\n * (in the theme's own font) plus the retry mark, or `--save-status-width`, so the row it\n * sits in never reflows as the state changes. It is status text, not a\n * button; the only control is the retry mark, shown after a failed save.\n * It draws no edge.\n */\nconst SaveStatus = React.forwardRef<HTMLSpanElement, SaveStatusProps>(\n ({ status, onRetry, labels, unsavedAs = \"text\", retryLabel = \"Retry saving\", className, ...props }, ref) => {\n const label = labels?.[status] ?? DEFAULT_LABELS[status]\n const asDot = status === \"unsaved\" && unsavedAs === \"dot\"\n const showRetry = status === \"refused\" && onRetry\n return (\n <span\n ref={ref}\n data-slot=\"save-status\"\n data-state={status}\n data-as={asDot ? \"dot\" : undefined}\n className={cn(styles.root, className)}\n {...props}\n >\n <span data-slot=\"save-status-cell\" className={styles.cell}>\n {STATES.map((state) => (\n <span key={state} data-slot=\"save-status-sizer\" className={styles.sizer} aria-hidden=\"true\">\n {labels?.[state] ?? DEFAULT_LABELS[state]}\n </span>\n ))}\n {asDot ? <span data-slot=\"save-status-dot\" className={styles.dot} aria-hidden=\"true\" /> : null}\n <span\n data-slot=\"save-status-text\"\n className={cn(styles.text, asDot && styles.hidden)}\n role=\"status\"\n aria-live=\"polite\"\n >\n {label}\n </span>\n </span>\n {showRetry ? (\n <button\n type=\"button\"\n data-slot=\"save-status-retry\"\n className={styles.retry}\n aria-label={retryLabel}\n onClick={onRetry}\n >\n <ArrowClockwise aria-hidden=\"true\" />\n </button>\n ) : (\n <span data-slot=\"save-status-retry-reserve\" className={styles.reserve} aria-hidden=\"true\" />\n )}\n </span>\n )\n }\n)\nSaveStatus.displayName = \"SaveStatus\"\n\nexport { SaveStatus }\n"
913
939
  },
914
940
  {
915
941
  "path": "components/ui/save-status/save-status.module.css",
916
942
  "type": "registry:ui",
917
- "content": "/* SaveStatus (VI-657)\n *\n * A readout, not a control: status text in a fixed-width slot, so the row\n * never reflows as Saved / Saving / Unsaved / Couldn't save swap in. It draws\n * no edge at all (no border, no outline, no box). The retry mark is the\n * editor's icon mark (VI-659): the icon Button's size, pad and ink, with its\n * hit target on ::before so it adds no height to the row. */\n.root {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: var(--save-status-width, 7.5rem);\n /* Tall enough for the retry mark in every state, so it adds no height. */\n min-height: var(--button-icon-size, 1rem);\n font-size: var(--font-size-xs, 0.75rem);\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Family, case and tracking are inherited properties, so any unlayered\n fallback would override something: `inherit` beats a layered utility,\n `revert-layer` drops an unlayered consumer rule. In visor-base, the lowest\n Visor layer, every consumer rule still wins, and when a token is unset\n revert-layer leaves the readout as if these declarations were absent. */\n@layer visor-base {\n .root {\n font-family: var(--save-status-font-family, revert-layer);\n text-transform: var(--save-status-text-transform, revert-layer);\n letter-spacing: var(--save-status-letter-spacing, revert-layer);\n }\n}\n\n.root[data-state=\"saved\"] {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.root[data-state=\"refused\"] {\n color: var(--text-error, #b91c1c);\n}\n\n/* A longer custom label (a translation) truncates inside the slot rather\n than painting past it. */\n.text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.retry {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.retry::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.retry > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .retry:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.retry:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
943
+ "content": "/* SaveStatus (VI-657)\n *\n * A readout, not a control: status text in a fixed-width slot, so the row\n * never reflows as Saved / Saving / Syncing / Unsaved / Couldn't save swap in. It draws\n * no edge at all (no border, no outline, no box). The retry mark is the\n * editor's icon mark (VI-659): the icon Button's size, pad and ink, with its\n * hit target on ::before so it adds no height to the row. */\n.root {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n /* Sized from content: the cell below is as wide as the longest label in\n the current font, and the retry mark's room is always reserved, so the\n slot is one width in every state. The token overrides it. */\n width: var(--save-status-width, auto);\n /* Tall enough for the retry mark in every state, so it adds no height. */\n min-height: var(--button-icon-size, 1rem);\n font-size: var(--font-size-xs, 0.75rem);\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Family, case and tracking are inherited properties, so any unlayered\n fallback would override something: `inherit` beats a layered utility,\n `revert-layer` drops an unlayered consumer rule. In visor-base, the lowest\n Visor layer, every consumer rule still wins, and when a token is unset\n revert-layer leaves the readout as if these declarations were absent. */\n@layer visor-base {\n .root {\n font-family: var(--save-status-font-family, revert-layer);\n text-transform: var(--save-status-text-transform, revert-layer);\n letter-spacing: var(--save-status-letter-spacing, revert-layer);\n }\n}\n\n.root[data-state=\"saved\"] {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.root[data-state=\"syncing\"] {\n color: var(--save-status-syncing-color, var(--text-secondary, #6b7280));\n}\n\n.root[data-state=\"refused\"] {\n color: var(--text-error, #b91c1c);\n}\n\n/* A longer custom label (a translation) truncates inside the slot rather\n than painting past it. */\n.cell {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n align-items: center;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n}\n\n/* Every label, stacked invisibly in the one cell, sets its width. */\n.sizer,\n.text,\n.dot {\n grid-area: 1 / 1;\n}\n\n.sizer {\n visibility: hidden;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Unsaved as a dot alone (unsavedAs=\"dot\"). The dot sits where the word\n would start and takes the unsaved ink; the slot is unchanged, so the row\n does not move. The label stays in the DOM as visually hidden text. */\n.dot {\n justify-self: start;\n width: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n height: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n.hidden {\n width: 1px;\n height: 1px;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/* The retry mark's room, kept when the mark is not shown, so showing it\n never widens the slot. */\n.reserve {\n flex: none;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n}\n\n.retry {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.retry::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.retry > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .retry:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.retry:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
918
944
  }
919
945
  ]
920
946
  },
@@ -1207,6 +1233,31 @@
1207
1233
  }
1208
1234
  ]
1209
1235
  },
1236
+ {
1237
+ "name": "action-row",
1238
+ "type": "registry:ui",
1239
+ "description": "One tappable list row: a button (or a link through asChild) with a leading icon, a title, an optional second line and a trailing value, badge or caret. Selected, open and disabled states each read a token; ActionRowList stacks rows with a hairline between them. The trailing slot is non-interactive.",
1240
+ "category": "data-display",
1241
+ "dependencies": [
1242
+ "@radix-ui/react-slot",
1243
+ "@loworbitstudio/visor-core"
1244
+ ],
1245
+ "registryDependencies": [
1246
+ "utils"
1247
+ ],
1248
+ "files": [
1249
+ {
1250
+ "path": "components/ui/action-row/action-row.tsx",
1251
+ "type": "registry:ui",
1252
+ "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./action-row.module.css\"\n\n/**\n * Selectors for anything a keyboard or pointer can act on. The trailing slot\n * sits inside the row's own button or link, so none of these may live there:\n * nesting interactive content is invalid HTML and breaks name, role and value\n * (WCAG 4.1.2).\n */\nconst INTERACTIVE_SELECTOR = [\n \"a[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"audio[controls]\",\n \"video[controls]\",\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[role=\"button\"]',\n '[role=\"link\"]',\n '[role=\"switch\"]',\n '[role=\"checkbox\"]',\n '[role=\"radio\"]',\n '[role=\"menuitem\"]',\n '[role=\"tab\"]',\n].join(\",\")\n\n/** Roles whose selected state is `aria-selected`; `aria-current` is not allowed on them. */\nconst SELECTABLE_ROLES = new Set([\"option\", \"tab\", \"row\", \"gridcell\", \"treeitem\"])\n\nexport interface ActionRowProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n /** The row's title. With `asChild`, pass the one element to render as (an `<a>`) and put the title in it. */\n children: React.ReactNode\n /** Render as the single child element (an `<a href>`, a router Link) instead of a `<button>`. */\n asChild?: boolean\n /** Leading slot: an icon, an avatar. Decorative; the title carries the name. */\n leading?: React.ReactNode\n /** Optional second line under the title. */\n line?: React.ReactNode\n /**\n * Trailing slot: a value, a badge, a caret. Non-interactive: the row is\n * already one button or link, so a Switch or a second button here is invalid\n * and fires a development warning.\n */\n trailing?: React.ReactNode\n /** The row is the current choice. Sets `aria-current` (`aria-selected` on option, tab, row, gridcell and treeitem roles). */\n selected?: boolean\n /** The row has opened something (a picker, a popover). Sets `aria-expanded` and `data-state=\"open\"`. */\n open?: boolean\n disabled?: boolean\n}\n\n/** Warns once per mount, in development only, when the trailing slot holds a focusable element. */\nfunction useTrailingInteractiveWarning(\n ref: React.RefObject<HTMLElement | null>,\n trailing: React.ReactNode\n) {\n const warned = React.useRef(false)\n React.useEffect(() => {\n if (process.env.NODE_ENV === \"production\" || warned.current || trailing == null) return\n const found = ref.current?.querySelector(INTERACTIVE_SELECTOR)\n if (found) {\n warned.current = true\n // eslint-disable-next-line no-console\n console.warn(\n `[ActionRow] the trailing slot holds an interactive <${found.tagName.toLowerCase()}>. ` +\n \"The row is already one button or link, so nested interactive content is invalid HTML \" +\n \"(WCAG 4.1.2). Keep the trailing slot to a value, badge or caret; a row that needs a \" +\n \"Switch is a label row with the Switch as its own control.\"\n )\n }\n })\n}\n\n/**\n * ActionRow — one tappable list row: a leading icon, a title, an optional\n * second line and a trailing value or caret. The whole row is one `<button>`\n * (or an `<a>` through `asChild`), at least 44px tall.\n *\n * Rest, hover, focus-visible, selected, open and disabled each have a token.\n * Stack rows in an `ActionRowList` for a hairline between them. A row that\n * opens in place (an accordion) is the consumer's: this primitive is the\n * closed row, and `open` only styles a row that has opened something.\n */\nconst ActionRow = React.forwardRef<HTMLButtonElement, ActionRowProps>(\n (\n {\n className,\n asChild = false,\n leading,\n line,\n trailing,\n selected = false,\n open,\n disabled = false,\n type,\n role,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n const trailingRef = React.useRef<HTMLSpanElement | null>(null)\n useTrailingInteractiveWarning(trailingRef, trailing)\n\n const selectedAttr = role && SELECTABLE_ROLES.has(role) ? \"aria-selected\" : \"aria-current\"\n const stateProps: Record<string, unknown> = {}\n if (selected) stateProps[selectedAttr] = props[\"aria-current\"] ?? \"true\"\n if (open !== undefined) {\n stateProps[\"aria-expanded\"] = open\n stateProps[\"data-state\"] = open ? \"open\" : \"closed\"\n }\n if (disabled) {\n stateProps[\"data-disabled\"] = \"\"\n if (asChild) {\n // A link has no `disabled`; keep it out of the tab order and inert.\n stateProps[\"aria-disabled\"] = true\n stateProps.tabIndex = -1\n } else {\n stateProps.disabled = true\n }\n }\n\n const body = (title: React.ReactNode) => (\n <>\n {leading ? (\n <span data-slot=\"action-row-leading\" className={styles.leading} aria-hidden=\"true\">\n {leading}\n </span>\n ) : null}\n <span data-slot=\"action-row-body\" className={styles.body}>\n <span data-slot=\"action-row-title\" className={styles.title}>\n {title}\n </span>\n {line ? (\n <>\n {\" \"}\n <span data-slot=\"action-row-line\" className={styles.line}>\n {line}\n </span>\n </>\n ) : null}\n </span>\n {trailing != null && trailing !== false ? \" \" : null}\n {trailing != null && trailing !== false ? (\n <span data-slot=\"action-row-trailing\" className={styles.trailing} ref={trailingRef}>\n {trailing}\n </span>\n ) : null}\n </>\n )\n\n return (\n <Comp\n ref={ref}\n {...(asChild ? {} : { type: type ?? \"button\" })}\n role={role}\n data-slot=\"action-row\"\n data-selected={selected ? \"true\" : undefined}\n className={cn(styles.root, className)}\n onClick={\n disabled && asChild\n ? (event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault()\n : onClick\n }\n {...props}\n {...stateProps}\n >\n {asChild && React.isValidElement<{ children?: React.ReactNode }>(children)\n ? React.cloneElement(children, undefined, body(children.props.children))\n : body(children)}\n </Comp>\n )\n }\n)\nActionRow.displayName = \"ActionRow\"\n\nexport interface ActionRowListProps extends React.HTMLAttributes<HTMLUListElement> {\n /** The rows. Each is wrapped in a list item, so a screen reader announces \"list, N items\". */\n children: React.ReactNode\n}\n\n/**\n * ActionRowList — stacks ActionRows with a hairline between them. The hairline\n * reads `--hairline-width`, so `hairlines: off` in a theme removes it with no\n * layout shift.\n */\nconst ActionRowList = React.forwardRef<HTMLUListElement, ActionRowListProps>(\n ({ className, children, ...props }, ref) => (\n <ul ref={ref} data-slot=\"action-row-list\" className={cn(styles.list, className)} {...props}>\n {React.Children.toArray(children).map((child, index) => (\n <li key={React.isValidElement(child) && child.key != null ? child.key : index} className={styles.item}>\n {child}\n </li>\n ))}\n </ul>\n )\n)\nActionRowList.displayName = \"ActionRowList\"\n\nexport { ActionRow, ActionRowList }\n"
1253
+ },
1254
+ {
1255
+ "path": "components/ui/action-row/action-row.module.css",
1256
+ "type": "registry:ui",
1257
+ "content": "/* ActionRow — one tappable list row: leading icon, title, optional line and a\n * trailing value or caret. Every state reads a theme-bindable token (the\n * `action-row` component-token family) with a Tier-1 fallback, so an unbound\n * theme renders from the semantic surface ramp.\n */\n.root {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--action-row-gap, var(--spacing-3, 0.75rem));\n box-sizing: border-box;\n width: 100%;\n /* 44px at every width: the whole row is the target (WCAG 2.5.8 asks 24px). */\n min-height: var(--action-row-min-height, 2.75rem);\n padding: var(--action-row-padding, var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem));\n margin: 0;\n border: 0;\n border-radius: var(--action-row-radius, var(--radius-md, 0.375rem));\n background-color: transparent;\n color: var(--text-primary, #111827);\n font-size: var(--font-size-base, 1rem);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Selected edge. The row draws no resting edge. The selected state's edge is a\n state edge, so it sits on --control-state-edge-width, the width focus and\n invalid use: `edges: off` removes resting edges\n and leaves this one (WCAG 1.4.11, 1.4.1). Inset outline: no layout shift. */\n.root::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n outline: var(--control-state-edge-width, 1px) solid transparent;\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.root:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .root:hover:not([data-disabled]) {\n background-color: var(--action-row-hover-bg, var(--surface-interactive-hover, #f3f4f6));\n }\n}\n\n/* Open: the row has opened something (aria-expanded or data-state=\"open\"). */\n.root[aria-expanded=\"true\"],\n.root[data-state=\"open\"] {\n background-color: var(--action-row-open-bg, var(--surface-interactive-hover, #f3f4f6));\n}\n\n/* Selected: a lifted fill (--surface-interactive-active, one step above hover:\n a neutral lift in every theme, not the accent --surface-selected) with a light\n inset edge, never white. The edge (a light --text-secondary, at least 3:1\n against the fill and the page) and aria-current / aria-selected carry the\n state alongside the fill, so colour is never the only cue. */\n.root[data-selected=\"true\"] {\n background-color: var(--action-row-selected-bg, var(--surface-interactive-active, #e5e7eb));\n}\n\n.root[data-selected=\"true\"]::after {\n outline-color: var(--action-row-selected-edge-color, var(--text-secondary, #6b7280));\n}\n\n.root[data-disabled] {\n cursor: not-allowed;\n opacity: var(--action-row-disabled-opacity, var(--opacity-50, 0.5));\n}\n\n.leading {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: var(--action-row-leading-size, 1.25rem);\n line-height: 1;\n color: var(--action-row-leading-color, var(--text-secondary, #6b7280));\n}\n\n.leading svg {\n display: block;\n}\n\n.body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-width: 0;\n}\n\n.title {\n font-weight: var(--action-row-title-weight, var(--font-weight-medium, 500));\n line-height: var(--line-height-snug, 1.375);\n overflow-wrap: anywhere;\n}\n\n.line {\n font-size: var(--font-size-sm, 0.875rem);\n line-height: var(--line-height-snug, 1.375);\n color: var(--action-row-line-color, var(--text-secondary, #6b7280));\n overflow-wrap: anywhere;\n}\n\n.trailing {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n margin-inline-start: auto;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--action-row-trailing-color, var(--text-secondary, #6b7280));\n}\n\n.trailing svg {\n display: block;\n}\n\n/* List: a hairline between rows, on the VI-680 switch. The reserved top border\n keeps its width when --hairline-width is 0 (only the paint is removed), so\n turning hairlines off never moves a row. */\n.list {\n display: flex;\n flex-direction: column;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item + .item {\n border-top: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, var(--border-default, #e5e7eb)), var(--hairline, var(--border-default, #e5e7eb))) 1 / var(--hairline-width, var(--stroke-width-thin, 1px)) 0 0 0;\n}\n"
1258
+ }
1259
+ ]
1260
+ },
1210
1261
  {
1211
1262
  "name": "collapsible",
1212
1263
  "type": "registry:ui",
@@ -1249,12 +1300,12 @@
1249
1300
  {
1250
1301
  "path": "components/ui/radio-group/radio-group.tsx",
1251
1302
  "type": "registry:ui",
1252
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { CircleIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./radio-group.module.css\"\n\nexport type RadioGroupProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, ...props }, ref) => {\n return (\n <RadioGroupPrimitive.Root\n data-slot=\"radio-group\"\n className={cn(styles.root, className)}\n ref={ref}\n {...props}\n />\n )\n})\nRadioGroup.displayName = \"RadioGroup\"\n\nexport type RadioGroupItemProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(({ className, ...props }, ref) => {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className={styles.indicator}\n >\n <CircleIcon weight=\"fill\" className={styles.icon} />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n})\nRadioGroupItem.displayName = \"RadioGroupItem\"\n\nexport { RadioGroup, RadioGroupItem }\n"
1303
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { CheckIcon, CircleIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./radio-group.module.css\"\n\nexport type RadioGroupVariant = \"default\" | \"card\"\n\nexport interface RadioGroupProps\n extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> {\n /**\n * `default` draws a dot beside a label. `card` draws each item as a card with\n * an icon, a title and a one-line description.\n */\n variant?: RadioGroupVariant\n}\n\nconst RadioGroupVariantContext = React.createContext<RadioGroupVariant>(\"default\")\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, variant = \"default\", ...props }, ref) => {\n return (\n <RadioGroupVariantContext.Provider value={variant}>\n <RadioGroupPrimitive.Root\n data-slot=\"radio-group\"\n data-variant={variant}\n className={cn(styles.root, variant === \"card\" && styles.rootCard, className)}\n ref={ref}\n {...props}\n />\n </RadioGroupVariantContext.Provider>\n )\n})\nRadioGroup.displayName = \"RadioGroup\"\n\nexport interface RadioGroupItemProps\n extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, \"title\"> {\n /** Card variant: leading icon or swatch. Decorative, hidden from assistive tech. */\n icon?: React.ReactNode\n /** Card variant: the option's name. It is the item's accessible name. */\n title?: React.ReactNode\n /** Card variant: one line saying what the option means. It describes the item. */\n description?: React.ReactNode\n}\n\nfunction joinIds(...ids: Array<string | undefined>): string | undefined {\n const joined = ids.filter(Boolean).join(\" \")\n return joined || undefined\n}\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(({ className, icon, title, description, children, ...props }, ref) => {\n const variant = React.useContext(RadioGroupVariantContext)\n const uid = React.useId()\n const titleId = `${uid}-title`\n const descriptionId = `${uid}-description`\n\n if (variant === \"card\") {\n const hasTitle = title !== undefined && title !== null\n const hasDescription = description !== undefined && description !== null\n const {\n \"aria-describedby\": describedBy,\n \"aria-labelledby\": labelledBy,\n ...rest\n } = props\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(styles.card, className)}\n aria-labelledby={labelledBy ?? (hasTitle ? titleId : undefined)}\n aria-describedby={joinIds(hasDescription ? descriptionId : undefined, describedBy)}\n ref={ref}\n {...rest}\n >\n {icon ? (\n <span data-slot=\"radio-group-card-icon\" className={styles.cardIcon} aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className={styles.cardText}>\n {hasTitle ? (\n <span id={titleId} data-slot=\"radio-group-card-title\" className={styles.cardTitle}>\n {title}\n </span>\n ) : null}\n {hasDescription ? (\n <span\n id={descriptionId}\n data-slot=\"radio-group-card-description\"\n className={styles.cardDescription}\n >\n {description}\n </span>\n ) : null}\n {children}\n </span>\n <span className={styles.cardMark} aria-hidden=\"true\">\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className={styles.cardIndicator}\n >\n <CheckIcon weight=\"bold\" className={styles.cardCheck} />\n </RadioGroupPrimitive.Indicator>\n </span>\n </RadioGroupPrimitive.Item>\n )\n }\n\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className={styles.indicator}\n >\n <CircleIcon weight=\"fill\" className={styles.icon} />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n})\nRadioGroupItem.displayName = \"RadioGroupItem\"\n\nexport { RadioGroup, RadioGroupItem }\n"
1253
1304
  },
1254
1305
  {
1255
1306
  "path": "components/ui/radio-group/radio-group.module.css",
1256
1307
  "type": "registry:ui",
1257
- "content": "/* RadioGroup root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* RadioGroup item (the button) */\n.item {\n position: relative;\n display: flex;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n border: 1px solid var(--radio-border, var(--border-default, #e5e7eb));\n background-color: transparent;\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .item:hover:not(:focus-visible):not(:disabled) {\n border-color: var(--border-strong, #d1d5db);\n }\n}\n\n.item:focus-visible {\n border-color: var(--border-focus, #111827);\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.item:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.item[data-state=\"checked\"] {\n border-color: var(--interactive-primary-bg, var(--primary, #111827));\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Indicator */\n.indicator {\n display: grid;\n place-content: center;\n color: currentColor;\n}\n\n.icon {\n width: 0.5rem;\n height: 0.5rem;\n}\n"
1308
+ "content": "/* RadioGroup root */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* RadioGroup item (the button)\n\n VI-668: the border only holds box geometry and is always transparent. The\n visible resting edge is an inset outline on the shared --control-edge-*\n tokens, so --control-edge-width: 0 on any ancestor removes it with no layout\n shift. --radio-border is the per-component colour override. */\n.item {\n position: relative;\n display: flex;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n border: 1px solid transparent;\n background-color: transparent;\n /* With the edge switched off an unchecked dot would vanish, and the dot is\n the control. So it takes a fill whose size is derived from the switch: 0\n while the edge is on, the whole box once the edge width drops to 0. */\n background-image: linear-gradient(var(--radio-edgeless-bg, var(--control-edge-color, var(--border-default, #e5e7eb))), var(--radio-edgeless-bg, var(--control-edge-color, var(--border-default, #e5e7eb))));\n background-size: calc(max(0px, 1px - var(--control-edge-width, 1px)) * 1000);\n background-origin: border-box;\n background-repeat: no-repeat;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--radio-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .item:hover:not(:focus-visible):not(:disabled) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus draws at --control-state-edge-width, which the edge switch does not touch. */\n.item:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.item:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.item[data-state=\"checked\"] {\n outline-color: var(--interactive-primary-bg, var(--primary, #111827));\n background-image: none;\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Indicator */\n.indicator {\n display: grid;\n place-content: center;\n color: currentColor;\n}\n\n.icon {\n width: 0.5rem;\n height: 0.5rem;\n}\n\n/* ---------------------------------------------------------------------------\n Card variant (VI-668). Each item is a card: icon, title, one-line\n description and a check mark. The border is geometry only; the resting edge\n is an inset outline on the shared --control-edge-* tokens, so\n --control-edge-width: 0 removes it. The selected card is a lifted fill with a\n light inset edge, never white, and it also carries a check, so selection is\n never colour alone.\n --------------------------------------------------------------------------- */\n.rootCard {\n gap: var(--spacing-3, 0.75rem);\n}\n\n.card {\n position: relative;\n display: flex;\n width: 100%;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border: 1px solid transparent;\n border-radius: var(--radio-card-radius, var(--radius-lg, 0.5rem));\n background-color: var(--radio-card-bg, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n text-align: left;\n outline: var(--control-edge-width, var(--stroke-width-thin, 1px)) var(--control-edge-style, solid) var(--radio-card-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, var(--stroke-width-thin, 1px)));\n cursor: pointer;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .card:hover:not(:focus-visible):not(:disabled):not([data-state=\"checked\"]) {\n outline-color: var(--border-strong, #d1d5db);\n background-color: var(--radio-card-hover-bg, var(--surface-interactive-hover, var(--surface-muted, #f3f4f6)));\n }\n}\n\n.card:focus-visible {\n outline: var(--control-state-edge-width, var(--stroke-width-thin, 1px)) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, var(--stroke-width-thin, 1px)));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 25%, transparent);\n}\n\n.card[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, var(--stroke-width-thin, 1px)) solid var(--border-error, var(--destructive, #dc2626));\n outline-offset: calc(-1 * var(--control-state-edge-width, var(--stroke-width-thin, 1px)));\n}\n\n.card[data-state=\"checked\"] {\n background-color: var(--radio-card-selected-bg, color-mix(in srgb, var(--text-primary, #111827) 10%, var(--radio-card-bg, var(--surface-card, #ffffff))));\n outline-color: var(--radio-card-selected-border, color-mix(in srgb, var(--text-primary, #111827) 40%, transparent));\n}\n\n/* A disabled card stays in place and stays readable: dimmed, not removed. */\n.card:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-60, 0.6);\n}\n\n.cardIcon {\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n width: var(--radio-card-icon-size, 1.5rem);\n height: var(--radio-card-icon-size, 1.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--radio-card-icon-size, 1.5rem);\n}\n\n.card[data-state=\"checked\"] .cardIcon {\n color: var(--text-primary, #111827);\n}\n\n.cardText {\n display: flex;\n min-width: 0;\n flex: 1;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.cardTitle {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: var(--line-height-tight, 1.25);\n color: var(--text-primary, #111827);\n}\n\n.cardDescription {\n font-size: var(--font-size-xs, 0.75rem);\n line-height: var(--line-height-snug, 1.375);\n color: var(--text-secondary, #6b7280);\n}\n\n/* The mark: a ring that holds a check once the card is chosen. */\n.cardMark {\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: var(--radius-full, 9999px);\n}\n\n.cardIndicator {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n border-radius: inherit;\n background-color: var(--radio-card-mark-bg, var(--interactive-primary-bg, var(--primary, #111827)));\n color: var(--radio-card-mark-text, var(--interactive-primary-text, #f9fafb));\n}\n\n.cardCheck {\n width: 0.75rem;\n height: 0.75rem;\n}\n"
1258
1309
  }
1259
1310
  ]
1260
1311
  },
@@ -1325,12 +1376,12 @@
1325
1376
  {
1326
1377
  "path": "components/ui/toggle-group/toggle-group.tsx",
1327
1378
  "type": "registry:ui",
1328
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./toggle-group.module.css\"\n\n/* ─── Context ───────────────────────────────────────────────────────── */\n\ntype ToggleGroupContextValue = {\n variant?: \"default\" | \"outline\" | null\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | null\n}\n\nconst ToggleGroupContext = React.createContext<ToggleGroupContextValue>({})\n\n/* ─── Sliding indicator hook ────────────────────────────────────────── */\n\nfunction useSlidingIndicator(\n rootRef: React.RefObject<HTMLElement | null>,\n variant: string | null | undefined,\n) {\n const indicatorRef = React.useRef<HTMLSpanElement>(null)\n\n const updateIndicator = React.useCallback(() => {\n const root = rootRef.current\n const indicator = indicatorRef.current\n if (!root || !indicator || variant !== \"outline\") return\n\n const activeItem = root.querySelector(\n '[data-state=\"on\"]',\n ) as HTMLElement | null\n\n if (!activeItem) {\n indicator.style.opacity = \"0\"\n return\n }\n\n indicator.style.opacity = \"1\"\n indicator.style.width = `${activeItem.offsetWidth}px`\n indicator.style.height = `${activeItem.offsetHeight}px`\n indicator.style.transform = `translate(${activeItem.offsetLeft}px, ${activeItem.offsetTop}px)`\n }, [rootRef, variant])\n\n // Update on mount and whenever children change state\n React.useEffect(() => {\n const root = rootRef.current\n if (!root || variant !== \"outline\") return\n\n // Initial position\n updateIndicator()\n\n // Observe data-state changes on children\n const observer = new MutationObserver(updateIndicator)\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n return () => observer.disconnect()\n }, [rootRef, variant, updateIndicator])\n\n return indicatorRef\n}\n\n/* ─── ToggleGroup ───────────────────────────────────────────────────── */\n\nconst toggleGroupVariants = cva(styles.root, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outline: styles.variantOutline,\n },\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type ToggleGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> &\n VariantProps<typeof toggleGroupVariants>\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n ToggleGroupProps\n>(({ className, variant, size, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ variant, size }),\n [variant, size]\n )\n const internalRef = React.useRef<HTMLDivElement>(null)\n // Sliding indicator only works for single-select toggle groups\n const useIndicator = variant === \"outline\" && props.type !== \"multiple\"\n const indicatorRef = useSlidingIndicator(internalRef, useIndicator ? variant : undefined)\n\n // Merge internal ref with forwarded ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-type={props.type ?? \"single\"}\n className={cn(toggleGroupVariants({ variant, size }), className)}\n ref={mergedRef}\n {...props}\n >\n {useIndicator && (\n <span\n ref={indicatorRef}\n className={styles.indicator}\n aria-hidden=\"true\"\n />\n )}\n {props.children}\n </ToggleGroupPrimitive.Root>\n </ToggleGroupContext.Provider>\n )\n})\nToggleGroup.displayName = \"ToggleGroup\"\n\n/* ─── ToggleGroupItem ───────────────────────────────────────────────── */\n\nconst toggleGroupItemVariants = cva(styles.item, {\n variants: {\n variant: {\n default: styles.itemVariantDefault,\n outline: styles.itemVariantOutline,\n },\n size: {\n xs: styles.itemSizeXs,\n sm: styles.itemSizeSm,\n md: styles.itemSizeMd,\n lg: styles.itemSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ToggleGroupItemProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>,\n VariantProps<typeof toggleGroupItemVariants> {}\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ToggleGroupItemProps\n>(({ className, variant, size, ...props }, ref) => {\n const ctx = React.useContext(ToggleGroupContext)\n const resolvedVariant = variant ?? ctx.variant\n const resolvedSize = size ?? ctx.size\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n className={cn(toggleGroupItemVariants({ variant: resolvedVariant, size: resolvedSize }), className)}\n ref={ref}\n {...props}\n />\n )\n})\nToggleGroupItem.displayName = \"ToggleGroupItem\"\n\nexport { ToggleGroup, ToggleGroupItem, toggleGroupVariants, toggleGroupItemVariants }\n"
1379
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./toggle-group.module.css\"\n\n/* ─── Context ───────────────────────────────────────────────────────── */\n\ntype ToggleGroupContextValue = {\n variant?: \"default\" | \"outline\" | null\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | null\n}\n\nconst ToggleGroupContext = React.createContext<ToggleGroupContextValue>({})\n\n/* ─── Sliding indicator hook ────────────────────────────────────────── */\n\nfunction useSlidingIndicator(\n rootRef: React.RefObject<HTMLElement | null>,\n variant: string | null | undefined,\n) {\n const indicatorRef = React.useRef<HTMLSpanElement>(null)\n\n const updateIndicator = React.useCallback(() => {\n const root = rootRef.current\n const indicator = indicatorRef.current\n if (!root || !indicator || variant !== \"outline\") return\n\n const activeItem = root.querySelector(\n '[data-state=\"on\"]',\n ) as HTMLElement | null\n\n if (!activeItem) {\n indicator.style.opacity = \"0\"\n return\n }\n\n indicator.style.opacity = \"1\"\n indicator.style.width = `${activeItem.offsetWidth}px`\n indicator.style.height = `${activeItem.offsetHeight}px`\n indicator.style.transform = `translate(${activeItem.offsetLeft}px, ${activeItem.offsetTop}px)`\n }, [rootRef, variant])\n\n // Update on mount and whenever children change state\n React.useEffect(() => {\n const root = rootRef.current\n if (!root || variant !== \"outline\") return\n\n // Initial position\n updateIndicator()\n\n // Observe data-state changes on children\n const observer = new MutationObserver(updateIndicator)\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n return () => observer.disconnect()\n }, [rootRef, variant, updateIndicator])\n\n return indicatorRef\n}\n\n/* ─── ToggleGroup ───────────────────────────────────────────────────── */\n\nconst toggleGroupVariants = cva(styles.root, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outline: styles.variantOutline,\n },\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type ToggleGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> &\n VariantProps<typeof toggleGroupVariants>\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n ToggleGroupProps\n>(({ className, variant, size, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ variant, size }),\n [variant, size]\n )\n const internalRef = React.useRef<HTMLDivElement>(null)\n // Sliding indicator only works for single-select toggle groups\n const useIndicator = variant === \"outline\" && props.type !== \"multiple\"\n const indicatorRef = useSlidingIndicator(internalRef, useIndicator ? variant : undefined)\n\n // Merge internal ref with forwarded ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-type={props.type ?? \"single\"}\n className={cn(toggleGroupVariants({ variant, size }), className)}\n ref={mergedRef}\n {...props}\n >\n {useIndicator && (\n <span\n ref={indicatorRef}\n className={styles.indicator}\n aria-hidden=\"true\"\n />\n )}\n {props.children}\n </ToggleGroupPrimitive.Root>\n </ToggleGroupContext.Provider>\n )\n})\nToggleGroup.displayName = \"ToggleGroup\"\n\n/* ─── ToggleGroupItem ───────────────────────────────────────────────── */\n\nconst toggleGroupItemVariants = cva(styles.item, {\n variants: {\n variant: {\n default: styles.itemVariantDefault,\n outline: styles.itemVariantOutline,\n },\n size: {\n xs: styles.itemSizeXs,\n sm: styles.itemSizeSm,\n md: styles.itemSizeMd,\n lg: styles.itemSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ToggleGroupItemProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>,\n VariantProps<typeof toggleGroupItemVariants> {}\n\n/**\n * Wrap bare text in a block label so the stylesheet can trim its line box to the\n * capital-to-baseline band (see `.label`), centring the capitals in the item\n * whatever the font (VI-684). Elements (icons) pass through.\n */\nfunction wrapLabel(children: React.ReactNode): React.ReactNode {\n return React.Children.map(children, (child) =>\n typeof child === \"string\" || typeof child === \"number\" ? (\n <span className={styles.label} data-slot=\"toggle-group-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ToggleGroupItemProps\n>(({ className, variant, size, children, ...props }, ref) => {\n const ctx = React.useContext(ToggleGroupContext)\n const resolvedVariant = variant ?? ctx.variant\n const resolvedSize = size ?? ctx.size\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n className={cn(toggleGroupItemVariants({ variant: resolvedVariant, size: resolvedSize }), className)}\n ref={ref}\n {...props}\n >\n {wrapLabel(children)}\n </ToggleGroupPrimitive.Item>\n )\n})\nToggleGroupItem.displayName = \"ToggleGroupItem\"\n\nexport { ToggleGroup, ToggleGroupItem, toggleGroupVariants, toggleGroupItemVariants }\n"
1329
1380
  },
1330
1381
  {
1331
1382
  "path": "components/ui/toggle-group/toggle-group.module.css",
1332
1383
  "type": "registry:ui",
1333
- "content": "/* ToggleGroup root */\n.root {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.variantDefault {\n /* items have no outer border, rely on item bg */\n}\n\n.variantOutline {\n position: relative;\n border: 1px solid var(--border-default, #e5e7eb);\n gap: 0;\n}\n\n/* Sliding indicator pill */\n.indicator {\n position: absolute;\n top: 0;\n left: 0;\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-radius: var(--radius-md, 0.375rem);\n opacity: 0;\n pointer-events: none;\n z-index: 0;\n transition:\n transform var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Match indicator radius to item radius per size */\n.sizeXs .indicator { border-radius: var(--radius-sm, 0.25rem); }\n.sizeSm .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeMd .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeLg .indicator { border-radius: var(--radius-lg, 0.5rem); }\n\n.sizeXs {\n /* inherited by items */\n}\n\n.variantOutline.sizeXs {\n padding: calc(var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n}\n\n.sizeSm {\n /* inherited by items */\n}\n\n.variantOutline.sizeSm {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeMd {\n /* inherited by items */\n}\n\n.variantOutline.sizeMd {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeLg {\n /* inherited by items */\n}\n\n.variantOutline.sizeLg {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-xl, 0.75rem);\n}\n\n/* ToggleGroup item */\n.item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n user-select: none;\n white-space: nowrap;\n}\n\n.item:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.item:focus-visible {\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.item:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* Default variant items */\n@media (hover: hover) {\n .itemVariantDefault:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n.itemVariantDefault[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Outline variant items — indicator provides the active background */\n.itemVariantOutline {\n position: relative;\n z-index: 1;\n}\n\n@media (hover: hover) {\n .itemVariantOutline:hover:not(:disabled):not([data-state=\"on\"]) {\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 40%, transparent);\n }\n}\n\n.itemVariantOutline[data-state=\"on\"] {\n background-color: transparent;\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Multiple-select outline — items need their own active bg (no sliding indicator) */\n:global([data-type=\"multiple\"]) .itemVariantOutline[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Sizes */\n.itemSizeXs {\n height: 1.5rem;\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-sm, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeSm {\n height: 1.75rem;\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeMd {\n height: 2.25rem;\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n.itemSizeLg {\n height: 2.75rem;\n padding: 0 var(--spacing-4, 1rem);\n border-radius: var(--radius-lg, 0.5rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n"
1384
+ "content": "/* ToggleGroup root */\n.root {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.variantDefault {\n /* items have no outer border, rely on item bg */\n}\n\n.variantOutline {\n position: relative;\n border: 1px solid var(--border-default, #e5e7eb);\n gap: 0;\n}\n\n/* Sliding indicator pill */\n.indicator {\n position: absolute;\n top: 0;\n left: 0;\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-radius: var(--radius-md, 0.375rem);\n opacity: 0;\n pointer-events: none;\n z-index: 0;\n transition:\n transform var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Match indicator radius to item radius per size */\n.sizeXs .indicator { border-radius: var(--radius-sm, 0.25rem); }\n.sizeSm .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeMd .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeLg .indicator { border-radius: var(--radius-lg, 0.5rem); }\n\n.sizeXs {\n /* inherited by items */\n}\n\n.variantOutline.sizeXs {\n padding: calc(var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n}\n\n.sizeSm {\n /* inherited by items */\n}\n\n.variantOutline.sizeSm {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeMd {\n /* inherited by items */\n}\n\n.variantOutline.sizeMd {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeLg {\n /* inherited by items */\n}\n\n.variantOutline.sizeLg {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-xl, 0.75rem);\n}\n\n/* ToggleGroup item */\n.item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n user-select: none;\n white-space: nowrap;\n}\n\n.item:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.item:focus-visible {\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.item:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* Default variant items */\n@media (hover: hover) {\n .itemVariantDefault:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n.itemVariantDefault[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Outline variant items — indicator provides the active background */\n.itemVariantOutline {\n position: relative;\n z-index: 1;\n}\n\n@media (hover: hover) {\n .itemVariantOutline:hover:not(:disabled):not([data-state=\"on\"]) {\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 40%, transparent);\n }\n}\n\n.itemVariantOutline[data-state=\"on\"] {\n background-color: transparent;\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Multiple-select outline — items need their own active bg (no sliding indicator) */\n:global([data-type=\"multiple\"]) .itemVariantOutline[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Sizes */\n.itemSizeXs {\n --label-box-height: 1.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-sm, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeSm {\n --label-box-height: 1.75rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeMd {\n --label-box-height: 2.25rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n.itemSizeLg {\n --label-box-height: 2.75rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n border-radius: var(--radius-lg, 0.5rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The label is a\n block span whose line box is trimmed to the capital-to-baseline band, so the\n flex container centres the ink and not the font's ascent/descent. Without\n text-box (Firefox) it is a plain block and rendering is unchanged. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took. WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
1334
1385
  }
1335
1386
  ]
1336
1387
  },
@@ -2644,12 +2695,12 @@
2644
2695
  {
2645
2696
  "path": "components/ui/empty-state/empty-state.tsx",
2646
2697
  "type": "registry:ui",
2647
- "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./empty-state.module.css\"\n\nconst emptyStateVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n tone: {\n default: styles.toneDefault,\n subtle: styles.toneSubtle,\n },\n /**\n * Surface treatment.\n * - `default` — base placeholder (dashed/borderless per `tone`).\n * - `editorial` — filled card on `--surface-card` with a 64px circular\n * icon chip; an admin-editorial composition. Opt-in; overrides the\n * `tone` surface and `size` icon/heading scale.\n */\n variant: {\n default: undefined,\n editorial: styles.editorial,\n },\n /**\n * Semantic intent — clarifies the cause of the empty state and styles\n * the icon chip accordingly (Borealis spec §5).\n * - `first-use` — no items yet; accent-tinted chip; encourages a creation CTA.\n * - `zero-results` — filter or search returned nothing; accent-tinted chip;\n * encourages a clear-filter action.\n * - `no-access` — permission or feature gate; neutral chip with lock icon.\n */\n intent: {\n \"first-use\": styles.intentFirstUse,\n \"zero-results\": styles.intentZeroResults,\n \"no-access\": styles.intentNoAccess,\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"default\",\n variant: \"default\",\n },\n})\n\ntype HeadingElement = \"h2\" | \"h3\" | \"h4\"\n\nexport interface EmptyStateProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof emptyStateVariants> {\n /** Optional leading visual — typically a Phosphor icon. */\n icon?: React.ReactNode\n /**\n * When true (or when `intent` is set), wraps the icon in a circular chip\n * whose color is controlled by the `intent` variant. Ignored if no `icon` is\n * provided.\n */\n iconWrap?: boolean\n /** Short direct statement of the empty condition. Required. */\n heading: React.ReactNode\n /** Optional 1-2 sentence explanation or guidance. */\n description?: React.ReactNode\n /** Primary CTA slot — typically a Button. */\n action?: React.ReactNode\n /** De-emphasized fallback action — typically a Button. */\n secondaryAction?: React.ReactNode\n /** Heading level for the heading slot. Defaults to `h3`. */\n headingAs?: HeadingElement\n}\n\nconst EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(\n (\n {\n className,\n size,\n tone,\n variant,\n intent,\n icon,\n iconWrap,\n heading,\n description,\n action,\n secondaryAction,\n headingAs = \"h3\",\n ...props\n },\n ref\n ) => {\n const Heading = headingAs as React.ElementType\n const hasBothActions = Boolean(action) && Boolean(secondaryAction)\n const hasAnyAction = Boolean(action) || Boolean(secondaryAction)\n // Auto-activate icon wrap when intent is set, unless explicitly disabled\n const shouldWrapIcon = icon != null && (iconWrap ?? intent != null)\n\n return (\n <div\n ref={ref}\n role=\"status\"\n data-slot=\"empty-state\"\n data-tone={tone ?? \"default\"}\n data-variant={variant ?? \"default\"}\n {...(intent ? { \"data-intent\": intent } : {})}\n className={cn(emptyStateVariants({ size, tone, variant, intent }), className)}\n {...props}\n >\n {icon ? (\n shouldWrapIcon ? (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.iconWrap}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n ) : (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n )\n ) : null}\n <Heading data-slot=\"empty-state-heading\" className={styles.heading}>\n {heading}\n </Heading>\n {description ? (\n <p\n data-slot=\"empty-state-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n {hasAnyAction ? (\n <div\n data-slot=\"empty-state-actions\"\n className={styles.actions}\n {...(hasBothActions ? { role: \"group\" } : {})}\n >\n {action ? (\n <div\n data-slot=\"empty-state-action\"\n className={styles.action}\n >\n {action}\n </div>\n ) : null}\n {secondaryAction ? (\n <div\n data-slot=\"empty-state-secondary-action\"\n className={styles.secondaryAction}\n >\n {secondaryAction}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n }\n)\nEmptyState.displayName = \"EmptyState\"\n\nexport { EmptyState, emptyStateVariants }\n"
2698
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./empty-state.module.css\"\n\nconst emptyStateVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n tone: {\n default: styles.toneDefault,\n subtle: styles.toneSubtle,\n },\n /**\n * Surface treatment.\n * - `default` — base placeholder (dashed/borderless per `tone`).\n * - `editorial` — filled card on `--surface-card` with a 64px circular\n * icon chip; an admin-editorial composition. Opt-in; overrides the\n * `tone` surface and `size` icon/heading scale.\n */\n variant: {\n default: undefined,\n editorial: styles.editorial,\n },\n /**\n * Semantic intent — clarifies the cause of the empty state and styles\n * the icon chip accordingly (Borealis spec §5).\n * - `first-use` — no items yet; accent-tinted chip; encourages a creation CTA.\n * - `zero-results` — filter or search returned nothing; accent-tinted chip;\n * encourages a clear-filter action.\n * - `no-access` — permission or feature gate; neutral chip with lock icon.\n * - `error` — a failed load; icon and heading in the destructive colour,\n * `role=\"alert\"`, and an unchipped icon unless `iconWrap` is set.\n */\n intent: {\n \"first-use\": styles.intentFirstUse,\n \"zero-results\": styles.intentZeroResults,\n \"no-access\": styles.intentNoAccess,\n error: styles.intentError,\n },\n /**\n * Fill the container and centre the content both ways. For a flex or\n * fixed-height parent that gives the empty state room.\n */\n fill: {\n true: styles.fill,\n false: undefined,\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"default\",\n variant: \"default\",\n fill: false,\n },\n})\n\ntype HeadingElement = \"h2\" | \"h3\" | \"h4\"\n\nexport interface EmptyStateProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof emptyStateVariants> {\n /** Optional leading visual — typically a Phosphor icon. */\n icon?: React.ReactNode\n /**\n * When true (or when `intent` is set, except `error`), wraps the icon in a circular chip\n * whose color is controlled by the `intent` variant. Ignored if no `icon` is\n * provided.\n */\n iconWrap?: boolean\n /** Short direct statement of the empty condition. Required. */\n heading: React.ReactNode\n /** Optional 1-2 sentence explanation or guidance. */\n description?: React.ReactNode\n /** Primary CTA slot — typically a Button. */\n action?: React.ReactNode\n /** De-emphasized fallback action — typically a Button. */\n secondaryAction?: React.ReactNode\n /** Heading level for the heading slot. Defaults to `h3`. */\n headingAs?: HeadingElement\n}\n\nconst EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(\n (\n {\n className,\n size,\n tone,\n variant,\n intent,\n fill,\n icon,\n iconWrap,\n heading,\n description,\n action,\n secondaryAction,\n headingAs = \"h3\",\n ...props\n },\n ref\n ) => {\n const Heading = headingAs as React.ElementType\n const hasBothActions = Boolean(action) && Boolean(secondaryAction)\n const hasAnyAction = Boolean(action) || Boolean(secondaryAction)\n const isError = intent === \"error\"\n // Auto-activate icon wrap when intent is set, unless explicitly disabled.\n // The error intent keeps the thin, unchipped icon unless iconWrap is set.\n const shouldWrapIcon =\n icon != null && (iconWrap ?? (intent != null && !isError))\n\n return (\n <div\n ref={ref}\n role={isError ? \"alert\" : \"status\"}\n data-slot=\"empty-state\"\n data-tone={tone ?? \"default\"}\n data-variant={variant ?? \"default\"}\n {...(intent ? { \"data-intent\": intent } : {})}\n className={cn(emptyStateVariants({ size, tone, variant, intent, fill }), className)}\n {...props}\n >\n {icon ? (\n shouldWrapIcon ? (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.iconWrap}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n ) : (\n <div\n data-slot=\"empty-state-icon\"\n className={styles.icon}\n aria-hidden=\"true\"\n >\n {icon}\n </div>\n )\n ) : null}\n <Heading data-slot=\"empty-state-heading\" className={styles.heading}>\n {heading}\n </Heading>\n {description ? (\n <p\n data-slot=\"empty-state-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n {hasAnyAction ? (\n <div\n data-slot=\"empty-state-actions\"\n className={styles.actions}\n {...(hasBothActions ? { role: \"group\" } : {})}\n >\n {action ? (\n <div\n data-slot=\"empty-state-action\"\n className={styles.action}\n >\n {action}\n </div>\n ) : null}\n {secondaryAction ? (\n <div\n data-slot=\"empty-state-secondary-action\"\n className={styles.secondaryAction}\n >\n {secondaryAction}\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n }\n)\nEmptyState.displayName = \"EmptyState\"\n\nexport { EmptyState, emptyStateVariants }\n"
2648
2699
  },
2649
2700
  {
2650
2701
  "path": "components/ui/empty-state/empty-state.module.css",
2651
2702
  "type": "registry:ui",
2652
- "content": "/* Empty State base: centered placeholder for empty lists, tables, regions */\n.base {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n width: 100%;\n min-width: 0;\n gap: var(--empty-state-gap, var(--spacing-2, 0.5rem));\n color: var(--empty-state-text-color, var(--text-secondary, #6b7280));\n border-radius: var(--empty-state-radius, var(--radius-lg, 0.75rem));\n container-type: inline-size;\n container-name: empty-state;\n}\n\n/* Size variants — padding, gap, icon/heading scale */\n.sizeSm {\n padding: var(--spacing-4, 1rem) var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n padding: var(--empty-state-padding, var(--spacing-8, 2rem) var(--spacing-5, 1.25rem));\n gap: var(--empty-state-gap, var(--spacing-2, 0.5rem));\n}\n\n.sizeLg {\n padding: var(--spacing-12, 3rem) var(--spacing-6, 1.5rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tone: default — dashed-edge card on muted surface.\n VI-655: the border only holds box geometry. The dashed edge is an inset\n outline on the shared drop-edge tokens, so --control-edge-width: 0 removes it\n with every other control's edge, and --control-drop-edge-width brings the\n dashed edges back on their own. --empty-state-border is the per-component\n colour override. */\n.toneDefault {\n background-color: var(--empty-state-bg, var(--surface-muted, #f9fafb));\n border: 1px solid transparent;\n outline: var(--control-drop-edge-width, var(--control-edge-width, 1px)) var(--control-drop-edge-style, dashed) var(--empty-state-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-drop-edge-width, var(--control-edge-width, 1px)));\n}\n\n/* Tone: subtle — borderless, transparent */\n.toneSubtle {\n background-color: transparent;\n border: 0;\n}\n\n/* Icon slot — leading visual above the heading */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n margin-bottom: var(--spacing-1, 0.25rem);\n}\n\n/* Icon wrap — circular chip behind the icon; activated by intent variants */\n.iconWrap {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--empty-state-icon-radius, var(--radius-full, 9999px));\n width: var(--empty-state-icon-size, 72px);\n height: var(--empty-state-icon-size, 72px);\n margin-bottom: var(--spacing-3, 0.75rem);\n background-color: var(--empty-state-icon-bg, var(--surface-subtle, #f5f5f6));\n color: var(--empty-state-icon-color, var(--text-tertiary, #6b7280));\n}\n\n.iconWrap svg {\n width: 32px;\n height: 32px;\n}\n\n/* Intent: first-use — accent-tinted chip, primary action CTA encouraged */\n.intentFirstUse .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: zero-results — accent-tinted chip, clear-filter secondary action */\n.intentZeroResults .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: no-access — neutral chip, lock icon */\n.intentNoAccess .iconWrap {\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-secondary, #6b7280);\n}\n\n.sizeSm .icon {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeSm .icon svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.sizeMd .icon {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n.sizeMd .icon svg {\n width: var(--spacing-8, 2rem);\n height: var(--spacing-8, 2rem);\n}\n\n.sizeLg .icon {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n.sizeLg .icon svg {\n width: var(--spacing-12, 3rem);\n height: var(--spacing-12, 3rem);\n}\n\n/* Heading — short direct statement */\n.heading {\n margin: 0;\n font-family: var(--empty-state-heading-font-family, var(--font-family-heading, inherit));\n font-weight: var(--empty-state-heading-font-weight, var(--font-weight-semibold, 600));\n line-height: var(--line-height-tight, 1.25);\n color: var(--empty-state-heading-color, var(--text-primary, #111827));\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .heading {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .heading {\n font-size: var(--empty-state-heading-font-size, var(--font-size-base, 1rem));\n}\n\n.sizeLg .heading {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n/* Description — supporting guidance */\n.description {\n margin: 0;\n font-size: var(--empty-state-description-font-size, var(--font-size-sm, 0.875rem));\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--empty-state-description-color, var(--text-secondary, #6b7280));\n max-width: 48ch;\n}\n\n.sizeSm .description {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Action cluster — wraps primary + secondary actions */\n.actions {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--empty-state-actions-gap, var(--spacing-2, 0.5rem));\n flex-wrap: wrap;\n margin-top: var(--spacing-3, 0.75rem);\n}\n\n.sizeSm .actions {\n margin-top: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg .actions {\n margin-top: var(--spacing-4, 1rem);\n}\n\n.action {\n display: inline-flex;\n}\n\n.secondaryAction {\n display: inline-flex;\n}\n\n/* Container query: stack actions vertically on very narrow regions */\n@container empty-state (max-width: 320px) {\n .actions {\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n }\n\n .action,\n .secondaryAction {\n width: 100%;\n justify-content: center;\n }\n}\n\n/* ─────────────────────────────────────────────────────────────\n * Variant: editorial — filled card with a circular icon chip.\n * Opt-in admin-editorial composition. Self-contained: overrides the\n * `tone` surface and the `size` icon/heading scale. Declared after the\n * size rules so its equal-specificity child overrides win by source order.\n * ───────────────────────────────────────────────────────────── */\n.editorial {\n background-color: var(--surface-card, #ffffff);\n border: 0;\n outline: none;\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-16, 4rem) var(--spacing-8, 2rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* 64px circular icon chip */\n.editorial .icon {\n width: 64px;\n height: 64px;\n border-radius: var(--radius-full, 999px);\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-tertiary, #6b7280);\n font-size: 28px;\n margin-bottom: var(--spacing-3, 0.75rem);\n}\n\n.editorial .icon svg {\n width: 28px;\n height: 28px;\n}\n\n.editorial .heading {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.editorial .description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-tertiary, #6b7280);\n max-width: 360px;\n}\n\n.editorial .actions {\n margin-top: var(--spacing-3, 0.75rem);\n}\n"
2703
+ "content": "/* Empty State base: centered placeholder for empty lists, tables, regions */\n.base {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n width: 100%;\n min-width: 0;\n gap: var(--empty-state-gap, var(--spacing-2, 0.5rem));\n color: var(--empty-state-text-color, var(--text-secondary, #6b7280));\n border-radius: var(--empty-state-radius, var(--radius-lg, 0.75rem));\n container-type: inline-size;\n container-name: empty-state;\n}\n\n/* Size variants — padding, gap, icon/heading scale */\n.sizeSm {\n padding: var(--spacing-4, 1rem) var(--spacing-4, 1rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeMd {\n padding: var(--empty-state-padding, var(--spacing-8, 2rem) var(--spacing-5, 1.25rem));\n gap: var(--empty-state-gap, var(--spacing-2, 0.5rem));\n}\n\n.sizeLg {\n padding: var(--spacing-12, 3rem) var(--spacing-6, 1.5rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Tone: default — dashed-edge card on muted surface.\n VI-655: the border only holds box geometry. The dashed edge is an inset\n outline on the shared drop-edge tokens, so --control-edge-width: 0 removes it\n with every other control's edge, and --control-drop-edge-width brings the\n dashed edges back on their own. --empty-state-border is the per-component\n colour override. */\n.toneDefault {\n background-color: var(--empty-state-bg, var(--surface-muted, #f9fafb));\n border: 1px solid transparent;\n outline: var(--control-drop-edge-width, var(--control-edge-width, 1px)) var(--control-drop-edge-style, dashed) var(--empty-state-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-drop-edge-width, var(--control-edge-width, 1px)));\n}\n\n/* Tone: subtle — borderless, transparent */\n.toneSubtle {\n background-color: transparent;\n border: 0;\n}\n\n/* Icon slot — leading visual above the heading */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n line-height: 1;\n margin-bottom: var(--spacing-1, 0.25rem);\n}\n\n/* Icon wrap — circular chip behind the icon; activated by intent variants */\n.iconWrap {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--empty-state-icon-radius, var(--radius-full, 9999px));\n width: var(--empty-state-icon-size, 72px);\n height: var(--empty-state-icon-size, 72px);\n margin-bottom: var(--spacing-3, 0.75rem);\n background-color: var(--empty-state-icon-bg, var(--surface-subtle, #f5f5f6));\n color: var(--empty-state-icon-color, var(--text-tertiary, #6b7280));\n}\n\n.iconWrap svg {\n width: 32px;\n height: 32px;\n}\n\n/* Intent: first-use — accent-tinted chip, primary action CTA encouraged */\n.intentFirstUse .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: zero-results — accent-tinted chip, clear-filter secondary action */\n.intentZeroResults .iconWrap {\n background-color: var(--surface-accent-subtle, #eff6ff);\n color: var(--surface-accent-default, #3b82f6);\n}\n\n/* Intent: no-access — neutral chip, lock icon */\n.intentNoAccess .iconWrap {\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-secondary, #6b7280);\n}\n\n/* Intent: error — a failed load. Icon and heading wear the destructive colour;\n the icon stays unchipped unless iconWrap is set (then the chip is tinted). */\n.intentError .icon {\n color: var(--empty-state-error-color, var(--destructive, #dc2626));\n}\n\n.intentError .iconWrap {\n background-color: color-mix(in srgb, var(--empty-state-error-color, var(--destructive, #dc2626)) 20%, transparent);\n color: var(--empty-state-error-color, var(--destructive, #dc2626));\n}\n\n.intentError .heading {\n color: var(--empty-state-error-color, var(--destructive, #dc2626));\n}\n\n/* Fill: take the container's open space; base already centres both ways. */\n.fill {\n flex: 1 1 auto;\n align-self: stretch;\n height: 100%;\n min-height: 0;\n}\n\n.sizeSm .icon {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeSm .icon svg {\n width: var(--spacing-5, 1.25rem);\n height: var(--spacing-5, 1.25rem);\n}\n\n.sizeMd .icon {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n.sizeMd .icon svg {\n width: var(--spacing-8, 2rem);\n height: var(--spacing-8, 2rem);\n}\n\n.sizeLg .icon {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n.sizeLg .icon svg {\n width: var(--spacing-12, 3rem);\n height: var(--spacing-12, 3rem);\n}\n\n/* Heading — short direct statement */\n.heading {\n margin: 0;\n font-family: var(--empty-state-heading-font-family, var(--font-family-heading, inherit));\n font-weight: var(--empty-state-heading-font-weight, var(--font-weight-semibold, 600));\n line-height: var(--line-height-tight, 1.25);\n color: var(--empty-state-heading-color, var(--text-primary, #111827));\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n.sizeSm .heading {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd .heading {\n font-size: var(--empty-state-heading-font-size, var(--font-size-base, 1rem));\n}\n\n.sizeLg .heading {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n/* Description — supporting guidance */\n.description {\n margin: 0;\n font-size: var(--empty-state-description-font-size, var(--font-size-sm, 0.875rem));\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--empty-state-description-color, var(--text-secondary, #6b7280));\n max-width: 48ch;\n}\n\n.sizeSm .description {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Action cluster — wraps primary + secondary actions */\n.actions {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--empty-state-actions-gap, var(--spacing-2, 0.5rem));\n flex-wrap: wrap;\n margin-top: var(--spacing-3, 0.75rem);\n}\n\n.sizeSm .actions {\n margin-top: var(--spacing-2, 0.5rem);\n}\n\n.sizeLg .actions {\n margin-top: var(--spacing-4, 1rem);\n}\n\n.action {\n display: inline-flex;\n}\n\n.secondaryAction {\n display: inline-flex;\n}\n\n/* Container query: stack actions vertically on very narrow regions */\n@container empty-state (max-width: 320px) {\n .actions {\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n }\n\n .action,\n .secondaryAction {\n width: 100%;\n justify-content: center;\n }\n}\n\n/* ─────────────────────────────────────────────────────────────\n * Variant: editorial — filled card with a circular icon chip.\n * Opt-in admin-editorial composition. Self-contained: overrides the\n * `tone` surface and the `size` icon/heading scale. Declared after the\n * size rules so its equal-specificity child overrides win by source order.\n * ───────────────────────────────────────────────────────────── */\n.editorial {\n background-color: var(--surface-card, #ffffff);\n border: 0;\n outline: none;\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-16, 4rem) var(--spacing-8, 2rem);\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* 64px circular icon chip */\n.editorial .icon {\n width: 64px;\n height: 64px;\n border-radius: var(--radius-full, 999px);\n background-color: var(--surface-subtle, #f5f5f6);\n color: var(--text-tertiary, #6b7280);\n font-size: 28px;\n margin-bottom: var(--spacing-3, 0.75rem);\n}\n\n.editorial .icon svg {\n width: 28px;\n height: 28px;\n}\n\n.editorial .heading {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.editorial .description {\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-tertiary, #6b7280);\n max-width: 360px;\n}\n\n.editorial .actions {\n margin-top: var(--spacing-3, 0.75rem);\n}\n"
2653
2704
  }
2654
2705
  ]
2655
2706
  },
@@ -4378,12 +4429,12 @@
4378
4429
  {
4379
4430
  "path": "blocks/month-calendar/month-calendar.tsx",
4380
4431
  "type": "registry:block",
4381
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./month-calendar.module.css\"\n\n/**\n * Status tone driving the leading dot on an event chip. Each tone binds to a\n * Visor semantic status token so the dot adopts the active theme's palette.\n */\nexport type MonthCalendarStatus =\n | \"default\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n\n/** Series tint index — keyed to the theme's five-stop chart ramp. */\nexport type MonthCalendarSeries = 1 | 2 | 3 | 4 | 5\n\nexport interface MonthCalendarEvent {\n /** Stable key for the event. */\n id: string\n /**\n * The day this event lands in. Only the calendar date (year/month/day) is\n * used for placement — any time component is ignored.\n */\n date: Date\n /** Chip label. */\n title: string\n /**\n * Status tone for the leading dot. Omit for a neutral dot. Binds to the\n * `--surface-{success,warning,error,info}-default` semantic tokens.\n */\n status?: MonthCalendarStatus\n /**\n * Series tint (1–5). Events sharing an index render with the same background\n * tint and accent bar, keyed to the theme's chart color ramp — the standard\n * way to color-code a recurring series or a resource lane.\n */\n series?: MonthCalendarSeries\n}\n\nexport interface MonthCalendarViewOption {\n /** Machine value emitted via `onViewChange`. */\n value: string\n /** Human label rendered in the segment. */\n label: React.ReactNode\n}\n\nexport interface MonthCalendarProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Any date within the month to display. Controlled. */\n month: Date\n /**\n * Fired with the first day of the previous / next month when the month-nav\n * arrows are used.\n */\n onMonthChange?: (month: Date) => void\n /** Events to place into day cells. */\n events?: MonthCalendarEvent[]\n /**\n * The day the grid marks as \"today\". Omit to mark none — the block never\n * reads the system clock itself, keeping server and client render identical\n * (no hydration mismatch). Pass `new Date()` from a client boundary to opt in.\n */\n today?: Date\n /** Selected day, highlighted distinctly from today. */\n selectedDate?: Date\n /** Fired when a day cell is activated (only when provided — cells are inert otherwise). */\n onSelectDate?: (date: Date) => void\n /** Fired when an event chip is activated (only when provided — chips are inert otherwise). */\n onEventSelect?: (event: MonthCalendarEvent) => void\n /** First column of the week: 0 = Sunday (default), 1 = Monday. */\n weekStartsOn?: 0 | 1\n /** Max chips shown per day before collapsing to a \"+N more\" row. Default 3. */\n maxChipsPerDay?: number\n /** BCP-47 locale for the month title and weekday headers. Default `\"en-US\"`. */\n locale?: string\n /** View-mode options for the segmented control. Default Month / Week / Day. */\n viewOptions?: MonthCalendarViewOption[]\n /** Active view value (controlled). Falls back to internal state when omitted. */\n view?: string\n /** Fired when a view-mode segment is chosen. */\n onViewChange?: (view: string) => void\n}\n\nconst DEFAULT_VIEW_OPTIONS: MonthCalendarViewOption[] = [\n { value: \"month\", label: \"Month\" },\n { value: \"week\", label: \"Week\" },\n { value: \"day\", label: \"Day\" },\n]\n\nconst DAYS_IN_GRID = 42\n// 2023-01-01 was a Sunday — a fixed anchor for deriving localized weekday names.\nconst WEEKDAY_ANCHOR_YEAR = 2023\n\nfunction firstOfMonth(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nfunction addMonths(date: Date, delta: number): Date {\n return new Date(date.getFullYear(), date.getMonth() + delta, 1)\n}\n\nfunction isSameDay(a: Date | undefined, b: Date): boolean {\n return (\n a !== undefined &&\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n )\n}\n\nfunction dayKey(date: Date): string {\n return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`\n}\n\nconst MonthCalendar = React.forwardRef<HTMLDivElement, MonthCalendarProps>(\n function MonthCalendar(\n {\n month,\n onMonthChange,\n events = [],\n today,\n selectedDate,\n onSelectDate,\n onEventSelect,\n weekStartsOn = 0,\n maxChipsPerDay = 3,\n locale = \"en-US\",\n viewOptions = DEFAULT_VIEW_OPTIONS,\n view,\n onViewChange,\n className,\n ...rest\n },\n ref\n ) {\n const [internalView, setInternalView] = React.useState<string>(\n () => view ?? viewOptions[0]?.value ?? \"month\"\n )\n const activeView = view ?? internalView\n\n const handleViewSelect = React.useCallback(\n (next: string) => {\n if (view === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [view, onViewChange]\n )\n\n const monthLabel = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n year: \"numeric\",\n }).format(month),\n [locale, month]\n )\n\n const weekdayLabels = React.useMemo(() => {\n const formatter = new Intl.DateTimeFormat(locale, { weekday: \"short\" })\n return Array.from({ length: 7 }, (_, i) =>\n formatter.format(\n new Date(WEEKDAY_ANCHOR_YEAR, 0, 1 + ((weekStartsOn + i) % 7))\n )\n )\n }, [locale, weekStartsOn])\n\n const cells = React.useMemo(() => {\n const first = firstOfMonth(month)\n const offset = (first.getDay() - weekStartsOn + 7) % 7\n return Array.from({ length: DAYS_IN_GRID }, (_, i) =>\n new Date(first.getFullYear(), first.getMonth(), 1 - offset + i)\n )\n }, [month, weekStartsOn])\n\n const eventsByDay = React.useMemo(() => {\n const map = new Map<string, MonthCalendarEvent[]>()\n for (const event of events) {\n const key = dayKey(event.date)\n const bucket = map.get(key)\n if (bucket) bucket.push(event)\n else map.set(key, [event])\n }\n return map\n }, [events])\n\n const dayLabelFormatter = React.useMemo(\n () =>\n new Intl.DateTimeFormat(locale, {\n month: \"long\",\n day: \"numeric\",\n year: \"numeric\",\n }),\n [locale]\n )\n\n const displayedMonth = month.getMonth()\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"month-calendar\"\n {...rest}\n >\n <div className={styles.header} data-slot=\"month-calendar-header\">\n <div className={styles.nav} data-slot=\"month-calendar-nav\">\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, -1))}\n disabled={!onMonthChange}\n aria-label=\"Previous month\"\n data-slot=\"month-calendar-prev\"\n >\n <CaretLeft weight=\"bold\" aria-hidden />\n </button>\n <h2 className={styles.monthLabel} data-slot=\"month-calendar-label\">\n {monthLabel}\n </h2>\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(addMonths(month, 1))}\n disabled={!onMonthChange}\n aria-label=\"Next month\"\n data-slot=\"month-calendar-next\"\n >\n <CaretRight weight=\"bold\" aria-hidden />\n </button>\n </div>\n\n {viewOptions.length > 0 ? (\n <div\n className={styles.segmented}\n role=\"group\"\n aria-label=\"Calendar view\"\n data-slot=\"month-calendar-view\"\n >\n {viewOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n className={styles.segment}\n data-active={option.value === activeView ? \"true\" : undefined}\n aria-pressed={option.value === activeView}\n onClick={() => handleViewSelect(option.value)}\n >\n {option.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n\n <div className={styles.weekdays} data-slot=\"month-calendar-weekdays\">\n {weekdayLabels.map((label, i) => (\n <div key={i} className={styles.weekday} aria-hidden=\"true\">\n {label}\n </div>\n ))}\n </div>\n\n <div className={styles.grid} data-slot=\"month-calendar-grid\">\n {cells.map((cell) => {\n const dayEvents = eventsByDay.get(dayKey(cell)) ?? []\n const visible = dayEvents.slice(0, maxChipsPerDay)\n const overflow = dayEvents.length - visible.length\n const outside = cell.getMonth() !== displayedMonth\n const isToday = isSameDay(today, cell)\n const isSelected = isSameDay(selectedDate, cell)\n const dayLabel = dayLabelFormatter.format(cell)\n const cellLabel =\n dayEvents.length > 0\n ? `${dayLabel}, ${dayEvents.length} event${dayEvents.length === 1 ? \"\" : \"s\"}`\n : dayLabel\n\n const DayTag: React.ElementType = onSelectDate ? \"button\" : \"div\"\n\n return (\n <div\n key={dayKey(cell)}\n className={styles.cell}\n data-slot=\"month-calendar-day\"\n data-outside={outside ? \"true\" : undefined}\n data-today={isToday ? \"true\" : undefined}\n data-selected={isSelected ? \"true\" : undefined}\n >\n <DayTag\n className={styles.dayHead}\n {...(onSelectDate\n ? {\n type: \"button\" as const,\n onClick: () => onSelectDate(cell),\n \"aria-label\": cellLabel,\n \"aria-pressed\": isSelected,\n }\n : {})}\n data-slot=\"month-calendar-day-head\"\n >\n <span className={styles.dayNumber}>{cell.getDate()}</span>\n </DayTag>\n\n {dayEvents.length > 0 ? (\n <div\n className={styles.events}\n data-slot=\"month-calendar-day-events\"\n >\n {visible.map((event) => {\n const ChipTag: React.ElementType = onEventSelect\n ? \"button\"\n : \"div\"\n return (\n <ChipTag\n key={event.id}\n className={styles.chip}\n data-status={event.status ?? \"default\"}\n data-series={event.series}\n data-slot=\"month-calendar-event\"\n {...(onEventSelect\n ? {\n type: \"button\" as const,\n onClick: () => onEventSelect(event),\n }\n : {})}\n >\n <span\n className={styles.dot}\n data-status={event.status ?? \"default\"}\n aria-hidden=\"true\"\n />\n <span className={styles.chipLabel}>{event.title}</span>\n </ChipTag>\n )\n })}\n {overflow > 0 ? (\n <div className={styles.overflow}>+{overflow} more</div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n )\n }\n)\n\nMonthCalendar.displayName = \"MonthCalendar\"\n\nexport { MonthCalendar }\n"
4432
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\"\n\nimport { cn } from \"../../lib/utils\"\nimport styles from \"./month-calendar.module.css\"\n\n/**\n * Status tone driving the leading dot on an event chip. Each tone binds to a\n * Visor semantic status token so the dot adopts the active theme's palette.\n */\nexport type MonthCalendarStatus =\n | \"default\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n\n/** Series tint index — keyed to the theme's five-stop chart ramp. */\nexport type MonthCalendarSeries = 1 | 2 | 3 | 4 | 5\n\n/**\n * A calendar date. A `\"YYYY-MM-DD\"` string is the canonical form: it is read\n * field by field and never passed through `Date`, so a stay cannot shift by a\n * day across time zones. A `Date` is read through its local calendar fields\n * (`getFullYear` / `getMonth` / `getDate`), as before — any time is ignored.\n */\nexport type MonthCalendarDate = string | Date\n\nexport interface MonthCalendarEvent {\n /** Stable key for the event. */\n id: string\n /**\n * The day this event lands in (legacy form of `start`). Only the calendar\n * date is used — any time component is ignored.\n */\n date?: MonthCalendarDate\n /** First day of the event, `\"YYYY-MM-DD\"`. Wins over `date` when both are set. */\n start?: MonthCalendarDate\n /**\n * Last day of the event, inclusive. When set the event is a span: it draws as\n * one bar across the days it covers, cut at week edges with a continued mark\n * (and stacked in lanes when spans overlap). Without it the event is a chip on\n * one day.\n */\n end?: MonthCalendarDate\n /** Chip / bar label. */\n title: string\n /**\n * Status tone for the leading dot. Omit for a neutral dot. Binds to the\n * `--surface-{success,warning,error,info}-default` semantic tokens.\n */\n status?: MonthCalendarStatus\n /**\n * Series tint (1–5). Events sharing an index render with the same background\n * tint and accent bar, keyed to the theme's chart color ramp — the standard\n * way to color-code a recurring series or a resource lane.\n */\n series?: MonthCalendarSeries\n /**\n * A colour token for this event, for any colour a series index cannot name (a\n * region's). A custom-property name (`\"--region-berlin\"`), a `var()` call or\n * any CSS colour. Wins over `series`; the tint and accent bar are derived the\n * same way.\n */\n color?: string\n}\n\n/** One week row of the grid, handed to `renderWeekDetail`. */\nexport interface MonthCalendarWeek {\n /** Row index in the grid, 0–5. */\n index: number\n /** First day of the row, `\"YYYY-MM-DD\"`. */\n start: string\n /** Last day of the row, `\"YYYY-MM-DD\"`. */\n end: string\n /** The seven days of the row, `\"YYYY-MM-DD\"`. */\n days: string[]\n /** The selected day this row holds, `\"YYYY-MM-DD\"`. */\n selected: string\n}\n\n/** A day cell, handed to `renderDayMark`. */\nexport interface MonthCalendarDay {\n /** `\"YYYY-MM-DD\"`. */\n date: string\n /** False for the dimmed days of the neighbouring months. */\n inMonth: boolean\n isToday: boolean\n isSelected: boolean\n}\n\nexport interface MonthCalendarViewOption {\n /** Machine value emitted via `onViewChange`. */\n value: string\n /** Human label rendered in the segment. */\n label: React.ReactNode\n}\n\nexport interface MonthCalendarProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Any date within the month to display. Controlled. */\n month: MonthCalendarDate\n /**\n * Fired with the first day of the previous / next month (a local-midnight\n * `Date`) when the month-nav arrows are used.\n */\n onMonthChange?: (month: Date) => void\n /** Events to place into day cells. */\n events?: MonthCalendarEvent[]\n /**\n * The day the grid marks as \"today\". Omit to mark none — the block never\n * reads the system clock itself, keeping server and client render identical\n * (no hydration mismatch). Pass `new Date()` from a client boundary to opt in.\n */\n today?: MonthCalendarDate\n /** Selected day, highlighted distinctly from today. */\n selectedDate?: MonthCalendarDate\n /**\n * Fired when a day cell is activated (only when provided — cells are inert\n * otherwise). Receives a local-midnight `Date` and the `\"YYYY-MM-DD\"` string.\n */\n onSelectDate?: (date: Date, iso: string) => void\n /**\n * Fired when an event chip or span bar is clicked (only when provided).\n * Span bars are pointer-only art; the keyboard route to a span is its day.\n */\n onEventSelect?: (event: MonthCalendarEvent) => void\n /** First column of the week: 0 = Sunday (default), 1 = Monday. */\n weekStartsOn?: 0 | 1\n /** Max chips shown per day before collapsing to a \"+N more\" row. Default 3. */\n maxChipsPerDay?: number\n /**\n * Max lanes of overlapping span bars per week before a day shows \"+N\" for the\n * spans that did not fit. Default 3. The \"+N\" opens the day.\n */\n maxLanes?: number\n /**\n * Content for the slot under the week that holds `selectedDate`. The slot\n * opens in the page flow, pushes the later weeks down, points an arrow at the\n * selected day, and takes focus when the day is activated. Return nothing to\n * keep it closed.\n */\n renderWeekDetail?: (week: MonthCalendarWeek) => React.ReactNode\n /** A consumer mark in each day cell's head, opposite the day number (a count, a range end). */\n renderDayMark?: (day: MonthCalendarDay) => React.ReactNode\n /** BCP-47 locale for the month title and weekday headers. Default `\"en-US\"`. */\n locale?: string\n /** View-mode options for the segmented control. Default Month / Week / Day. */\n viewOptions?: MonthCalendarViewOption[]\n /** Active view value (controlled). Falls back to internal state when omitted. */\n view?: string\n /** Fired when a view-mode segment is chosen. */\n onViewChange?: (view: string) => void\n}\n\nconst DEFAULT_VIEW_OPTIONS: MonthCalendarViewOption[] = [\n { value: \"month\", label: \"Month\" },\n { value: \"week\", label: \"Week\" },\n { value: \"day\", label: \"Day\" },\n]\n\nconst WEEKS = 6\nconst DAYS_IN_WEEK = 7\nconst DAYS_IN_GRID = WEEKS * DAYS_IN_WEEK\nconst MS_PER_DAY = 86_400_000\n// 2023-01-01 was a Sunday — a fixed anchor for deriving localized weekday names.\nconst SUNDAY_ANCHOR = Date.UTC(2023, 0, 1) / MS_PER_DAY\n\n/*\n * Dates are handled as integer day numbers (days since 1970-01-01, UTC). A\n * calendar date has no time zone, so every step — parsing, ordering, adding a\n * day, reading the weekday, formatting — runs on UTC fields and never builds a\n * local-time Date from an instant. A DST edge cannot move a day.\n */\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})/\n\nfunction toDayNumber(value: MonthCalendarDate | undefined): number | undefined {\n if (value === undefined) return undefined\n let y: number\n let m: number\n let d: number\n if (typeof value === \"string\") {\n const match = ISO_DATE.exec(value)\n if (!match) return undefined\n y = Number(match[1])\n m = Number(match[2]) - 1\n d = Number(match[3])\n } else {\n y = value.getFullYear()\n m = value.getMonth()\n d = value.getDate()\n }\n const n = Date.UTC(y, m, d) / MS_PER_DAY\n return Number.isNaN(n) ? undefined : n\n}\n\nfunction utcDate(day: number): Date {\n return new Date(day * MS_PER_DAY)\n}\n\nfunction toIso(day: number): string {\n return utcDate(day).toISOString().slice(0, 10)\n}\n\n/** A local-midnight `Date` for the calendar day — what the callbacks hand back. */\nfunction toLocalDate(day: number): Date {\n const d = utcDate(day)\n return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())\n}\n\nfunction firstOfMonthDay(day: number): number {\n const d = utcDate(day)\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) / MS_PER_DAY\n}\n\nfunction monthOffsetDay(day: number, delta: number): number {\n const d = utcDate(day)\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + delta, 1) / MS_PER_DAY\n}\n\n/** Resolve a colour token: `--name` becomes `var(--name)`, anything else passes through. */\nfunction resolveColor(color: string): string {\n return color.startsWith(\"--\") ? `var(${color})` : color\n}\n\ninterface NormalizedEvent {\n event: MonthCalendarEvent\n start: number\n /** Inclusive last day; undefined for a single-day chip. */\n end: number | undefined\n}\n\ninterface Segment {\n item: NormalizedEvent\n /** 0-based column of the first day this segment covers in its week. */\n col: number\n span: number\n continuedBefore: boolean\n continuedAfter: boolean\n lane: number\n}\n\nfunction normalize(events: MonthCalendarEvent[]): NormalizedEvent[] {\n const out: NormalizedEvent[] = []\n for (const event of events) {\n const start = toDayNumber(event.start ?? event.date)\n if (start === undefined) continue\n const rawEnd = toDayNumber(event.end)\n out.push({\n event,\n start,\n end: rawEnd === undefined ? undefined : Math.max(rawEnd, start),\n })\n }\n return out\n}\n\n/** Cut each span at the week's edges and give it the lowest lane it fits in. */\nfunction layoutWeek(weekStart: number, spans: NormalizedEvent[]): Segment[] {\n const weekEnd = weekStart + DAYS_IN_WEEK - 1\n const ordered = spans\n .filter((s) => s.start <= weekEnd && (s.end as number) >= weekStart)\n .sort((a, b) => a.start - b.start || (b.end as number) - (a.end as number))\n const laneEnds: number[] = []\n return ordered.map((item) => {\n const first = Math.max(item.start, weekStart)\n const last = Math.min(item.end as number, weekEnd)\n let lane = laneEnds.findIndex((end) => end < first)\n if (lane === -1) lane = laneEnds.length\n laneEnds[lane] = last\n return {\n item,\n col: first - weekStart,\n span: last - first + 1,\n continuedBefore: item.start < weekStart,\n continuedAfter: (item.end as number) > weekEnd,\n lane,\n }\n })\n}\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nconst MonthCalendar = React.forwardRef<HTMLDivElement, MonthCalendarProps>(\n function MonthCalendar(\n {\n month,\n onMonthChange,\n events = [],\n today,\n selectedDate,\n onSelectDate,\n onEventSelect,\n weekStartsOn = 0,\n maxChipsPerDay = 3,\n maxLanes = 3,\n renderWeekDetail,\n renderDayMark,\n locale = \"en-US\",\n viewOptions = DEFAULT_VIEW_OPTIONS,\n view,\n onViewChange,\n className,\n ...rest\n },\n ref\n ) {\n const [internalView, setInternalView] = React.useState<string>(\n () => view ?? viewOptions[0]?.value ?? \"month\"\n )\n const activeView = view ?? internalView\n\n const handleViewSelect = React.useCallback(\n (next: string) => {\n if (view === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [view, onViewChange]\n )\n\n const slotId = React.useId()\n const gridRef = React.useRef<HTMLDivElement>(null)\n const slotRef = React.useRef<HTMLDivElement>(null)\n const focusSlotNext = React.useRef(false)\n const [focusedDay, setFocusedDay] = React.useState<number | undefined>()\n\n const monthDay = toDayNumber(month) ?? 0\n const firstDay = firstOfMonthDay(monthDay)\n const displayedMonth = utcDate(firstDay).getUTCMonth()\n const todayDay = toDayNumber(today)\n const selectedDay = toDayNumber(selectedDate)\n\n const formatUtc = React.useCallback(\n (options: Intl.DateTimeFormatOptions) =>\n new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" }),\n [locale]\n )\n\n const monthLabel = React.useMemo(\n () =>\n formatUtc({ month: \"long\", year: \"numeric\" }).format(utcDate(firstDay)),\n [formatUtc, firstDay]\n )\n\n const weekdayLabels = React.useMemo(() => {\n const formatter = formatUtc({ weekday: \"short\" })\n return Array.from({ length: DAYS_IN_WEEK }, (_, i) =>\n formatter.format(\n utcDate(SUNDAY_ANCHOR + ((weekStartsOn + i) % DAYS_IN_WEEK))\n )\n )\n }, [formatUtc, weekStartsOn])\n\n // Day number of the grid's first cell — the weekday the month starts on,\n // read from UTC fields, backed up to the week start.\n const gridStart = React.useMemo(() => {\n const offset = (utcDate(firstDay).getUTCDay() - weekStartsOn + 7) % 7\n return firstDay - offset\n }, [firstDay, weekStartsOn])\n\n const normalized = React.useMemo(() => normalize(events), [events])\n\n const chipsByDay = React.useMemo(() => {\n const map = new Map<number, MonthCalendarEvent[]>()\n for (const { event, start, end } of normalized) {\n if (end !== undefined) continue\n const bucket = map.get(start)\n if (bucket) bucket.push(event)\n else map.set(start, [event])\n }\n return map\n }, [normalized])\n\n const spans = React.useMemo(\n () => normalized.filter((n) => n.end !== undefined),\n [normalized]\n )\n\n const weeks = React.useMemo(\n () =>\n Array.from({ length: WEEKS }, (_, w) => {\n const start = gridStart + w * DAYS_IN_WEEK\n const segments = layoutWeek(start, spans)\n const used = segments.reduce((max, s) => Math.max(max, s.lane + 1), 0)\n return { start, segments, lanes: Math.min(used, Math.max(maxLanes, 0)) }\n }),\n [gridStart, spans, maxLanes]\n )\n\n const dayLabelFormatter = React.useMemo(\n () => formatUtc({ month: \"long\", day: \"numeric\", year: \"numeric\" }),\n [formatUtc]\n )\n\n /** \"July 12, 2026, Berlin stay, day 2 of 4, Launch party\" */\n const labelFor = (day: number, segments: Segment[]): string => {\n const parts = [dayLabelFormatter.format(utcDate(day))]\n for (const { item } of segments) {\n const end = item.end as number\n if (day < item.start || day > end) continue\n parts.push(\n `${item.event.title}, day ${day - item.start + 1} of ${end - item.start + 1}`\n )\n }\n for (const event of chipsByDay.get(day) ?? []) parts.push(event.title)\n return parts.join(\", \")\n }\n\n // The one roving tab stop: the last focused day, else the selected day,\n // today, or the first of the month.\n const inGrid = (day: number | undefined): day is number =>\n day !== undefined && day >= gridStart && day < gridStart + DAYS_IN_GRID\n const tabDay = inGrid(focusedDay)\n ? focusedDay\n : inGrid(selectedDay)\n ? selectedDay\n : inGrid(todayDay)\n ? todayDay\n : firstDay\n\n const selectedWeek =\n selectedDay !== undefined && inGrid(selectedDay)\n ? Math.floor((selectedDay - gridStart) / DAYS_IN_WEEK)\n : -1\n const detail =\n renderWeekDetail && selectedWeek >= 0 && selectedDay !== undefined\n ? renderWeekDetail({\n index: selectedWeek,\n start: toIso(weeks[selectedWeek]!.start),\n end: toIso(weeks[selectedWeek]!.start + DAYS_IN_WEEK - 1),\n days: Array.from({ length: DAYS_IN_WEEK }, (_, i) =>\n toIso(weeks[selectedWeek]!.start + i)\n ),\n selected: toIso(selectedDay),\n })\n : null\n const slotOpen = detail !== null && detail !== undefined && detail !== false\n\n // Focus moves into the slot when a day is activated and opens it — never\n // because a consumer changed `selectedDate` on its own.\n React.useEffect(() => {\n if (!focusSlotNext.current || !slotRef.current) return\n focusSlotNext.current = false\n const target = slotRef.current.querySelector<HTMLElement>(FOCUSABLE)\n ;(target ?? slotRef.current).focus()\n })\n\n const focusDay = (day: number) => {\n const cell = gridRef.current?.querySelector<HTMLElement>(\n `[data-mc-day=\"${day}\"]`\n )\n const target = cell?.matches(\"[data-mc-focus]\")\n ? cell\n : cell?.querySelector<HTMLElement>(\"[data-mc-focus]\")\n target?.focus()\n }\n\n const handleGridKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n const cell = (event.target as HTMLElement).closest<HTMLElement>(\n \"[data-mc-day]\"\n )\n if (!cell) return\n const day = Number(cell.dataset.mcDay)\n const col = (day - gridStart) % DAYS_IN_WEEK\n const delta: Record<string, number> = {\n ArrowLeft: -1,\n ArrowRight: 1,\n ArrowUp: -DAYS_IN_WEEK,\n ArrowDown: DAYS_IN_WEEK,\n Home: -col,\n End: DAYS_IN_WEEK - 1 - col,\n }\n const step = delta[event.key]\n if (step === undefined) return\n event.preventDefault()\n const next = Math.min(\n Math.max(day + step, gridStart),\n gridStart + DAYS_IN_GRID - 1\n )\n setFocusedDay(next)\n focusDay(next)\n }\n\n const select = (day: number) => {\n if (renderWeekDetail) {\n focusSlotNext.current = true\n // A re-select that changes nothing renders nothing, so drop the intent.\n setTimeout(() => {\n focusSlotNext.current = false\n }, 0)\n }\n onSelectDate?.(toLocalDate(day), toIso(day))\n }\n\n const renderChip = (event: MonthCalendarEvent) => {\n const ChipTag: React.ElementType = onEventSelect ? \"button\" : \"div\"\n const tinted = event.color !== undefined || event.series !== undefined\n return (\n <ChipTag\n key={event.id}\n className={styles.chip}\n data-status={event.status ?? \"default\"}\n data-series={event.series}\n data-tinted={tinted ? \"true\" : undefined}\n style={\n event.color !== undefined\n ? ({ \"--mc-series\": resolveColor(event.color) } as React.CSSProperties)\n : undefined\n }\n data-slot=\"month-calendar-event\"\n {...(onEventSelect\n ? { type: \"button\" as const, onClick: () => onEventSelect(event) }\n : {})}\n >\n <span\n className={styles.dot}\n data-status={event.status ?? \"default\"}\n aria-hidden=\"true\"\n />\n <span className={styles.chipLabel}>{event.title}</span>\n </ChipTag>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.root, className)}\n data-slot=\"month-calendar\"\n {...rest}\n >\n <div className={styles.header} data-slot=\"month-calendar-header\">\n <div className={styles.nav} data-slot=\"month-calendar-nav\">\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(toLocalDate(monthOffsetDay(firstDay, -1)))}\n disabled={!onMonthChange}\n aria-label=\"Previous month\"\n data-slot=\"month-calendar-prev\"\n >\n <CaretLeft weight=\"bold\" aria-hidden />\n </button>\n <h2 className={styles.monthLabel} data-slot=\"month-calendar-label\">\n {monthLabel}\n </h2>\n <button\n type=\"button\"\n className={styles.navButton}\n onClick={() => onMonthChange?.(toLocalDate(monthOffsetDay(firstDay, 1)))}\n disabled={!onMonthChange}\n aria-label=\"Next month\"\n data-slot=\"month-calendar-next\"\n >\n <CaretRight weight=\"bold\" aria-hidden />\n </button>\n </div>\n\n {viewOptions.length > 0 ? (\n <div\n className={styles.segmented}\n role=\"group\"\n aria-label=\"Calendar view\"\n data-slot=\"month-calendar-view\"\n >\n {viewOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n className={styles.segment}\n data-active={option.value === activeView ? \"true\" : undefined}\n aria-pressed={option.value === activeView}\n onClick={() => handleViewSelect(option.value)}\n >\n {option.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n\n <div\n ref={gridRef}\n className={styles.grid}\n role=\"grid\"\n aria-label={monthLabel}\n data-slot=\"month-calendar-grid\"\n onKeyDown={handleGridKeyDown}\n >\n <div\n className={styles.weekdays}\n role=\"row\"\n data-slot=\"month-calendar-weekdays\"\n >\n {weekdayLabels.map((label, i) => (\n <div\n key={i}\n className={styles.weekday}\n role=\"columnheader\"\n aria-label={formatUtc({ weekday: \"long\" }).format(\n utcDate(SUNDAY_ANCHOR + ((weekStartsOn + i) % DAYS_IN_WEEK))\n )}\n >\n <span aria-hidden=\"true\">{label}</span>\n </div>\n ))}\n </div>\n\n {weeks.map((week, w) => (\n <React.Fragment key={week.start}>\n <div\n className={styles.week}\n role=\"row\"\n data-slot=\"month-calendar-week\"\n style={{ \"--mc-lanes\": week.lanes } as React.CSSProperties}\n >\n {Array.from({ length: DAYS_IN_WEEK }, (_, c) => {\n const day = week.start + c\n const chips = chipsByDay.get(day) ?? []\n const visible = chips.slice(0, maxChipsPerDay)\n const overflow = chips.length - visible.length\n const hiddenSpans = week.segments.filter(\n (s) =>\n s.lane >= week.lanes &&\n day >= s.item.start &&\n day <= (s.item.end as number)\n ).length\n const outside = utcDate(day).getUTCMonth() !== displayedMonth\n const isToday = day === todayDay\n const isSelected = day === selectedDay\n const label = labelFor(day, week.segments)\n const iso = toIso(day)\n const expanded = isSelected && slotOpen\n const isTab = day === tabDay\n const mark = renderDayMark?.({\n date: iso,\n inMonth: !outside,\n isToday,\n isSelected,\n })\n\n return (\n <div\n key={day}\n className={styles.cell}\n role=\"gridcell\"\n aria-selected={isSelected}\n data-mc-day={day}\n data-date={iso}\n data-slot=\"month-calendar-day\"\n data-outside={outside ? \"true\" : undefined}\n data-today={isToday ? \"true\" : undefined}\n data-selected={isSelected ? \"true\" : undefined}\n {...(onSelectDate\n ? {}\n : {\n \"aria-label\": label,\n \"aria-current\": isToday ? (\"date\" as const) : undefined,\n tabIndex: isTab ? 0 : -1,\n \"data-mc-focus\": \"\",\n onFocus: () => setFocusedDay(day),\n })}\n >\n <div className={styles.top}>\n {onSelectDate ? (\n <button\n type=\"button\"\n className={styles.dayHead}\n onClick={() => select(day)}\n onFocus={() => setFocusedDay(day)}\n aria-label={label}\n aria-current={isToday ? \"date\" : undefined}\n aria-expanded={expanded ? true : undefined}\n aria-controls={expanded ? slotId : undefined}\n tabIndex={isTab ? 0 : -1}\n data-mc-focus=\"\"\n data-slot=\"month-calendar-day-head\"\n >\n <span className={styles.dayNumber}>\n {utcDate(day).getUTCDate()}\n </span>\n </button>\n ) : (\n <div\n className={styles.dayHead}\n data-slot=\"month-calendar-day-head\"\n >\n <span className={styles.dayNumber} aria-hidden=\"true\">\n {utcDate(day).getUTCDate()}\n </span>\n </div>\n )}\n {mark !== undefined && mark !== null && mark !== false ? (\n <span\n className={styles.mark}\n data-slot=\"month-calendar-day-mark\"\n >\n {mark}\n </span>\n ) : null}\n </div>\n\n {week.lanes > 0 ? (\n <div className={styles.lanes} aria-hidden=\"true\" />\n ) : null}\n {hiddenSpans > 0 ? (\n <span\n className={styles.moreSpans}\n aria-hidden=\"true\"\n data-slot=\"month-calendar-more-spans\"\n onClick={onSelectDate ? () => select(day) : undefined}\n >\n +{hiddenSpans}\n </span>\n ) : null}\n\n {chips.length > 0 ? (\n <div\n className={styles.events}\n data-slot=\"month-calendar-day-events\"\n >\n {visible.map(renderChip)}\n {overflow > 0 ? (\n <div className={styles.overflow}>+{overflow} more</div>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n })}\n\n {week.segments.length > 0 ? (\n <div\n className={styles.bars}\n aria-hidden=\"true\"\n data-slot=\"month-calendar-bars\"\n >\n {week.segments\n .filter((s) => s.lane < week.lanes)\n .map((s) => {\n const { event } = s.item\n const tinted =\n event.color !== undefined || event.series !== undefined\n return (\n <div\n key={`${event.id}-${s.col}`}\n className={styles.bar}\n data-slot=\"month-calendar-span\"\n data-event-id={event.id}\n data-status={event.status ?? \"default\"}\n data-series={event.series}\n data-tinted={tinted ? \"true\" : undefined}\n data-continued-before={s.continuedBefore ? \"true\" : undefined}\n data-continued-after={s.continuedAfter ? \"true\" : undefined}\n data-clickable={onEventSelect ? \"true\" : undefined}\n onClick={\n onEventSelect ? () => onEventSelect(event) : undefined\n }\n style={\n {\n gridColumn: `${s.col + 1} / span ${s.span}`,\n \"--mc-lane\": s.lane,\n ...(event.color !== undefined\n ? { \"--mc-series\": resolveColor(event.color) }\n : {}),\n } as React.CSSProperties\n }\n >\n {s.continuedBefore ? (\n <CaretLeft\n className={styles.barCaret}\n weight=\"bold\"\n aria-hidden\n data-slot=\"month-calendar-span-continued-before\"\n />\n ) : null}\n <span className={styles.barLabel}>{event.title}</span>\n {s.continuedAfter ? (\n <CaretRight\n className={styles.barCaret}\n weight=\"bold\"\n aria-hidden\n data-slot=\"month-calendar-span-continued-after\"\n />\n ) : null}\n </div>\n )\n })}\n </div>\n ) : null}\n </div>\n\n {slotOpen && w === selectedWeek && selectedDay !== undefined ? (\n <div\n className={styles.slotRow}\n role=\"row\"\n data-slot=\"month-calendar-week-detail\"\n style={\n {\n \"--mc-arrow-col\": (selectedDay - gridStart) % DAYS_IN_WEEK,\n } as React.CSSProperties\n }\n >\n <div role=\"gridcell\" aria-colspan={DAYS_IN_WEEK} className={styles.slotCell}>\n <div\n ref={slotRef}\n id={slotId}\n className={styles.slot}\n role=\"region\"\n aria-label={dayLabelFormatter.format(utcDate(selectedDay))}\n tabIndex={-1}\n >\n {detail}\n </div>\n </div>\n </div>\n ) : null}\n </React.Fragment>\n ))}\n </div>\n </div>\n )\n }\n)\n\nMonthCalendar.displayName = \"MonthCalendar\"\n\nexport { MonthCalendar }\n"
4382
4433
  },
4383
4434
  {
4384
4435
  "path": "blocks/month-calendar/month-calendar.module.css",
4385
4436
  "type": "registry:block",
4386
- "content": "/* Month Calendar\n * A theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid\n * under a weekday header, with month navigation, a view-mode segmented control,\n * and per-cell event chips carrying a status dot and an optional series tint.\n *\n * Every color, size, spacing, stroke, radius, and motion value binds to a Visor\n * token so the block adopts the active theme without modification. Selectors are\n * anchored on a local class to satisfy the docs bundler's pure-selector rule.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n background: var(--surface-card);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-lg);\n overflow: hidden;\n color: var(--text-primary);\n}\n\n/* ── Header: month nav + view segmented control ── */\n.header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem);\n}\n\n.nav {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.navButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 32px — icon-button sizing; sizing tokens deferred (Rule 5 exception). */\n width: 2rem;\n height: 2rem;\n padding: 0;\n color: var(--text-secondary);\n background: transparent;\n border: var(--stroke-width-thin) solid transparent;\n border-radius: var(--radius-md);\n cursor: pointer;\n}\n\n.navButton:hover:not(:disabled) {\n background: var(--surface-interactive-hover);\n color: var(--text-primary);\n}\n\n.navButton:disabled {\n opacity: var(--opacity-40);\n cursor: default;\n}\n\n.navButton:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.monthLabel {\n margin: 0;\n min-width: 10ch;\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary);\n}\n\n/* ── View-mode segmented control ── */\n.segmented {\n display: inline-flex;\n align-items: center;\n gap: var(--stroke-width-thin);\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-subtle);\n border: var(--stroke-width-thin) solid var(--border-muted);\n border-radius: var(--radius-md);\n}\n\n.segment {\n appearance: none;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n background: transparent;\n border: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.segment:hover:not([data-active]) {\n color: var(--text-primary);\n}\n\n.segment[data-active] {\n color: var(--text-primary);\n background: var(--surface-card);\n box-shadow: var(--shadow-xs);\n}\n\n.segment:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* ── Weekday header row ── */\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n background: var(--surface-subtle);\n}\n\n.weekday {\n padding: var(--spacing-2, 0.5rem);\n text-align: center;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary);\n}\n\n/* ── 6×7 day grid ── */\n/* The hairline background shows through the 1px gaps to draw the grid lines. */\n.grid {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n grid-auto-rows: minmax(0, 1fr);\n gap: var(--stroke-width-thin);\n background: var(--border-muted);\n border-top: var(--stroke-width-thin) solid var(--border-muted);\n}\n\n.cell {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n /* 104px — day-cell min height; sizing tokens deferred (Rule 5 exception). */\n min-height: 6.5rem;\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-card);\n}\n\n.cell[data-outside] {\n background: var(--surface-subtle);\n}\n\n.cell[data-outside] .dayNumber {\n color: var(--text-disabled);\n}\n\n.cell[data-selected] {\n background: var(--interactive-primary-soft);\n}\n\n/* ── Day number head (button when the day is selectable) ── */\n.dayHead {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n align-self: flex-start;\n /* 24px — day-number badge sizing; sizing tokens deferred (Rule 5 exception). */\n min-width: 1.5rem;\n height: 1.5rem;\n padding: 0 var(--spacing-1, 0.25rem);\n color: inherit;\n background: transparent;\n border: none;\n border-radius: var(--radius-full);\n cursor: default;\n}\n\n.dayHead:is(button) {\n cursor: pointer;\n}\n\n.dayHead:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.dayHead:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.dayNumber {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.cell[data-today] .dayHead {\n background: var(--interactive-primary-bg);\n}\n\n.cell[data-today] .dayNumber {\n color: var(--interactive-primary-text);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* ── Event chips ── */\n.events {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n min-width: 0;\n}\n\n.chip {\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: 100%;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n text-align: start;\n color: var(--text-primary);\n background: var(--surface-subtle);\n border: none;\n border-radius: var(--radius-sm);\n cursor: default;\n}\n\n.chip:is(button) {\n cursor: pointer;\n}\n\n.chip:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.chip:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* Series tint — background wash + leading accent bar keyed to the chart ramp. */\n.chip[data-series=\"1\"] {\n --mc-series: var(--chart-1);\n}\n.chip[data-series=\"2\"] {\n --mc-series: var(--chart-2);\n}\n.chip[data-series=\"3\"] {\n --mc-series: var(--chart-3);\n}\n.chip[data-series=\"4\"] {\n --mc-series: var(--chart-4);\n}\n.chip[data-series=\"5\"] {\n --mc-series: var(--chart-5);\n}\n\n.chip[data-series] {\n background: color-mix(in srgb, var(--mc-series) 16%, transparent);\n box-shadow: inset var(--stroke-width-medium) 0 0 0 var(--mc-series);\n}\n\n.chip[data-series]:is(button):hover {\n background: color-mix(in srgb, var(--mc-series) 28%, transparent);\n}\n\n.chipLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* ── Status dot ── */\n.dot {\n flex-shrink: 0;\n /* 8px dot — bound to the spacing scale. */\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full);\n background: var(--text-tertiary);\n}\n\n.dot[data-status=\"success\"] {\n background: var(--surface-success-default);\n}\n.dot[data-status=\"warning\"] {\n background: var(--surface-warning-default);\n}\n.dot[data-status=\"danger\"] {\n background: var(--surface-error-default);\n}\n.dot[data-status=\"info\"] {\n background: var(--surface-info-default);\n}\n\n/* ── Overflow row ── */\n.overflow {\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .segment {\n transition: none;\n }\n}\n"
4437
+ "content": "/* Month Calendar\n * A theme-portable month event-grid (scheduler) block: a 6×7 day-cell grid\n * under a weekday header, with month navigation, a view-mode segmented control,\n * and per-cell event chips carrying a status dot and an optional series tint.\n *\n * Every color, size, spacing, stroke, radius, and motion value binds to a Visor\n * token so the block adopts the active theme without modification. Selectors are\n * anchored on a local class to satisfy the docs bundler's pure-selector rule.\n */\n\n/* Resting edge: an inset outline on the shared control-edge tokens, so\n * --control-edge-width: 0 on any ancestor removes it (VI-655). The border keeps\n * its box and paints nothing. */\n.root {\n display: flex;\n flex-direction: column;\n background: var(--surface-card);\n border: var(--stroke-width-thin) solid transparent;\n outline: var(--control-edge-width, var(--stroke-width-thin, 1px)) var(--control-edge-style, solid) var(--control-edge-color, var(--border-muted, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-lg);\n overflow: hidden;\n color: var(--text-primary);\n}\n\n/* ── Header: month nav + view segmented control ── */\n.header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-3, 0.75rem);\n padding: var(--spacing-4, 1rem);\n}\n\n.nav {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.navButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 32px — icon-button sizing; sizing tokens deferred (Rule 5 exception). */\n width: 2rem;\n height: 2rem;\n padding: 0;\n color: var(--text-secondary);\n background: transparent;\n border: var(--stroke-width-thin) solid transparent;\n border-radius: var(--radius-md);\n cursor: pointer;\n}\n\n.navButton:hover:not(:disabled) {\n background: var(--surface-interactive-hover);\n color: var(--text-primary);\n}\n\n.navButton:disabled {\n opacity: var(--opacity-40);\n cursor: default;\n}\n\n.navButton:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.monthLabel {\n margin: 0;\n min-width: 10ch;\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary);\n}\n\n/* ── View-mode segmented control ── */\n.segmented {\n display: inline-flex;\n align-items: center;\n gap: var(--stroke-width-thin);\n padding: var(--spacing-1, 0.25rem);\n background: var(--surface-subtle);\n border: var(--stroke-width-thin) solid transparent;\n outline: var(--control-edge-width, var(--stroke-width-thin, 1px)) var(--control-edge-style, solid) var(--control-edge-color, var(--border-muted, #e5e7eb));\n outline-offset: calc(-1 * var(--control-edge-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-md);\n}\n\n.segment {\n appearance: none;\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n background: transparent;\n border: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-fast, 100ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.segment:hover:not([data-active]) {\n color: var(--text-primary);\n}\n\n.segment[data-active] {\n color: var(--text-primary);\n background: var(--surface-card);\n box-shadow: var(--shadow-xs);\n}\n\n.segment:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* ── Weekday header row ── */\n/* Grid lines are hairlines: inset shadows on the shared hairline tokens, so\n * --hairline-width: 0 removes them without moving a box (VI-680). */\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n box-shadow: inset 0 var(--hairline-width, var(--stroke-width-thin)) 0 0 var(--hairline, var(--border-muted));\n background: var(--surface-subtle);\n}\n\n.weekday {\n padding: var(--spacing-2, 0.5rem);\n text-align: center;\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary);\n}\n\n/* ── Month grid: 6 week rows × 7 day cells ── */\n.grid {\n display: flex;\n flex-direction: column;\n}\n\n.week {\n position: relative;\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n /* One lane of span bars: 20px tall, 4px apart — sizing tokens deferred (Rule 5 exception). */\n --mc-lane-height: 1.25rem;\n --mc-lane-step: calc(var(--mc-lane-height) + var(--spacing-1, 0.25rem));\n}\n\n.cell {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n /* 104px — day-cell min height; sizing tokens deferred (Rule 5 exception). */\n min-height: 6.5rem;\n padding: var(--spacing-1, 0.25rem);\n min-width: 0;\n background: var(--surface-card);\n /* Right and bottom hairline draws the grid; each cell owns its two edges. */\n box-shadow:\n inset calc(-1 * var(--hairline-width, var(--stroke-width-thin))) calc(-1 * var(--hairline-width, var(--stroke-width-thin))) 0 0 var(--hairline, var(--border-muted));\n}\n\n.cell:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: calc(-1 * var(--focus-ring-width));\n}\n\n.cell[data-outside] {\n background: var(--surface-subtle);\n}\n\n.cell[data-outside] .dayNumber {\n color: var(--text-disabled);\n}\n\n.cell[data-selected] {\n background: var(--interactive-primary-soft);\n}\n\n/* Selected is also drawn, not only tinted: an accent rule across the cell top\n * (WCAG 1.4.1 — no state carried by colour alone). */\n.cell[data-selected]::before {\n content: \"\";\n position: absolute;\n inset: 0 0 auto 0;\n height: var(--stroke-width-thick);\n background: var(--interactive-primary-bg);\n}\n\n.top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-1, 0.25rem);\n /* 24px — matches the day-number badge. */\n min-height: 1.5rem;\n}\n\n.mark {\n min-width: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-variant-numeric: tabular-nums;\n color: var(--text-secondary);\n}\n\n/* Room for the span bars the week row draws over its cells. */\n.lanes {\n flex-shrink: 0;\n height: calc(var(--mc-lanes, 0) * var(--mc-lane-step) - var(--spacing-1, 0.25rem));\n}\n\n.moreSpans {\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n}\n\n/* ── Span bars ── */\n.bars {\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-columns: repeat(7, minmax(0, 1fr));\n grid-template-rows: auto;\n pointer-events: none;\n}\n\n.bar {\n grid-row: 1;\n align-self: start;\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n min-width: 0;\n height: var(--mc-lane-height);\n /* Clears the cell padding, the 24px day head and its gap. */\n margin-top: calc(var(--spacing-1, 0.25rem) + 1.5rem + var(--spacing-1, 0.25rem) + var(--mc-lane, 0) * var(--mc-lane-step));\n margin-inline: var(--spacing-1, 0.25rem);\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary);\n background: color-mix(in srgb, var(--mc-series, var(--text-tertiary)) 24%, var(--surface-card));\n box-shadow: inset var(--stroke-width-medium) 0 0 0 var(--mc-series, var(--text-tertiary));\n border-radius: var(--radius-sm);\n}\n\n.bar[data-clickable] {\n pointer-events: auto;\n cursor: pointer;\n}\n\n/* A bar cut at a week edge runs flat into it and carries a caret. */\n.bar[data-continued-before] {\n margin-inline-start: 0;\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n box-shadow: none;\n}\n\n.bar[data-continued-after] {\n margin-inline-end: 0;\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n}\n\n.barLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.barCaret {\n flex-shrink: 0;\n color: var(--text-secondary);\n}\n\n/* ── Week detail slot ── */\n.slotRow {\n display: block;\n position: relative;\n}\n\n.slotCell {\n display: block;\n}\n\n.slot {\n position: relative;\n padding: var(--spacing-4, 1rem);\n background: var(--surface-subtle);\n animation: mc-slot-in var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.slot:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: calc(-1 * var(--focus-ring-width));\n}\n\n/* The arrow points up at the selected day's column. */\n.slot::before {\n content: \"\";\n position: absolute;\n bottom: 100%;\n left: calc((var(--mc-arrow-col, 0) + 0.5) / 7 * 100%);\n /* 16×8 pointer — sizing tokens deferred (Rule 5 exception). */\n width: 1rem;\n height: 0.5rem;\n transform: translateX(-50%);\n background: var(--surface-subtle);\n clip-path: polygon(50% 0, 0 100%, 100% 100%);\n}\n\n@keyframes mc-slot-in {\n from {\n opacity: 0;\n }\n}\n\n/* ── Day number head (button when the day is selectable) ── */\n.dayHead {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 24px — day-number badge sizing; sizing tokens deferred (Rule 5 exception). */\n min-width: 1.5rem;\n height: 1.5rem;\n padding: 0 var(--spacing-1, 0.25rem);\n color: inherit;\n background: transparent;\n border: none;\n border-radius: var(--radius-full);\n cursor: default;\n}\n\n.dayHead:is(button) {\n cursor: pointer;\n}\n\n.dayHead:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.dayHead:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n.dayNumber {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.cell[data-today] .dayHead {\n background: var(--interactive-primary-bg);\n}\n\n.cell[data-today] .dayNumber {\n color: var(--interactive-primary-text);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* ── Event chips ── */\n.events {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n min-width: 0;\n}\n\n.chip {\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: 100%;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n text-align: start;\n color: var(--text-primary);\n background: var(--surface-subtle);\n border: none;\n border-radius: var(--radius-sm);\n cursor: default;\n}\n\n.chip:is(button) {\n cursor: pointer;\n}\n\n.chip:is(button):hover {\n background: var(--surface-interactive-hover);\n}\n\n.chip:focus-visible {\n outline: var(--focus-ring-width) solid var(--border-focus);\n outline-offset: var(--focus-ring-offset);\n}\n\n/* Series tint — background wash + leading accent bar keyed to the chart ramp. */\n/* An event's own colour token (inline --mc-series) wins over the series index. */\n.chip[data-series=\"1\"],\n.bar[data-series=\"1\"] {\n --mc-series: var(--chart-1);\n}\n.chip[data-series=\"2\"],\n.bar[data-series=\"2\"] {\n --mc-series: var(--chart-2);\n}\n.chip[data-series=\"3\"],\n.bar[data-series=\"3\"] {\n --mc-series: var(--chart-3);\n}\n.chip[data-series=\"4\"],\n.bar[data-series=\"4\"] {\n --mc-series: var(--chart-4);\n}\n.chip[data-series=\"5\"],\n.bar[data-series=\"5\"] {\n --mc-series: var(--chart-5);\n}\n\n.chip[data-tinted] {\n background: color-mix(in srgb, var(--mc-series) 16%, transparent);\n box-shadow: inset var(--stroke-width-medium) 0 0 0 var(--mc-series);\n}\n\n.chip[data-tinted]:is(button):hover {\n background: color-mix(in srgb, var(--mc-series) 28%, transparent);\n}\n\n.chipLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* ── Status dot ── */\n.dot {\n flex-shrink: 0;\n /* 8px dot — bound to the spacing scale. */\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full);\n background: var(--text-tertiary);\n}\n\n.dot[data-status=\"success\"] {\n background: var(--surface-success-default);\n}\n.dot[data-status=\"warning\"] {\n background: var(--surface-warning-default);\n}\n.dot[data-status=\"danger\"] {\n background: var(--surface-error-default);\n}\n.dot[data-status=\"info\"] {\n background: var(--surface-info-default);\n}\n\n/* ── Overflow row ── */\n.overflow {\n padding: 0 var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-tertiary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .segment {\n transition: none;\n }\n\n .slot {\n animation: none;\n }\n}\n"
4387
4438
  }
4388
4439
  ]
4389
4440
  },
@@ -5266,7 +5317,7 @@
5266
5317
  {
5267
5318
  "path": "components/devtools/source-inspector/visor-component-names.generated.ts",
5268
5319
  "type": "registry:devtool",
5269
- "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
5320
+ "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActionRow\",\n \"ActionRowList\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Column\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogBack\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogContext\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogScrim\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadioGroupVariantContext\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetContext\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetScrim\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TimePicker\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
5270
5321
  }
5271
5322
  ]
5272
5323
  },