@loworbitstudio/visor 1.27.0 → 1.29.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 +19 -1
- package/dist/index.js +179 -1
- package/dist/registry.json +107 -17
- package/dist/visor-manifest.json +391 -7
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -91,12 +91,12 @@
|
|
|
91
91
|
{
|
|
92
92
|
"path": "components/ui/text/text.tsx",
|
|
93
93
|
"type": "registry:ui",
|
|
94
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./text.module.css\"\n\nconst textVariants = cva(styles.base, {\n variants: {\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n xl: styles.sizeXl,\n },\n weight: {\n normal: styles.weightNormal,\n medium: styles.weightMedium,\n semibold: styles.weightSemibold,\n bold: styles.weightBold,\n },\n color: {\n primary: styles.colorPrimary,\n secondary: styles.colorSecondary,\n tertiary: styles.colorTertiary,\n inherit: styles.colorInherit,\n },\n leading: {\n tight: styles.leadingTight,\n snug: styles.leadingSnug,\n normal: styles.leadingNormal,\n relaxed: styles.leadingRelaxed,\n loose: styles.leadingLoose,\n },\n },\n defaultVariants: {\n size: \"md\",\n weight: \"normal\",\n color: \"primary\",\n leading: \"normal\",\n },\n})\n\nexport interface TextProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"color\">,\n VariantProps<typeof textVariants> {\n as?: \"p\" | \"span\" | \"div\" | \"label\"\n}\n\nconst Text = React.forwardRef<HTMLElement, TextProps>(\n ({ className, as: Tag = \"p\", size, weight, color, leading, ...props }, ref) => {\n return (\n <Tag\n ref={ref as React.Ref<never>}\n data-slot=\"text\"\n className={cn(textVariants({ size, weight, color, leading }), className)}\n {...props}\n />\n )\n }\n)\nText.displayName = \"Text\"\n\nexport { Text, textVariants }\n"
|
|
94
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./text.module.css\"\n\nconst textVariants = cva(styles.base, {\n variants: {\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n xl: styles.sizeXl,\n },\n weight: {\n normal: styles.weightNormal,\n medium: styles.weightMedium,\n semibold: styles.weightSemibold,\n bold: styles.weightBold,\n },\n color: {\n primary: styles.colorPrimary,\n secondary: styles.colorSecondary,\n tertiary: styles.colorTertiary,\n warning: styles.colorWarning,\n inherit: styles.colorInherit,\n },\n leading: {\n tight: styles.leadingTight,\n snug: styles.leadingSnug,\n normal: styles.leadingNormal,\n relaxed: styles.leadingRelaxed,\n loose: styles.leadingLoose,\n },\n },\n defaultVariants: {\n size: \"md\",\n weight: \"normal\",\n color: \"primary\",\n leading: \"normal\",\n },\n})\n\nexport interface TextProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"color\">,\n VariantProps<typeof textVariants> {\n as?: \"p\" | \"span\" | \"div\" | \"label\"\n}\n\nconst Text = React.forwardRef<HTMLElement, TextProps>(\n ({ className, as: Tag = \"p\", size, weight, color, leading, ...props }, ref) => {\n return (\n <Tag\n ref={ref as React.Ref<never>}\n data-slot=\"text\"\n className={cn(textVariants({ size, weight, color, leading }), className)}\n {...props}\n />\n )\n }\n)\nText.displayName = \"Text\"\n\nexport { Text, textVariants }\n"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"path": "components/ui/text/text.module.css",
|
|
98
98
|
"type": "registry:ui",
|
|
99
|
-
"content": "/* Text base styles */\n.base {\n font-family: var(--font-body, inherit);\n margin: 0;\n}\n\n/* Sizes */\n.sizeXs {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeSm {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeMd {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeLg {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n.sizeXl {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n/* Weights */\n.weightNormal {\n font-weight: var(--font-weight-normal, 400);\n}\n\n.weightMedium {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.weightSemibold {\n font-weight: var(--font-weight-semibold, 600);\n}\n\n.weightBold {\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Colors */\n.colorPrimary {\n color: var(--text-primary, #111827);\n}\n\n.colorSecondary {\n color: var(--text-secondary, #6b7280);\n}\n\n.colorTertiary {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.colorInherit {\n color: inherit;\n}\n\n/* Line height */\n.leadingTight {\n line-height: var(--line-height-tight, 1.25);\n}\n\n.leadingSnug {\n line-height: var(--line-height-snug, 1.375);\n}\n\n.leadingNormal {\n line-height: var(--line-height-normal, 1.5);\n}\n\n.leadingRelaxed {\n line-height: var(--line-height-relaxed, 1.625);\n}\n\n.leadingLoose {\n line-height: var(--line-height-loose, 2);\n}\n"
|
|
99
|
+
"content": "/* Text base styles */\n.base {\n font-family: var(--font-body, inherit);\n margin: 0;\n}\n\n/* Sizes\n * A theme sets each size's type size, case and tracking through\n * --text-<size>-* (VI-656). Case and tracking live in visor-base, below. */\n.sizeXs {\n font-size: var(--text-xs-font-size, var(--font-size-xs, 0.75rem));\n}\n\n.sizeSm {\n font-size: var(--text-sm-font-size, var(--font-size-sm, 0.875rem));\n}\n\n.sizeMd {\n font-size: var(--text-md-font-size, var(--font-size-base, 1rem));\n}\n\n.sizeLg {\n font-size: var(--text-lg-font-size, var(--font-size-lg, 1.125rem));\n}\n\n.sizeXl {\n font-size: var(--text-xl-font-size, var(--font-size-xl, 1.25rem));\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 .sizeXs {\n text-transform: var(--text-xs-text-transform, revert-layer);\n letter-spacing: var(--text-xs-letter-spacing, revert-layer);\n }\n\n .sizeSm {\n text-transform: var(--text-sm-text-transform, revert-layer);\n letter-spacing: var(--text-sm-letter-spacing, revert-layer);\n }\n\n .sizeMd {\n text-transform: var(--text-md-text-transform, revert-layer);\n letter-spacing: var(--text-md-letter-spacing, revert-layer);\n }\n\n .sizeLg {\n text-transform: var(--text-lg-text-transform, revert-layer);\n letter-spacing: var(--text-lg-letter-spacing, revert-layer);\n }\n\n .sizeXl {\n text-transform: var(--text-xl-text-transform, revert-layer);\n letter-spacing: var(--text-xl-letter-spacing, revert-layer);\n }\n}\n\n/* Weights */\n.weightNormal {\n font-weight: var(--font-weight-normal, 400);\n}\n\n.weightMedium {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.weightSemibold {\n font-weight: var(--font-weight-semibold, 600);\n}\n\n.weightBold {\n font-weight: var(--font-weight-bold, 700);\n}\n\n/* Colors */\n.colorPrimary {\n color: var(--text-primary, #111827);\n}\n\n.colorSecondary {\n color: var(--text-secondary, #6b7280);\n}\n\n.colorTertiary {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.colorWarning {\n color: var(--text-warning, #b45309);\n}\n\n.colorInherit {\n color: inherit;\n}\n\n/* Line height */\n.leadingTight {\n line-height: var(--line-height-tight, 1.25);\n}\n\n.leadingSnug {\n line-height: var(--line-height-snug, 1.375);\n}\n\n.leadingNormal {\n line-height: var(--line-height-normal, 1.5);\n}\n\n.leadingRelaxed {\n line-height: var(--line-height-relaxed, 1.625);\n}\n\n.leadingLoose {\n line-height: var(--line-height-loose, 2);\n}\n"
|
|
100
100
|
}
|
|
101
101
|
]
|
|
102
102
|
},
|
|
@@ -142,12 +142,37 @@
|
|
|
142
142
|
{
|
|
143
143
|
"path": "components/ui/button/button.tsx",
|
|
144
144
|
"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 },\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}\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 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"
|
|
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"
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"path": "components/ui/button/button.module.css",
|
|
149
149
|
"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 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), border-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 border-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 border-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.sizeSm {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeMd {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n 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(--font-size-base, 1rem);\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(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n border-radius: var(--radius-lg, 0.5rem);\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 border-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"
|
|
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"
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "inline-edit",
|
|
156
|
+
"type": "registry:ui",
|
|
157
|
+
"description": "Inline-editable text — the text, then a pencil. A click or Enter turns it into an input in place, at the same size; Enter, blur or Tab commits, Escape restores, and clearing it brings back a muted default.",
|
|
158
|
+
"category": "form",
|
|
159
|
+
"dependencies": [
|
|
160
|
+
"@phosphor-icons/react",
|
|
161
|
+
"@loworbitstudio/visor-core"
|
|
162
|
+
],
|
|
163
|
+
"registryDependencies": [
|
|
164
|
+
"utils"
|
|
165
|
+
],
|
|
166
|
+
"files": [
|
|
167
|
+
{
|
|
168
|
+
"path": "components/ui/inline-edit/inline-edit.tsx",
|
|
169
|
+
"type": "registry:ui",
|
|
170
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PencilSimple } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./inline-edit.module.css\"\n\ntype InlineEditElement = \"span\" | \"div\" | \"p\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n\nexport interface InlineEditProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"defaultValue\" | \"children\"> {\n /** The committed text. An empty string means \"no override\": `defaultValue` shows instead. */\n value: string\n /** Shown, muted, while `value` is empty. Committing an empty string brings it back. */\n defaultValue?: string\n /** Called with the trimmed text on Enter, blur or Tab, only when it differs from `value`. */\n onCommit: (value: string) => void\n /** The field's name, e.g. \"Section title\". Names the input, and the pencil as \"Edit {label}\". */\n label: string\n /** Accessible name of the pencil. Defaults to `Edit {label}`. */\n editLabel?: string\n /** The element to render. Typography inherits from it, so a title keeps its heading's size. */\n as?: InlineEditElement\n /** Open in the editing state on mount, without moving focus. */\n defaultEditing?: boolean\n}\n\nconst InlineEdit = React.forwardRef<HTMLElement, InlineEditProps>(\n (\n {\n value,\n defaultValue = \"\",\n onCommit,\n label,\n editLabel,\n as: Comp = \"span\",\n defaultEditing = false,\n className,\n ...props\n },\n ref\n ) => {\n const [editing, setEditing] = React.useState(defaultEditing)\n const [draft, setDraft] = React.useState(defaultEditing ? value : \"\")\n const inputRef = React.useRef<HTMLInputElement>(null)\n const pencilRef = React.useRef<HTMLButtonElement>(null)\n // Set once Enter or Escape has closed the editor, so the blur that follows\n // the input unmounting does not commit a second time.\n const settled = React.useRef(false)\n // Where focus goes after the next render: into the input when editing\n // starts, back to the pencil after Enter or Escape.\n const pendingFocus = React.useRef<\"input\" | \"pencil\" | null>(null)\n\n React.useEffect(() => {\n if (pendingFocus.current === \"input\" && inputRef.current) {\n inputRef.current.focus()\n inputRef.current.select()\n } else if (pendingFocus.current === \"pencil\") {\n pencilRef.current?.focus()\n }\n pendingFocus.current = null\n }, [editing])\n\n const start = () => {\n settled.current = false\n pendingFocus.current = \"input\"\n setDraft(value)\n setEditing(true)\n }\n\n const finish = (commit: boolean, returnFocus: boolean) => {\n if (settled.current) return\n settled.current = true\n if (returnFocus) pendingFocus.current = \"pencil\"\n setEditing(false)\n const next = draft.trim()\n if (commit && next !== value) onCommit(next)\n }\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.nativeEvent.isComposing) return\n if (event.key === \"Enter\") {\n event.preventDefault()\n finish(true, true)\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n finish(false, true)\n }\n }\n\n const isDefault = value === \"\"\n\n return (\n <Comp\n ref={ref as React.Ref<never>}\n data-slot=\"inline-edit\"\n data-state={editing ? \"editing\" : \"rest\"}\n className={cn(styles.root, className)}\n {...props}\n >\n {editing ? (\n // The field is sized by a hidden copy of the draft (CSS ::after), so\n // the input is as wide as the text it replaces, not its container.\n <span data-slot=\"inline-edit-field\" className={styles.field} data-value={draft || defaultValue}>\n <input\n ref={inputRef}\n type=\"text\"\n // size={1} drops the input's own ~20-character width, so only the\n // hidden copy of the draft sizes the field.\n size={1}\n data-slot=\"inline-edit-input\"\n className={styles.input}\n aria-label={label}\n value={draft}\n placeholder={defaultValue}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={onKeyDown}\n onBlur={() => finish(true, false)}\n />\n </span>\n ) : (\n <>\n <span\n data-slot=\"inline-edit-text\"\n data-default={isDefault || undefined}\n className={cn(styles.text, isDefault && styles.textDefault)}\n onClick={start}\n >\n {isDefault ? defaultValue : value}\n </span>\n <button\n ref={pencilRef}\n type=\"button\"\n data-slot=\"inline-edit-pencil\"\n className={styles.pencil}\n aria-label={editLabel ?? `Edit ${label}`}\n onClick={start}\n >\n <PencilSimple aria-hidden=\"true\" />\n </button>\n </>\n )}\n </Comp>\n )\n }\n)\nInlineEdit.displayName = \"InlineEdit\"\n\nexport { InlineEdit }\n"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"path": "components/ui/inline-edit/inline-edit.module.css",
|
|
174
|
+
"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"
|
|
151
176
|
}
|
|
152
177
|
]
|
|
153
178
|
},
|
|
@@ -173,7 +198,7 @@
|
|
|
173
198
|
{
|
|
174
199
|
"path": "components/ui/input/input.module.css",
|
|
175
200
|
"type": "registry:ui",
|
|
176
|
-
"content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n border:
|
|
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"
|
|
177
202
|
}
|
|
178
203
|
]
|
|
179
204
|
},
|
|
@@ -224,7 +249,7 @@
|
|
|
224
249
|
{
|
|
225
250
|
"path": "components/ui/textarea/textarea.module.css",
|
|
226
251
|
"type": "registry:ui",
|
|
227
|
-
"content": "/* Textarea base styles */\n.base {\n display: flex;\n width: 100%;\n resize: none;\n
|
|
252
|
+
"content": "/* Textarea base styles */\n.base {\n display: flex;\n width: 100%;\n resize: none;\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(--textarea-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(--textarea-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n.base::placeholder {\n 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. */\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 cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[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/* Size variants */\n.sizeSm {\n min-height: 4rem;\n padding: 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.sizeMd {\n min-height: 6.25rem;\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.sizeLg {\n min-height: 8rem;\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"
|
|
228
253
|
}
|
|
229
254
|
]
|
|
230
255
|
},
|
|
@@ -250,7 +275,7 @@
|
|
|
250
275
|
{
|
|
251
276
|
"path": "components/ui/checkbox/checkbox.module.css",
|
|
252
277
|
"type": "registry:ui",
|
|
253
|
-
"content": "/* Checkbox root styles\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: 1.125rem box, 4px radius, var(--surface-subtle) background, and\n var(--hairline-strong) border. */\n.root {\n position: relative;\n display: flex;\n width: var(--checkbox-size, 1rem);\n height: var(--checkbox-size, 1rem);\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: var(--checkbox-radius, var(--radius-sm, 0.25rem));\n border: var(--checkbox-
|
|
278
|
+
"content": "/* Checkbox root styles\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: 1.125rem box, 4px radius, var(--surface-subtle) background, and\n var(--hairline-strong) edge.\n\n VI-655: 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. --checkbox-border is the per-component colour override. */\n.root {\n position: relative;\n display: flex;\n width: var(--checkbox-size, 1rem);\n height: var(--checkbox-size, 1rem);\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: var(--checkbox-radius, var(--radius-sm, 0.25rem));\n border: 1px solid transparent;\n background-color: var(--checkbox-bg, transparent);\n /* With the edge switched off an unchecked box would vanish, and the box is\n the control, not decoration. So it takes a fill whose size is derived from\n the switch: calc(max(0px, 1px - width) * 1000) is 0 while the edge is on\n (nothing paints) and covers the box once the edge width drops to 0. */\n background-image: linear-gradient(var(--checkbox-edgeless-bg, var(--control-edge-color, var(--border-default, #e5e7eb))), var(--checkbox-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 transition: outline-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), 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(--checkbox-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:global([data-density=\"editorial\"]) .root {\n width: 1.125rem;\n height: 1.125rem;\n border-radius: 4px;\n outline-color: var(--hairline-strong, #e5e7eb);\n background-color: var(--surface-subtle);\n}\n\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]):not([data-state=\"checked\"]):not([data-state=\"indeterminate\"]) {\n outline-color: var(--border-strong, #d1d5db);\n background-color: transparent;\n }\n}\n\n@media (hover: hover) {\n :global([data-density=\"editorial\"]) .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]):not([data-state=\"checked\"]):not([data-state=\"indeterminate\"]) {\n background-color: var(--surface-subtle);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.root: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.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[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.root[data-state=\"checked\"] {\n outline-color: var(--checkbox-border-checked, var(--interactive-primary-bg, var(--primary, #111827)));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, var(--primary, #111827)));\n background-image: none;\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.root[data-state=\"indeterminate\"] {\n outline-color: var(--checkbox-border-checked, var(--interactive-primary-bg, var(--primary, #111827)));\n background-color: var(--checkbox-bg-checked, var(--interactive-primary-bg, var(--primary, #111827)));\n background-image: none;\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Indicator */\n.indicator {\n display: grid;\n place-content: center;\n color: currentColor;\n transition: none;\n}\n\n.icon {\n width: 0.875rem;\n height: 0.875rem;\n}\n"
|
|
254
279
|
}
|
|
255
280
|
]
|
|
256
281
|
},
|
|
@@ -276,7 +301,7 @@
|
|
|
276
301
|
{
|
|
277
302
|
"path": "components/ui/select/select.module.css",
|
|
278
303
|
"type": "registry:ui",
|
|
279
|
-
"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
|
|
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"
|
|
280
305
|
}
|
|
281
306
|
]
|
|
282
307
|
},
|
|
@@ -302,7 +327,7 @@
|
|
|
302
327
|
{
|
|
303
328
|
"path": "components/ui/switch/switch.module.css",
|
|
304
329
|
"type": "registry:ui",
|
|
305
|
-
"content": "/* Switch root styles */\n.root {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n border-radius: 9999px;\n border: 1px solid transparent;\n background-color: var(--switch-track-bg, var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),
|
|
330
|
+
"content": "/* Switch root styles */\n.root {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n border-radius: 9999px;\n /* VI-655: the border only holds box geometry. The track's resting edge is an\n inset outline on the shared --control-edge-* tokens; it is transparent\n unless a theme sets --switch-edge-color, and --control-edge-width: 0\n removes it with every other control's edge. */\n border: 1px solid transparent;\n background-color: var(--switch-track-bg, var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), 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(--switch-edge-color, transparent);\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n background-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.root: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.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[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.root[data-state=\"checked\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Size variants\n The default size takes its dimensions from the theme (VI-656):\n --switch-track-width / -height, and --switch-knob-inset, the gap between the\n knob and the track's outer edge on every side. The private properties below\n exist only while --switch-knob-inset is set; unset, they are invalid, so the\n knob falls back to its shipped 1rem size and position and the track keeps\n whatever inline padding it had (revert-layer). Set, the track's inline\n padding goes to 0 so the inset alone places the knob. The 1px is the\n geometry border (see .root), which the inset is measured across. */\n.sizeDefault {\n height: var(--switch-track-height, 1.15rem);\n width: var(--switch-track-width, 2rem);\n --_switch-pad: calc(var(--switch-knob-inset) * 0);\n --_switch-knob-size: calc(var(--switch-track-height, 1.15rem) - 2 * var(--switch-knob-inset));\n --_switch-knob-start: calc(var(--switch-knob-inset) - 1px);\n}\n\n/* In visor-base so an unset inset leaves the track's padding to whatever reset\n or browser default applies, and a set inset zeroes it only where no consumer\n rule speaks. */\n@layer visor-base {\n .sizeDefault {\n padding-inline: var(--_switch-pad, revert-layer);\n }\n}\n\n.sizeSm {\n height: 0.875rem;\n width: 1.5rem;\n}\n\n/* Thumb */\n.thumb {\n display: block;\n border-radius: 9999px;\n background-color: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-sm);\n transition: transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n pointer-events: none;\n}\n\n/* Thumb size — derived from parent via CSS nesting */\n.sizeDefault .thumb {\n width: var(--_switch-knob-size, 1rem);\n height: var(--_switch-knob-size, 1rem);\n}\n\n.sizeSm .thumb {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.root[data-state=\"unchecked\"] .thumb {\n transform: translateX(0);\n}\n\n.sizeDefault[data-state=\"unchecked\"] .thumb {\n transform: translateX(var(--_switch-knob-start, 0));\n}\n\n/* The knob's right edge sits the same distance from the track's right edge as\n its left edge does from the left: flush inside the 1px geometry border by\n default (2rem - 1rem - 2px, the shipped 100% - 2px), or the inset when set. */\n.sizeDefault[data-state=\"checked\"] .thumb {\n transform: translateX(calc(var(--switch-track-width, 2rem) - 100% - 2px - var(--_switch-knob-start, 0px)));\n}\n\n.sizeSm[data-state=\"checked\"] .thumb {\n transform: translateX(calc(100% - 2px));\n}\n"
|
|
306
331
|
}
|
|
307
332
|
]
|
|
308
333
|
},
|
|
@@ -328,7 +353,7 @@
|
|
|
328
353
|
{
|
|
329
354
|
"path": "components/ui/field/field.module.css",
|
|
330
355
|
"type": "registry:ui",
|
|
331
|
-
"content": "/* Field */\n.field {\n display: flex;\n width: 100%;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.fieldVertical {\n flex-direction: column;\n}\n\n.fieldHorizontal {\n flex-direction: row;\n align-items: center;\n}\n\n/* Field Label\n * Default: canonical 14px / text-primary. Density axis: editorial tightens\n * to 13px / text-secondary. Switch via data-density=\"editorial\" on any ancestor. */\n.fieldLabel {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, currentColor);\n line-height: 1.4;\n}\n\n:global([data-density=\"editorial\"]) .fieldLabel {\n font-size: 13px;\n color: var(--text-secondary);\n}\n\n/* Field Description\n * Default: canonical 12px / text-secondary. Density axis: editorial tightens\n * to 11px / text-tertiary. */\n.fieldDescription {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-secondary, currentColor);\n line-height: 1.5;\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .fieldDescription {\n font-size: 11px;\n color: var(--text-tertiary);\n}\n\n/* Field Error\n * Default: canonical 14px. Density axis: editorial tightens to 13px. */\n.fieldError {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-error, currentColor);\n}\n\n:global([data-density=\"editorial\"]) .fieldError {\n font-size: 13px;\n}\n\n/* Leading-icon layout — opt-in. Only applied when the consumer passes an\n * `icon` to FieldError, so the default (text-only) error is unchanged. Lays\n * the icon + message out as a centered row; the icon sizes itself in em so it\n * tracks the error's font-size (a 14px WarningCircle at the editorial 13px\n * ramp), matching the prototype's field-error icon sizing (14px). */\n.fieldErrorWithIcon {\n display: flex;\n align-items: center;\n /* 6px gap — no --spacing-1.5 token exists, so the 6px step uses\n calc(--spacing-1 + --spacing-1/2) (the same pattern badge/kbd use). */\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n}\n\n.fieldErrorIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 1.077em;\n line-height: 1;\n}\n\n.fieldErrorList {\n margin-left: var(--spacing-4, 1rem);\n list-style: disc;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n"
|
|
356
|
+
"content": "/* Field */\n.field {\n display: flex;\n width: 100%;\n gap: var(--spacing-2, 0.5rem);\n}\n\n.fieldVertical {\n flex-direction: column;\n}\n\n.fieldHorizontal {\n flex-direction: row;\n align-items: center;\n}\n\n/* Field Label\n * Default: canonical 14px / text-primary. Density axis: editorial tightens\n * to 13px / text-secondary. Switch via data-density=\"editorial\" on any ancestor.\n * A theme sets size, case, tracking and colour through --field-label-* (VI-656);\n * each hook drives both densities. Case and tracking live in visor-base, below. */\n.fieldLabel {\n font-size: var(--field-label-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--font-weight-medium, 500);\n color: var(--field-label-color, var(--text-primary, currentColor));\n line-height: 1.4;\n}\n\n:global([data-density=\"editorial\"]) .fieldLabel {\n font-size: var(--field-label-font-size, 13px);\n color: var(--field-label-color, var(--text-secondary));\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 .fieldLabel {\n text-transform: var(--field-label-text-transform, revert-layer);\n letter-spacing: var(--field-label-letter-spacing, revert-layer);\n }\n}\n\n/* Field Description\n * Default: canonical 12px / text-secondary. Density axis: editorial tightens\n * to 11px / text-tertiary. */\n.fieldDescription {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-secondary, currentColor);\n line-height: 1.5;\n margin: 0;\n}\n\n:global([data-density=\"editorial\"]) .fieldDescription {\n font-size: 11px;\n color: var(--text-tertiary);\n}\n\n/* Field Error\n * Default: canonical 14px. Density axis: editorial tightens to 13px. */\n.fieldError {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-error, currentColor);\n}\n\n:global([data-density=\"editorial\"]) .fieldError {\n font-size: 13px;\n}\n\n/* Leading-icon layout — opt-in. Only applied when the consumer passes an\n * `icon` to FieldError, so the default (text-only) error is unchanged. Lays\n * the icon + message out as a centered row; the icon sizes itself in em so it\n * tracks the error's font-size (a 14px WarningCircle at the editorial 13px\n * ramp), matching the prototype's field-error icon sizing (14px). */\n.fieldErrorWithIcon {\n display: flex;\n align-items: center;\n /* 6px gap — no --spacing-1.5 token exists, so the 6px step uses\n calc(--spacing-1 + --spacing-1/2) (the same pattern badge/kbd use). */\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 2);\n}\n\n.fieldErrorIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 1.077em;\n line-height: 1;\n}\n\n.fieldErrorList {\n margin-left: var(--spacing-4, 1rem);\n list-style: disc;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n}\n"
|
|
332
357
|
}
|
|
333
358
|
]
|
|
334
359
|
},
|
|
@@ -402,7 +427,7 @@
|
|
|
402
427
|
{
|
|
403
428
|
"path": "components/ui/badge/badge.module.css",
|
|
404
429
|
"type": "registry:ui",
|
|
405
|
-
"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.variantDefault {\n background-color: var(--interactive-primary-bg, currentColor);\n color: var(--interactive-primary-text, var(--text-inverse, #f9fafb));\n border-color: transparent;\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, var(--surface-subtle, transparent));\n color: 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(--surface-card, #ffffff);\n}\n\n.variantOutline {\n background-color: transparent;\n color: 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(--surface-error-subtle, transparent);\n color: var(--text-error, currentColor);\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantDestructive {\n background-color: color-mix(in srgb, var(--destructive) 20%, transparent);\n}\n\n.variantSuccess {\n background-color: var(--surface-success-subtle, transparent);\n color: var(--text-success, currentColor);\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantSuccess {\n background-color: color-mix(in srgb, var(--success) 18%, transparent);\n}\n\n.variantWarning {\n background-color: var(--surface-warning-subtle, transparent);\n color: var(--text-warning, currentColor);\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantWarning {\n background-color: color-mix(in srgb, var(--warning) 20%, transparent);\n}\n\n.variantInfo {\n background-color: var(--surface-info-subtle, transparent);\n color: var(--text-info, currentColor);\n border-color: transparent;\n}\n\n:global([data-density=\"editorial\"]) .variantInfo {\n background-color: color-mix(in srgb, var(--info) 18%, transparent);\n}\n\n.variantNeutral {\n background-color: var(--surface-muted, #f3f4f6);\n color: 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(--surface-error-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledSuccess {\n background-color: var(--surface-success-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledWarning {\n background-color: var(--surface-warning-default, transparent);\n color: var(--text-inverse, currentColor);\n border-color: transparent;\n}\n\n.variantFilledInfo {\n background-color: var(--surface-info-default, transparent);\n color: 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(--color-neutral-600, #4b5563);\n color: 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"
|
|
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"
|
|
406
431
|
}
|
|
407
432
|
]
|
|
408
433
|
},
|
|
@@ -449,7 +474,7 @@
|
|
|
449
474
|
{
|
|
450
475
|
"path": "components/ui/chip/chip.module.css",
|
|
451
476
|
"type": "registry:ui",
|
|
452
|
-
"content": "/* Chip base */\n.base {\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 border: var(--chip-border, var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb));\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 border-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 border-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Variant: outlined */\n.variantOutlined {\n background-color: transparent;\n border-color: var(--border-default, #e5e7eb);\n color: var(--text-primary, #111827);\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 border-color: transparent;\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/* 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\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 border-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 border-color: var(--chip-selected-border-color, var(--surface-accent-default, #3b82f6));\n color: var(--chip-selected-text-color, var(--text-link, #2563eb));\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 border-color: transparent;\n color: var(--text-primary);\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 border instead of accent tint.\n * Compose with .selected to override the accent colours. */\n.selectedNeutral {\n background-color: var(--surface-card, #ffffff);\n border-color: var(--border-strong, #d1d5db);\n color: var(--text-primary, #111827);\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 border-color: var(--border-strong, #d1d5db);\n color: var(--text-primary, #111827);\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"
|
|
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"
|
|
453
478
|
}
|
|
454
479
|
]
|
|
455
480
|
},
|
|
@@ -554,7 +579,7 @@
|
|
|
554
579
|
{
|
|
555
580
|
"path": "components/ui/spinner/spinner-orb.tsx",
|
|
556
581
|
"type": "registry:ui",
|
|
557
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {
|
|
582
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport type { OrbState } from \"thinking-orbs\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./spinner.module.css\"\n\n/** The nine thinking-orbs animations. */\nexport type SpinnerOrbState = OrbState\n\n/**\n * thinking-orbs is fetched the first time an orb renders, never before\n * (VI-660). `spinner.tsx` imports this file for every Spinner, so a static\n * import here shipped the canvas engine to every consumer of the ring — a busy\n * button, a \"Saving…\" line — which never draws an orb.\n */\nconst ThinkingOrb = React.lazy(() =>\n import(\"thinking-orbs\").then((m) => ({ default: m.ThinkingOrb }))\n)\n\n/**\n * Spinner size → orb preset. thinking-orbs ships 20 and 64 as hand-tuned\n * designs (not a scale factor); 32 is interpolated between them upstream.\n */\nconst ORB_PX = { xs: 20, sm: 32, md: 64 } as const\n\ninterface OrbInk {\n /** rgb() tint for the dots — the orb keeps its depth ramp on it. */\n color?: string\n /** True when the surrounding text is light, i.e. the orb sits on a dark substrate. */\n dark: boolean\n}\n\nlet probeCtx: CanvasRenderingContext2D | null | undefined\n\n/**\n * Normalise any computed CSS colour (oklch, color-mix, hex, …) to the\n * `rgb()` form thinking-orbs accepts, by painting one pixel.\n */\nfunction toRgb(value: string): [number, number, number] | null {\n if (probeCtx === undefined) {\n const canvas = document.createElement(\"canvas\")\n canvas.width = canvas.height = 1\n probeCtx = canvas.getContext(\"2d\", { willReadFrequently: true })\n }\n if (!probeCtx || !value) return null\n probeCtx.clearRect(0, 0, 1, 1)\n probeCtx.fillStyle = value\n probeCtx.fillRect(0, 0, 1, 1)\n const [r, g, b] = probeCtx.getImageData(0, 0, 1, 1).data\n return [r, g, b]\n}\n\nfunction readInk(host: HTMLElement, probe: HTMLElement): OrbInk | null {\n const text = toRgb(getComputedStyle(host).color)\n const tint = toRgb(getComputedStyle(probe).color)\n if (!text || !tint) return null\n const [r, g, b] = text\n return {\n color: `rgb(${tint.join(\", \")})`,\n dark: (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.5,\n }\n}\n\n/**\n * Resolve the orb's ink from Visor's tokens rather than thinking-orbs' own\n * `auto` detection, which only knows a `.dark`/`.light` class or the OS\n * preference and so misreads Visor's `html:not(.dark)` light mode. Re-reads\n * when the root theme or the OS scheme changes.\n */\nfunction useOrbInk(\n hostRef: React.RefObject<HTMLSpanElement | null>,\n probeRef: React.RefObject<HTMLSpanElement | null>,\n tone: string\n): OrbInk | null {\n const [ink, setInk] = React.useState<OrbInk | null>(null)\n\n React.useLayoutEffect(() => {\n const host = hostRef.current\n const probe = probeRef.current\n if (!host || !probe) return\n const read = () => {\n const next = readInk(host, probe)\n setInk((prev) =>\n prev?.color === next?.color && prev?.dark === next?.dark ? prev : next\n )\n }\n read()\n const observer = new MutationObserver(read)\n for (const el of [document.documentElement, document.body]) {\n observer.observe(el, {\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"data-theme\"],\n })\n }\n const scheme =\n typeof matchMedia === \"undefined\"\n ? null\n : matchMedia(\"(prefers-color-scheme: dark)\")\n scheme?.addEventListener(\"change\", read)\n return () => {\n observer.disconnect()\n scheme?.removeEventListener(\"change\", read)\n }\n }, [hostRef, probeRef, tone])\n\n return ink\n}\n\nexport interface SpinnerOrbProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"color\"> {\n state: SpinnerOrbState\n size: \"xs\" | \"sm\" | \"md\"\n tone: \"default\" | \"primary\"\n}\n\nconst SpinnerOrb = React.forwardRef<HTMLSpanElement, SpinnerOrbProps>(\n ({ className, style, state, size, tone, children, ...props }, ref) => {\n const hostRef = React.useRef<HTMLSpanElement | null>(null)\n const probeRef = React.useRef<HTMLSpanElement | null>(null)\n const ink = useOrbInk(hostRef, probeRef, tone)\n const px = ORB_PX[size]\n\n const setRef = React.useCallback(\n (node: HTMLSpanElement | null) => {\n hostRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n return (\n <span\n ref={setRef}\n data-slot=\"spinner\"\n data-variant=\"orb\"\n data-orb={state}\n data-size={size}\n data-tone={tone}\n className={cn(styles.orb, className)}\n style={{ width: px, height: px, ...style }}\n {...props}\n >\n <span\n ref={probeRef}\n className={cn(\n styles.orbInk,\n tone === \"primary\" && styles.orbInkPrimary\n )}\n />\n {/* The box is sized inline, so it holds still on hydrate and while\n thinking-orbs loads; the canvas mounts into it once it arrives. */}\n <React.Suspense fallback={null}>\n <ThinkingOrb\n state={state}\n size={px}\n theme={ink ? (ink.dark ? \"dark\" : \"light\") : \"auto\"}\n color={ink?.color}\n role=\"presentation\"\n aria-hidden=\"true\"\n />\n </React.Suspense>\n {children}\n </span>\n )\n }\n)\nSpinnerOrb.displayName = \"SpinnerOrb\"\n\nexport { SpinnerOrb }\n"
|
|
558
583
|
},
|
|
559
584
|
{
|
|
560
585
|
"path": "components/ui/spinner/spinner.module.css",
|
|
@@ -868,6 +893,31 @@
|
|
|
868
893
|
}
|
|
869
894
|
]
|
|
870
895
|
},
|
|
896
|
+
{
|
|
897
|
+
"name": "save-status",
|
|
898
|
+
"type": "registry:ui",
|
|
899
|
+
"description": "Autosave readout — Saved / Saving / Unsaved / Couldn't save in a fixed-width slot that never reflows its row, with a retry mark after a failed save. aria-live polite; draws no edge. Pairs with the use-autosave hook.",
|
|
900
|
+
"category": "feedback",
|
|
901
|
+
"dependencies": [
|
|
902
|
+
"@phosphor-icons/react",
|
|
903
|
+
"@loworbitstudio/visor-core"
|
|
904
|
+
],
|
|
905
|
+
"registryDependencies": [
|
|
906
|
+
"utils"
|
|
907
|
+
],
|
|
908
|
+
"files": [
|
|
909
|
+
{
|
|
910
|
+
"path": "components/ui/save-status/save-status.tsx",
|
|
911
|
+
"type": "registry:ui",
|
|
912
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowClockwise } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./save-status.module.css\"\n\n/** The states `useAutosave` reports. */\nexport type SaveStatusState = \"saved\" | \"saving\" | \"unsaved\" | \"refused\"\n\nconst DEFAULT_LABELS: Record<SaveStatusState, string> = {\n saved: \"Saved\",\n saving: \"Saving\",\n unsaved: \"Unsaved\",\n refused: \"Couldn't save\",\n}\n\nexport interface SaveStatusProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Where the value stands — pass `useAutosave(...).status`. */\n status: SaveStatusState\n /** Shows a retry mark while `status` is `refused`. Pass `useAutosave(...).retry`. */\n onRetry?: () => void\n /** Override any of the four labels (e.g. to translate them). */\n labels?: Partial<Record<SaveStatusState, string>>\n /** Accessible name of the retry mark. Defaults to \"Retry saving\". */\n retryLabel?: string\n}\n\n/**\n * The autosave readout: status text in a fixed-width slot, so the row it\n * sits in never reflows as the state changes. It is status text, not a\n * button; the only control is the retry mark, shown after a failed save.\n * It draws no edge.\n */\nconst SaveStatus = React.forwardRef<HTMLSpanElement, SaveStatusProps>(\n ({ status, onRetry, labels, retryLabel = \"Retry saving\", className, ...props }, ref) => {\n const label = labels?.[status] ?? DEFAULT_LABELS[status]\n return (\n <span\n ref={ref}\n data-slot=\"save-status\"\n data-state={status}\n className={cn(styles.root, className)}\n {...props}\n >\n <span data-slot=\"save-status-text\" className={styles.text} role=\"status\" aria-live=\"polite\">\n {label}\n </span>\n {status === \"refused\" && onRetry ? (\n <button\n type=\"button\"\n data-slot=\"save-status-retry\"\n className={styles.retry}\n aria-label={retryLabel}\n onClick={onRetry}\n >\n <ArrowClockwise aria-hidden=\"true\" />\n </button>\n ) : null}\n </span>\n )\n }\n)\nSaveStatus.displayName = \"SaveStatus\"\n\nexport { SaveStatus }\n"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
"path": "components/ui/save-status/save-status.module.css",
|
|
916
|
+
"type": "registry:ui",
|
|
917
|
+
"content": "/* SaveStatus (VI-657)\n *\n * A readout, not a control: status text in a fixed-width slot, so the row\n * never reflows as Saved / Saving / Unsaved / Couldn't save swap in. It draws\n * no edge at all (no border, no outline, no box). The retry mark is the\n * editor's icon mark (VI-659): the icon Button's size, pad and ink, with its\n * hit target on ::before so it adds no height to the row. */\n.root {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n width: var(--save-status-width, 7.5rem);\n /* Tall enough for the retry mark in every state, so it adds no height. */\n min-height: var(--button-icon-size, 1rem);\n font-size: var(--font-size-xs, 0.75rem);\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Family, case and tracking are inherited properties, so any unlayered\n fallback would override something: `inherit` beats a layered utility,\n `revert-layer` drops an unlayered consumer rule. In visor-base, the lowest\n Visor layer, every consumer rule still wins, and when a token is unset\n revert-layer leaves the readout as if these declarations were absent. */\n@layer visor-base {\n .root {\n font-family: var(--save-status-font-family, revert-layer);\n text-transform: var(--save-status-text-transform, revert-layer);\n letter-spacing: var(--save-status-letter-spacing, revert-layer);\n }\n}\n\n.root[data-state=\"saved\"] {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.root[data-state=\"refused\"] {\n color: var(--text-error, #b91c1c);\n}\n\n/* A longer custom label (a translation) truncates inside the slot rather\n than painting past it. */\n.text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.retry {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.retry::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.retry > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .retry:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.retry:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
918
|
+
}
|
|
919
|
+
]
|
|
920
|
+
},
|
|
871
921
|
{
|
|
872
922
|
"name": "scroll-area",
|
|
873
923
|
"type": "registry:ui",
|
|
@@ -1621,7 +1671,7 @@
|
|
|
1621
1671
|
{
|
|
1622
1672
|
"path": "components/ui/file-upload/file-upload.module.css",
|
|
1623
1673
|
"type": "registry:ui",
|
|
1624
|
-
"content": "/* File Upload */\n.wrapper {\n position: relative;\n}\n\n.root {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n
|
|
1674
|
+
"content": "/* File Upload */\n.wrapper {\n position: relative;\n}\n\n.root {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n /* VI-655: the 2px border only holds box geometry. The dashed edge is drawn\n on ::after (below) so the root's own outline stays free for the focus\n ring. */\n border: 2px solid transparent;\n border-radius: var(--radius-lg, 0.5rem);\n padding: var(--spacing-8, 2rem);\n cursor: pointer;\n transition: background-color var(--motion-duration-fast, 150ms) var(--motion-easing-default, ease);\n}\n\n/* The drop target's dashed edge: an inset outline over the border box, on the\n shared drop-edge tokens at 2x weight. --control-edge-width: 0 removes it\n with every other control's edge; --control-drop-edge-width brings the dashed\n edges back on their own. */\n.root::after {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: inherit;\n outline: calc(2 * var(--control-drop-edge-width, var(--control-edge-width, 1px))) var(--control-drop-edge-style, dashed) var(--control-edge-color, var(--border-default, #e5e7eb));\n outline-offset: calc(-2 * var(--control-drop-edge-width, var(--control-edge-width, 1px)));\n pointer-events: none;\n transition: outline-color var(--motion-duration-fast, 150ms) var(--motion-easing-default, ease);\n}\n\n@media (hover: hover) {\n .root:hover:not(.disabled) {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .root:hover:not(.disabled)::after {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n.root: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.dragging {\n background-color: var(--interactive-primary-bg-subtle, #eff6ff);\n}\n\n.dragging::after {\n outline-color: var(--interactive-primary-bg, #3b82f6);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.defaultContent {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n text-align: center;\n}\n\n.icon {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.text {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-secondary, #6b7280);\n}\n\n.hint {\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-tertiary, #9ca3af);\n}\n"
|
|
1625
1675
|
}
|
|
1626
1676
|
]
|
|
1627
1677
|
},
|
|
@@ -1989,6 +2039,33 @@
|
|
|
1989
2039
|
}
|
|
1990
2040
|
]
|
|
1991
2041
|
},
|
|
2042
|
+
{
|
|
2043
|
+
"name": "tin-input",
|
|
2044
|
+
"type": "registry:ui",
|
|
2045
|
+
"description": "A write-only, masked 9-digit TIN/EIN input with a last-four echo that never re-displays the value. Digits never enter the DOM; password-manager prompts are suppressed.",
|
|
2046
|
+
"category": "form",
|
|
2047
|
+
"dependencies": [
|
|
2048
|
+
"@loworbitstudio/visor-core"
|
|
2049
|
+
],
|
|
2050
|
+
"registryDependencies": [
|
|
2051
|
+
"utils",
|
|
2052
|
+
"input",
|
|
2053
|
+
"button",
|
|
2054
|
+
"field"
|
|
2055
|
+
],
|
|
2056
|
+
"files": [
|
|
2057
|
+
{
|
|
2058
|
+
"path": "components/ui/tin-input/tin-input.tsx",
|
|
2059
|
+
"type": "registry:ui",
|
|
2060
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Input } from \"../input/input\"\nimport { Button } from \"../button/button\"\nimport { FieldDescription, FieldError } from \"../field/field\"\nimport styles from \"./tin-input.module.css\"\n\nexport type TinKind = \"ssn\" | \"ein\"\n\nexport interface TinInputProps\n extends Pick<\n React.InputHTMLAttributes<HTMLInputElement>,\n | \"id\"\n | \"aria-label\"\n | \"aria-labelledby\"\n | \"aria-describedby\"\n | \"aria-required\"\n > {\n /** Display grouping: SSN `###-##-####` or EIN `##-#######`. Both are 9 digits. */\n kind: TinKind\n /**\n * Called with the 9 digits once the entry is complete, and with `null` while\n * it is incomplete or cleared. The only place the digits ever leave the\n * component.\n */\n onValueChange: (digits: string | null) => void\n /**\n * Last four digits of a TIN already on file. Renders the on-file echo with a\n * Replace button. A new value brings the echo back after a Replace.\n */\n lastFour?: string\n /** Called when Replace swaps the on-file echo for an empty field. */\n onReplace?: () => void\n disabled?: boolean\n /** Error message, rendered through the Visor field convention and bound by `aria-describedby`. */\n error?: string\n /** Size of the field and the Replace button. `lg` meets 44px touch targets. */\n size?: \"sm\" | \"md\" | \"lg\"\n className?: string\n}\n\nconst TIN_LENGTH = 9\nconst MASK = \"•\"\nconst GROUPS: Record<TinKind, readonly number[]> = { ssn: [3, 2, 4], ein: [2, 7] }\nconst PASTE_TOO_LONG = \"A taxpayer ID has 9 digits. That paste has more.\"\n\n/** Lays characters out in the kind's grouping, adding a dash only once the next group starts. */\nfunction group(kind: TinKind, chars: string): string {\n let out = \"\"\n let start = 0\n for (const size of GROUPS[kind]) {\n if (start >= chars.length) break\n out += (start > 0 ? \"-\" : \"\") + chars.slice(start, start + size)\n start += size\n }\n return out\n}\n\n/** Maps a caret offset in the masked value to an index into the digits. */\nfunction digitIndex(display: string, offset: number): number {\n return display.slice(0, offset).replace(/-/g, \"\").length\n}\n\n/**\n * A write-only, masked 9-digit TIN field. The input's value is only ever the\n * mask: every edit is intercepted before it reaches the DOM and applied to\n * digits held in memory, so the digits are never in `input.value` or in any\n * attribute. When focus moves elsewhere on the page, a complete entry shows\n * only the grouping mask and the last four, and the digits are dropped;\n * focusing it again clears it. Switching apps and back keeps the entry.\n */\nconst TinInput = React.forwardRef<HTMLInputElement, TinInputProps>(\n (\n {\n kind,\n onValueChange,\n lastFour,\n onReplace,\n disabled,\n error,\n size,\n className,\n id,\n \"aria-describedby\": ariaDescribedBy,\n ...aria\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n const digitsRef = React.useRef(\"\")\n // `caret` is a digit index. A fresh object per edit re-renders even when\n // the digit count is unchanged, so the caret is always put back.\n const [entry, setEntry] = React.useState({ count: 0, caret: 0 })\n const [committedLastFour, setCommittedLastFour] = React.useState<string | null>(null)\n const [pasteError, setPasteError] = React.useState<string | null>(null)\n const [replacing, setReplacing] = React.useState(false)\n // A new `lastFour` is a new TIN on file, so the echo comes back after a Replace.\n const [prevLastFour, setPrevLastFour] = React.useState(lastFour)\n if (lastFour !== prevLastFour) {\n setPrevLastFour(lastFour)\n setReplacing(false)\n }\n // Until hydration nothing intercepts keystrokes, so the server-rendered\n // field refuses input rather than showing the digits typed into it.\n const [hydrated, setHydrated] = React.useState(false)\n React.useEffect(() => setHydrated(true), [])\n // `undefined` until the first emission, so the first keystroke reports `null`.\n const lastEmittedNull = React.useRef<boolean | undefined>(undefined)\n\n const baseId = React.useId()\n const hintId = `${baseId}-hint`\n const errorId = `${baseId}-error`\n const echoId = `${baseId}-echo`\n\n const onFileLastFour = lastFour?.replace(/\\D/g, \"\").slice(-4)\n const onFile = Boolean(onFileLastFour) && !replacing\n const errorText = pasteError ?? error\n\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n const emit = (digits: string | null) => {\n if (digits === null && lastEmittedNull.current === true) return\n lastEmittedNull.current = digits === null\n onValueChange(digits)\n }\n\n const apply = (digits: string, caret: number) => {\n digitsRef.current = digits\n setPasteError(null)\n setEntry({ count: digits.length, caret })\n emit(digits.length === TIN_LENGTH ? digits : null)\n }\n\n /** Inserts the digits of `text` over the digit range `[from, to)`. */\n const insert = (text: string, from: number, to: number, isPaste: boolean) => {\n const typed = text.replace(/\\D/g, \"\")\n if (!typed) return\n const current = digitsRef.current\n const next = current.slice(0, from) + typed + current.slice(to)\n if (next.length > TIN_LENGTH) {\n // Typing past nine is ignored; a paste that would overflow is refused\n // whole rather than truncated into a different number.\n if (isPaste) setPasteError(PASTE_TOO_LONG)\n return\n }\n apply(next, from + typed.length)\n }\n\n const selection = (input: HTMLInputElement) => {\n const display = input.value\n return {\n from: digitIndex(display, input.selectionStart ?? display.length),\n to: digitIndex(display, input.selectionEnd ?? display.length),\n }\n }\n\n const handleBeforeInput = (event: InputEvent) => {\n event.preventDefault()\n const input = event.currentTarget as HTMLInputElement\n let { from, to } = selection(input)\n const type = event.inputType\n if (type.startsWith(\"insert\")) {\n const text = event.data ?? event.dataTransfer?.getData(\"text\") ?? \"\"\n insert(text, from, to, type !== \"insertText\")\n return\n }\n if (!type.startsWith(\"delete\")) return\n if (from === to) {\n // Digits have no words or lines, so those deletions take everything\n // on that side of the caret.\n if (type === \"deleteContentBackward\") from = Math.max(0, from - 1)\n else if (type === \"deleteContentForward\") to = Math.min(digitsRef.current.length, to + 1)\n else if (type.endsWith(\"Backward\")) from = 0\n else if (type.endsWith(\"Forward\")) to = digitsRef.current.length\n }\n if (from === to) return\n const current = digitsRef.current\n apply(current.slice(0, from) + current.slice(to), from)\n }\n const beforeInputRef = React.useRef(handleBeforeInput)\n beforeInputRef.current = handleBeforeInput\n\n // React's `onBeforeInput` is a synthetic event without `inputType`, and it\n // does not see deletions, so the native event is attached directly.\n React.useEffect(() => {\n const input = inputRef.current\n if (!input || onFile) return\n const listener = (event: Event) => beforeInputRef.current(event as InputEvent)\n input.addEventListener(\"beforeinput\", listener)\n return () => input.removeEventListener(\"beforeinput\", listener)\n }, [onFile])\n\n React.useLayoutEffect(() => {\n const input = inputRef.current\n if (!input || input !== document.activeElement) return\n const offset = group(kind, MASK.repeat(entry.caret)).length\n input.setSelectionRange(offset, offset)\n }, [entry, kind])\n\n React.useEffect(() => {\n if (replacing) inputRef.current?.focus()\n }, [replacing])\n\n const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {\n event.preventDefault()\n const { from, to } = selection(event.currentTarget)\n insert(event.clipboardData.getData(\"text\"), from, to, true)\n }\n\n // Reached only by input the beforeinput listener could not cancel (IME\n // composition). The mask holds no digits while editing, so any digit in\n // the value was just typed; keep them, and React restores the mask.\n const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = event.target.value\n const kept = value.split(MASK).length - 1\n const next = digitsRef.current.slice(0, kept) + value.replace(/\\D/g, \"\")\n if (next.length <= TIN_LENGTH && next !== digitsRef.current) apply(next, next.length)\n }\n\n const handleBlur = () => {\n if (digitsRef.current.length !== TIN_LENGTH) return\n // Switching apps blurs the field too, and coming back refocuses it, which\n // would clear a committed entry. Commit only when focus moved within the page.\n if (!document.hasFocus()) return\n setCommittedLastFour(digitsRef.current.slice(-4))\n digitsRef.current = \"\"\n }\n\n const clearEntry = () => {\n digitsRef.current = \"\"\n setCommittedLastFour(null)\n setPasteError(null)\n setEntry({ count: 0, caret: 0 })\n emit(null)\n }\n\n const handleFocus = () => {\n if (committedLastFour !== null) clearEntry()\n }\n\n const handleReplace = () => {\n // After `lastFour` changes, Replace is offered again; it still starts empty.\n clearEntry()\n setReplacing(true)\n onReplace?.()\n }\n\n const describedBy = (...ids: Array<string | false | undefined>) =>\n [ariaDescribedBy, ...ids].filter(Boolean).join(\" \") || undefined\n\n const errorNode = errorText ? <FieldError id={errorId}>{errorText}</FieldError> : null\n\n if (onFile) {\n return (\n <div data-slot=\"tin-input\" className={cn(styles.root, className)}>\n <div className={styles.onFile}>\n <Input\n ref={setRefs}\n id={id}\n size={size}\n className={styles.onFileValue}\n value={`On file · ending ${onFileLastFour}`}\n readOnly\n disabled={disabled}\n passwordManagers=\"ignore\"\n autoComplete=\"off\"\n aria-invalid={errorText ? true : undefined}\n aria-describedby={describedBy(errorText && errorId)}\n {...aria}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size={size}\n disabled={disabled}\n onClick={handleReplace}\n >\n Replace\n </Button>\n </div>\n {errorNode}\n </div>\n )\n }\n\n const value =\n committedLastFour !== null\n ? group(kind, MASK.repeat(TIN_LENGTH - 4) + committedLastFour)\n : group(kind, MASK.repeat(entry.count))\n\n return (\n <div data-slot=\"tin-input\" className={cn(styles.root, className)}>\n <Input\n ref={setRefs}\n id={id}\n size={size}\n className={styles.input}\n type=\"text\"\n value={value}\n onChange={handleChange}\n onPaste={handlePaste}\n onBlur={handleBlur}\n onFocus={handleFocus}\n readOnly={!hydrated}\n disabled={disabled}\n passwordManagers=\"ignore\"\n // No `pattern`: the value is always the mask, so `[0-9]*` would hold\n // the field :invalid and block a native form submit. `inputMode`\n // already raises the numeric keypad on every current mobile browser.\n inputMode=\"numeric\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n autoCapitalize=\"off\"\n spellCheck={false}\n enterKeyHint=\"next\"\n aria-invalid={errorText ? true : undefined}\n aria-describedby={describedBy(\n hintId,\n committedLastFour !== null && echoId,\n errorText && errorId\n )}\n {...aria}\n />\n {committedLastFour !== null && (\n <span id={echoId} className={styles.srOnly}>\n ending in {committedLastFour}\n </span>\n )}\n <FieldDescription id={hintId}>9 digits</FieldDescription>\n {errorNode}\n </div>\n )\n }\n)\nTinInput.displayName = \"TinInput\"\n\nexport { TinInput }\n"
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
"path": "components/ui/tin-input/tin-input.module.css",
|
|
2064
|
+
"type": "registry:ui",
|
|
2065
|
+
"content": "/* TIN input — the entry field, or the on-file echo with its Replace button.\n * The gap matches Field's, so control, hint and error keep the field rhythm\n * and order whether or not the component sits inside a Field. */\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n width: 100%;\n min-width: 0;\n container: tin-input / inline-size;\n}\n\n/* Equal-width digits keep the grouping mask steady as it fills. */\n.input {\n font-variant-numeric: tabular-nums;\n}\n\n.onFile {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n\n/* The echo is read-only: secondary ink says it is not an editable value. */\n.onFileValue {\n flex: 1;\n color: var(--text-secondary, #6b7280);\n font-variant-numeric: tabular-nums;\n}\n\n/* Too narrow for the lg echo and Replace side by side without clipping the\n * last four (a phone-width form), so Replace drops under the echo. 20rem is\n * intentional: the narrowest row that holds the lg pair whole. */\n@container tin-input (max-width: 20rem) {\n .onFile {\n flex-direction: column;\n align-items: flex-start;\n }\n\n .onFileValue {\n flex: none;\n }\n}\n\n/* Carries \"ending in 1234\" to assistive tech in place of the bullets. */\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"
|
|
2066
|
+
}
|
|
2067
|
+
]
|
|
2068
|
+
},
|
|
1992
2069
|
{
|
|
1993
2070
|
"name": "tag-input",
|
|
1994
2071
|
"type": "registry:ui",
|
|
@@ -2010,7 +2087,7 @@
|
|
|
2010
2087
|
{
|
|
2011
2088
|
"path": "components/ui/tag-input/tag-input.module.css",
|
|
2012
2089
|
"type": "registry:ui",
|
|
2013
|
-
"content": "/* Tag input wrapper — styled like an Input */\n.wrapper {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n min-height: 2.25rem;\n width: 100%;\n border-radius: var(--radius-md, 0.375rem);\n border: 1px solid var(--tag-input-border, var(--border-default, #e5e7eb));\n background-color: var(--tag-input-bg, var(--surface-interactive-default, #f9fafb));\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n cursor: text;\n transition:
|
|
2090
|
+
"content": "/* Tag input wrapper — styled like an Input */\n.wrapper {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n min-height: 2.25rem;\n width: 100%;\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 outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--tag-input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n background-color: var(--tag-input-bg, var(--surface-interactive-default, #f9fafb));\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n cursor: text;\n transition: 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@media (hover: hover) {\n .wrapper:hover:not(.wrapperFocused):not(.wrapperDisabled) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus draws at --control-state-edge-width, which the edge switch does not\n touch. */\n.wrapperFocused {\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.wrapperDisabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* Individual tag chip */\n.tag {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n padding: calc(var(--spacing-1, 0.25rem) / 4) var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full, 9999px);\n background-color: var(--surface-muted, #f3f4f6);\n /* The tag chip's edge follows the same switch as the field around it. */\n border: 1px solid transparent;\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 font-size: var(--font-size-xs, 0.75rem);\n line-height: 1.5;\n color: var(--text-primary, #111827);\n}\n\n.tagLabel {\n white-space: nowrap;\n}\n\n.tagRemove {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 0.875rem;\n height: 0.875rem;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--text-secondary, #9ca3af);\n cursor: pointer;\n border-radius: var(--radius-full, 9999px);\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .tagRemove:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.tagRemove svg {\n width: 0.625rem;\n height: 0.625rem;\n}\n\n/* Inline text input */\n.input {\n flex: 1;\n min-width: 4rem;\n height: 1.5rem;\n padding: 0;\n border: none;\n background: transparent;\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--text-primary, #111827);\n outline: none;\n}\n\n.input::placeholder {\n color: var(--text-secondary, #9ca3af);\n}\n"
|
|
2014
2091
|
}
|
|
2015
2092
|
]
|
|
2016
2093
|
},
|
|
@@ -2572,7 +2649,7 @@
|
|
|
2572
2649
|
{
|
|
2573
2650
|
"path": "components/ui/empty-state/empty-state.module.css",
|
|
2574
2651
|
"type": "registry:ui",
|
|
2575
|
-
"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
|
|
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"
|
|
2576
2653
|
}
|
|
2577
2654
|
]
|
|
2578
2655
|
},
|
|
@@ -3323,6 +3400,19 @@
|
|
|
3323
3400
|
}
|
|
3324
3401
|
]
|
|
3325
3402
|
},
|
|
3403
|
+
{
|
|
3404
|
+
"name": "use-autosave",
|
|
3405
|
+
"type": "registry:hook",
|
|
3406
|
+
"description": "Autosave for one value: debounces the write (600ms), runs one write at a time so the newest value wins, flushes a pending change on pagehide, tab hide and unmount, and reports saved · saving · unsaved · refused with retry().",
|
|
3407
|
+
"category": "hooks",
|
|
3408
|
+
"files": [
|
|
3409
|
+
{
|
|
3410
|
+
"path": "hooks/use-autosave.ts",
|
|
3411
|
+
"type": "registry:hook",
|
|
3412
|
+
"content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\n/** Where an autosaved value stands. */\nexport type AutosaveStatus = \"saved\" | \"saving\" | \"unsaved\" | \"refused\"\n\n/** Passed to `write`: whether the page is being left (use `keepalive` or `sendBeacon`). */\nexport interface AutosaveWriteContext {\n leaving: boolean\n}\n\nexport interface UseAutosaveOptions<T> {\n /** Debounce, in milliseconds, after the last change. Defaults to 600. */\n delay?: number\n /** Return false to refuse a value: it is never written, and the hook goes to `refused`. */\n validate?: (value: T) => boolean\n /** Decides whether a value is already saved. Defaults to `Object.is`. */\n isEqual?: (a: T, b: T) => boolean\n}\n\nexport interface UseAutosaveResult {\n status: AutosaveStatus\n /** Why the hook is `refused`: `validate` rejected the value, or `write` failed. */\n reason: \"invalid\" | \"failed\" | null\n /** What `write` threw or rejected with, while `reason` is `\"failed\"`. */\n error: unknown\n /** Write the current value now. */\n retry: () => void\n /** Write a pending change now instead of waiting for the debounce. */\n flush: () => void\n}\n\ninterface State {\n status: AutosaveStatus\n reason: UseAutosaveResult[\"reason\"]\n error: unknown\n}\n\nconst SAVED: State = { status: \"saved\", reason: null, error: undefined }\nconst SAVING: State = { status: \"saving\", reason: null, error: undefined }\nconst UNSAVED: State = { status: \"unsaved\", reason: null, error: undefined }\n\ninterface Live<T> {\n value: T\n write: (value: T, context: AutosaveWriteContext) => unknown\n validate?: (value: T) => boolean\n isEqual: (a: T, b: T) => boolean\n delay: number\n}\n\n/**\n * The autosave state machine. Built once per hook instance; it reads the\n * latest value and callbacks from `live`, so timers and page listeners never\n * see a stale render.\n */\nfunction createAutosave<T>(live: Live<T>, setState: (state: State) => void) {\n /** The last value `write` confirmed. The first render's value counts as saved. */\n let saved = live.value\n /** The value of the newest write started. */\n let sent = live.value\n /** Id of the newest write started; responses to older ones are ignored. */\n let seq = 0\n let inFlight = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n /** A debounced write is waiting for the one in flight. */\n let queued = false\n let mounted = false\n\n const set = (next: State) => {\n if (mounted) setState(next)\n }\n /** The value the store will hold once in-flight writes settle. */\n const target = () => (inFlight > 0 ? sent : saved)\n const clearTimer = () => {\n if (timer !== null) clearTimeout(timer)\n timer = null\n }\n\n function start(next: T, leaving: boolean) {\n const id = ++seq\n sent = next\n inFlight++\n set(SAVING)\n\n const settle = (ok: boolean, error?: unknown) => {\n inFlight--\n if (id !== seq) return // a newer write owns the state\n if (ok) saved = next\n if (queued) {\n queued = false\n attempt(false, true)\n } else if (timer !== null) {\n set(UNSAVED)\n } else if (!ok) {\n set({ status: \"refused\", reason: \"failed\", error })\n } else {\n set(live.isEqual(live.value, saved) ? SAVED : UNSAVED)\n }\n }\n\n let result: unknown\n try {\n result = live.write(next, { leaving })\n } catch (error) {\n settle(false, error)\n return\n }\n Promise.resolve(result).then(\n () => settle(true),\n (error: unknown) => settle(false, error)\n )\n }\n\n /**\n * Validate and write the current value, unless the store already has it.\n * `serialize` makes it wait for a write in flight; leaving the page does not.\n */\n function attempt(leaving: boolean, serialize: boolean, force = false) {\n const next = live.value\n if (!force && live.isEqual(next, target())) {\n set(inFlight > 0 ? SAVING : SAVED)\n return\n }\n if (live.validate && !live.validate(next)) {\n set({ status: \"refused\", reason: \"invalid\", error: undefined })\n return\n }\n if (serialize && inFlight > 0) {\n queued = true\n set(UNSAVED)\n return\n }\n start(next, leaving)\n }\n\n /** Write a waiting change now, without waiting for a write in flight. */\n const flushPending = (leaving: boolean) => {\n if (timer === null && !queued) return\n clearTimer()\n queued = false\n attempt(leaving, false)\n }\n\n return {\n /** A new value: schedule a write, or cancel one if the store already has it. */\n change() {\n clearTimer()\n if (live.isEqual(live.value, target())) {\n queued = false\n set(inFlight > 0 ? SAVING : SAVED)\n return\n }\n set(UNSAVED)\n timer = setTimeout(() => {\n timer = null\n attempt(false, true)\n }, live.delay)\n },\n retry() {\n clearTimer()\n queued = false\n attempt(false, true, true)\n },\n flush: () => flushPending(false),\n /** Page listeners, and the unmount flush. */\n mount() {\n mounted = true\n const onPageHide = () => flushPending(true)\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") flushPending(true)\n }\n const onBeforeUnload = (event: BeforeUnloadEvent) => {\n flushPending(true)\n if (inFlight > 0) {\n event.preventDefault()\n // Chrome and Safari still read returnValue to show the prompt.\n event.returnValue = \"\"\n }\n }\n window.addEventListener(\"pagehide\", onPageHide)\n window.addEventListener(\"beforeunload\", onBeforeUnload)\n document.addEventListener(\"visibilitychange\", onVisibility)\n return () => {\n window.removeEventListener(\"pagehide\", onPageHide)\n window.removeEventListener(\"beforeunload\", onBeforeUnload)\n document.removeEventListener(\"visibilitychange\", onVisibility)\n mounted = false\n flushPending(false)\n }\n },\n }\n}\n\n/**\n * Autosave for one value. Changes are written through `write` after `delay`\n * ms of quiet, with no Save button.\n *\n * - One write at a time. A change made while a write is in flight waits for\n * it, then only the newest value is written, so the store sees edits in\n * order. A response from an older write never overrides a newer state.\n * - Nothing pending is lost: a waiting change is written at once on\n * `pagehide`, when the tab is hidden, on `beforeunload` and on unmount.\n * `beforeunload` asks the user to stay only while a write is in flight.\n * - `validate` belongs to the caller. A value it refuses is never written,\n * stays in the control, and puts the hook in `refused`.\n *\n * The value passed on the first render is treated as already saved. Pass a\n * stable value (or `isEqual`) for objects, or every render counts as a change.\n */\nexport function useAutosave<T>(\n value: T,\n write: (value: T, context: AutosaveWriteContext) => unknown,\n options: UseAutosaveOptions<T> = {}\n): UseAutosaveResult {\n const { delay = 600, validate, isEqual = Object.is } = options\n const [state, setState] = useState<State>(SAVED)\n const live = useRef<Live<T>>({ value, write, validate, isEqual, delay }).current\n live.value = value\n live.write = write\n live.validate = validate\n live.isEqual = isEqual\n live.delay = delay\n const [autosave] = useState(() => createAutosave(live, setState))\n\n useEffect(() => autosave.mount(), [autosave])\n useEffect(() => autosave.change(), [autosave, value])\n\n return { status: state.status, reason: state.reason, error: state.error, retry: autosave.retry, flush: autosave.flush }\n}\n"
|
|
3413
|
+
}
|
|
3414
|
+
]
|
|
3415
|
+
},
|
|
3326
3416
|
{
|
|
3327
3417
|
"name": "use-click-outside",
|
|
3328
3418
|
"type": "registry:hook",
|
|
@@ -5176,7 +5266,7 @@
|
|
|
5176
5266
|
{
|
|
5177
5267
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
5178
5268
|
"type": "registry:devtool",
|
|
5179
|
-
"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 \"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 \"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 \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\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"
|
|
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"
|
|
5180
5270
|
}
|
|
5181
5271
|
]
|
|
5182
5272
|
},
|