@loworbitstudio/visor 1.29.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.
- package/dist/CHANGELOG.json +13 -1
- package/dist/index.js +314 -4
- package/dist/registry.json +95 -44
- package/dist/visor-manifest.json +396 -16
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -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",
|
|
@@ -142,12 +168,12 @@
|
|
|
142
168
|
{
|
|
143
169
|
"path": "components/ui/button/button.tsx",
|
|
144
170
|
"type": "registry:ui",
|
|
145
|
-
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./button.module.css\"\nimport { Spinner } from \"../spinner/spinner\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"../tooltip/tooltip\"\n\nconst buttonVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n ghost: styles.variantGhost,\n destructive: styles.variantDestructive,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n dlg: styles.sizeDlg,\n icon: styles.sizeIcon,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\n/**\n * Fallback thresholds for the `pending` affordance, in milliseconds.\n *\n * The delay suppresses the busy glyph for actions that resolve fast enough\n * that showing one would read as a flicker; the minimum duration keeps it on\n * screen long enough to be legible once it has appeared. Both are overridable\n * per call site (`pendingDelay` / `pendingMinDuration`) and per theme\n * (`--button-pending-delay` / `--button-pending-min-duration`).\n */\nexport const BUTTON_PENDING_DELAY_MS = 200\nexport const BUTTON_PENDING_MIN_DURATION_MS = 300\n\n/** Parses a CSS time value (`200ms`, `0.2s`, bare `200`) into milliseconds. */\nfunction parseCssDuration(raw: string | null | undefined): number | undefined {\n if (!raw) return undefined\n const match = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s)?$/.exec(raw.trim())\n if (!match) return undefined\n const value = Number.parseFloat(match[1])\n if (!Number.isFinite(value)) return undefined\n return match[2] === \"s\" ? value * 1000 : value\n}\n\n/**\n * Resolution order for a threshold: explicit prop, then the themed custom\n * property read off the live element, then the module fallback.\n *\n * The custom properties are deliberately NOT declared on `.base` — an\n * element-level declaration would shadow anything a theme emits on `:root`,\n * which is the defect VI-625 fixed on Spinner.\n */\nfunction resolveThreshold(\n element: HTMLElement | null,\n customProperty: string,\n propValue: number | undefined,\n fallback: number\n): number {\n if (typeof propValue === \"number\" && Number.isFinite(propValue)) {\n return Math.max(0, propValue)\n }\n if (element && typeof window !== \"undefined\" && typeof window.getComputedStyle === \"function\") {\n const themed = parseCssDuration(\n window.getComputedStyle(element).getPropertyValue(customProperty)\n )\n if (themed !== undefined) return themed\n }\n return fallback\n}\n\n/**\n * Turns a raw `pending` boolean into a *paintable* one: false for the first\n * `delay` ms, then true for at least `minDuration` ms once shown.\n *\n * Without the delay a 150ms action strobes; without the minimum a 250ms action\n * shows a one-frame flash, which reads as a glitch rather than as progress.\n */\nfunction usePendingVisibility(\n pending: boolean,\n elementRef: React.RefObject<HTMLElement | null>,\n delay: number | undefined,\n minDuration: number | undefined\n): boolean {\n const [visible, setVisible] = React.useState(false)\n const visibleRef = React.useRef(false)\n const shownAtRef = React.useRef(0)\n\n const show = React.useCallback((next: boolean) => {\n visibleRef.current = next\n setVisible(next)\n }, [])\n\n React.useEffect(() => {\n if (pending) {\n const delayMs = resolveThreshold(\n elementRef.current,\n \"--button-pending-delay\",\n delay,\n BUTTON_PENDING_DELAY_MS\n )\n if (delayMs <= 0) {\n shownAtRef.current = Date.now()\n show(true)\n return\n }\n const armed = setTimeout(() => {\n shownAtRef.current = Date.now()\n show(true)\n }, delayMs)\n return () => clearTimeout(armed)\n }\n\n // Never painted — nothing to unwind, and no minimum to honour.\n if (!visibleRef.current) return\n\n const minMs = resolveThreshold(\n elementRef.current,\n \"--button-pending-min-duration\",\n minDuration,\n BUTTON_PENDING_MIN_DURATION_MS\n )\n const remaining = minMs - (Date.now() - shownAtRef.current)\n if (remaining <= 0) {\n show(false)\n return\n }\n const held = setTimeout(() => show(false), remaining)\n return () => clearTimeout(held)\n }, [pending, delay, minDuration, elementRef, show])\n\n return visible\n}\n\n/** Spinner ring size that reads correctly inside each button size. */\nconst PENDING_SPINNER_SIZE: Record<string, \"xs\" | \"sm\"> = {\n sm: \"xs\",\n dlg: \"xs\",\n md: \"sm\",\n lg: \"sm\",\n icon: \"sm\",\n}\n\n/**\n * An icon-only button has no text for assistive tech to read, so it needs an\n * `aria-label` (or `aria-labelledby`). Warns once per mount in development;\n * silent in production.\n */\nfunction useIconLabelWarning(\n size: string | null | undefined,\n asChild: boolean | undefined,\n children: React.ReactNode,\n props: React.ButtonHTMLAttributes<HTMLButtonElement>\n) {\n // Under asChild the name can live on the child, which is not inspectable\n // here, so only a plain <button> is checked. A text child or a title is a name.\n const hasText = React.Children.toArray(children).some((c) => typeof c === \"string\" || typeof c === \"number\")\n const unlabelled =\n size === \"icon\" && !asChild && !hasText && !props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title\n React.useEffect(() => {\n if (unlabelled && process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n '[Button] size=\"icon\" renders no text, so it needs an accessible name. ' +\n \"Pass aria-label (or aria-labelledby) describing the action.\"\n )\n }\n }, [unlabelled])\n}\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n /**\n * When true, the button renders in an inert gated state — visually dimmed,\n * cursor not-allowed, and click handlers suppressed. Uses `aria-disabled`\n * instead of the native `disabled` attribute so the button remains\n * keyboard-focusable (required for the tooltip to be accessible).\n *\n * Note: a `<TooltipProvider>` ancestor is required when `gatedReason` is set.\n */\n gated?: boolean\n /**\n * Explanation surfaced in an anchored tooltip when the button is gated.\n * Only rendered when `gated` is also true.\n * Example: \"You can't delete this — you're not an owner\"\n */\n gatedReason?: string\n /**\n * When true, the button reports that its action is in flight: a busy glyph\n * appears, `aria-busy` is set, and activation is suppressed so the action\n * cannot be double-submitted.\n *\n * **The geometry is held.** The idle children stay in the box at zero\n * opacity and the glyph paints over them, so the control never resizes and\n * never shoves its siblings. That is the whole point — a pending state that\n * reflows has not fixed the visible defect.\n *\n * Like `gated`, this does NOT set the native `disabled` attribute: the\n * button stays focusable, which matters because it is the user's focus\n * anchor while they wait.\n */\n pending?: boolean\n /**\n * Optional text shown beside the busy glyph while pending. Purely visual —\n * it is `aria-hidden`, and the accessible name stays the idle label so the\n * control is not renamed mid-action. Pair `pending` with your own\n * `role=\"status\"` region when the message needs to be spoken.\n */\n pendingLabel?: React.ReactNode\n /**\n * Milliseconds to wait before painting the busy glyph. Overrides the themed\n * `--button-pending-delay`. @default 200\n */\n pendingDelay?: number\n /**\n * Milliseconds to keep the busy glyph on screen once painted. Overrides the\n * themed `--button-pending-min-duration`. @default 300\n */\n pendingMinDuration?: number\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant,\n size,\n asChild,\n gated,\n gatedReason,\n pending,\n pendingLabel,\n pendingDelay,\n pendingMinDuration,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n\n useIconLabelWarning(size, asChild, children, props)\n\n const elementRef = React.useRef<HTMLButtonElement | null>(null)\n const mergeRef = React.useCallback(\n (node: HTMLButtonElement | null) => {\n elementRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n const pendingVisible = usePendingVisibility(\n pending === true,\n elementRef,\n pendingDelay,\n pendingMinDuration\n )\n\n // Activation follows what the user can see: suppressed while the action is\n // in flight AND for as long as the glyph is still held on screen, so a\n // control that looks busy never silently accepts a click.\n const inert = gated === true || pending === true || pendingVisible\n\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (inert) {\n /* `preventDefault` as well as swallowing the handler, and this is\n * load-bearing: most buttons `pending` and `gated` exist for are a\n * `type=\"submit\"` inside a form, and suppressing `onClick` alone\n * leaves the browser's own ACTIVATION BEHAVIOUR intact — the form\n * submits anyway, through `onSubmit`, which this never sees. Measured\n * in a consumer: a second click on a pending submit mailed a second\n * one-time code. A keyboard Enter on a focused submit button\n * dispatches a click too, so this covers that path as well. */\n e.preventDefault()\n return\n }\n onClick?.(e)\n },\n [inert, onClick]\n )\n\n // `asChild` hands a single child straight to Slot, so the pending chrome\n // (which needs its own wrapper elements) cannot be injected. The state\n // attributes and activation suppression still apply.\n const showPendingChrome = !asChild && pendingVisible\n const spinnerSize = PENDING_SPINNER_SIZE[size ?? \"md\"] ?? \"sm\"\n\n const button = (\n <Comp\n className={cn(buttonVariants({ variant, size }), className)}\n ref={mergeRef}\n aria-disabled={gated || pendingVisible ? true : undefined}\n aria-busy={pending === true || pendingVisible ? true : undefined}\n data-gated={gated ? \"true\" : undefined}\n data-gated-reason={gated && gatedReason ? gatedReason : undefined}\n data-pending={pendingVisible ? \"true\" : undefined}\n onClick={handleClick}\n {...props}\n >\n {showPendingChrome ? (\n <>\n <span className={styles.pendingLabel} data-slot=\"button-label\">\n {children}\n </span>\n <span\n className={styles.pendingOverlay}\n data-slot=\"button-pending\"\n aria-hidden=\"true\"\n >\n <Spinner size={spinnerSize} />\n {pendingLabel ? (\n <span className={styles.pendingText}>{pendingLabel}</span>\n ) : null}\n </span>\n </>\n ) : (\n children\n )}\n </Comp>\n )\n\n if (gated && gatedReason) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{gatedReason}</TooltipContent>\n </Tooltip>\n )\n }\n\n return button\n }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
|
|
171
|
+
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./button.module.css\"\nimport { Spinner } from \"../spinner/spinner\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"../tooltip/tooltip\"\n\nconst buttonVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n ghost: styles.variantGhost,\n destructive: styles.variantDestructive,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n dlg: styles.sizeDlg,\n icon: styles.sizeIcon,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\n/**\n * Fallback thresholds for the `pending` affordance, in milliseconds.\n *\n * The delay suppresses the busy glyph for actions that resolve fast enough\n * that showing one would read as a flicker; the minimum duration keeps it on\n * screen long enough to be legible once it has appeared. Both are overridable\n * per call site (`pendingDelay` / `pendingMinDuration`) and per theme\n * (`--button-pending-delay` / `--button-pending-min-duration`).\n */\nexport const BUTTON_PENDING_DELAY_MS = 200\nexport const BUTTON_PENDING_MIN_DURATION_MS = 300\n\n/** Parses a CSS time value (`200ms`, `0.2s`, bare `200`) into milliseconds. */\nfunction parseCssDuration(raw: string | null | undefined): number | undefined {\n if (!raw) return undefined\n const match = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s)?$/.exec(raw.trim())\n if (!match) return undefined\n const value = Number.parseFloat(match[1])\n if (!Number.isFinite(value)) return undefined\n return match[2] === \"s\" ? value * 1000 : value\n}\n\n/**\n * Resolution order for a threshold: explicit prop, then the themed custom\n * property read off the live element, then the module fallback.\n *\n * The custom properties are deliberately NOT declared on `.base` — an\n * element-level declaration would shadow anything a theme emits on `:root`,\n * which is the defect VI-625 fixed on Spinner.\n */\nfunction resolveThreshold(\n element: HTMLElement | null,\n customProperty: string,\n propValue: number | undefined,\n fallback: number\n): number {\n if (typeof propValue === \"number\" && Number.isFinite(propValue)) {\n return Math.max(0, propValue)\n }\n if (element && typeof window !== \"undefined\" && typeof window.getComputedStyle === \"function\") {\n const themed = parseCssDuration(\n window.getComputedStyle(element).getPropertyValue(customProperty)\n )\n if (themed !== undefined) return themed\n }\n return fallback\n}\n\n/**\n * Turns a raw `pending` boolean into a *paintable* one: false for the first\n * `delay` ms, then true for at least `minDuration` ms once shown.\n *\n * Without the delay a 150ms action strobes; without the minimum a 250ms action\n * shows a one-frame flash, which reads as a glitch rather than as progress.\n */\nfunction usePendingVisibility(\n pending: boolean,\n elementRef: React.RefObject<HTMLElement | null>,\n delay: number | undefined,\n minDuration: number | undefined\n): boolean {\n const [visible, setVisible] = React.useState(false)\n const visibleRef = React.useRef(false)\n const shownAtRef = React.useRef(0)\n\n const show = React.useCallback((next: boolean) => {\n visibleRef.current = next\n setVisible(next)\n }, [])\n\n React.useEffect(() => {\n if (pending) {\n const delayMs = resolveThreshold(\n elementRef.current,\n \"--button-pending-delay\",\n delay,\n BUTTON_PENDING_DELAY_MS\n )\n if (delayMs <= 0) {\n shownAtRef.current = Date.now()\n show(true)\n return\n }\n const armed = setTimeout(() => {\n shownAtRef.current = Date.now()\n show(true)\n }, delayMs)\n return () => clearTimeout(armed)\n }\n\n // Never painted — nothing to unwind, and no minimum to honour.\n if (!visibleRef.current) return\n\n const minMs = resolveThreshold(\n elementRef.current,\n \"--button-pending-min-duration\",\n minDuration,\n BUTTON_PENDING_MIN_DURATION_MS\n )\n const remaining = minMs - (Date.now() - shownAtRef.current)\n if (remaining <= 0) {\n show(false)\n return\n }\n const held = setTimeout(() => show(false), remaining)\n return () => clearTimeout(held)\n }, [pending, delay, minDuration, elementRef, show])\n\n return visible\n}\n\n/** Spinner ring size that reads correctly inside each button size. */\nconst PENDING_SPINNER_SIZE: Record<string, \"xs\" | \"sm\"> = {\n sm: \"xs\",\n dlg: \"xs\",\n md: \"sm\",\n lg: \"sm\",\n icon: \"sm\",\n}\n\n/**\n * An icon-only button has no text for assistive tech to read, so it needs an\n * `aria-label` (or `aria-labelledby`). Warns once per mount in development;\n * silent in production.\n */\nfunction useIconLabelWarning(\n size: string | null | undefined,\n asChild: boolean | undefined,\n children: React.ReactNode,\n props: React.ButtonHTMLAttributes<HTMLButtonElement>\n) {\n // Under asChild the name can live on the child, which is not inspectable\n // here, so only a plain <button> is checked. A text child or a title is a name.\n const hasText = React.Children.toArray(children).some((c) => typeof c === \"string\" || typeof c === \"number\")\n const unlabelled =\n size === \"icon\" && !asChild && !hasText && !props[\"aria-label\"] && !props[\"aria-labelledby\"] && !props.title\n React.useEffect(() => {\n if (unlabelled && process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n '[Button] size=\"icon\" renders no text, so it needs an accessible name. ' +\n \"Pass aria-label (or aria-labelledby) describing the action.\"\n )\n }\n }, [unlabelled])\n}\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n /**\n * When true, the button renders in an inert gated state — visually dimmed,\n * cursor not-allowed, and click handlers suppressed. Uses `aria-disabled`\n * instead of the native `disabled` attribute so the button remains\n * keyboard-focusable (required for the tooltip to be accessible).\n *\n * Note: a `<TooltipProvider>` ancestor is required when `gatedReason` is set.\n */\n gated?: boolean\n /**\n * Explanation surfaced in an anchored tooltip when the button is gated.\n * Only rendered when `gated` is also true.\n * Example: \"You can't delete this — you're not an owner\"\n */\n gatedReason?: string\n /**\n * When true, the button reports that its action is in flight: a busy glyph\n * appears, `aria-busy` is set, and activation is suppressed so the action\n * cannot be double-submitted.\n *\n * **The geometry is held.** The idle children stay in the box at zero\n * opacity and the glyph paints over them, so the control never resizes and\n * never shoves its siblings. That is the whole point — a pending state that\n * reflows has not fixed the visible defect.\n *\n * Like `gated`, this does NOT set the native `disabled` attribute: the\n * button stays focusable, which matters because it is the user's focus\n * anchor while they wait.\n */\n pending?: boolean\n /**\n * Optional text shown beside the busy glyph while pending. Purely visual —\n * it is `aria-hidden`, and the accessible name stays the idle label so the\n * control is not renamed mid-action. Pair `pending` with your own\n * `role=\"status\"` region when the message needs to be spoken.\n */\n pendingLabel?: React.ReactNode\n /**\n * Milliseconds to wait before painting the busy glyph. Overrides the themed\n * `--button-pending-delay`. @default 200\n */\n pendingDelay?: number\n /**\n * Milliseconds to keep the busy glyph on screen once painted. Overrides the\n * themed `--button-pending-min-duration`. @default 300\n */\n pendingMinDuration?: number\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 (`text-box`, see `.label`). Flexbox centres the line\n * box, whose height comes from the font's ascent and descent rather than from\n * where its capitals sit, so an untrimmed label lands off-centre by an amount\n * that differs per font (VI-682). Elements (icons, spinners) 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=\"button-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant,\n size,\n asChild,\n gated,\n gatedReason,\n pending,\n pendingLabel,\n pendingDelay,\n pendingMinDuration,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n\n useIconLabelWarning(size, asChild, children, props)\n\n const elementRef = React.useRef<HTMLButtonElement | null>(null)\n const mergeRef = React.useCallback(\n (node: HTMLButtonElement | null) => {\n elementRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n const pendingVisible = usePendingVisibility(\n pending === true,\n elementRef,\n pendingDelay,\n pendingMinDuration\n )\n\n // Activation follows what the user can see: suppressed while the action is\n // in flight AND for as long as the glyph is still held on screen, so a\n // control that looks busy never silently accepts a click.\n const inert = gated === true || pending === true || pendingVisible\n\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (inert) {\n /* `preventDefault` as well as swallowing the handler, and this is\n * load-bearing: most buttons `pending` and `gated` exist for are a\n * `type=\"submit\"` inside a form, and suppressing `onClick` alone\n * leaves the browser's own ACTIVATION BEHAVIOUR intact — the form\n * submits anyway, through `onSubmit`, which this never sees. Measured\n * in a consumer: a second click on a pending submit mailed a second\n * one-time code. A keyboard Enter on a focused submit button\n * dispatches a click too, so this covers that path as well. */\n e.preventDefault()\n return\n }\n onClick?.(e)\n },\n [inert, onClick]\n )\n\n // `asChild` hands a single child straight to Slot, so the pending chrome\n // (which needs its own wrapper elements) cannot be injected. The state\n // attributes and activation suppression still apply.\n const showPendingChrome = !asChild && pendingVisible\n const spinnerSize = PENDING_SPINNER_SIZE[size ?? \"md\"] ?? \"sm\"\n\n const button = (\n <Comp\n className={cn(buttonVariants({ variant, size }), className)}\n ref={mergeRef}\n aria-disabled={gated || pendingVisible ? true : undefined}\n aria-busy={pending === true || pendingVisible ? true : undefined}\n data-gated={gated ? \"true\" : undefined}\n data-gated-reason={gated && gatedReason ? gatedReason : undefined}\n data-pending={pendingVisible ? \"true\" : undefined}\n onClick={handleClick}\n {...props}\n >\n {showPendingChrome ? (\n <>\n <span className={styles.pendingLabel} data-slot=\"button-label\">\n {wrapLabel(children)}\n </span>\n <span\n className={styles.pendingOverlay}\n data-slot=\"button-pending\"\n aria-hidden=\"true\"\n >\n <Spinner size={spinnerSize} />\n {pendingLabel ? (\n <span className={styles.pendingText}>{pendingLabel}</span>\n ) : null}\n </span>\n </>\n ) : asChild && React.isValidElement<{ children?: React.ReactNode }>(children) ? (\n // Slot merges onto its one child, so the child's own text is wrapped\n // in place; its props and ref are kept by cloneElement.\n React.cloneElement(children, undefined, wrapLabel(children.props.children))\n ) : (\n wrapLabel(children)\n )}\n </Comp>\n )\n\n if (gated && gatedReason) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{gatedReason}</TooltipContent>\n </Tooltip>\n )\n }\n\n return button\n }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
|
|
146
172
|
},
|
|
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/* 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 height: 2rem;\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 height: 2.5rem;\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 height: 2.125rem;\n line-height: normal;\n}\n\n.sizeLg {\n height: 3rem;\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
|
|
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
|
|
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
|
},
|
|
@@ -628,12 +654,12 @@
|
|
|
628
654
|
{
|
|
629
655
|
"path": "components/ui/tooltip/tooltip.tsx",
|
|
630
656
|
"type": "registry:ui",
|
|
631
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tooltip.module.css\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n React.ElementRef<typeof TooltipPrimitive.Content>,\n
|
|
657
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tooltip.module.css\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\ninterface TooltipContentProps\n extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> {\n /**\n * Element the tooltip portals into. Defaults to `document.body`. Pass the\n * root of a class-scoped theme so the tooltip inherits that theme's tokens.\n */\n container?: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Portal>[\"container\"]\n}\n\nconst TooltipContent = React.forwardRef<\n React.ElementRef<typeof TooltipPrimitive.Content>,\n TooltipContentProps\n>(({ className, sideOffset = 4, container, ...props }, ref) => {\n return (\n <TooltipPrimitive.Portal container={container}>\n <TooltipPrimitive.Content\n ref={ref}\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n className={cn(styles.content, className)}\n {...props}\n />\n </TooltipPrimitive.Portal>\n )\n})\nTooltipContent.displayName = \"TooltipContent\"\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }\nexport type { TooltipContentProps }\n"
|
|
632
658
|
},
|
|
633
659
|
{
|
|
634
660
|
"path": "components/ui/tooltip/tooltip.module.css",
|
|
635
661
|
"type": "registry:ui",
|
|
636
|
-
"content": "/* Tooltip content */\n.content {\n z-index: 50;\n display: inline-flex;\n width: fit-content;\n max-width: 20rem;\n align-items: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-overlay, #111827);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-inverse, #ffffff);\n box-shadow: var(--shadow-md);\n animation-duration: var(--motion-duration-150, 150ms);\n animation-timing-function: var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"delayed-open\"] {\n animation-name: fadeIn;\n}\n\n.content[data-state=\"closed\"] {\n animation-name: fadeOut;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
662
|
+
"content": "/* Tooltip content */\n.content {\n z-index: 50;\n display: inline-flex;\n width: fit-content;\n max-width: 20rem;\n align-items: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--tooltip-bg, var(--surface-overlay, #111827));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--tooltip-text, var(--text-inverse, #ffffff));\n box-shadow: var(--shadow-md);\n animation-duration: var(--motion-duration-150, 150ms);\n animation-timing-function: var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"delayed-open\"] {\n animation-name: fadeIn;\n}\n\n.content[data-state=\"closed\"] {\n animation-name: fadeOut;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
637
663
|
}
|
|
638
664
|
]
|
|
639
665
|
},
|
|
@@ -709,7 +735,7 @@
|
|
|
709
735
|
{
|
|
710
736
|
"path": "components/ui/coherence-check/coherence-check.module.css",
|
|
711
737
|
"type": "registry:ui",
|
|
712
|
-
"content": "/* ─── CheckGroup ─────────────────────────────────────────────────────────── */\n\n.group {\n margin-bottom: var(--spacing-5, 1.25rem);\n}\n\n/* Uppercase group header */\n.groupHeading {\n margin: 0 0 var(--spacing-2, 0.5rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-extrabold, 800);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n line-height: 1.4;\n}\n\n/* ─── CheckRow ───────────────────────────────────────────────────────────── */\n\n.row {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n margin-bottom: var(--spacing-2, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Last row in a group has no bottom margin */\n.row:last-child {\n margin-bottom: 0;\n}\n\n/* ─── State icon circle ─────────────────────────────────────────────────── */\n\n.icon {\n flex: none;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--radius-full, 9999px);\n display: grid;\n place-items: center;\n margin-top: calc(var(--spacing-1, 0.25rem) / 4);\n color: var(--text-inverse, #ffffff);\n}\n\n.icon svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* State — icon background tints */\n.statePass .icon {\n background: var(--success, #22c55e);\n}\n\n.stateWarn .icon {\n background: var(--warning, #f59e0b);\n}\n\n.stateFail .icon {\n background: var(--destructive, #ef4444);\n}\n\n/* ─── Body (title + description) ─────────────────────────────────────────── */\n\n.body {\n flex: 1;\n min-width: 0;\n}\n\n.rowTitle {\n display: block;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n.rowDescription {\n margin: calc(var(--spacing-1, 0.25rem) * 0.75) 0 0;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n/* Inline code token support within description */\n.rowDescription code {\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.75rem);\n background: var(--surface-muted, #f3f4f6);\n padding: var(--stroke-width-thin, 1px) var(--spacing-1, 0.25rem);\n border-radius: var(--radius-sm, 0.125rem);\n}\n\n/* ─── Fix action button (ghost) ──────────────────────────────────────────── */\n\n.fixButton {\n flex: none;\n align-self: center;\n height: var(--spacing-8, 2rem);\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background: transparent;\n color: var(--text-primary, #111827);\n cursor: pointer;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-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 white-space: nowrap;\n}\n\n.fixButton:hover {\n background: var(--surface-muted, #f3f4f6);\n border-color: var(--border-strong, #d1d5db);\n}\n\n.fixButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
738
|
+
"content": "/* ─── CheckGroup ─────────────────────────────────────────────────────────── */\n\n.group {\n margin-bottom: var(--spacing-5, 1.25rem);\n}\n\n/* Uppercase group header */\n.groupHeading {\n margin: 0 0 var(--spacing-2, 0.5rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-extrabold, 800);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n line-height: 1.4;\n}\n\n/* ─── CheckRow ───────────────────────────────────────────────────────────── */\n\n.row {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-3, 0.75rem);\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n margin-bottom: var(--spacing-2, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Last row in a group has no bottom margin */\n.row:last-child {\n margin-bottom: 0;\n}\n\n/* ─── State icon circle ─────────────────────────────────────────────────── */\n\n.icon {\n flex: none;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--radius-full, 9999px);\n display: grid;\n place-items: center;\n margin-top: calc(var(--spacing-1, 0.25rem) / 4);\n color: var(--text-inverse, #ffffff);\n}\n\n.icon svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* State — icon background tints */\n.statePass .icon {\n background: var(--success, #22c55e);\n}\n\n.stateWarn .icon {\n background: var(--warning, #f59e0b);\n}\n\n.stateFail .icon {\n background: var(--destructive, #ef4444);\n}\n\n/* ─── Body (title + description) ─────────────────────────────────────────── */\n\n.body {\n flex: 1;\n min-width: 0;\n}\n\n.rowTitle {\n display: block;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n.rowDescription {\n margin: calc(var(--spacing-1, 0.25rem) * 0.75) 0 0;\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #6b7280);\n line-height: 1.5;\n}\n\n/* Inline code token support within description */\n.rowDescription code {\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.75rem);\n background: var(--surface-muted, #f3f4f6);\n padding: var(--stroke-width-thin, 1px) var(--spacing-1, 0.25rem);\n border-radius: var(--radius-sm, 0.125rem);\n}\n\n/* ─── Fix action button (ghost) ──────────────────────────────────────────── */\n\n.fixButton {\n flex: none;\n align-self: center;\n height: var(--spacing-8, 2rem);\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n background: transparent;\n color: var(--text-primary, #111827);\n cursor: pointer;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n border-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 white-space: nowrap;\n}\n\n.fixButton:hover {\n background: var(--surface-muted, #f3f4f6);\n border-color: var(--border-strong, #d1d5db);\n}\n\n.fixButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
713
739
|
}
|
|
714
740
|
]
|
|
715
741
|
},
|
|
@@ -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: var(--hairline-
|
|
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\
|
|
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
|
},
|
|
@@ -838,7 +864,7 @@
|
|
|
838
864
|
{
|
|
839
865
|
"path": "components/ui/dropdown-menu/dropdown-menu.module.css",
|
|
840
866
|
"type": "registry:ui",
|
|
841
|
-
"content": "/* Dropdown menu content\n *\n * Density axis: editorial admin values are baked in as a first-class density\n * layer, switched by data-density=\"editorial\" on any ancestor. Default rendering\n * is byte-identical to the pre-refactor canonical fallbacks. Theme-agnostic:\n * editorial values resolve from palette tokens (--surface-elev, --hairline,\n * --text-tertiary, --text-muted, --destructive) — correct on entr AND animal. */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .content {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 1px var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Breakout variant — use inside scroll-clipped containers (ex: DataTable rows) */\n.contentBreakout {\n z-index: 200;\n box-shadow: var(--shadow-xl, 0 28px 64px rgba(0, 0, 0, 0.55), 0 8px 16px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.06));\n}\n\n/* Sub content */\n.subContent {\n z-index: 50;\n min-width: 9rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .subContent {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 1px var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.subContent[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.subContent[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Menu item\n * Default: canonical spacing, radius-md, font-size-sm, interactive-hover bg.\n * Density axis: editorial uses 13px, 8px 10px padding, radius-sm, surface-subtle hover. */\n.item {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .item {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n/* Leading icon — default: inherit item color and 1em size.\n * Density axis: editorial uses text-tertiary color and 16px size. */\n.item > svg {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .item > svg {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\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:global([data-density=\"editorial\"]) .item:focus,\n:global([data-density=\"editorial\"]) .item[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemDestructive {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n/* Destructive icons track the destructive color, overriding the tertiary tint. */\n.itemDestructive > svg {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n.itemDestructive:focus,\n.itemDestructive[data-highlighted] {\n background-color: var(--surface-error-subtle, rgba(239, 68, 68, 0.1));\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n:global([data-density=\"editorial\"]) .itemDestructive:focus,\n:global([data-density=\"editorial\"]) .itemDestructive[data-highlighted] {\n background-color: color-mix(in srgb, var(--destructive) 14%, transparent);\n}\n\n.itemInset {\n padding-left: calc(var(--spacing-8, 2rem) + var(--spacing-1, 0.25rem) * 1.5);\n}\n\n/* Checkbox and radio items */\n.checkboxItem,\n.radioItem {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-8, 2rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem,\n:global([data-density=\"editorial\"]) .radioItem {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n padding-right: var(--spacing-8, 2rem);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.checkboxItem:focus,\n.checkboxItem[data-highlighted],\n.radioItem:focus,\n.radioItem[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem:focus,\n:global([data-density=\"editorial\"]) .checkboxItem[data-highlighted],\n:global([data-density=\"editorial\"]) .radioItem:focus,\n:global([data-density=\"editorial\"]) .radioItem[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.checkboxItem[data-disabled],\n.radioItem[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Item indicator */\n.itemIndicator {\n pointer-events: none;\n position: absolute;\n right: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n}\n\n/* Label\n * Default: canonical 12px / text-secondary / sentence-case.\n * Density axis: editorial uses 11px / uppercase / 0.08em tracking / text-tertiary / 6px 10px 2px padding. */\n.label {\n padding: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: inherit;\n letter-spacing: normal;\n text-transform: none;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-tertiary);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Separator\n * Default: spacing-1 margin, border-default color.\n * Density axis: editorial uses 4px 2px margin, hairline color. */\n.separator {\n height: 1px;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n background-color: var(--border-default, #e5e7eb);\n}\n\n:global([data-density=\"editorial\"]) .separator {\n background-color: var(--hairline, var(--border-subtle, transparent));\n margin: var(--spacing-1, 0.25rem) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Shortcut\n * Default: 12px / 0.1em tracking / text-secondary.\n * Density axis: editorial uses 11px / 0.04em tracking / text-muted. */\n.shortcut {\n margin-left: auto;\n font-size: var(--font-size-xs, 0.75rem);\n letter-spacing: 0.1em;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .shortcut {\n font-size: 11px;\n color: var(--text-muted);\n letter-spacing: 0.04em;\n}\n\n/* Sub trigger */\n.subTrigger {\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.subTrigger > svg:not(.subTriggerIcon) {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .subTrigger > svg:not(.subTriggerIcon) {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\n}\n\n.subTrigger:focus,\n.subTrigger[data-state=\"open\"] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger:focus,\n:global([data-density=\"editorial\"]) .subTrigger[data-state=\"open\"] {\n background-color: var(--surface-subtle);\n}\n\n.subTriggerIcon {\n margin-left: auto;\n width: 1rem;\n height: 1rem;\n color: currentColor;\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
867
|
+
"content": "/* Dropdown menu content\n *\n * Density axis: editorial admin values are baked in as a first-class density\n * layer, switched by data-density=\"editorial\" on any ancestor. Default rendering\n * is byte-identical to the pre-refactor canonical fallbacks. Theme-agnostic:\n * editorial values resolve from palette tokens (--surface-elev, --hairline,\n * --text-tertiary, --text-muted, --destructive) — correct on entr AND animal. */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .content {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 var(--hairline-width, 1px) var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Breakout variant — use inside scroll-clipped containers (ex: DataTable rows) */\n.contentBreakout {\n z-index: 200;\n box-shadow: var(--shadow-xl, 0 28px 64px rgba(0, 0, 0, 0.55), 0 8px 16px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.06));\n}\n\n/* Sub content */\n.subContent {\n z-index: 50;\n min-width: 9rem;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-1, 0.25rem);\n box-shadow:\n 0 0 #0000,\n var(--shadow-lg);\n}\n\n:global([data-density=\"editorial\"]) .subContent {\n gap: var(--stroke-width-thin, 1px);\n border-radius: var(--radius-md);\n background: var(--surface-elev, color-mix(in srgb, var(--surface-card), var(--surface-muted, var(--surface-card))));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n box-shadow:\n inset 0 0 0 var(--hairline-width, 1px) var(--hairline, var(--border-subtle, transparent)),\n var(--shadow-lg);\n}\n\n.subContent[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.subContent[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n/* Menu item\n * Default: canonical spacing, radius-md, font-size-sm, interactive-hover bg.\n * Density axis: editorial uses 13px, 8px 10px padding, radius-sm, surface-subtle hover. */\n.item {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .item {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n/* Leading icon — default: inherit item color and 1em size.\n * Density axis: editorial uses text-tertiary color and 16px size. */\n.item > svg {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .item > svg {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\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:global([data-density=\"editorial\"]) .item:focus,\n:global([data-density=\"editorial\"]) .item[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemDestructive {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n/* Destructive icons track the destructive color, overriding the tertiary tint. */\n.itemDestructive > svg {\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n.itemDestructive:focus,\n.itemDestructive[data-highlighted] {\n background-color: var(--surface-error-subtle, rgba(239, 68, 68, 0.1));\n color: var(--text-error, var(--destructive, #ef4444));\n}\n\n:global([data-density=\"editorial\"]) .itemDestructive:focus,\n:global([data-density=\"editorial\"]) .itemDestructive[data-highlighted] {\n background-color: color-mix(in srgb, var(--destructive) 14%, transparent);\n}\n\n.itemInset {\n padding-left: calc(var(--spacing-8, 2rem) + var(--spacing-1, 0.25rem) * 1.5);\n}\n\n/* Checkbox and radio items */\n.checkboxItem,\n.radioItem {\n position: relative;\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n padding-right: var(--spacing-8, 2rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem,\n:global([data-density=\"editorial\"]) .radioItem {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n padding-right: var(--spacing-8, 2rem);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.checkboxItem:focus,\n.checkboxItem[data-highlighted],\n.radioItem:focus,\n.radioItem[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .checkboxItem:focus,\n:global([data-density=\"editorial\"]) .checkboxItem[data-highlighted],\n:global([data-density=\"editorial\"]) .radioItem:focus,\n:global([data-density=\"editorial\"]) .radioItem[data-highlighted] {\n background-color: var(--surface-subtle);\n}\n\n.checkboxItem[data-disabled],\n.radioItem[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Item indicator */\n.itemIndicator {\n pointer-events: none;\n position: absolute;\n right: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n}\n\n/* Label\n * Default: canonical 12px / text-secondary / sentence-case.\n * Density axis: editorial uses 11px / uppercase / 0.08em tracking / text-tertiary / 6px 10px 2px padding. */\n.label {\n padding: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: inherit;\n letter-spacing: normal;\n text-transform: none;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-tertiary);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Separator\n * Default: spacing-1 margin, border-default color.\n * Density axis: editorial uses 4px 2px margin, hairline color. */\n.separator {\n height: 1px;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n background-color: var(--border-default, #e5e7eb);\n}\n\n:global([data-density=\"editorial\"]) .separator {\n /* The 1px box is geometry; the line is an inset shadow whose spread is\n * --hairline-width, so 0 empties the box and nothing moves. */\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--hairline-width, 1px) var(--hairline, var(--border-subtle, transparent));\n margin: var(--spacing-1, 0.25rem) calc(var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Shortcut\n * Default: 12px / 0.1em tracking / text-secondary.\n * Density axis: editorial uses 11px / 0.04em tracking / text-muted. */\n.shortcut {\n margin-left: auto;\n font-size: var(--font-size-xs, 0.75rem);\n letter-spacing: 0.1em;\n color: var(--text-secondary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .shortcut {\n font-size: 11px;\n color: var(--text-muted);\n letter-spacing: 0.04em;\n}\n\n/* Sub trigger */\n.subTrigger {\n display: flex;\n cursor: default;\n user-select: none;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger {\n font-size: 13px;\n padding: var(--spacing-2, 0.5rem) calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n gap: var(--spacing-3);\n border-radius: var(--radius-sm);\n}\n\n.subTrigger > svg:not(.subTriggerIcon) {\n flex-shrink: 0;\n color: currentColor;\n width: 1em;\n height: 1em;\n}\n\n:global([data-density=\"editorial\"]) .subTrigger > svg:not(.subTriggerIcon) {\n color: var(--text-tertiary);\n width: 16px;\n height: 16px;\n}\n\n.subTrigger:focus,\n.subTrigger[data-state=\"open\"] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n:global([data-density=\"editorial\"]) .subTrigger:focus,\n:global([data-density=\"editorial\"]) .subTrigger[data-state=\"open\"] {\n background-color: var(--surface-subtle);\n}\n\n.subTriggerIcon {\n margin-left: auto;\n width: 1rem;\n height: 1rem;\n color: currentColor;\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
|
|
842
868
|
}
|
|
843
869
|
]
|
|
844
870
|
},
|
|
@@ -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
|
|
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 border-bottom: var(--tabs-line-border-bottom, 1px solid var(--hairline, var(--border-default, #e5e7eb)));\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
|
|
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,
|
|
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
|
},
|
|
@@ -1042,7 +1068,7 @@
|
|
|
1042
1068
|
{
|
|
1043
1069
|
"path": "components/ui/popover/popover.module.css",
|
|
1044
1070
|
"type": "registry:ui",
|
|
1045
|
-
"content": "/* Popover content */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-4, 1rem);\n box-shadow: var(--shadow-lg);\n}\n\n/* Footer — right-aligned action row separated from the body by a top border.\n Negative horizontal margins let the border span the full popover width while\n the action row stays aligned with body content (offsets PopoverContent's\n padding: var(--spacing-4)). */\n.footer {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n gap: var(--spacing-2, 0.5rem);\n margin-top: var(--spacing-3, 0.75rem);\n padding-top: var(--spacing-3, 0.75rem);\n margin-inline: calc(-1 * var(--spacing-4, 1rem));\n padding-inline: var(--spacing-4, 1rem);\n border-top: 1px solid var(--border-default, #e5e7eb);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n\n/* ─── SelectionList ─────────────────────────────────────────────────── */\n\n.selectionLabel {\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n padding-bottom: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text-secondary, #6b7280);\n}\n\n.selectionList {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n outline: none;\n}\n\n.selectionItem {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n cursor: pointer;\n outline: none;\n user-select: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n.selectionItem:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n}\n\n.selectionItem:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #6366f1);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.selectionItemSelected {\n background-color: var(--surface-subtle, #f9fafb);\n}\n\n.selectionItemSelected:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n}\n\n.selectionItemDisabled {\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n cursor: default;\n}\n\n.selectionItemLeadingIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Indicator plate — always rendered for stable row geometry */\n.selectionIndicator {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1rem; /* 16px */\n height: 1rem; /* 16px */\n transition:\n background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Checkbox: rounded-4px plate */\n.selectionIndicatorCheckbox {\n border-radius: 4px;\n border: var(--stroke-width-thin, 1px) solid var(--hairline-strong, #d1d5db);\n background-color: var(--surface-card, #ffffff);\n color: transparent;\n}\n\n.selectionIndicatorCheckbox.selectionIndicatorSelected {\n background-color: var(--primary, #6366f1);\n
|
|
1071
|
+
"content": "/* Popover content */\n.content {\n z-index: 50;\n min-width: 12rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--surface-popover, var(--surface-card, #ffffff));\n color: var(--text-primary, #111827);\n padding: var(--spacing-4, 1rem);\n box-shadow: var(--shadow-lg);\n}\n\n/* Footer — right-aligned action row separated from the body by a top border.\n Negative horizontal margins let the border span the full popover width while\n the action row stays aligned with body content (offsets PopoverContent's\n padding: var(--spacing-4)). */\n.footer {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n gap: var(--spacing-2, 0.5rem);\n margin-top: var(--spacing-3, 0.75rem);\n padding-top: var(--spacing-3, 0.75rem);\n margin-inline: calc(-1 * var(--spacing-4, 1rem));\n padding-inline: var(--spacing-4, 1rem);\n border-top: 1px solid var(--border-default, #e5e7eb);\n}\n\n.content[data-state=\"open\"] {\n animation: contentShow var(--motion-duration-fast, 100ms) var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"closed\"] {\n animation: contentHide var(--motion-duration-fast, 100ms) var(--motion-easing-exit, ease-in);\n}\n\n@keyframes contentShow {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes contentHide {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n\n/* ─── SelectionList ─────────────────────────────────────────────────── */\n\n.selectionLabel {\n padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);\n padding-bottom: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text-secondary, #6b7280);\n}\n\n.selectionList {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n outline: none;\n}\n\n.selectionItem {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n cursor: pointer;\n outline: none;\n user-select: none;\n transition: background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n.selectionItem:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n}\n\n.selectionItem:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #6366f1);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.selectionItemSelected {\n background-color: var(--surface-subtle, #f9fafb);\n}\n\n.selectionItemSelected:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n}\n\n.selectionItemDisabled {\n opacity: var(--opacity-40, 0.4);\n pointer-events: none;\n cursor: default;\n}\n\n.selectionItemLeadingIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Indicator plate — always rendered for stable row geometry */\n.selectionIndicator {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1rem; /* 16px */\n height: 1rem; /* 16px */\n transition:\n background-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out),\n border-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out),\n outline-color var(--motion-duration-fast, 100ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Checkbox: rounded-4px plate */\n.selectionIndicatorCheckbox {\n border-radius: 4px;\n border: var(--stroke-width-thin, 1px) solid transparent;\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 background-color: var(--surface-card, #ffffff);\n color: transparent;\n}\n\n.selectionIndicatorCheckbox.selectionIndicatorSelected {\n background-color: var(--primary, #6366f1);\n outline-color: var(--primary, #6366f1);\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.selectionIndicatorCheckbox svg {\n width: 0.625rem; /* 10px */\n height: 0.625rem;\n}\n\n/* Radio: circular plate with ring */\n.selectionIndicatorRadio {\n border-radius: 9999px;\n border: var(--stroke-width-thin, 1px) solid transparent;\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 background-color: var(--surface-card, #ffffff);\n}\n\n.selectionIndicatorRadio.selectionIndicatorSelected {\n border: 1.5px solid var(--primary, #6366f1);\n outline: none;\n}\n\n/* Radio filled dot when selected */\n.selectionRadioDot {\n display: block;\n width: 0.5rem; /* 8px */\n height: 0.5rem;\n border-radius: 9999px;\n background-color: var(--primary, #6366f1);\n}\n\n.selectionItemLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Count badge — right-aligned, tabular numerals, muted */\n.selectionCount {\n margin-left: auto;\n flex-shrink: 0;\n font-size: var(--font-size-xs, 0.75rem);\n font-variant-numeric: tabular-nums;\n color: var(--text-secondary, #6b7280);\n}\n"
|
|
1046
1072
|
}
|
|
1047
1073
|
]
|
|
1048
1074
|
},
|
|
@@ -1177,7 +1203,7 @@
|
|
|
1177
1203
|
{
|
|
1178
1204
|
"path": "components/ui/table/table.module.css",
|
|
1179
1205
|
"type": "registry:ui",
|
|
1180
|
-
"content": "/* Table container — radius/shadow are theme-driven so a consumer that wraps the\n table in its own rounded shell (for example with a bulk bar on top) can flatten them.\n Under the editorial density axis these default to 0/none (flush admin shell).\n\n VI-625: every declaration below reads a component-scoped `--table-*` hook with\n its previous expression as the fallback, so a theme that binds nothing renders\n identically and a theme that binds the `table` family retunes every admin list\n page at once. Contract: the component-tokens module in packages/theme-engine/src */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--dt-container-radius, var(--radius-lg, 0.5rem));\n box-shadow: var(--dt-container-shadow, var(--shadow-sm));\n}\n\n/* Editorial density — bakes the editorial-admin surface values into the density\n axis. Ancestor [data-density=\"editorial\"] (set by DataTable's density prop or\n an app-root attribute) triggers these rules. The var() reads keep --dt-* hooks\n working for per-theme overrides; the fallbacks resolve to the editorial values\n when no hook is set.\n Turbopack pure-selector rule: never bare :global — always paired with a local class. */\n:global([data-density=\"editorial\"]) .container {\n border-radius: var(--dt-container-radius, 0);\n box-shadow: var(--dt-container-shadow, none);\n}\n\n:global([data-density=\"editorial\"]) .head {\n background-color: var(--table-head-bg, var(--dt-header-bg, var(--surface-card)));\n}\n\n:global([data-density=\"editorial\"]) .cell {\n background-color: var(--table-cell-bg, var(--dt-row-bg, color-mix(in srgb, var(--surface-page) 75%, var(--surface-card) 25%)));\n}\n\n/* Table */\n.table {\n width: 100%;\n caption-side: bottom;\n border-collapse: collapse;\n font-size: var(--table-font-size, var(--font-size-sm, 0.875rem));\n color: var(--table-text-color, var(--text-primary, #111827));\n}\n\n/* TableHeader */\n.header tr {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n/* TableBody */\n.body tr:last-child {\n border-bottom: none;\n}\n\n/* TableFooter */\n.footer {\n border-top: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--table-footer-bg, var(--surface-muted, #f3f4f6));\n font-weight: var(--font-weight-medium, 500);\n}\n\n.footer tr:last-child {\n border-bottom: none;\n}\n\n/* TableRow */\n.row {\n border-bottom: var(--table-row-border, 1px solid var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.row:not([data-slot=\"data-table-group-row\"]):hover {\n background-color: var(--table-row-hover-bg, var(--surface-muted, #f3f4f6));\n}\n\n.row[data-state=\"selected\"] {\n background-color: var(--table-row-selected-bg, var(--surface-muted, #f3f4f6));\n}\n\n/* TableHead — bg via --table-head-bg → --dt-header-bg (theme-driven; transparent\n default). The type hooks (family/size/letter-spacing/text-transform) fall back\n to `inherit`, which is the initial computed value for each of those inherited\n properties — so an unbound theme renders exactly as before they existed. */\n.head {\n height: var(--table-head-height, 3rem);\n padding-left: var(--table-head-padding-x, var(--spacing-3, 0.75rem));\n padding-right: var(--table-head-padding-x, var(--spacing-3, 0.75rem));\n text-align: left;\n vertical-align: middle;\n font-family: var(--table-head-font-family, inherit);\n font-size: var(--table-head-font-size, inherit);\n font-weight: var(--table-head-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--table-head-letter-spacing, inherit);\n text-transform: var(--table-head-text-transform, inherit);\n white-space: nowrap;\n color: var(--table-head-color, var(--text-primary, #111827));\n background-color: var(--table-head-bg, var(--dt-header-bg, transparent));\n}\n\n/* TableCell — bg via --table-cell-bg → --dt-row-bg (theme-driven; transparent\n default). Painting on the cell gives a uniform row surface regardless of tr\n selected/hover state. */\n.cell {\n padding: var(--table-cell-padding, var(--spacing-3, 0.75rem));\n vertical-align: middle;\n font-size: var(--table-cell-font-size, inherit);\n color: var(--table-cell-color, var(--text-primary, #111827));\n background-color: var(--table-cell-bg, var(--dt-row-bg, transparent));\n border-top: var(--table-cell-border-top, 1px solid var(--hairline, transparent));\n}\n\n/* TableCaption */\n.caption {\n margin-top: var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--table-caption-color, var(--text-secondary, #6b7280));\n}\n"
|
|
1206
|
+
"content": "/* Table container — radius/shadow are theme-driven so a consumer that wraps the\n table in its own rounded shell (for example with a bulk bar on top) can flatten them.\n Under the editorial density axis these default to 0/none (flush admin shell).\n\n VI-625: every declaration below reads a component-scoped `--table-*` hook with\n its previous expression as the fallback, so a theme that binds nothing renders\n identically and a theme that binds the `table` family retunes every admin list\n page at once. Contract: the component-tokens module in packages/theme-engine/src */\n.container {\n position: relative;\n width: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n border-radius: var(--dt-container-radius, var(--radius-lg, 0.5rem));\n box-shadow: var(--dt-container-shadow, var(--shadow-sm));\n}\n\n/* Editorial density — bakes the editorial-admin surface values into the density\n axis. Ancestor [data-density=\"editorial\"] (set by DataTable's density prop or\n an app-root attribute) triggers these rules. The var() reads keep --dt-* hooks\n working for per-theme overrides; the fallbacks resolve to the editorial values\n when no hook is set.\n Turbopack pure-selector rule: never bare :global — always paired with a local class. */\n:global([data-density=\"editorial\"]) .container {\n border-radius: var(--dt-container-radius, 0);\n box-shadow: var(--dt-container-shadow, none);\n}\n\n:global([data-density=\"editorial\"]) .head {\n background-color: var(--table-head-bg, var(--dt-header-bg, var(--surface-card)));\n}\n\n:global([data-density=\"editorial\"]) .cell {\n background-color: var(--table-cell-bg, var(--dt-row-bg, color-mix(in srgb, var(--surface-page) 75%, var(--surface-card) 25%)));\n}\n\n/* Table */\n.table {\n width: 100%;\n caption-side: bottom;\n border-collapse: collapse;\n font-size: var(--table-font-size, var(--font-size-sm, 0.875rem));\n color: var(--table-text-color, var(--text-primary, #111827));\n}\n\n/* TableHeader */\n.header tr {\n border-bottom: 1px solid var(--border-default, #e5e7eb);\n}\n\n/* TableBody */\n.body tr:last-child {\n border-bottom: none;\n}\n\n/* TableFooter */\n.footer {\n border-top: 1px solid var(--border-default, #e5e7eb);\n background-color: var(--table-footer-bg, var(--surface-muted, #f3f4f6));\n font-weight: var(--font-weight-medium, 500);\n}\n\n.footer tr:last-child {\n border-bottom: none;\n}\n\n/* TableRow */\n.row {\n border-bottom: var(--table-row-border, 1px solid var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.row:not([data-slot=\"data-table-group-row\"]):hover {\n background-color: var(--table-row-hover-bg, var(--surface-muted, #f3f4f6));\n}\n\n.row[data-state=\"selected\"] {\n background-color: var(--table-row-selected-bg, var(--surface-muted, #f3f4f6));\n}\n\n/* TableHead — bg via --table-head-bg → --dt-header-bg (theme-driven; transparent\n default). The type hooks (family/size/letter-spacing/text-transform) fall back\n to `inherit`, which is the initial computed value for each of those inherited\n properties — so an unbound theme renders exactly as before they existed. */\n.head {\n height: var(--table-head-height, 3rem);\n padding-left: var(--table-head-padding-x, var(--spacing-3, 0.75rem));\n padding-right: var(--table-head-padding-x, var(--spacing-3, 0.75rem));\n text-align: left;\n vertical-align: middle;\n font-family: var(--table-head-font-family, inherit);\n font-size: var(--table-head-font-size, inherit);\n font-weight: var(--table-head-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--table-head-letter-spacing, inherit);\n text-transform: var(--table-head-text-transform, inherit);\n white-space: nowrap;\n color: var(--table-head-color, var(--text-primary, #111827));\n background-color: var(--table-head-bg, var(--dt-header-bg, transparent));\n}\n\n/* TableCell — bg via --table-cell-bg → --dt-row-bg (theme-driven; transparent\n default). Painting on the cell gives a uniform row surface regardless of tr\n selected/hover state. */\n.cell {\n padding: var(--table-cell-padding, var(--spacing-3, 0.75rem));\n vertical-align: middle;\n font-size: var(--table-cell-font-size, inherit);\n color: var(--table-cell-color, var(--text-primary, #111827));\n background-color: var(--table-cell-bg, var(--dt-row-bg, transparent));\n border-top: var(--table-cell-border-top, var(--hairline-width, 1px) solid var(--hairline, transparent));\n}\n\n/* TableCaption */\n.caption {\n margin-top: var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--table-caption-color, var(--text-secondary, #6b7280));\n}\n"
|
|
1181
1207
|
}
|
|
1182
1208
|
]
|
|
1183
1209
|
},
|
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
},
|
|
@@ -2188,7 +2239,7 @@
|
|
|
2188
2239
|
{
|
|
2189
2240
|
"path": "components/ui/editable-block/editable-block.module.css",
|
|
2190
2241
|
"type": "registry:ui",
|
|
2191
|
-
"content": "/* EditableBlock\n *\n * Canvas brand block — a card tile with an uppercase label header (+ done\n * check), a value body, a hover-revealed edit icon, and an editing state\n * (focus ring, inline input + save button, AI action slot).\n *\n * Visual source: docs/design/brand-workbench/journey (HTML) L274-285\n * (prototype selectors: block, bk, bv, ed, editrow, aibtn)\n *\n * All values flow through CSS custom properties. No hard-coded colors or\n * magic numbers — every value traces to a design token or is documented.\n */\n\n/* ─── Root tile ─────────────────────────────────────────────────────────── */\n\n.root {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, rgba(0,0,0,0.06));\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-3_5, 0.875rem); /* prototype: 14px */\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n min-height: 6rem; /* prototype: 96px */\n position: relative;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n transition:\n
|
|
2242
|
+
"content": "/* EditableBlock\n *\n * Canvas brand block — a card tile with an uppercase label header (+ done\n * check), a value body, a hover-revealed edit icon, and an editing state\n * (focus ring, inline input + save button, AI action slot).\n *\n * Visual source: docs/design/brand-workbench/journey (HTML) L274-285\n * (prototype selectors: block, bk, bv, ed, editrow, aibtn)\n *\n * All values flow through CSS custom properties. No hard-coded colors or\n * magic numbers — every value traces to a design token or is documented.\n */\n\n/* ─── Root tile ─────────────────────────────────────────────────────────── */\n\n.root {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, rgba(0,0,0,0.06));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-lg, 0.75rem);\n padding: var(--spacing-3_5, 0.875rem); /* prototype: 14px */\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n min-height: 6rem; /* prototype: 96px */\n position: relative;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n transition:\n outline-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/* ─── State: default (hover) ────────────────────────────────────────────── */\n\n.stateDefault:hover {\n outline-color: var(--border-default, #e5e7eb);\n}\n\n/* Reveal edit icon on hover — use CSS opacity so resting state is still\n in the DOM (avoids SSR/hydration race on mount). */\n.stateDefault:hover .editIcon {\n opacity: var(--opacity-80, 0.8);\n}\n\n/* ─── State: done ───────────────────────────────────────────────────────── */\n\n.stateDone:hover {\n outline-color: var(--border-default, #e5e7eb);\n}\n\n.stateDone:hover .editIcon {\n opacity: var(--opacity-80, 0.8);\n}\n\n/* ─── State: editing ────────────────────────────────────────────────────── */\n\n.stateEditing {\n /* Focus-ring shadow — exception per token-rules Rule 2 (focus ring variant) */\n outline-color: var(--primary, #111827);\n box-shadow: 0 0 0 var(--spacing-1, 0.25rem) var(--interactive-primary-soft, color-mix(in srgb, var(--interactive-primary-bg, #111827) 12%, transparent));\n cursor: default;\n}\n\n.stateEditing .label {\n color: var(--primary, #111827);\n}\n\n/* ─── Header row ────────────────────────────────────────────────────────── */\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 7px ≈ 1.5 × 4px */\n margin-bottom: var(--spacing-2, 0.5rem); /* prototype: 8px */\n}\n\n/* ─── Label ─────────────────────────────────────────────────────────────── */\n\n.label {\n font-size: var(--font-size-2xs, 0.65625rem); /* prototype: 10.5px */\n font-weight: 800;\n letter-spacing: 0.07em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* ─── Done check icon ───────────────────────────────────────────────────── */\n\n.doneCheck {\n display: inline-flex;\n align-items: center;\n color: var(--success, #22c55e);\n}\n\n.doneCheck > svg {\n width: var(--spacing-3, 0.75rem); /* prototype: 12px */\n height: var(--spacing-3, 0.75rem);\n stroke-width: 3;\n}\n\n/* ─── Value body ────────────────────────────────────────────────────────── */\n\n.value {\n font-size: var(--font-size-sm, 0.84375rem); /* prototype: 13.5px ≈ sm */\n color: var(--text-primary, #111827);\n line-height: var(--line-height-snug, 1.45);\n flex: 1;\n}\n\n/* ─── Hover-revealed edit icon ──────────────────────────────────────────── */\n\n.editIcon {\n position: absolute;\n top: var(--spacing-2_5, 0.6875rem); /* prototype: 11px */\n right: var(--spacing-2_5, 0.6875rem);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-tertiary, #6b7280);\n /* Resting state opacity=0 via CSS (visible on hover via .stateDefault:hover rule).\n Must remain in DOM at rest for accessibility & SSR hydration safety. */\n opacity: 0;\n background: none;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.editIcon:focus-visible {\n opacity: var(--opacity-80, 0.8);\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.editIcon > svg {\n width: var(--spacing-3_5, 0.875rem); /* prototype: 14px */\n height: var(--spacing-3_5, 0.875rem);\n}\n\n/* ─── Edit row (input + save button) ────────────────────────────────────── */\n\n.editRow {\n display: flex;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 7px */\n margin-top: var(--spacing-2_5, 0.625rem); /* prototype: 10px */\n}\n\n/* Inline edit input */\n.editInput {\n flex: 1;\n height: var(--spacing-8, 2rem); /* prototype: 32px */\n border-radius: var(--radius-md, 0.5rem);\n background: var(--surface-subtle, #f9fafb);\n border: var(--stroke-width-thin, 1px) solid\n color-mix(in srgb, var(--primary, #111827) 30%, transparent);\n font-size: var(--font-size-sm, 0.84375rem); /* prototype: 13px */\n font-weight: 600;\n color: var(--text-primary, #111827);\n padding: 0 var(--spacing-2_5, 0.625rem); /* prototype: 0 10px */\n outline: none;\n}\n\n.editInput:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Save button (checkmark) */\n.saveButton {\n width: var(--spacing-8, 2rem); /* prototype: 32px */\n height: var(--spacing-8, 2rem);\n border-radius: var(--radius-md, 0.5rem);\n background: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n display: grid;\n place-items: center;\n flex: none;\n border: none;\n cursor: pointer;\n transition:\n background-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.saveButton:hover {\n background: color-mix(in srgb, var(--primary, #111827) 88%, white);\n}\n\n.saveButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.saveButton > svg {\n width: var(--spacing-3_5, 0.9375rem); /* prototype: 15px */\n height: var(--spacing-3_5, 0.9375rem);\n stroke-width: 2.4;\n}\n\n/* ─── AI action button ───────────────────────────────────────────────────── */\n\n.aiAction {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1_5, 0.375rem); /* prototype: 6px */\n margin-top: var(--spacing-2, 0.5rem); /* prototype: 9px ≈ spacing-2 */\n font-size: var(--font-size-xs, 0.71875rem); /* prototype: 11.5px */\n font-weight: 700;\n color: var(--primary, #111827);\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.aiAction:hover {\n opacity: var(--opacity-80, 0.8);\n}\n\n.aiAction:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--primary, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.aiAction > svg {\n width: var(--spacing-3, 0.8125rem); /* prototype: 13px */\n height: var(--spacing-3, 0.8125rem);\n}\n\n/* ─── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .root,\n .label,\n .editIcon,\n .saveButton,\n .aiAction {\n transition: none;\n }\n}\n"
|
|
2192
2243
|
}
|
|
2193
2244
|
]
|
|
2194
2245
|
},
|
|
@@ -2363,7 +2414,7 @@
|
|
|
2363
2414
|
{
|
|
2364
2415
|
"path": "components/ui/specimen-card/specimen-card.module.css",
|
|
2365
2416
|
"type": "registry:ui",
|
|
2366
|
-
"content": "/* Specimen Card\n *\n * A labeled frame that pairs a context label + \"feel\" descriptor with\n * arbitrary live component children — proving how a brand voice/tone\n * renders on real components.\n *\n * Two uses:\n * (a) Tone-by-context grid — card frame with uppercase context token +\n * italic feel descriptor above the live specimen.\n * (b) Speaking block — same frame with multiple specimens stacked and\n * an optional voice-key attribution footer.\n *\n * All values flow through CSS custom properties so themes can tune\n * without forking the component.\n */\n\n.card {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-3, 0.75rem);\n box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Label row — context token + feel descriptor */\n.labelRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Context label — uppercase small caps, secondary color */\n.contextLabel {\n font-size: 11px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-secondary, #6b7280);\n line-height: 1;\n}\n\n/* Feel descriptor — italic, tertiary, pushed to the end */\n.feelText {\n margin-left: auto;\n font-size: 11px;\n color: var(--text-tertiary, #9ca3af);\n font-style: italic;\n line-height: 1;\n}\n\n/* Body — vertical stack of live specimen children */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Footer — hairline-separated voice key attribution */\n.footer {\n border-top: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n margin-top: var(--spacing-1, 0.25rem);\n padding-top: var(--spacing-2, 0.5rem);\n font-size: 10.5px;\n color: var(--text-tertiary, #9ca3af);\n display: flex;\n align-items: center;\n gap: var(--spacing-1-5, 0.375rem);\n line-height: 1.4;\n}\n"
|
|
2417
|
+
"content": "/* Specimen Card\n *\n * A labeled frame that pairs a context label + \"feel\" descriptor with\n * arbitrary live component children — proving how a brand voice/tone\n * renders on real components.\n *\n * Two uses:\n * (a) Tone-by-context grid — card frame with uppercase context token +\n * italic feel descriptor above the live specimen.\n * (b) Speaking block — same frame with multiple specimens stacked and\n * an optional voice-key attribution footer.\n *\n * All values flow through CSS custom properties so themes can tune\n * without forking the component.\n */\n\n.card {\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-3, 0.75rem);\n box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Label row — context token + feel descriptor */\n.labelRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Context label — uppercase small caps, secondary color */\n.contextLabel {\n font-size: 11px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-secondary, #6b7280);\n line-height: 1;\n}\n\n/* Feel descriptor — italic, tertiary, pushed to the end */\n.feelText {\n margin-left: auto;\n font-size: 11px;\n color: var(--text-tertiary, #9ca3af);\n font-style: italic;\n line-height: 1;\n}\n\n/* Body — vertical stack of live specimen children */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Footer — hairline-separated voice key attribution */\n.footer {\n border-top: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, #e5e7eb), var(--hairline, #e5e7eb)) 1 / var(--hairline-width, var(--stroke-width-thin, 1px)) 0 0 0;\n margin-top: var(--spacing-1, 0.25rem);\n padding-top: var(--spacing-2, 0.5rem);\n font-size: 10.5px;\n color: var(--text-tertiary, #9ca3af);\n display: flex;\n align-items: center;\n gap: var(--spacing-1-5, 0.375rem);\n line-height: 1.4;\n}\n"
|
|
2367
2418
|
}
|
|
2368
2419
|
]
|
|
2369
2420
|
},
|
|
@@ -2537,7 +2588,7 @@
|
|
|
2537
2588
|
{
|
|
2538
2589
|
"path": "components/ui/bulk-action-bar/bulk-action-bar.module.css",
|
|
2539
2590
|
"type": "registry:ui",
|
|
2540
|
-
"content": "/* Bulk Action Bar: appears when rows are selected. Sticky by default. */\n.base {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n background-color: var(--surface-card, #ffffff);\n border-top: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n border-radius: var(--radius-lg, 0.75rem);\n}\n\n/* Sticky variant — pinned to the viewport bottom. */\n.sticky {\n position: fixed;\n bottom: var(--spacing-4, 1rem);\n left: var(--spacing-4, 1rem);\n right: var(--spacing-4, 1rem);\n /*\n * Toolbar layer. No --z-index-toolbar token exists yet in\n * @loworbitstudio/visor-core — promote to a token once the z-index scale\n * is formalized.\n */\n z-index: 50;\n animation: bulk-action-bar-enter var(--motion-duration-moderate, 300ms)\n var(--motion-easing-standard, ease-out);\n}\n\n/* Inline variant — flows in the document, no fixed positioning. */\n.inline {\n position: relative;\n width: 100%;\n}\n\n/* Flat/embedded variant — no shadow, no radius, border-top hairline only.\n Use inside table cards or panels where the floating card look is wrong. */\n.flat {\n box-shadow: none;\n border-radius: 0;\n}\n\n/* Editorial density — the blessed admin strip: the inline bar sheds the\n floating-card chrome entirely (flat, card-tier fill, hairline below, dense\n padding), the count reads 13px tertiary, and actions are pushed to the right\n edge and read muted. Matches the blessed admin-ui reference builds. */\n:global([data-density=\"editorial\"]) .inline {\n background-color: var(--surface-card, #ffffff);\n box-shadow: none;\n border-radius: 0;\n border-top: none;\n border-bottom: 1px solid var(--hairline, var(--border-default, #e5e7eb));\n padding: var(--spacing-2, 0.5rem) var(--spacing-5, 1.25rem);\n}\n\n:global([data-density=\"editorial\"]) .count {\n font-size: 0.8125rem;\n color: var(--text-tertiary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .actions {\n justify-content: flex-end;\n}\n\n/* Action buttons read muted under editorial; a destructive action keeps its\n own inline color (style attribute wins over this). */\n:global([data-density=\"editorial\"]) .actions button {\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Selection count label */\n.count {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n white-space: nowrap;\n flex: 0 0 auto;\n}\n\n/* Actions cluster — horizontal, wraps on narrow rows */\n.actions {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-2, 0.5rem);\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Dismiss button — sits flush on the right */\n.dismiss {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.dismiss svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* Entrance animation: slide up + fade in. */\n@keyframes bulk-action-bar-enter {\n from {\n opacity: 0;\n transform: translateY(var(--spacing-4, 1rem));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Respect prefers-reduced-motion — skip the slide, keep it mounted. */\n@media (prefers-reduced-motion: reduce) {\n .sticky {\n animation: none;\n }\n}\n"
|
|
2591
|
+
"content": "/* Bulk Action Bar: appears when rows are selected. Sticky by default. */\n.base {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n background-color: var(--surface-card, #ffffff);\n border-top: 1px solid var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));\n border-radius: var(--radius-lg, 0.75rem);\n}\n\n/* Sticky variant — pinned to the viewport bottom. */\n.sticky {\n position: fixed;\n bottom: var(--spacing-4, 1rem);\n left: var(--spacing-4, 1rem);\n right: var(--spacing-4, 1rem);\n /*\n * Toolbar layer. No --z-index-toolbar token exists yet in\n * @loworbitstudio/visor-core — promote to a token once the z-index scale\n * is formalized.\n */\n z-index: 50;\n animation: bulk-action-bar-enter var(--motion-duration-moderate, 300ms)\n var(--motion-easing-standard, ease-out);\n}\n\n/* Inline variant — flows in the document, no fixed positioning. */\n.inline {\n position: relative;\n width: 100%;\n}\n\n/* Flat/embedded variant — no shadow, no radius, border-top hairline only.\n Use inside table cards or panels where the floating card look is wrong. */\n.flat {\n box-shadow: none;\n border-radius: 0;\n}\n\n/* Editorial density — the blessed admin strip: the inline bar sheds the\n floating-card chrome entirely (flat, card-tier fill, hairline below, dense\n padding), the count reads 13px tertiary, and actions are pushed to the right\n edge and read muted. Matches the blessed admin-ui reference builds. */\n:global([data-density=\"editorial\"]) .inline {\n background-color: var(--surface-card, #ffffff);\n box-shadow: none;\n border-radius: 0;\n border-top: none;\n border-bottom: 1px solid transparent;\n border-image: linear-gradient(var(--hairline, var(--border-default, #e5e7eb)), var(--hairline, var(--border-default, #e5e7eb))) 1 / 0 0 var(--hairline-width, 1px) 0;\n padding: var(--spacing-2, 0.5rem) var(--spacing-5, 1.25rem);\n}\n\n:global([data-density=\"editorial\"]) .count {\n font-size: 0.8125rem;\n color: var(--text-tertiary, #6b7280);\n}\n\n:global([data-density=\"editorial\"]) .actions {\n justify-content: flex-end;\n}\n\n/* Action buttons read muted under editorial; a destructive action keeps its\n own inline color (style attribute wins over this). */\n:global([data-density=\"editorial\"]) .actions button {\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Selection count label */\n.count {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n white-space: nowrap;\n flex: 0 0 auto;\n}\n\n/* Actions cluster — horizontal, wraps on narrow rows */\n.actions {\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-2, 0.5rem);\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Dismiss button — sits flush on the right */\n.dismiss {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.dismiss svg {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n}\n\n/* Entrance animation: slide up + fade in. */\n@keyframes bulk-action-bar-enter {\n from {\n opacity: 0;\n transform: translateY(var(--spacing-4, 1rem));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Respect prefers-reduced-motion — skip the slide, keep it mounted. */\n@media (prefers-reduced-motion: reduce) {\n .sticky {\n animation: none;\n }\n}\n"
|
|
2541
2592
|
}
|
|
2542
2593
|
]
|
|
2543
2594
|
},
|
|
@@ -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
|
|
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
|
},
|
|
@@ -2876,7 +2927,7 @@
|
|
|
2876
2927
|
{
|
|
2877
2928
|
"path": "components/ui/section-nav/section-nav.module.css",
|
|
2878
2929
|
"type": "registry:ui",
|
|
2879
|
-
"content": "/* SectionNav — link/anchor-based section sub-navigation.\n *\n * A horizontal strip of links: leading Phosphor icon + label + optional\n * trailing count pill. The active item gets text-primary, a static 2px\n * primary underline, and a primary-tinted count pill.\n *\n * Distinct from Tabs: items navigate via href (next/link asChild), there are\n * no content panels, and the underline is static (not an animated indicator).\n *\n * Theme-agnostic: all values reference CSS custom property tokens.\n */\n\n/* Root — the nav strip with a hairline baseline the active underline sits on. */\n.root {\n display: flex;\n align-items: stretch;\n gap: var(--spacing-2, 0.5rem);\n border-bottom: var(--stroke-width-thin, 1px) solid
|
|
2930
|
+
"content": "/* SectionNav — link/anchor-based section sub-navigation.\n *\n * A horizontal strip of links: leading Phosphor icon + label + optional\n * trailing count pill. The active item gets text-primary, a static 2px\n * primary underline, and a primary-tinted count pill.\n *\n * Distinct from Tabs: items navigate via href (next/link asChild), there are\n * no content panels, and the underline is static (not an animated indicator).\n *\n * Theme-agnostic: all values reference CSS custom property tokens.\n */\n\n/* Root — the nav strip with a hairline baseline the active underline sits on. */\n.root {\n display: flex;\n align-items: stretch;\n gap: var(--spacing-2, 0.5rem);\n border-bottom: 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 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n /* No overflow-x: a scroll container would clip the items' -1px overlap of\n this hairline, shaving the bottom pixel off the active underline. */\n}\n\n/* Item — a single link: icon + label + optional count pill. */\n.item {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n white-space: nowrap;\n color: var(--text-tertiary, #9ca3af);\n text-decoration: none;\n cursor: pointer;\n /* Reserve the 2px underline so resting/active items don't shift vertically. */\n border-bottom: 2px solid transparent;\n margin-bottom: calc(-1 * var(--stroke-width-thin, 1px));\n background: transparent;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.item:hover {\n color: var(--text-primary, #111827);\n}\n\n.item:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Active — text-primary + 2px primary underline. */\n.itemActive {\n color: var(--text-primary, #111827);\n border-bottom-color: var(--primary, #2563eb);\n}\n\n/* Leading icon — tertiary at rest, recolors with the active item. */\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.itemActive .icon {\n color: var(--text-primary, #111827);\n}\n\n/* Label text. */\n.label {\n min-width: 0;\n}\n\n/* Trailing 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: var(--stroke-width-thin, 1px) calc(var(--spacing-1, 0.25rem) * 1.5);\n font-size: 11px;\n font-weight: var(--font-weight-medium, 500);\n font-variant-numeric: tabular-nums;\n line-height: normal;\n}\n\n/* Neutral (resting) count pill — subtle surface, tertiary text. */\n.countNeutral {\n background-color: var(--surface-subtle, #f3f4f6);\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Primary-tinted count pill — active item. */\n.countActive {\n background-color: color-mix(in srgb, var(--primary, #2563eb) 22%, transparent);\n color: var(--primary, #2563eb);\n}\n"
|
|
2880
2931
|
}
|
|
2881
2932
|
]
|
|
2882
2933
|
},
|
|
@@ -2901,7 +2952,7 @@
|
|
|
2901
2952
|
{
|
|
2902
2953
|
"path": "components/ui/doc-nav/doc-nav.module.css",
|
|
2903
2954
|
"type": "registry:ui",
|
|
2904
|
-
"content": "/* DocNav — manifest-driven, grouped/collapsible doc navigation.\n *\n * Renders peer, collapsible groups from a manifest slice: a pinned Shared set,\n * accordion product groups (one open at a time), and an Appendix bucket for\n * ad-hoc docs. The group row wraps (flex-wrap) and is NEVER an overflow-x\n * scroll strip — the run-off-the-edge defect this component exists to kill.\n *\n * Theme-agnostic: every value references a CSS custom property token.\n */\n\n/* Root — the wrapping row of peer groups. */\n.root {\n /* Accent-pop indirection (B2): the active pill / caret / hub / pinned frame\n * all route through this single token so a consumer (DocFrame) can retint the\n * whole nav in one place. Defaults to the theme's vivid `--accent` — not\n * `--primary`, which reads dull on muted-primary themes such as Strata. */\n --doc-nav-accent: var(--accent, #2563eb);\n /* Group cluster radius — a controlled, fixed radius the doc-nav owns, NOT the\n * theme `--radius-*` scale, which decorative themes inflate far past what a\n * dense nav cluster wants (for example Strata inflates `--radius-xl` to 32px).\n * The approved design pins ~12px per theme for exactly this reason; any single\n * theme radius token renders ~6px on some themes and ~32px on others.\n * Overridable by a consumer/theme via this variable. */\n --doc-nav-group-radius: 0.75rem;\n /* Resting pill fill — a recessed well, darker than the group's `--surface-card`\n * in BOTH modes. The surface ramp inverts light↔dark (page is darkest in dark\n * but lightest in light), so a single surface token can't stay \"below\" the card;\n * mixing the card toward neutral-950 darkens it directionally in either mode.\n * Overridable by a consumer/theme. */\n --doc-nav-pill-bg: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 20%\n );\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n /* Deliberately no overflow-x: groups wrap down, never scroll sideways. */\n}\n\n/* Group — a bordered, surface-backed cluster: header + pill row. */\n.group {\n display: inline-flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--doc-nav-group-radius, var(--radius-md, 0.375rem));\n background: var(--surface-card, #ffffff);\n}\n\n/* Pinned — the Shared group stays open; a subtle frame keyed to the group hue.\n * Routed through overridable tokens (P5) so borderless themes (Animal / ENTR)\n * can null the frame — set `--doc-nav-pin-border: transparent` and\n * `--doc-nav-pin-bg: var(--surface-card)`. Default derives from the group's\n * shared hue, not `--primary` (which read as an unwanted border on those\n * borderless themes). */\n.groupPinned {\n border-color: var(\n --doc-nav-pin-border,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 32%, transparent)\n );\n background: var(\n --doc-nav-pin-bg,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 6%, var(--surface-card, #ffffff))\n );\n}\n\n/* Collapsed — a group reduces to a single pill-shaped chip. */\n.groupCollapsed {\n padding: 0;\n border-color: transparent;\n background: transparent;\n}\n\n/* Header — the group label row. As a chip when collapsed, plain when open. */\n.head {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n /* Collapse the label's line box to the glyph height so the all-caps text\n * optically centers against the dot/caret (matching `.pill`). Without this the\n * caps ride high — the empty descender space pushes the line-box center below\n * the visual center of the caps. */\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n\n/* Button variant — collapsible group heads are clickable. */\n.headButton {\n cursor: pointer;\n border: 0;\n background: transparent;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.headButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.headButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Collapsed group head — reads as a resting chip. */\n.groupCollapsed .head {\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n /* A collapsed chip stands alone in the nav row, so it carries more vertical\n * padding than the tight open-group label (the design gives it ~7px 12px). */\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n /* The chip reads as the group's card surface (matching the design's chip-bg),\n * not a lighter subtle surface — `--surface-subtle` resolves as a mid-gray in\n * dark themes. */\n background: var(--surface-card, #111827);\n}\n\n/* Caret — accent-popped fill glyph (P6). */\n.caret {\n flex-shrink: 0;\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Scope dot — a per-group grouping cue, color-coded with a soft glow (P1).\n * `color` carries the group hue so `currentColor` drives both the fill and the\n * glow; a consumer overrides the whole group via `--doc-nav-group-accent`.\n * Default (product groups) is the vivid accent; pinned/appendix retint below. */\n.scopeDot {\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-group-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Shared (pinned) group reads in the info hue. */\n.groupPinned .scopeDot {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n/* Appendix reads muted and un-glowed — an ad-hoc bucket, not a product. */\n.group[data-role=\"appendix\"] .scopeDot {\n color: var(--doc-nav-group-accent, var(--text-tertiary, #9ca3af));\n box-shadow: none;\n}\n\n/* Group label text. */\n.groupLabel {\n min-width: 0;\n}\n\n/* Trailing count badge — a hairline mono pill on a collapsed group (N2). */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: var(--spacing-4, 1rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) * 1.5);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n /* ~9px — a quarter down from the xs step, matching the design's tiny badge. */\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.75);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Pill row — wraps within the group. */\n.pills {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Pill — a single doc link. Mono · UPPERCASE · letter-spaced (B1). */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n background: var(--doc-nav-pill-bg, var(--surface-card, #111827));\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n line-height: 1;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n text-decoration: none;\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pill:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.pill:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Shared-group pills carry the group's tint at rest (P4) — hover/active win. */\n.groupPinned .pill:not(.pillActive) {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n.groupPinned .pill:not(.pillActive):hover {\n color: var(--text-primary, #111827);\n}\n\n/* Active — the current doc, resolved from currentPath. Contrast-safe (VI-612):\n * the label is `--text-primary` (always legible), NOT raw `--doc-nav-accent`,\n * which is unreadable when a theme's accent is white/near-white (Blacklight, say)\n * on a light card. The active pill reads as active via a distinct opaque surface\n * plus an accent ring blended with a hairline so it never fully vanishes. */\n.pillActive {\n color: var(--text-primary, #111827);\n border-color: color-mix(\n in srgb,\n var(--doc-nav-accent, var(--accent, #2563eb)) 55%,\n var(--hairline, var(--border-default, #e5e7eb))\n );\n background: var(\n --surface-selected,\n color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--doc-nav-accent, var(--accent, #2563eb)) 14%\n )\n );\n}\n\n/* Hub — the group's overview entry gets a leading accent dot with a glow (N1).\n * `color` carries the accent so `currentColor` drives fill + glow together. */\n.pillHub::before {\n content: \"\";\n width: calc(var(--spacing-1, 0.25rem) * 1.25);\n height: calc(var(--spacing-1, 0.25rem) * 1.25);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Leading order number — mono, a step under the pill (B1). */\n.pillNum {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.8);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.pillActive .pillNum {\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Label text. */\n.pillLabel {\n min-width: 0;\n}\n\n/* External-link badge. */\n.pillExternal {\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
|
|
2955
|
+
"content": "/* DocNav — manifest-driven, grouped/collapsible doc navigation.\n *\n * Renders peer, collapsible groups from a manifest slice: a pinned Shared set,\n * accordion product groups (one open at a time), and an Appendix bucket for\n * ad-hoc docs. The group row wraps (flex-wrap) and is NEVER an overflow-x\n * scroll strip — the run-off-the-edge defect this component exists to kill.\n *\n * Theme-agnostic: every value references a CSS custom property token.\n */\n\n/* Root — the wrapping row of peer groups. */\n.root {\n /* Accent-pop indirection (B2): the active pill / caret / hub / pinned frame\n * all route through this single token so a consumer (DocFrame) can retint the\n * whole nav in one place. Defaults to the theme's vivid `--accent` — not\n * `--primary`, which reads dull on muted-primary themes such as Strata. */\n --doc-nav-accent: var(--accent, #2563eb);\n /* Group cluster radius — a controlled, fixed radius the doc-nav owns, NOT the\n * theme `--radius-*` scale, which decorative themes inflate far past what a\n * dense nav cluster wants (for example Strata inflates `--radius-xl` to 32px).\n * The approved design pins ~12px per theme for exactly this reason; any single\n * theme radius token renders ~6px on some themes and ~32px on others.\n * Overridable by a consumer/theme via this variable. */\n --doc-nav-group-radius: 0.75rem;\n /* Resting pill fill — a recessed well, darker than the group's `--surface-card`\n * in BOTH modes. The surface ramp inverts light↔dark (page is darkest in dark\n * but lightest in light), so a single surface token can't stay \"below\" the card;\n * mixing the card toward neutral-950 darkens it directionally in either mode.\n * Overridable by a consumer/theme. */\n --doc-nav-pill-bg: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 20%\n );\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n /* Deliberately no overflow-x: groups wrap down, never scroll sideways. */\n}\n\n/* Group — a bordered, surface-backed cluster: header + pill row. */\n.group {\n display: inline-flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--doc-nav-group-radius, var(--radius-md, 0.375rem));\n background: var(--surface-card, #ffffff);\n}\n\n/* Pinned — the Shared group stays open; a subtle frame keyed to the group hue.\n * Routed through overridable tokens (P5) so borderless themes (Animal / ENTR)\n * can null the frame — set `--doc-nav-pin-border: transparent` and\n * `--doc-nav-pin-bg: var(--surface-card)`. Default derives from the group's\n * shared hue, not `--primary` (which read as an unwanted border on those\n * borderless themes). */\n.groupPinned {\n outline-color: var(\n --doc-nav-pin-border,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 32%, transparent)\n );\n background: var(\n --doc-nav-pin-bg,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 6%, var(--surface-card, #ffffff))\n );\n}\n\n/* Collapsed — a group reduces to a single pill-shaped chip. */\n.groupCollapsed {\n padding: 0;\n outline-color: transparent;\n background: transparent;\n}\n\n/* Header — the group label row. As a chip when collapsed, plain when open. */\n.head {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n /* Collapse the label's line box to the glyph height so the all-caps text\n * optically centers against the dot/caret (matching `.pill`). Without this the\n * caps ride high — the empty descender space pushes the line-box center below\n * the visual center of the caps. */\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n\n/* Button variant — collapsible group heads are clickable. */\n.headButton {\n cursor: pointer;\n border: 0;\n background: transparent;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.headButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.headButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Collapsed group head — reads as a resting chip. */\n.groupCollapsed .head {\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-full, 9999px);\n /* A collapsed chip stands alone in the nav row, so it carries more vertical\n * padding than the tight open-group label (the design gives it ~7px 12px). */\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n /* The chip reads as the group's card surface (matching the design's chip-bg),\n * not a lighter subtle surface — `--surface-subtle` resolves as a mid-gray in\n * dark themes. */\n background: var(--surface-card, #111827);\n}\n\n/* Caret — accent-popped fill glyph (P6). */\n.caret {\n flex-shrink: 0;\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Scope dot — a per-group grouping cue, color-coded with a soft glow (P1).\n * `color` carries the group hue so `currentColor` drives both the fill and the\n * glow; a consumer overrides the whole group via `--doc-nav-group-accent`.\n * Default (product groups) is the vivid accent; pinned/appendix retint below. */\n.scopeDot {\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-group-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Shared (pinned) group reads in the info hue. */\n.groupPinned .scopeDot {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n/* Appendix reads muted and un-glowed — an ad-hoc bucket, not a product. */\n.group[data-role=\"appendix\"] .scopeDot {\n color: var(--doc-nav-group-accent, var(--text-tertiary, #9ca3af));\n box-shadow: none;\n}\n\n/* Group label text. */\n.groupLabel {\n min-width: 0;\n}\n\n/* Trailing count badge — a hairline mono pill on a collapsed group (N2). */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: var(--spacing-4, 1rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) * 1.5);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n /* ~9px — a quarter down from the xs step, matching the design's tiny badge. */\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.75);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Pill row — wraps within the group. */\n.pills {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Pill — a single doc link. Mono · UPPERCASE · letter-spaced (B1). */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n background: var(--doc-nav-pill-bg, var(--surface-card, #111827));\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n line-height: 1;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n text-decoration: none;\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pill:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.pill:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Shared-group pills carry the group's tint at rest (P4) — hover/active win. */\n.groupPinned .pill:not(.pillActive) {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n.groupPinned .pill:not(.pillActive):hover {\n color: var(--text-primary, #111827);\n}\n\n/* Active — the current doc, resolved from currentPath. Contrast-safe (VI-612):\n * the label is `--text-primary` (always legible), NOT raw `--doc-nav-accent`,\n * which is unreadable when a theme's accent is white/near-white (Blacklight, say)\n * on a light card. The active pill reads as active via a distinct opaque surface\n * plus an accent ring blended with a hairline so it never fully vanishes. */\n.pillActive {\n color: var(--text-primary, #111827);\n border-color: color-mix(\n in srgb,\n var(--doc-nav-accent, var(--accent, #2563eb)) 55%,\n var(--hairline, var(--border-default, #e5e7eb))\n );\n background: var(\n --surface-selected,\n color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--doc-nav-accent, var(--accent, #2563eb)) 14%\n )\n );\n}\n\n/* Hub — the group's overview entry gets a leading accent dot with a glow (N1).\n * `color` carries the accent so `currentColor` drives fill + glow together. */\n.pillHub::before {\n content: \"\";\n width: calc(var(--spacing-1, 0.25rem) * 1.25);\n height: calc(var(--spacing-1, 0.25rem) * 1.25);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Leading order number — mono, a step under the pill (B1). */\n.pillNum {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.8);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.pillActive .pillNum {\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Label text. */\n.pillLabel {\n min-width: 0;\n}\n\n/* External-link badge. */\n.pillExternal {\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
|
|
2905
2956
|
}
|
|
2906
2957
|
]
|
|
2907
2958
|
},
|
|
@@ -2927,7 +2978,7 @@
|
|
|
2927
2978
|
{
|
|
2928
2979
|
"path": "components/ui/doc-frame/doc-frame.module.css",
|
|
2929
2980
|
"type": "registry:ui",
|
|
2930
|
-
"content": "/* DocFrame — the themed doc-page shell that wraps DocNav.\n *\n * A sticky header (a flexible brand/logo slot + an OVERVIEW home pill + a\n * right-aligned meta slot, then the DocNav slot) above a content wrapper (the\n * doc as children). The header sticks to the page scroll so the brand + nav\n * never leave view. Theme-agnostic: every value references a CSS custom property\n * token, so the shell adopts the active project theme.\n *\n * No ancestor sets overflow: hidden/auto — that would create a scroll container\n * and break the viewport-anchored sticky header.\n */\n\n/* Shell root — a centered, max-width column. */\n.frame {\n /* A tight, controlled glyph radius the frame owns — NOT the theme `--radius-*`\n * scale, which decorative themes inflate far past what a 24px brand chip wants\n * (Strata inflates `--radius-md` to 10px, `--radius-xl` to 32px). Overridable\n * by a consumer/theme via this variable. */\n --doc-frame-glyph-radius: 0.375rem;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n max-width: var(--doc-frame-max-width, 75rem);\n margin-inline: auto;\n background: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n/* Sticky chrome — the header row + nav row. The page surface (opaque, so\n * scrolled content never bleeds through) sits below the card-surfaced nav\n * groups + chips, which read lighter on top. Sticks to the nearest scroll\n * ancestor (the page/viewport). */\n.chrome {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n flex-direction: column;\n background: var(--surface-page, #ffffff);\n border-bottom: var(--stroke-width-thin, 1px) solid
|
|
2981
|
+
"content": "/* DocFrame — the themed doc-page shell that wraps DocNav.\n *\n * A sticky header (a flexible brand/logo slot + an OVERVIEW home pill + a\n * right-aligned meta slot, then the DocNav slot) above a content wrapper (the\n * doc as children). The header sticks to the page scroll so the brand + nav\n * never leave view. Theme-agnostic: every value references a CSS custom property\n * token, so the shell adopts the active project theme.\n *\n * No ancestor sets overflow: hidden/auto — that would create a scroll container\n * and break the viewport-anchored sticky header.\n */\n\n/* Shell root — a centered, max-width column. */\n.frame {\n /* A tight, controlled glyph radius the frame owns — NOT the theme `--radius-*`\n * scale, which decorative themes inflate far past what a 24px brand chip wants\n * (Strata inflates `--radius-md` to 10px, `--radius-xl` to 32px). Overridable\n * by a consumer/theme via this variable. */\n --doc-frame-glyph-radius: 0.375rem;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n max-width: var(--doc-frame-max-width, 75rem);\n margin-inline: auto;\n background: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n/* Sticky chrome — the header row + nav row. The page surface (opaque, so\n * scrolled content never bleeds through) sits below the card-surfaced nav\n * groups + chips, which read lighter on top. Sticks to the nearest scroll\n * ancestor (the page/viewport). */\n.chrome {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n flex-direction: column;\n background: var(--surface-page, #ffffff);\n border-bottom: 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 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n}\n\n/* Header row — brand/logo slot, then the OVERVIEW home pill, a flexible spacer,\n * and the right-aligned meta slot. A hairline rule separates it from the nav. */\n.headerRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n border-bottom: 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 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n}\n\n.brandSlot {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Nav row — holds the DocNav slot. */\n.navRow {\n display: flex;\n min-width: 0;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n}\n\n/* Content wrapper — the doc, as children. */\n.content {\n flex: 1 1 auto;\n padding: var(--spacing-6, 1.5rem) var(--spacing-5, 1.25rem)\n var(--spacing-8, 2rem);\n}\n\n/* ─── brand slot ─────────────────────────────────────────────────────────── */\n\n/* Wraps the theme-logo layer + the text wordmark. */\n.brand {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n\n/* Text wordmark — the resting/fallback brand (a leading glyph chip + the name).\n * Mode-adaptive via --text-primary. Set in the theme's display face so a text\n * wordmark carries the theme's brand character (say, Animal's condensed\n * \"Sequel 100 Black 95\" rather than the body \"Ratio\"); degrades to the heading\n * face, then the ambient sans, on themes that ship no --font-display — matching\n * the display-font convention in stat-card / page-header / section-intro. */\n.wordmark {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-family: var(--font-display, var(--font-family-heading, inherit));\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: -0.01em;\n line-height: 1.1;\n color: var(--text-primary, #111827);\n}\n\n/* Brand glyph — a single-letter mark in the theme's primary brand color, its\n * guaranteed-contrasting on-primary text. A controlled small radius (see\n * `--doc-frame-glyph-radius`), not the ballooning theme radius scale. */\n.glyph {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--doc-frame-glyph-radius, 0.375rem);\n background: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n}\n\n/* Theme-logo layer — the active theme's brand logo SVG (mode-aware via the\n * --brand-logo custom property). Hidden until the client effect confirms the\n * theme ships one AND it loads, so the resting state is always the visible\n * wordmark and a failed load never leaves an empty slot. */\n.themeLogo {\n display: none;\n height: var(--doc-frame-logo-height, var(--spacing-5, 1.25rem));\n /* An explicit min-width keeps a masked/tinted mark from collapsing to a\n * zero-width box on themes that ship no --brand-logo-aspect-ratio. */\n min-width: var(--spacing-8, 2rem);\n aspect-ratio: var(--brand-logo-aspect-ratio, 3 / 1);\n background-image: var(--brand-logo);\n background-repeat: no-repeat;\n background-position: left center;\n background-size: contain;\n}\n\n/* When the theme provides --brand-logo, show the image and drop the wordmark. */\n.brand[data-theme-logo] .themeLogo {\n display: inline-block;\n}\n\n.brand[data-theme-logo] .wordmark {\n display: none;\n}\n\n/* ─── header chrome — home pill + meta ───────────────────────────────────── */\n\n/* OVERVIEW / home pill — a bordered mono chip with a leading compass glyph. */\n.home {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n /* ~6px 12px — a compact chip that clears the mono caps. */\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-full, 9999px);\n background: var(--surface-card, #ffffff);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n /* Collapse the line box to the glyph height so the all-caps label optically\n * centers against the compass icon. */\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n text-decoration: none;\n white-space: nowrap;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n outline-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.home:hover {\n color: var(--text-primary, #111827);\n outline-color: var(--hairline-strong, var(--border-default, #d1d5db));\n}\n\n.home: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/* Compass glyph — a hair larger than the caps (the design's 13px vs 11px);\n * inherits the pill color via currentColor. */\n.homeIcon {\n flex-shrink: 0;\n width: var(--font-size-sm, 0.875rem);\n height: var(--font-size-sm, 0.875rem);\n}\n\n/* Meta slot — a right-aligned breadcrumb/status line, mono UPPERCASE, tertiary. */\n.meta {\n flex-shrink: 0;\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n white-space: nowrap;\n}\n\n/* ─── mobile ─────────────────────────────────────────────────────────────── */\n\n@media (max-width: 640px) {\n .headerRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .navRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .content {\n padding: var(--spacing-5, 1.25rem) var(--spacing-4, 1rem)\n var(--spacing-6, 1.5rem);\n }\n}\n"
|
|
2931
2982
|
}
|
|
2932
2983
|
]
|
|
2933
2984
|
},
|
|
@@ -2952,7 +3003,7 @@
|
|
|
2952
3003
|
{
|
|
2953
3004
|
"path": "components/ui/fidelity-mirror/fidelity-mirror.module.css",
|
|
2954
3005
|
"type": "registry:ui",
|
|
2955
|
-
"content": "/* FidelityMirror — the design-left / built-right comparison surface.\n *\n * Renders the pure DESIGN source (left) beside the BUILT render (right) in\n * recessed capture wells, with a verdict/diff indicator, numbered delta\n * callouts, a classed delta legend, and a drag-to-reveal overlay mode. Composes\n * inside DocFrame's content, and goes full-bleed on widescreen so each pane is\n * ~50vw (the compare is meant for fullscreen monitors).\n *\n * Theme-agnostic: every value references a Visor token. The card/cluster radius\n * and the recessed well fill are component-OWNED (fixed) tokens, NOT the theme\n * `--radius-*` scale or a bare `--surface-*` token — decorative themes inflate\n * `--radius-xl` to 32px and lift `--surface-subtle` ABOVE the card in dark mode,\n * which is precisely the drift this component exists to surface.\n */\n\n/* Root — the framed compare. */\n.root {\n /* Fixed cluster radius the component OWNS — NOT the theme `--radius-*` scale,\n * which decorative themes inflate far past what a dense compare cluster wants\n * (Space inflates `--radius-xl` to 32px). Overridable by a consumer/theme. */\n --fm-radius: 0.75rem;\n --fm-well-radius: 0.625rem;\n /* Widescreen gutter/padding — the outer left/right padding AND the center\n * gutter resolve to the SAME value, so each pane is ~50vw: (100vw - 3*pad)/2. */\n --fm-pad: clamp(1.25rem, 3.2vw, 3.5rem);\n /* Recessed well fill — darker than `--surface-card` in BOTH modes (the DocNav\n * pill-bg convention). The surface ramp inverts light<->dark, so a single\n * surface token can't stay \"below\" the card; mixing the card toward\n * neutral-950 darkens it directionally in either mode. Overridable. */\n --fm-well: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 22%\n );\n /* Source identity hues (kept across split + overlay): design = info, built =\n * accent. These carry SOURCE identity, not decoration — do not retint. */\n --fm-design-hue: var(--info, #0ea5e9);\n --fm-built-hue: var(--accent, #2563eb);\n\n display: flex;\n flex-direction: column;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-radius);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n overflow: hidden;\n font-family: var(--font-sans, inherit);\n}\n\n/* Widescreen full-bleed — escape DocFrame's centered column and span the\n * viewport. Header + legend align to the panes' outer edge via `--fm-pad`; the\n * panes grid uses `gap: var(--fm-pad)` so each column resolves to ~50vw. */\n.bleed {\n width: 100vw;\n margin-left: calc(50% - 50vw);\n border-radius: 0;\n border-inline: 0;\n}\n.bleed .bar {\n padding-inline: var(--fm-pad);\n}\n.bleed .panes {\n padding-inline: var(--fm-pad);\n gap: var(--fm-pad);\n}\n.bleed .legend {\n padding-inline: var(--fm-pad);\n}\n.bleed .overlay {\n padding-inline: var(--fm-pad);\n}\n\n/* ── Header bar ──────────────────────────────────────────────────────────── */\n.bar {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-bottom: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n}\n.titleWrap {\n display: inline-flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n min-width: 0;\n}\n.title {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.02em;\n line-height: 1;\n color: var(--text-primary, #111827);\n}\n.subtitle {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Spacer — pushes the status + toggle to the right edge. */\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Platform chip — mono caps pill, hairline, card bg. */\n.chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-full, 9999px);\n background: var(--surface-card, #ffffff);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n}\n.chipDot {\n width: 0.375rem;\n height: 0.375rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--success, #22c55e);\n box-shadow: 0 0 0.375rem color-mix(in srgb, var(--success, #22c55e) 70%, transparent);\n}\n.chipIcon {\n width: 0.75rem;\n height: 0.75rem;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Status pill — the diff indicator, tinted by verdict. */\n.status {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.08em;\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.status[data-verdict=\"match\"] {\n color: var(--success, #22c55e);\n border-color: color-mix(in srgb, var(--success, #22c55e) 40%, transparent);\n background: color-mix(in srgb, var(--success, #22c55e) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"drift\"] {\n color: var(--warning, #f59e0b);\n border-color: color-mix(in srgb, var(--warning, #f59e0b) 42%, transparent);\n background: color-mix(in srgb, var(--warning, #f59e0b) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"fail\"] {\n color: var(--destructive, #ef4444);\n border-color: color-mix(in srgb, var(--destructive, #ef4444) 42%, transparent);\n background: color-mix(in srgb, var(--destructive, #ef4444) 12%, var(--surface-card, #ffffff));\n}\n.statusGlyph {\n width: 0.625rem;\n height: 0.625rem;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.4375rem currentColor;\n}\n.score {\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n opacity: var(--opacity-80, 0.85);\n}\n\n/* Segmented compare-mode toggle — split / overlay. */\n.seg {\n display: inline-flex;\n gap: calc(var(--spacing-1, 0.25rem) * 0.5);\n padding: calc(var(--spacing-1, 0.25rem) * 0.75);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-full, 9999px);\n background: var(--fm-well);\n}\n.segBtn {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) calc(var(--spacing-1, 0.25rem) * 2.5);\n border: 0;\n border-radius: var(--radius-full, 9999px);\n background: transparent;\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n.segIcon {\n width: 0.8125rem;\n height: 0.8125rem;\n}\n.segBtn[aria-pressed=\"true\"] {\n color: var(--text-primary, #111827);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n}\n.segBtn: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/* ── Panes: side-by-side ─────────────────────────────────────────────────── */\n.panes {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-4, 1rem);\n align-items: start;\n}\n.pane {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n.paneLabel {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n.scopeDot {\n width: 0.5rem;\n height: 0.5rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.5rem color-mix(in srgb, currentColor 60%, transparent);\n}\n.pane[data-side=\"design\"] .scopeDot {\n color: var(--fm-design-hue);\n}\n.pane[data-side=\"built\"] .scopeDot {\n color: var(--fm-built-hue);\n}\n.paneMeta {\n margin-left: auto;\n color: var(--text-tertiary, #9ca3af);\n letter-spacing: 0.1em;\n font-weight: var(--font-weight-medium, 500);\n}\n\n/* Capture well — the recessed frame that holds a captured surface. */\n.well {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n padding: var(--spacing-3, 0.75rem);\n overflow: hidden;\n}\n.pane[data-side=\"built\"] .well {\n box-shadow: inset 0 0 0 var(--stroke-width-thin, 1px)\n color-mix(in srgb, var(--fm-built-hue) 22%, transparent);\n}\n\n/* Captured media — an image or a live-route/external iframe fills the well. */\n.media {\n display: block;\n width: 100%;\n height: auto;\n border: 0;\n border-radius: calc(var(--fm-well-radius) - 0.25rem);\n}\n\n/* Device bezel — native / flutter screenshots read inside a subtle frame. */\n.bezel {\n width: 10.5rem;\n margin: 0 auto;\n border: 0.375rem solid color-mix(in srgb, var(--text-primary, #111827) 78%, transparent);\n border-radius: 1.625rem;\n overflow: hidden;\n background: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-lg);\n}\n.bezel .media {\n border-radius: 0;\n}\n\n/* Delta callout marker — a numbered accent dot pinned over a drift location. */\n.delta {\n position: absolute;\n z-index: 3;\n display: grid;\n place-items: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning, #f59e0b) 30%, transparent),\n var(--shadow-md);\n}\n\n/* ── Overlay / slider mode ───────────────────────────────────────────────── */\n.overlay {\n position: relative;\n padding: var(--spacing-4, 1rem);\n}\n.overlayFrame {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n overflow: hidden;\n}\n.layer {\n padding: var(--spacing-3, 0.75rem);\n}\n/* Clear space below the corner ribbons so they never collide with content. */\n.overlayFrame .layer {\n padding-top: var(--spacing-10, 2.5rem);\n}\n.layerTop {\n position: absolute;\n inset: 0;\n /* Revealed portion driven by `--fm-split` (design shows on the left). */\n clip-path: inset(0 calc(100% - var(--fm-split, 55%)) 0 0);\n}\n.handle {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--fm-split, 55%);\n width: 2px;\n background: var(--accent, #2563eb);\n box-shadow: 0 0 0.625rem color-mix(in srgb, var(--accent, #2563eb) 70%, transparent);\n z-index: 4;\n}\n.grip {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: grid;\n place-items: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--accent, #2563eb);\n color: var(--color-white, #ffffff);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #2563eb) 26%, transparent),\n var(--shadow-md);\n}\n.gripIcon {\n width: 0.9375rem;\n height: 0.9375rem;\n}\n.ribbon {\n position: absolute;\n z-index: 5;\n top: var(--spacing-3, 0.75rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--color-white, #ffffff);\n}\n.ribbon[data-side=\"design\"] {\n left: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-design-hue) 85%, var(--color-neutral-950, #030712));\n}\n.ribbon[data-side=\"built\"] {\n right: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-built-hue) 85%, var(--color-neutral-950, #030712));\n}\n.range {\n width: 100%;\n margin-top: var(--spacing-3, 0.75rem);\n accent-color: var(--accent, #2563eb);\n cursor: ew-resize;\n}\n.range: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/* ── Delta legend ────────────────────────────────────────────────────────── */\n.legend {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-top: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n}\n.legendHead {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n}\n.legendRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n}\n.legendNum {\n flex-shrink: 0;\n display: grid;\n place-items: center;\n width: 1.125rem;\n height: 1.125rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n}\n.tag {\n flex-shrink: 0;\n padding: calc(var(--spacing-1, 0.25rem) * 0.75) var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-sm, 0.25rem);\n background: var(--surface-subtle, #f3f4f6);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-secondary, #4b5563);\n}\n.legendText {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n color: var(--text-secondary, #4b5563);\n}\n.legendText b {\n color: var(--text-primary, #111827);\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Responsive: panes stack under 768px (design over built) — NEVER a sideways\n * scroll strip (the defect the doc-set surfaces exist to kill). */\n@media (max-width: 767px) {\n .panes {\n grid-template-columns: 1fr;\n }\n .bar {\n gap: var(--spacing-2, 0.5rem);\n }\n}\n"
|
|
3006
|
+
"content": "/* FidelityMirror — the design-left / built-right comparison surface.\n *\n * Renders the pure DESIGN source (left) beside the BUILT render (right) in\n * recessed capture wells, with a verdict/diff indicator, numbered delta\n * callouts, a classed delta legend, and a drag-to-reveal overlay mode. Composes\n * inside DocFrame's content, and goes full-bleed on widescreen so each pane is\n * ~50vw (the compare is meant for fullscreen monitors).\n *\n * Theme-agnostic: every value references a Visor token. The card/cluster radius\n * and the recessed well fill are component-OWNED (fixed) tokens, NOT the theme\n * `--radius-*` scale or a bare `--surface-*` token — decorative themes inflate\n * `--radius-xl` to 32px and lift `--surface-subtle` ABOVE the card in dark mode,\n * which is precisely the drift this component exists to surface.\n */\n\n/* Root — the framed compare. */\n.root {\n /* Fixed cluster radius the component OWNS — NOT the theme `--radius-*` scale,\n * which decorative themes inflate far past what a dense compare cluster wants\n * (Space inflates `--radius-xl` to 32px). Overridable by a consumer/theme. */\n --fm-radius: 0.75rem;\n --fm-well-radius: 0.625rem;\n /* Widescreen gutter/padding — the outer left/right padding AND the center\n * gutter resolve to the SAME value, so each pane is ~50vw: (100vw - 3*pad)/2. */\n --fm-pad: clamp(1.25rem, 3.2vw, 3.5rem);\n /* Recessed well fill — darker than `--surface-card` in BOTH modes (the DocNav\n * pill-bg convention). The surface ramp inverts light<->dark, so a single\n * surface token can't stay \"below\" the card; mixing the card toward\n * neutral-950 darkens it directionally in either mode. Overridable. */\n --fm-well: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 22%\n );\n /* Source identity hues (kept across split + overlay): design = info, built =\n * accent. These carry SOURCE identity, not decoration — do not retint. */\n --fm-design-hue: var(--info, #0ea5e9);\n --fm-built-hue: var(--accent, #2563eb);\n\n display: flex;\n flex-direction: column;\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--fm-radius);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n overflow: hidden;\n font-family: var(--font-sans, inherit);\n}\n\n/* Widescreen full-bleed — escape DocFrame's centered column and span the\n * viewport. Header + legend align to the panes' outer edge via `--fm-pad`; the\n * panes grid uses `gap: var(--fm-pad)` so each column resolves to ~50vw. */\n.bleed {\n width: 100vw;\n margin-left: calc(50% - 50vw);\n border-radius: 0;\n border-inline: 0;\n}\n.bleed .bar {\n padding-inline: var(--fm-pad);\n}\n.bleed .panes {\n padding-inline: var(--fm-pad);\n gap: var(--fm-pad);\n}\n.bleed .legend {\n padding-inline: var(--fm-pad);\n}\n.bleed .overlay {\n padding-inline: var(--fm-pad);\n}\n\n/* ── Header bar ──────────────────────────────────────────────────────────── */\n.bar {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-bottom: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, #e5e7eb), var(--hairline, #e5e7eb)) 1 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n}\n.titleWrap {\n display: inline-flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n min-width: 0;\n}\n.title {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.02em;\n line-height: 1;\n color: var(--text-primary, #111827);\n}\n.subtitle {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Spacer — pushes the status + toggle to the right edge. */\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Platform chip — mono caps pill, hairline, card bg. */\n.chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-full, 9999px);\n background: var(--surface-card, #ffffff);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n}\n.chipDot {\n width: 0.375rem;\n height: 0.375rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--success, #22c55e);\n box-shadow: 0 0 0.375rem color-mix(in srgb, var(--success, #22c55e) 70%, transparent);\n}\n.chipIcon {\n width: 0.75rem;\n height: 0.75rem;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Status pill — the diff indicator, tinted by verdict. */\n.status {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.08em;\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.status[data-verdict=\"match\"] {\n color: var(--success, #22c55e);\n border-color: color-mix(in srgb, var(--success, #22c55e) 40%, transparent);\n background: color-mix(in srgb, var(--success, #22c55e) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"drift\"] {\n color: var(--warning, #f59e0b);\n border-color: color-mix(in srgb, var(--warning, #f59e0b) 42%, transparent);\n background: color-mix(in srgb, var(--warning, #f59e0b) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"fail\"] {\n color: var(--destructive, #ef4444);\n border-color: color-mix(in srgb, var(--destructive, #ef4444) 42%, transparent);\n background: color-mix(in srgb, var(--destructive, #ef4444) 12%, var(--surface-card, #ffffff));\n}\n.statusGlyph {\n width: 0.625rem;\n height: 0.625rem;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.4375rem currentColor;\n}\n.score {\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n opacity: var(--opacity-80, 0.85);\n}\n\n/* Segmented compare-mode toggle — split / overlay. */\n.seg {\n display: inline-flex;\n gap: calc(var(--spacing-1, 0.25rem) * 0.5);\n padding: calc(var(--spacing-1, 0.25rem) * 0.75);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-full, 9999px);\n background: var(--fm-well);\n}\n.segBtn {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) calc(var(--spacing-1, 0.25rem) * 2.5);\n border: 0;\n border-radius: var(--radius-full, 9999px);\n background: transparent;\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n.segIcon {\n width: 0.8125rem;\n height: 0.8125rem;\n}\n.segBtn[aria-pressed=\"true\"] {\n color: var(--text-primary, #111827);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n}\n.segBtn: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/* ── Panes: side-by-side ─────────────────────────────────────────────────── */\n.panes {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-4, 1rem);\n align-items: start;\n}\n.pane {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n.paneLabel {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n.scopeDot {\n width: 0.5rem;\n height: 0.5rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.5rem color-mix(in srgb, currentColor 60%, transparent);\n}\n.pane[data-side=\"design\"] .scopeDot {\n color: var(--fm-design-hue);\n}\n.pane[data-side=\"built\"] .scopeDot {\n color: var(--fm-built-hue);\n}\n.paneMeta {\n margin-left: auto;\n color: var(--text-tertiary, #9ca3af);\n letter-spacing: 0.1em;\n font-weight: var(--font-weight-medium, 500);\n}\n\n/* Capture well — the recessed frame that holds a captured surface. */\n.well {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n padding: var(--spacing-3, 0.75rem);\n overflow: hidden;\n}\n.pane[data-side=\"built\"] .well {\n box-shadow: inset 0 0 0 var(--stroke-width-thin, 1px)\n color-mix(in srgb, var(--fm-built-hue) 22%, transparent);\n}\n\n/* Captured media — an image or a live-route/external iframe fills the well. */\n.media {\n display: block;\n width: 100%;\n height: auto;\n border: 0;\n border-radius: calc(var(--fm-well-radius) - 0.25rem);\n}\n\n/* Device bezel — native / flutter screenshots read inside a subtle frame. */\n.bezel {\n width: 10.5rem;\n margin: 0 auto;\n border: 0.375rem solid color-mix(in srgb, var(--text-primary, #111827) 78%, transparent);\n border-radius: 1.625rem;\n overflow: hidden;\n background: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-lg);\n}\n.bezel .media {\n border-radius: 0;\n}\n\n/* Delta callout marker — a numbered accent dot pinned over a drift location. */\n.delta {\n position: absolute;\n z-index: 3;\n display: grid;\n place-items: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning, #f59e0b) 30%, transparent),\n var(--shadow-md);\n}\n\n/* ── Overlay / slider mode ───────────────────────────────────────────────── */\n.overlay {\n position: relative;\n padding: var(--spacing-4, 1rem);\n}\n.overlayFrame {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n overflow: hidden;\n}\n.layer {\n padding: var(--spacing-3, 0.75rem);\n}\n/* Clear space below the corner ribbons so they never collide with content. */\n.overlayFrame .layer {\n padding-top: var(--spacing-10, 2.5rem);\n}\n.layerTop {\n position: absolute;\n inset: 0;\n /* Revealed portion driven by `--fm-split` (design shows on the left). */\n clip-path: inset(0 calc(100% - var(--fm-split, 55%)) 0 0);\n}\n.handle {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--fm-split, 55%);\n width: 2px;\n background: var(--accent, #2563eb);\n box-shadow: 0 0 0.625rem color-mix(in srgb, var(--accent, #2563eb) 70%, transparent);\n z-index: 4;\n}\n.grip {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: grid;\n place-items: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--accent, #2563eb);\n color: var(--color-white, #ffffff);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #2563eb) 26%, transparent),\n var(--shadow-md);\n}\n.gripIcon {\n width: 0.9375rem;\n height: 0.9375rem;\n}\n.ribbon {\n position: absolute;\n z-index: 5;\n top: var(--spacing-3, 0.75rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--color-white, #ffffff);\n}\n.ribbon[data-side=\"design\"] {\n left: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-design-hue) 85%, var(--color-neutral-950, #030712));\n}\n.ribbon[data-side=\"built\"] {\n right: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-built-hue) 85%, var(--color-neutral-950, #030712));\n}\n.range {\n width: 100%;\n margin-top: var(--spacing-3, 0.75rem);\n accent-color: var(--accent, #2563eb);\n cursor: ew-resize;\n}\n.range: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/* ── Delta legend ────────────────────────────────────────────────────────── */\n.legend {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-top: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, #e5e7eb), var(--hairline, #e5e7eb)) 1 / var(--hairline-width, var(--stroke-width-thin, 1px)) 0 0 0;\n}\n.legendHead {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n}\n.legendRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n}\n.legendNum {\n flex-shrink: 0;\n display: grid;\n place-items: center;\n width: 1.125rem;\n height: 1.125rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n}\n.tag {\n flex-shrink: 0;\n padding: calc(var(--spacing-1, 0.25rem) * 0.75) var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline, #e5e7eb);\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n border-radius: var(--radius-sm, 0.25rem);\n background: var(--surface-subtle, #f3f4f6);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-secondary, #4b5563);\n}\n.legendText {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n color: var(--text-secondary, #4b5563);\n}\n.legendText b {\n color: var(--text-primary, #111827);\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Responsive: panes stack under 768px (design over built) — NEVER a sideways\n * scroll strip (the defect the doc-set surfaces exist to kill). */\n@media (max-width: 767px) {\n .panes {\n grid-template-columns: 1fr;\n }\n .bar {\n gap: var(--spacing-2, 0.5rem);\n }\n}\n"
|
|
2956
3007
|
}
|
|
2957
3008
|
]
|
|
2958
3009
|
},
|
|
@@ -3027,7 +3078,7 @@
|
|
|
3027
3078
|
{
|
|
3028
3079
|
"path": "components/ui/infographic-bar/infographic-bar.module.css",
|
|
3029
3080
|
"type": "registry:ui",
|
|
3030
|
-
"content": "/* InfographicBar: lays out N stat-cards as a single continuous band.\n *\n * Corner strategy: the band owns the frame, radius, and elevation. A rounded\n * `border-radius` plus `overflow: hidden` clips the cells so the OUTER corners\n * round and the INNER corners stay square — no per-cell :first/:last radius\n * math, and it holds for any N. Each cell sheds its own border/radius/shadow\n * (see `.cell`) so the band reads as one surface, not bordered cards butting\n * together.\n *\n * Equal columns without knowing N: `grid-auto-flow: column` +\n * `grid-auto-columns: minmax(0, 1fr)`. The `minmax(0, …)` floor lets long\n * values shrink instead of forcing the band to overflow.\n */\n.bar {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n background-color: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n overflow: hidden;\n}\n\n/* Cell chrome reset — the band owns the chrome, so each stat-card drops its own\n * border, radius, and shadow. Scoped under `.bar` (a local class) to satisfy\n * the Turbopack pure-selector rule and to out-specify stat-card's own\n * base and variant rules. The cell BACKGROUND is left untouched so the\n * stat-card highlight variant still tints its cell.\n */\n.bar .cell {\n border: none;\n border-radius: 0;\n box-shadow: none;\n}\n\n/* Hairline divider on the leading edge of every cell after the first — this is\n * what makes the middle cells read as continuous rather than separate. Uses\n * `--hairline` (the house soft-separator token, softer than a full border) so\n * it stays subtle, and exposes `--infographic-bar-divider` so a theme or\n * consumer can retune it — including to `transparent` for a fully seamless\n * borderless band. The outer frame above references `--border-default`, which\n * borderless themes set to `transparent`, so the band melts into the page.\n */\n.bar .cell + .cell {\n border-left: var(--stroke-width-thin, 1px) solid
|
|
3081
|
+
"content": "/* InfographicBar: lays out N stat-cards as a single continuous band.\n *\n * Corner strategy: the band owns the frame, radius, and elevation. A rounded\n * `border-radius` plus `overflow: hidden` clips the cells so the OUTER corners\n * round and the INNER corners stay square — no per-cell :first/:last radius\n * math, and it holds for any N. Each cell sheds its own border/radius/shadow\n * (see `.cell`) so the band reads as one surface, not bordered cards butting\n * together.\n *\n * Equal columns without knowing N: `grid-auto-flow: column` +\n * `grid-auto-columns: minmax(0, 1fr)`. The `minmax(0, …)` floor lets long\n * values shrink instead of forcing the band to overflow.\n */\n.bar {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n background-color: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-lg, 0.75rem);\n box-shadow: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.05));\n overflow: hidden;\n}\n\n/* Cell chrome reset — the band owns the chrome, so each stat-card drops its own\n * border, radius, and shadow. Scoped under `.bar` (a local class) to satisfy\n * the Turbopack pure-selector rule and to out-specify stat-card's own\n * base and variant rules. The cell BACKGROUND is left untouched so the\n * stat-card highlight variant still tints its cell.\n */\n.bar .cell {\n border: none;\n border-radius: 0;\n box-shadow: none;\n}\n\n/* Hairline divider on the leading edge of every cell after the first — this is\n * what makes the middle cells read as continuous rather than separate. Uses\n * `--hairline` (the house soft-separator token, softer than a full border) so\n * it stays subtle, and exposes `--infographic-bar-divider` so a theme or\n * consumer can retune it — including to `transparent` for a fully seamless\n * borderless band. The outer frame above references `--border-default`, which\n * borderless themes set to `transparent`, so the band melts into the page.\n */\n.bar .cell + .cell {\n border-left: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--infographic-bar-divider, var(--hairline, #e5e7eb)), var(--infographic-bar-divider, var(--hairline, #e5e7eb))) 1 / 0 0 0 var(--hairline-width, var(--stroke-width-thin, 1px));\n}\n"
|
|
3031
3082
|
}
|
|
3032
3083
|
]
|
|
3033
3084
|
},
|
|
@@ -3345,7 +3396,7 @@
|
|
|
3345
3396
|
{
|
|
3346
3397
|
"path": "components/ui/structured-prompt/structured-prompt.module.css",
|
|
3347
3398
|
"type": "registry:ui",
|
|
3348
|
-
"content": "/* StructuredPrompt\n *\n * Inline \"mad-lib\" fill-in-the-blank card used in brand elicitation and\n * structured-prompt workflows. A card with an eyebrow header, flowing prose\n * at tall line-height, and inline slot chips (filled or empty) embedded in\n * the prose, plus a hint footer.\n */\n\n.root {\n width: 100%;\n border-radius: var(--radius-xl, 1rem);\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n overflow: hidden;\n}\n\n/* Header bar — small icon + uppercase eyebrow label, hairline bottom border */\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2-5, 0.625rem) var(--spacing-3-5, 0.875rem);\n border-bottom: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n /* 11.5px — no line-height token matches this tall-prose context; intentional */\n font-size: 11.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n}\n\n.headerIcon {\n display: inline-flex;\n align-items: center;\n /* Use accent color for the eyebrow icon, matching the design spec */\n color: var(--surface-accent-default, #3b82f6);\n flex-shrink: 0;\n /* 13px icon — sized to match the 11.5px label rhythm */\n font-size: 13px;\n line-height: 1;\n}\n\n.headerLabel {\n flex: 1;\n min-width: 0;\n}\n\n/* Body — prose container with tall line-height so inline chips breathe */\n.body {\n padding: var(--spacing-3-5, 0.875rem);\n /* 15px — closest to --font-size-base but intentionally smaller for prose density */\n font-size: 15px;\n /* 2.05 line-height — no token matches; intentional value from design spec (elicit-core HiFi prototype) */\n line-height: 2.05;\n color: var(--text-primary, #111827);\n}\n\n/* Slot chip — inline; rendered inside flowing prose text */\n.slot {\n display: inline-flex;\n align-items: center;\n /* min-height 30px — no spacing token; intentional to match chip rhythm */\n min-height: 30px;\n padding: var(--spacing-0-5, 0.125rem) var(--spacing-2-5, 0.625rem);\n margin: 0 var(--spacing-0-5, 0.125rem);\n border-radius: var(--radius-md, 0.5rem);\n border-width: var(--stroke-width-thin, 1px);\n border-style: solid;\n /* 14px — intentionally one step below body prose for visual hierarchy */\n font-size: 14px;\n cursor: default;\n vertical-align: middle;\n line-height: 1.4;\n text-decoration: none;\n -webkit-appearance: none;\n appearance: none;\n}\n\n/* Focus ring — only when rendered as <button> */\n.slot:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #3b82f6);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Filled slot — accent-subtle bg + accent-default border + text-link color + 600 weight.\n * Uses the same token triplet as Chip's selected state so it adapts correctly in dark mode.\n */\n.slotFilled {\n background: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* Empty slot — dashed border, muted bg, italic, tertiary text */\n.slotEmpty {\n color: var(--text-tertiary, #6b7280);\n font-style: italic;\n font-weight: var(--font-weight-medium, 500);\n background: var(--surface-subtle, #f9fafb);\n border-style: dashed;\n border-color: var(--border-default, #e5e7eb);\n cursor: text;\n}\n\n/* Hint footer — small tertiary text below the body */\n.hint {\n padding: 0 var(--spacing-3-5, 0.875rem) var(--spacing-3, 0.75rem);\n /* 11.5px — matches header eyebrow size; intentional */\n font-size: 11.5px;\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .slot {\n transition: none;\n }\n}\n"
|
|
3399
|
+
"content": "/* StructuredPrompt\n *\n * Inline \"mad-lib\" fill-in-the-blank card used in brand elicitation and\n * structured-prompt workflows. A card with an eyebrow header, flowing prose\n * at tall line-height, and inline slot chips (filled or empty) embedded in\n * the prose, plus a hint footer.\n */\n\n.root {\n width: 100%;\n border-radius: var(--radius-xl, 1rem);\n background: var(--surface-card, #ffffff);\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb);\n box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));\n overflow: hidden;\n}\n\n/* Header bar — small icon + uppercase eyebrow label, hairline bottom border */\n.header {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2-5, 0.625rem) var(--spacing-3-5, 0.875rem);\n border-bottom: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, #e5e7eb), var(--hairline, #e5e7eb)) 1 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n /* 11.5px — no line-height token matches this tall-prose context; intentional */\n font-size: 11.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text-tertiary, #6b7280);\n}\n\n.headerIcon {\n display: inline-flex;\n align-items: center;\n /* Use accent color for the eyebrow icon, matching the design spec */\n color: var(--surface-accent-default, #3b82f6);\n flex-shrink: 0;\n /* 13px icon — sized to match the 11.5px label rhythm */\n font-size: 13px;\n line-height: 1;\n}\n\n.headerLabel {\n flex: 1;\n min-width: 0;\n}\n\n/* Body — prose container with tall line-height so inline chips breathe */\n.body {\n padding: var(--spacing-3-5, 0.875rem);\n /* 15px — closest to --font-size-base but intentionally smaller for prose density */\n font-size: 15px;\n /* 2.05 line-height — no token matches; intentional value from design spec (elicit-core HiFi prototype) */\n line-height: 2.05;\n color: var(--text-primary, #111827);\n}\n\n/* Slot chip — inline; rendered inside flowing prose text */\n.slot {\n display: inline-flex;\n align-items: center;\n /* min-height 30px — no spacing token; intentional to match chip rhythm */\n min-height: 30px;\n padding: var(--spacing-0-5, 0.125rem) var(--spacing-2-5, 0.625rem);\n margin: 0 var(--spacing-0-5, 0.125rem);\n border-radius: var(--radius-md, 0.5rem);\n border-width: var(--stroke-width-thin, 1px);\n border-style: solid;\n /* 14px — intentionally one step below body prose for visual hierarchy */\n font-size: 14px;\n cursor: default;\n vertical-align: middle;\n line-height: 1.4;\n text-decoration: none;\n -webkit-appearance: none;\n appearance: none;\n}\n\n/* Focus ring — only when rendered as <button> */\n.slot:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #3b82f6);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Filled slot — accent-subtle bg + accent-default border + text-link color + 600 weight.\n * Uses the same token triplet as Chip's selected state so it adapts correctly in dark mode.\n */\n.slotFilled {\n background: var(--surface-accent-subtle, #eff6ff);\n border-color: var(--surface-accent-default, #3b82f6);\n color: var(--text-link, #2563eb);\n font-weight: var(--font-weight-semibold, 600);\n}\n\n/* Empty slot — dashed border, muted bg, italic, tertiary text */\n.slotEmpty {\n color: var(--text-tertiary, #6b7280);\n font-style: italic;\n font-weight: var(--font-weight-medium, 500);\n background: var(--surface-subtle, #f9fafb);\n border-style: dashed;\n border-color: var(--border-default, #e5e7eb);\n cursor: text;\n}\n\n/* Hint footer — small tertiary text below the body */\n.hint {\n padding: 0 var(--spacing-3-5, 0.875rem) var(--spacing-3, 0.75rem);\n /* 11.5px — matches header eyebrow size; intentional */\n font-size: 11.5px;\n color: var(--text-tertiary, #6b7280);\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .slot {\n transition: none;\n }\n}\n"
|
|
3349
3400
|
}
|
|
3350
3401
|
]
|
|
3351
3402
|
},
|
|
@@ -4220,7 +4271,7 @@
|
|
|
4220
4271
|
{
|
|
4221
4272
|
"path": "blocks/dialog-form/dialog-form.module.css",
|
|
4222
4273
|
"type": "registry:block",
|
|
4223
|
-
"content": "/* DialogForm block — Animal dialog substrate shell (VI-620).\n *\n * Restyles the `dialog` atom's DialogContent into the compact admin modal\n * panel, then adds the header / title / body / footer chrome the 10 hand-rolled\n * Animal modals converge onto. Every value traces to a Visor token so the\n * active theme (Animal, or any other) swaps the whole shell — no bespoke CSS.\n */\n\n/* Compact panel — overrides the atom's default 32rem / spacing-6 content. The\n * max-width is the `width` axis (--widthSm/Md/Lg below); the hairline border is a\n * token seam (--dialog-form-panel-border) a theme or the `border=\"none\"` variant\n * can null out — AN-237 removed it (the hairline reads as chrome the design\n * doesn't want) without editing this copy. */\n.panel {\n border-radius: var(--radius-xl, 0.75rem);\n padding: var(--spacing-5, 1.25rem);\n /* Floating panel — MUST be an opaque surface. `--surface-card` is a card-in-\n * flow token a theme may make translucent glass (blackout: rgba(255,255,255,\n * 0.04)); over the dimmed backdrop it has no opaque layer behind it and reads\n * see-through. `--surface-popover` is the opaque floating-panel token and is\n * identical to `--surface-card` on solid themes, so only glass themes change. */\n background: var(--surface-popover, var(--surface-page, #ffffff));\n border:
|
|
4274
|
+
"content": "/* DialogForm block — Animal dialog substrate shell (VI-620).\n *\n * Restyles the `dialog` atom's DialogContent into the compact admin modal\n * panel, then adds the header / title / body / footer chrome the 10 hand-rolled\n * Animal modals converge onto. Every value traces to a Visor token so the\n * active theme (Animal, or any other) swaps the whole shell — no bespoke CSS.\n */\n\n/* Compact panel — overrides the atom's default 32rem / spacing-6 content. The\n * max-width is the `width` axis (--widthSm/Md/Lg below); the hairline border is a\n * token seam (--dialog-form-panel-border) a theme or the `border=\"none\"` variant\n * can null out — AN-237 removed it (the hairline reads as chrome the design\n * doesn't want) without editing this copy. */\n.panel {\n border-radius: var(--radius-xl, 0.75rem);\n padding: var(--spacing-5, 1.25rem);\n /* Floating panel — MUST be an opaque surface. `--surface-card` is a card-in-\n * flow token a theme may make translucent glass (blackout: rgba(255,255,255,\n * 0.04)); over the dimmed backdrop it has no opaque layer behind it and reads\n * see-through. `--surface-popover` is the opaque floating-panel token and is\n * identical to `--surface-card` on solid themes, so only glass themes change. */\n background: var(--surface-popover, var(--surface-page, #ffffff));\n /* The border is geometry; the visible hairline is an inset outline at\n * --hairline-width, so --hairline-width: 0 removes it with no layout shift.\n * A bound --dialog-form-panel-border (a full border shorthand, or `none`) is\n * not a valid outline-style, so binding it drops the outline and draws the\n * bound border instead. */\n border: var(--dialog-form-panel-border, var(--stroke-width-thin, 1px) solid transparent);\n outline-style: var(--dialog-form-panel-border, solid);\n outline-width: var(--hairline-width, var(--stroke-width-thin, 1px));\n outline-color: var(--hairline, var(--border-default, #e5e7eb));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n box-shadow: var(--shadow-lg);\n}\n\n/* Width axis — max-width only. Token-backed so a theme can retune each step;\n * `md` (default) preserves the VI-620 30rem panel. */\n.widthSm {\n max-width: var(--dialog-form-width-sm, 24rem);\n}\n\n.widthMd {\n max-width: var(--dialog-form-width-md, 30rem);\n}\n\n.widthLg {\n max-width: var(--dialog-form-width-lg, 40rem);\n}\n\n/* Borderless variant — nulls the panel-border seam (border=\"none\"). Consumers\n * can equivalently set --dialog-form-panel-border: none in a theme. */\n.panelBorderless {\n --dialog-form-panel-border: none;\n}\n\n/* Header — title + optional description, tight stack above the body. */\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Title — dlg-title. Bold, primary ink. Size axis reconciles the two AN\n * sources: the 13.5px surface scale (default `sm`) and reuse.html's 15px\n * shared/confirm dialogs (`md`). Normalized onto the --text-N chrome scale. */\n.title {\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n margin: 0;\n line-height: 1.3;\n}\n\n.titleSm {\n font-size: var(--text-13, 0.8125rem);\n}\n\n.titleMd {\n font-size: var(--text-16, 1rem);\n}\n\n/* Body — vertical stack of DialogField blocks. */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n/* Footer — right-aligned dlg-btn action row. */\n.footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n margin-top: var(--spacing-5, 1.25rem);\n}\n"
|
|
4224
4275
|
}
|
|
4225
4276
|
]
|
|
4226
4277
|
},
|
|
@@ -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
|
},
|
|
@@ -5185,7 +5236,7 @@
|
|
|
5185
5236
|
{
|
|
5186
5237
|
"path": "components/visual/browser-frame/browser-frame.module.css",
|
|
5187
5238
|
"type": "registry:ui",
|
|
5188
|
-
"content": "/*\n * BrowserFrame — browser-chrome mockup frame.\n *\n * Ported from Blacklight EpkFrame (VI-570). Elevation is kept OUT so consumers\n * can compose with the lit / lit-soft / lit-strong utilities from visor-core/utilities.\n *\n * --browser-frame-bg: deep near-black background of the frame shell.\n * Override to use a theme surface token when the consumer has one.\n *\n * --browser-frame-focus-color: focus outline color on the URL pill link.\n * Defaults to var(--accent, currentColor) so Blacklight can bind its\n * keyed per-artist accent by setting this on the frame element.\n */\n\n.frame {\n overflow: hidden;\n border-radius: var(--radius-xl, 1rem);\n border: var(--stroke-width-thin) solid var(--hairline-strong, rgb(255 255 255 / var(--opacity-10, 0.10)));\n background-color: var(--browser-frame-bg, #0a0a0b);\n}\n\n/* Chrome bar */\n.chrome {\n position: relative;\n display: flex;\n height: var(--spacing-10, 40px);\n align-items: center;\n justify-content: center;\n border-bottom: var(--stroke-width-thin) solid var(--hairline, rgb(255 255 255 / var(--opacity-5, 0.05)));\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n}\n\n/* Traffic-light dots cluster */\n.dots {\n position: absolute;\n left: var(--spacing-3_5, 14px);\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n}\n\n.dot {\n display: block;\n height: 10px;\n width: 10px;\n border-radius: 50%;\n background-color: rgb(255 255 255 / var(--opacity-12, 0.12));\n}\n\n/* URL pill (non-linked) */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n border-radius: var(--radius-md, 0.375rem);\n background-color: rgb(255 255 255 / var(--opacity-5, 0.05));\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n padding-top: var(--spacing-1, 4px);\n padding-bottom: var(--spacing-1, 4px);\n font-size: 0.6875rem; /* 11px — intentional: mockup chrome reads at near-system scale */\n font-weight: var(--font-weight-light, 300);\n letter-spacing: 0.025em;\n color: rgb(255 255 255 / var(--opacity-60, 0.60));\n}\n\n.pillIcon {\n /* Phosphor icon inherits currentColor; the lock stays on the text baseline */\n flex-shrink: 0;\n}\n\n/* URL pill as a link */\n.pillLink {\n text-decoration: none;\n color: inherit;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n\n.pillLink:hover .pill {\n color: rgb(255 255 255 / var(--opacity-80, 0.80));\n}\n\n.pillLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--browser-frame-focus-color, var(--accent, currentColor));\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-md, 0.375rem);\n}\n"
|
|
5239
|
+
"content": "/*\n * BrowserFrame — browser-chrome mockup frame.\n *\n * Ported from Blacklight EpkFrame (VI-570). Elevation is kept OUT so consumers\n * can compose with the lit / lit-soft / lit-strong utilities from visor-core/utilities.\n *\n * --browser-frame-bg: deep near-black background of the frame shell.\n * Override to use a theme surface token when the consumer has one.\n *\n * --browser-frame-focus-color: focus outline color on the URL pill link.\n * Defaults to var(--accent, currentColor) so Blacklight can bind its\n * keyed per-artist accent by setting this on the frame element.\n */\n\n.frame {\n overflow: hidden;\n border-radius: var(--radius-xl, 1rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n outline: var(--hairline-width, var(--stroke-width-thin, 1px)) solid var(--hairline-strong, rgb(255 255 255 / var(--opacity-10, 0.10)));\n outline-offset: calc(-1 * var(--hairline-width, var(--stroke-width-thin, 1px)));\n background-color: var(--browser-frame-bg, #0a0a0b);\n}\n\n/* Chrome bar */\n.chrome {\n position: relative;\n display: flex;\n height: var(--spacing-10, 40px);\n align-items: center;\n justify-content: center;\n border-bottom: var(--stroke-width-thin, 1px) solid transparent;\n border-image: linear-gradient(var(--hairline, rgb(255 255 255 / var(--opacity-5, 0.05))), var(--hairline, rgb(255 255 255 / var(--opacity-5, 0.05)))) 1 / 0 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0;\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n}\n\n/* Traffic-light dots cluster */\n.dots {\n position: absolute;\n left: var(--spacing-3_5, 14px);\n display: flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n}\n\n.dot {\n display: block;\n height: 10px;\n width: 10px;\n border-radius: 50%;\n background-color: rgb(255 255 255 / var(--opacity-12, 0.12));\n}\n\n/* URL pill (non-linked) */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 4px);\n border-radius: var(--radius-md, 0.375rem);\n background-color: rgb(255 255 255 / var(--opacity-5, 0.05));\n padding-left: var(--spacing-3, 12px);\n padding-right: var(--spacing-3, 12px);\n padding-top: var(--spacing-1, 4px);\n padding-bottom: var(--spacing-1, 4px);\n font-size: 0.6875rem; /* 11px — intentional: mockup chrome reads at near-system scale */\n font-weight: var(--font-weight-light, 300);\n letter-spacing: 0.025em;\n color: rgb(255 255 255 / var(--opacity-60, 0.60));\n}\n\n.pillIcon {\n /* Phosphor icon inherits currentColor; the lock stays on the text baseline */\n flex-shrink: 0;\n}\n\n/* URL pill as a link */\n.pillLink {\n text-decoration: none;\n color: inherit;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n\n.pillLink:hover .pill {\n color: rgb(255 255 255 / var(--opacity-80, 0.80));\n}\n\n.pillLink:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--browser-frame-focus-color, var(--accent, currentColor));\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-md, 0.375rem);\n}\n"
|
|
5189
5240
|
}
|
|
5190
5241
|
]
|
|
5191
5242
|
},
|
|
@@ -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
|
},
|