@loworbitstudio/visor 1.31.0 → 1.34.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 +7 -1
- package/dist/index.js +81 -2
- package/dist/registry.json +47 -21
- package/dist/visor-manifest.json +256 -7
- package/package.json +2 -2
package/dist/registry.json
CHANGED
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
{
|
|
174
174
|
"path": "components/ui/button/button.module.css",
|
|
175
175
|
"type": "registry:ui",
|
|
176
|
-
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n /* VI-655: the border only holds box geometry and is always transparent. The\n faces that draw an edge (outline, gated, dlg ghost) draw it on ::after —\n see \"Edged faces\" below — so the button's own outline stays free for the\n focus ring. */\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Label (VI-682; shared spec: the label-centering doc under docs/). Bare text is wrapped in a block span whose line box is trimmed\n to the capital-height band, so the flex container centres the ink and not the\n font's ascent/descent (which put Product Sans ~1px high in a 40px button).\n Without text-box (Firefox) the span is a plain block and rendering is exactly\n what it was before. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n\n /* The compact dlg size is height:auto, so trimming would shrink its box. */\n .sizeDlg .label {\n text-box: normal;\n }\n}\n\n/* Blink only. Blink snaps the text baseline to a whole CSS px, which leaves the\n trimmed ink up to half a pixel off for fractional cap heights (12px type). The\n baseline lands at (box + cap) / 2; this hands back what the snap took. WebKit\n positions the baseline exactly, so correcting there would overshoot. The\n -apple-pay-button-style probe is the WebKit-only property that tells them apart. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n\n .sizeDlg .label {\n transform: none;\n }\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Edged faces (VI-655). The resting edge is an inset outline over the border\n box, on the shared --control-edge-* tokens, so --control-edge-width: 0 on any\n ancestor removes it with no layout shift. Each face sets only the colour, in\n the same source position its border-color used to hold. */\n.variantOutline,\n.base[data-gated=\"true\"],\n.sizeDlg.variantGhost {\n position: relative;\n}\n\n.variantOutline::after,\n.base[data-gated=\"true\"]::after,\n.sizeDlg.variantGhost::after {\n content: \"\";\n position: absolute;\n inset: -1px;\n border-radius: inherit;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) transparent;\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.variantOutline::after {\n outline-color: var(--control-edge-color, var(--border-strong, #e5e7eb));\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n.base[data-gated=\"true\"]::after {\n outline-color: var(--control-edge-color, var(--border-default, #e5e7eb));\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values.\n\n Type and rounding are theme hooks (VI-656): --button-<size>-font-size,\n -text-transform, -letter-spacing and -radius set one size, and the\n unprefixed --button-text-transform / -letter-spacing / -radius set every\n size. Unset, each size keeps its shipped type size and rounding; case and\n tracking live in visor-base, after the dlg size. */\n.sizeSm {\n --label-box-height: 2rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--button-sm-font-size, var(--font-size-xs, 0.75rem));\n border-radius: var(--button-sm-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeMd {\n --label-box-height: 2.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n font-size: var(--button-md-font-size, var(--font-size-sm, 0.875rem));\n border-radius: var(--button-md-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: var(--button-sm-font-size, 0.8125rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n --label-box-height: 2.125rem;\n height: var(--label-box-height);\n line-height: normal;\n}\n\n.sizeLg {\n --label-box-height: 3rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--button-lg-font-size, var(--font-size-base, 1rem));\n border-radius: var(--button-lg-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n/* Dialog-footer button — dlg-btn (Animal dialog substrate, VI-620).\n Compact modal-action metrics measured from the AN prototype (11.5px / 600,\n ~7px 13px padding, 7px radius), normalized onto Visor tokens: font-size-2xs\n (11px), spacing-2/spacing-3 padding (8/12px), radius-lg (8px). Height is\n auto so the compact padding drives the box. Color faces reuse the existing\n variant axis — primary via variant=\"default\", ghost via variant=\"ghost\"\n (refined below to a hairline-outlined, secondary-ink face), danger via\n variant=\"destructive\" (refined below to a subtle-danger face). */\n.sizeDlg {\n height: auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--button-dlg-font-size, var(--font-size-2xs, 0.6875rem));\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n border-radius: var(--button-dlg-radius, var(--button-radius, var(--radius-lg, 0.5rem)));\n}\n\n/* Icon-only button — a square mark around one glyph (VI-659). The box is the\n glyph plus a hit-target pad on every side, so width always equals height;\n --button-icon-size sets the glyph and --button-icon-pad the pad. A 1em glyph\n (Phosphor's default) takes the size from font-size; a glyph with its own\n width/height attributes is sized by the svg rule. No edge on ghost; on\n outline the edge is the shared ::after one, so --control-edge-width: 0 turns\n it off. */\n.sizeIcon {\n width: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n height: calc(var(--button-icon-size, 1rem) + 2 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n padding: 0;\n flex-shrink: 0;\n font-size: var(--button-icon-size, 1rem);\n border-radius: var(--button-icon-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeIcon > svg {\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n flex-shrink: 0;\n}\n\n/* Ghost icon ink — the mark sits beside what it acts on, so it rests quieter\n than a labelled ghost button and comes up to primary ink on hover. :where()\n keeps it at one-class weight, so the held (open / active) ghost rule above\n still wins. */\n:where(.sizeIcon).variantGhost {\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n}\n\n@media (hover: hover) {\n :where(.sizeIcon).variantGhost:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n/* Case and tracking are inherited properties, so any unlayered fallback would\n override something: `inherit` beats a layered utility, `revert-layer` drops\n an unlayered consumer rule. In visor-base, the lowest Visor layer (the reset's),\n every consumer rule still wins, and when the hook is unset revert-layer\n leaves the element exactly as if these declarations were absent. */\n@layer visor-base {\n .sizeSm {\n text-transform: var(--button-sm-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-sm-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeMd {\n text-transform: var(--button-md-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-md-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeLg {\n text-transform: var(--button-lg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-lg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n\n .sizeDlg {\n text-transform: var(--button-dlg-text-transform, var(--button-text-transform, revert-layer));\n letter-spacing: var(--button-dlg-letter-spacing, var(--button-letter-spacing, revert-layer));\n }\n}\n\n/* dlg ghost — transparent well with a hairline outline + secondary ink\n (AN dlg-btn ghost face: transparent / --ink-60 / 1px --hair). Two-class\n specificity keeps this scoped to the dlg size only. */\n.sizeDlg.variantGhost {\n color: var(--text-secondary, #6b7280);\n}\n\n.sizeDlg.variantGhost::after {\n outline-color: var(--control-edge-color, var(--hairline, var(--border-default, #e5e7eb)));\n}\n\n/* dlg danger — subtle-danger face (AN dlg-btn danger face: --red on a faint\n red fill), distinct from the solid `destructive` variant. */\n.sizeDlg.variantDestructive {\n background-color: var(--surface-error-subtle, color-mix(in srgb, var(--destructive, #ef4444) 8%, transparent));\n color: var(--text-error, var(--destructive, #b91c1c));\n}\n\n@media (hover: hover) {\n .sizeDlg.variantDestructive:hover {\n background-color: color-mix(in srgb, var(--destructive, #ef4444) 14%, transparent);\n }\n}\n\n/* Pending state — orthogonal to variant and size (VI-643).\n The idle children stay in the box at zero opacity so the control keeps its\n width, its height and its accessible name; the busy glyph paints over them\n in an absolutely-positioned overlay. A pending state that reflows has not\n fixed the defect it exists for, which is why nothing here is a leading slot\n that adds width.\n\n Themes tune the two timings via --button-pending-delay and\n --button-pending-min-duration. Neither is declared here on purpose: an\n element-level declaration shadows anything a theme emits on :root (VI-625). */\n.base[data-pending=\"true\"] {\n position: relative;\n cursor: progress;\n}\n\n/* No press dip while the action is in flight — the button is not accepting\n the click, so it must not pretend to. */\n.base[data-pending=\"true\"]:active {\n transform: none;\n transition: none;\n}\n\n/* Geometry holder. Mirrors .base's own flex metrics so wrapping the children\n does not change how they lay out. */\n.pendingLabel {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n opacity: 0;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.pendingOverlay {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n padding: inherit;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n pointer-events: none;\n}\n\n/* Clamped, so a pending label longer than the idle one can never widen the\n box it is being held inside. */\n.pendingText {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n"
|
|
176
|
+
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n /* VI-655: the border only holds box geometry and is always transparent. The\n faces that draw an edge (outline, gated, dlg ghost) draw it on ::after —\n see \"Edged faces\" below — so the button's own outline stays free for the\n focus ring. */\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Label (VI-682; shared spec: the label-centering doc under docs/). Bare text is wrapped in a block span whose line box is trimmed\n to the capital-height band, so the flex container centres the ink and not the\n font's ascent/descent (which put Product Sans ~1px high in a 40px button).\n Without text-box (Firefox) the span is a plain block and rendering is exactly\n what it was before. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n\n /* The compact dlg size is height:auto, so trimming would shrink its box. */\n .sizeDlg .label {\n text-box: normal;\n }\n}\n\n/* Blink only. Blink snaps the text baseline to a whole CSS px, which leaves the\n trimmed ink up to half a pixel off for fractional cap heights (12px type). The\n baseline lands at (box + cap) / 2; this hands back what the snap took. WebKit\n positions the baseline exactly, so correcting there would overshoot. The\n -apple-pay-button-style probe is the WebKit-only property that tells them apart. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n\n .sizeDlg .label {\n transform: none;\n }\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n}\n\n/* Edged faces (VI-655). The resting edge is an inset outline over the border\n box, on the shared --control-edge-* tokens, so --control-edge-width: 0 on any\n ancestor removes it with no layout shift. Each face sets only the colour, in\n the same source position its border-color used to hold. */\n.variantOutline,\n.base[data-gated=\"true\"],\n.sizeDlg.variantGhost {\n position: relative;\n}\n\n.variantOutline::after,\n.base[data-gated=\"true\"]::after,\n.sizeDlg.variantGhost::after {\n content: \"\";\n position: absolute;\n inset: -1px;\n border-radius: inherit;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) transparent;\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n pointer-events: none;\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.variantOutline::after {\n outline-color: var(--control-edge-color, var(--border-strong, #e5e7eb));\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n}\n\n.base[data-gated=\"true\"]::after {\n outline-color: var(--control-edge-color, var(--border-default, #e5e7eb));\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values.\n\n Type and rounding are theme hooks (VI-656, VI-663): --button-<size>-font-size,\n -font-weight, -text-transform, -letter-spacing and -radius set one size, and\n the unprefixed --button-font-weight / -text-transform / -letter-spacing /\n -radius set every size. Unset, each size keeps its shipped type size and rounding; case and\n tracking live in visor-base, after the dlg size. */\n.sizeSm {\n --label-box-height: 2rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--button-sm-font-size, var(--font-size-xs, 0.75rem));\n font-weight: var(--button-sm-font-weight, var(--button-font-weight, var(--font-weight-medium, 500)));\n border-radius: var(--button-sm-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n.sizeMd {\n --label-box-height: 2.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n font-size: var(--button-md-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--button-md-font-weight, var(--button-font-weight, var(--font-weight-medium, 500)));\n border-radius: var(--button-md-radius, var(--button-radius, var(--radius-md, 0.375rem)));\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: var(--button-sm-font-size, 0.8125rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n --label-box-height: 2.125rem;\n height: var(--label-box-height);\n line-height: normal;\n}\n\n.sizeLg {\n --label-box-height: 3rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--button-lg-font-size, var(--font-size-base, 1rem));\n font-weight: var(--button-lg-font-weight, var(--button-font-weight, var(--font-weight-medium, 500)));\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(--button-dlg-font-weight, var(--button-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"
|
|
177
177
|
}
|
|
178
178
|
]
|
|
179
179
|
},
|
|
@@ -219,12 +219,12 @@
|
|
|
219
219
|
{
|
|
220
220
|
"path": "components/ui/input/input.tsx",
|
|
221
221
|
"type": "registry:ui",
|
|
222
|
-
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { usePasswordManagersValue } from \"../../../lib/password-managers-context\"\nimport styles from \"./input.module.css\"\n\nconst inputVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\nexport interface InputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\">,\n VariantProps<typeof inputVariants> {\n /**\n * Optional leading icon rendered inside the field (e.g. a Phosphor icon).\n * The input picks up extra left padding so its text clears the icon. The\n * icon itself is `aria-hidden` — the field still needs its own label.\n */\n leadingIcon?: React.ReactNode\n /**\n * Whether password managers (1Password, Bitwarden, LastPass) should\n * offer to autofill this field. Defaults to `\"ignore\"` because most\n * Visor inputs live on non-auth forms (contact, marketing, settings)\n * where autofill icons are visual noise. Set to `\"allow\"` on login\n * and credential fields, or wrap the form in `<Form passwordManagers=\"allow\">`\n * to opt every descendant input in at once. The field-level prop always\n * wins over the form context. Browsers ignore `autocomplete=\"off\"` on\n * individual inputs, so `\"ignore\"` emits the per-manager data-*\n * attributes (`data-1p-ignore`, `data-bwignore`, `data-lpignore`,\n * `data-form-type=\"other\"`) that each manager respects.\n */\n passwordManagers?: \"ignore\" | \"allow\"\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n (\n {\n className,\n type,\n size,\n leadingIcon,\n passwordManagers,\n ...props\n },\n ref\n ) => {\n const resolved = usePasswordManagersValue(passwordManagers)\n const ignoreAttrs =\n resolved === \"ignore\"\n ? {\n \"data-1p-ignore\": \"true\",\n \"data-bwignore\": \"true\",\n \"data-lpignore\": \"true\",\n \"data-form-type\": \"other\",\n }\n : null\n const input = (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n inputVariants({ size }),\n leadingIcon && styles.hasLeadingIcon,\n className\n )}\n ref={
|
|
222
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { usePasswordManagersValue } from \"../../../lib/password-managers-context\"\nimport styles from \"./input.module.css\"\n\nconst inputVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\nexport interface InputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\">,\n VariantProps<typeof inputVariants> {\n /**\n * Optional leading icon rendered inside the field (e.g. a Phosphor icon).\n * The input picks up extra left padding so its text clears the icon. The\n * icon itself is `aria-hidden` — the field still needs its own label.\n */\n leadingIcon?: React.ReactNode\n /**\n * Fixed text drawn inside the field, before the value (a currency symbol,\n * for example). It sits in the same well as the value, in the secondary text\n * colour, and is never part of the value or the selection. Clicking it focuses\n * the input. It is wired to the input with `aria-describedby`, so assistive\n * tech reads it as a description of the field, not as its value.\n *\n * Replaces the native `prefix` HTML attribute (an RDFa vocabulary hook that\n * is never useful on an `<input>`); that attribute is not forwarded.\n */\n prefix?: React.ReactNode\n /** Fixed text drawn inside the field, after the value (`.epk.pro`, `h`). See `prefix`. */\n suffix?: React.ReactNode\n /**\n * Whether password managers (1Password, Bitwarden, LastPass) should\n * offer to autofill this field. Defaults to `\"ignore\"` because most\n * Visor inputs live on non-auth forms (contact, marketing, settings)\n * where autofill icons are visual noise. Set to `\"allow\"` on login\n * and credential fields, or wrap the form in `<Form passwordManagers=\"allow\">`\n * to opt every descendant input in at once. The field-level prop always\n * wins over the form context. Browsers ignore `autocomplete=\"off\"` on\n * individual inputs, so `\"ignore\"` emits the per-manager data-*\n * attributes (`data-1p-ignore`, `data-bwignore`, `data-lpignore`,\n * `data-form-type=\"other\"`) that each manager respects.\n */\n passwordManagers?: \"ignore\" | \"allow\"\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n (\n {\n className,\n type,\n size,\n leadingIcon,\n prefix,\n suffix,\n passwordManagers,\n ...props\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement | null>(null)\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 const affixId = React.useId()\n const resolved = usePasswordManagersValue(passwordManagers)\n const ignoreAttrs =\n resolved === \"ignore\"\n ? {\n \"data-1p-ignore\": \"true\",\n \"data-bwignore\": \"true\",\n \"data-lpignore\": \"true\",\n \"data-form-type\": \"other\",\n }\n : null\n const hasPrefix = prefix !== undefined && prefix !== null && prefix !== false\n const hasSuffix = suffix !== undefined && suffix !== null && suffix !== false\n const hasAffix = hasPrefix || hasSuffix\n\n if (hasAffix) {\n const prefixId = `${affixId}-prefix`\n const suffixId = `${affixId}-suffix`\n const describedBy =\n [props[\"aria-describedby\"], hasPrefix && prefixId, hasSuffix && suffixId]\n .filter(Boolean)\n .join(\" \") || undefined\n // The well belongs to the wrapper: it draws the one edge, ground and\n // focus ring around the affix and the value together. The input inside\n // is bare. A press on the affix would blur the input, so it is handed\n // back to the input instead (and the click then lands on the field).\n const focusInput = (e: React.MouseEvent) => {\n if (e.target === inputRef.current) return\n e.preventDefault()\n inputRef.current?.focus()\n }\n return (\n <span\n className={cn(\n styles.affixWrapper,\n size === \"sm\" && styles.affixSm,\n size === \"lg\" && styles.affixLg,\n className\n )}\n data-slot=\"input-wrapper\"\n data-size={size ?? \"md\"}\n onMouseDown={focusInput}\n >\n {leadingIcon ? (\n <span className={styles.affixIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n {hasPrefix ? (\n <span id={prefixId} className={styles.affix} data-slot=\"input-prefix\">\n {prefix}\n </span>\n ) : null}\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n styles.bare,\n size === \"sm\" ? styles.sizeSm : size === \"lg\" ? styles.sizeLg : styles.sizeMd,\n hasPrefix && styles.afterPrefix,\n hasSuffix && styles.beforeSuffix\n )}\n ref={setRefs}\n {...ignoreAttrs}\n {...props}\n aria-describedby={describedBy}\n />\n {hasSuffix ? (\n <span id={suffixId} className={styles.affix} data-slot=\"input-suffix\">\n {suffix}\n </span>\n ) : null}\n </span>\n )\n }\n\n const input = (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n inputVariants({ size }),\n leadingIcon && styles.hasLeadingIcon,\n className\n )}\n ref={setRefs}\n {...ignoreAttrs}\n {...props}\n />\n )\n\n if (!leadingIcon) return input\n\n return (\n <span className={styles.leadingIconWrapper} data-slot=\"input-wrapper\">\n <span className={styles.leadingIcon} aria-hidden=\"true\">\n {leadingIcon}\n </span>\n {input}\n </span>\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input, inputVariants }\n"
|
|
223
223
|
},
|
|
224
224
|
{
|
|
225
225
|
"path": "components/ui/input/input.module.css",
|
|
226
226
|
"type": "registry:ui",
|
|
227
|
-
"content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n /* VI-655: the border only holds box geometry and is always transparent. The\n visible resting edge is an outline pulled inside the box, drawn from the\n shared --control-edge-* tokens, so --control-edge-width: 0 on any ancestor\n removes it with no layout shift. --input-border stays the per-component\n colour override and still wins over the shared colour. */\n border: 1px solid transparent;\n /* --field-control-bg is the shared \"what surface does a form control sit on\"\n role. It defaults to var(--input-bg), so default inputs are unchanged. A\n form context (for example an in-dialog wrapper) can set --field-control-bg:\n var(--surface-subtle) to lift controls onto a different surface without a\n per-component prop. Under data-density=\"editorial\", the fallback chain\n defaults to var(--surface-card) while still honoring any scoped\n --field-control-bg override from a form context. */\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n:global([data-density=\"editorial\"]) .base {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.base::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch, so both stay visible with edges off. */\n.base:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n outline-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--input-bg, #f9fafb)))));\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444);\n}\n\n:global([data-density=\"editorial\"]) .base[aria-invalid=\"true\"] {\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--surface-card)))));\n}\n\n.base[aria-invalid=\"true\"]:focus-visible {\n outline-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 1.5px var(--border-error, #ef4444),\n 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 30%, transparent);\n}\n\n/* File input styles */\n.base[type=\"file\"] {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Leading-icon wrapper\n *\n * When the consumer passes a leadingIcon node, Input wraps its <input> in a\n * <span> and positions the icon absolutely inside it. The <input> itself\n * picks up .hasLeadingIcon, which adds enough left padding to clear the\n * icon (sized in em so it scales with the field's font-size). */\n.leadingIconWrapper {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n}\n\n.leadingIcon {\n position: absolute;\n left: var(--spacing-3, 0.75rem);\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--text-secondary, #9ca3af);\n pointer-events: none;\n font-size: 1.125em;\n line-height: 1;\n}\n\n/* Size variants\n *\n * Default rendering uses canonical values directly. Under data-density=\"editorial\"\n * (set on any ancestor), the editorial density axis bakes in the blessed admin\n * values — sm inputs at 33.5px height with --radius-md corners. Padding and the\n * md/lg sizes are not driven by the editorial treatment and remain canonical. */\n.sizeSm {\n height: 2.25rem;\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n height: 33.5px;\n border-radius: var(--radius-md);\n}\n\n.sizeMd {\n height: var(--input-height-md, auto);\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n}\n\n.sizeLg {\n height: var(--input-height-lg, auto);\n padding: var(--input-padding-lg, var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem));\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--input-radius-lg, var(--radius-sm, 0.5rem));\n}\n\n/* Baseline nudge (VI-684). A native <input> cannot be trimmed to its capital\n band without clipping descenders and accents, so its capitals sit wherever the\n font's line box puts them. Blink snaps that baseline to a whole CSS px, and the\n md and lg boxes (51px, 62px) have an odd-pixel centre, so the capitals land up\n to ~0.9px high. The ideal baseline is (box height + 1cap) / 2; this moves the\n padding from the bottom edge to the top by what rounding that baseline to a\n whole px takes away, so the snapped baseline lands on the nearest px to ideal\n and the capitals are within half a px of centre in any font. padding-top plus\n padding-bottom is unchanged, so the box is exactly as tall as before; a theme\n that sets --input-padding-md / --input-padding-lg replaces it, as before.\n Blink only: WebKit places the baseline exactly, so correcting there overshoots\n (-apple-pay-button-style is the WebKit-only probe). The height is 2px of\n border + both paddings + 1lh (line-height 1.5). */\n@supports (padding-top: round(1.4px, 1px)) and (not (-apple-pay-button-style: black)) {\n .sizeMd {\n --input-nudge: calc(round((2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh + 1cap) / 2, 1px) - (2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh + 1cap) / 2 + 0.5px);\n padding: var(--input-padding-md, calc(var(--spacing-3_5, 0.875rem) + var(--input-nudge)) var(--spacing-4, 1rem) calc(var(--spacing-3_5, 0.875rem) - var(--input-nudge)));\n }\n\n .sizeLg {\n --input-nudge: calc(round((2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh + 1cap) / 2, 1px) - (2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh + 1cap) / 2 + 0.5px);\n padding: var(--input-padding-lg, calc(var(--spacing-4_5, 1.125rem) + var(--input-nudge)) var(--spacing-5, 1.25rem) calc(var(--spacing-4_5, 1.125rem) - var(--input-nudge)));\n }\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n"
|
|
227
|
+
"content": "/* Input base styles */\n.base {\n width: 100%;\n min-width: 0;\n /* VI-655: the border only holds box geometry and is always transparent. The\n visible resting edge is an outline pulled inside the box, drawn from the\n shared --control-edge-* tokens, so --control-edge-width: 0 on any ancestor\n removes it with no layout shift. --input-border stays the per-component\n colour override and still wins over the shared colour. */\n border: 1px solid transparent;\n /* --field-control-bg is the shared \"what surface does a form control sit on\"\n role. It defaults to var(--input-bg), so default inputs are unchanged. A\n form context (for example an in-dialog wrapper) can set --field-control-bg:\n var(--surface-subtle) to lift controls onto a different surface without a\n per-component prop. Under data-density=\"editorial\", the fallback chain\n defaults to var(--surface-card) while still honoring any scoped\n --field-control-bg override from a form context. */\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n:global([data-density=\"editorial\"]) .base {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.base::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n@media (hover: hover) {\n .base:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch, so both stay visible with edges off. */\n.base:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.base:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.base[aria-invalid=\"true\"] {\n outline-color: transparent;\n background-color: var(--input-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--field-control-bg, var(--input-bg, #f9fafb)))));\n /* VI-663: the ring and halo widths are theme keys. Unset, the halo is 0px\n wide and paints nothing, so the look is the 1.5px inset ring alone. */\n box-shadow: inset 0 0 0 var(--control-invalid-ring-width, 1.5px) var(--border-error, #ef4444),\n 0 0 0 var(--control-invalid-halo-width, 0px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\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 var(--control-invalid-ring-width, 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-md-height, var(--input-height-md, auto));\n padding: var(--input-md-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-md-radius, var(--input-radius-md, var(--radius-sm, 0.5rem)));\n}\n\n.sizeLg {\n height: var(--input-height-lg, auto);\n padding: var(--input-padding-lg, var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem));\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--input-radius-lg, var(--radius-sm, 0.5rem));\n}\n\n/* Baseline nudge (VI-684). A native <input> cannot be trimmed to its capital\n band without clipping descenders and accents, so its capitals sit wherever the\n font's line box puts them: half a line above the box centre plus (ascent -\n descent) / 2, with ascent and descent each rounded to a whole px, and then the\n baseline snapped to a whole CSS px (Blink). Both depend on the font, so no one\n constant centres every face. The theme engine publishes the body face's\n ascent and descent as unitless em ratios (--font-ascent, --font-descent); the\n box height H is known, so the ideal baseline (H + 1cap) / 2 and the one the\n font produces can both be written out. --input-nudge is the gap between the\n nearest whole px to the ideal and the baseline the font produces; moving that\n much padding from the bottom edge to the top lands the snapped baseline on the\n whole px nearest the ideal, within half a px of centre in any font. padding-top\n plus padding-bottom is unchanged, so the box is exactly as tall as before. A\n theme that sets --input-padding-* (or the theme key md-padding) replaces it, as before; a face the engine has\n no metrics for gets no nudge (the var() fallback is 0px).\n Blink only: WebKit places the baseline exactly, so correcting there overshoots\n (-apple-pay-button-style is the WebKit-only probe). */\n@supports (padding-top: round(1.4px, 1px)) and (not (-apple-pay-button-style: black)) {\n .sizeSm {\n --input-nudge: calc(round((2.25rem + 1cap) / 2, 1px) - (2.25rem + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n padding: var(--input-padding-sm, calc(var(--spacing-1, 0.25rem) + var(--input-nudge, 0px)) var(--spacing-3, 0.75rem) calc(var(--spacing-1, 0.25rem) - var(--input-nudge, 0px)));\n }\n\n .sizeMd {\n --input-box: calc(2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh);\n --input-nudge: calc(round((var(--input-box) + 1cap) / 2, 1px) - (var(--input-box) + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n padding: var(--input-md-padding, var(--input-padding-md, calc(var(--spacing-3_5, 0.875rem) + var(--input-nudge, 0px)) var(--spacing-4, 1rem) calc(var(--spacing-3_5, 0.875rem) - var(--input-nudge, 0px))));\n }\n\n .sizeLg {\n --input-box: calc(2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh);\n --input-nudge: calc(round((var(--input-box) + 1cap) / 2, 1px) - (var(--input-box) + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n padding: var(--input-padding-lg, calc(var(--spacing-4_5, 1.125rem) + var(--input-nudge, 0px)) var(--spacing-5, 1.25rem) calc(var(--spacing-4_5, 1.125rem) - var(--input-nudge, 0px)));\n }\n}\n\n/* Leading-icon left-padding override.\n *\n * Declared AFTER the size variants so the padding-left value wins the\n * cascade against the shorthand `padding` each size sets. Double-class\n * selector bumps specificity above the size classes for good measure. */\n.sizeSm.hasLeadingIcon {\n padding-left: calc(var(--spacing-3, 0.75rem) * 2 + 1.125em);\n}\n\n.sizeMd.hasLeadingIcon {\n padding-left: calc(var(--spacing-4, 1rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n.sizeLg.hasLeadingIcon {\n padding-left: calc(var(--spacing-5, 1.25rem) + var(--spacing-3, 0.75rem) + 1.125em);\n}\n\n/* Prefix and suffix (VI-662)\n *\n * With an affix the WRAPPER is the well: it draws the one edge, the ground, the\n * radius and the focus and invalid states around the affix and the value\n * together, from the same tokens .base reads. The <input> inside is bare (no\n * edge, no ground) and is a flex item, so its content box starts where the\n * prefix ends and stops where the suffix begins: a value of any length scrolls\n * inside that box and never runs under an affix. The input keeps its 1px\n * transparent geometry border and its size class, so the wrapper is exactly as\n * tall as a plain Input. The edge is the shared --control-edge-* outline (no\n * visible border), so --control-edge-width: 0 removes it here too. */\n.affixWrapper {\n --affix-inset: var(--spacing-4, 1rem);\n --affix-pad-y: var(--spacing-3_5, 0.875rem);\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n cursor: text;\n background-color: var(--field-control-bg, var(--input-bg, var(--surface-interactive-default, #f9fafb)));\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1.5;\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\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.affixSm {\n --affix-pad-y: var(--spacing-1, 0.25rem);\n --affix-inset: var(--spacing-3, 0.75rem);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.affixLg {\n --affix-pad-y: var(--spacing-4_5, 1.125rem);\n --affix-inset: 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:global([data-density=\"editorial\"]) .affixWrapper {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .affixSm {\n border-radius: var(--radius-md);\n}\n\n@media (hover: hover) {\n .affixWrapper:hover:not(:has(> .bare:focus-visible)):not(:has(> .bare:disabled)):not(:has(> .bare[aria-invalid=\"true\"])) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n.affixWrapper:has(> .bare: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.affixWrapper:has(> .bare:disabled) {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.affixWrapper:has(> .bare[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\"]) .affixWrapper:has(> .bare[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.affixWrapper:has(> .bare[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/* The bare input: the value only. It keeps .base's geometry (width, 1px\n transparent border, line-height, size class) and drops its edge and ground. */\n.bare {\n flex: 1 1 auto;\n width: auto;\n min-width: 0;\n border: 1px solid transparent;\n border-radius: 0;\n background: transparent;\n box-shadow: none;\n outline: none;\n line-height: 1.5;\n color: inherit;\n}\n\n.bare::placeholder {\n color: var(--input-placeholder-color, var(--text-secondary, #9ca3af));\n}\n\n/* The side that meets an affix gives up its padding and border for the gap, so\n the gap is exactly --input-affix-gap. Two classes beat the size class. */\n.bare.afterPrefix {\n padding-left: var(--input-affix-gap, var(--spacing-2, 0.5rem));\n border-left-width: 0;\n}\n\n.bare.beforeSuffix {\n padding-right: var(--input-affix-gap, var(--spacing-2, 0.5rem));\n border-right-width: 0;\n}\n\n.affixIcon {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n margin-left: calc(var(--affix-inset) + 1px);\n color: var(--text-secondary, #9ca3af);\n font-size: 1.125em;\n line-height: 1;\n}\n\n.affixIcon + .affix {\n margin-left: var(--input-affix-gap, var(--spacing-2, 0.5rem));\n}\n\n/* The affix is part of the well, never of the value: it does not shrink, wrap\n or take the selection. The ink is the secondary text colour, retunable with\n --input-affix-color. */\n.affix {\n flex-shrink: 0;\n white-space: nowrap;\n user-select: none;\n color: var(--input-affix-color, var(--text-secondary, #6b7280));\n}\n\n/* The value is nudged by --input-nudge (the VI-684 per-font baseline correction,\n set on .sizeSm / .sizeMd / .sizeLg above from the theme's --font-ascent and\n --font-descent). The affix sits on the same baseline, so the wrapper computes\n the same nudge for the same box and the affix takes it. Blink only, like the\n value's. With no font metrics the nudge falls back to 0px, as the value's does.\n A theme that re-binds --input-padding-* drops the value's nudge; the affix then\n keeps it, which is the one case the two can differ. */\n@supports (padding-top: round(1.4px, 1px)) and (not (-apple-pay-button-style: black)) {\n .affixWrapper {\n --input-box: calc(2px + 2 * var(--spacing-3_5, 0.875rem) + 1lh);\n --input-nudge: calc(round((var(--input-box) + 1cap) / 2, 1px) - (var(--input-box) + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n }\n\n .affixSm {\n --input-nudge: calc(round((2.25rem + 1cap) / 2, 1px) - (2.25rem + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n }\n\n .affixLg {\n --input-box: calc(2px + 2 * var(--spacing-4_5, 1.125rem) + 1lh);\n --input-nudge: calc(round((var(--input-box) + 1cap) / 2, 1px) - (var(--input-box) + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n }\n\n /* The affix takes the nudge the way the input does, as padding moved from the\n bottom edge to the top, so its baseline snaps exactly as the value's does. */\n .affix {\n align-self: stretch;\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border-block: 1px solid transparent;\n padding-top: calc(var(--affix-pad-y) + var(--input-nudge, 0px));\n padding-bottom: calc(var(--affix-pad-y) - var(--input-nudge, 0px));\n }\n}\n\n.affix:first-child {\n margin-left: calc(var(--affix-inset) + 1px);\n}\n\n.affix:last-child {\n margin-right: calc(var(--affix-inset) + 1px);\n}\n\n"
|
|
228
228
|
}
|
|
229
229
|
]
|
|
230
230
|
},
|
|
@@ -275,7 +275,7 @@
|
|
|
275
275
|
{
|
|
276
276
|
"path": "components/ui/textarea/textarea.module.css",
|
|
277
277
|
"type": "registry:ui",
|
|
278
|
-
"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"
|
|
278
|
+
"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 /* VI-663: ring and halo widths are theme keys; unset they are the state\n edge and the focus ring, as before. */\n outline: var(--control-invalid-ring-width, var(--control-state-edge-width, 1px)) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-invalid-ring-width, var(--control-state-edge-width, 1px)));\n box-shadow: 0 0 0 var(--control-invalid-halo-width, 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(--textarea-padding, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--textarea-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"
|
|
279
279
|
}
|
|
280
280
|
]
|
|
281
281
|
},
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
{
|
|
328
328
|
"path": "components/ui/select/select.module.css",
|
|
329
329
|
"type": "registry:ui",
|
|
330
|
-
"content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n /* VI-655: transparent geometry border; the visible resting edge is an inset\n outline from the shared --control-edge-* tokens (as Input does). */\n border: 1px solid transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--select-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.trigger:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, 1px) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Borderless trigger variant (opt-in via <SelectTrigger variant=\"borderless\">).\n *\n * Closed trigger has no resting edge and sits on --field-control-bg — the same\n * shared form-control surface role Input uses — so a borderless Select matches a\n * borderless Input inside an in-dialog form context that sets\n * --field-control-bg: var(--surface-subtle). Default Select is unchanged. The\n * edge is made transparent (rather than removed) so the trigger's box size\n * stays identical to the default trigger and the focus ring keeps its geometry.\n * Declared after the base .trigger so these overrides win the cascade. */\n.triggerBorderless {\n outline-color: transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n /* In-form borderless selects match the Input they sit beside: full width and\n the same md height/padding (the prototype paired select and input controls inside its field rows). */\n width: 100%;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n}\n\n@media (hover: hover) {\n .triggerBorderless:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: transparent;\n }\n}\n\n.triggerBorderless:focus-visible {\n outline-color: transparent;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n:global([data-density=\"editorial\"]) .trigger {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .triggerBorderless {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.triggerSizeSm {\n --label-box-height: 2.25rem; /* VI-684: outer height, for the label correction */\n height: var(--label-box-height);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n --label-box-height: calc(1lh + var(--spacing-3_5, 0.875rem) * 2 + 2px);\n height: auto;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerSizeLg {\n --label-box-height: calc(1lh + var(--spacing-4_5, 1.125rem) * 2 + 2px);\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The value (or\n placeholder) span's line box is trimmed to the capital-to-baseline band, so\n the flex trigger centres the ink and not the font's ascent/descent.\n padding-block hands back the room the trim took, so the trigger's box is\n unchanged. Without text-box (Firefox) rendering is exactly what it was. */\n@supports (text-box: trim-both cap alphabetic) {\n .trigger > span {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2);\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took, from the trigger's outer height (--label-box-height, per size\n above). WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .trigger > span {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
|
|
330
|
+
"content": "/* Select Trigger */\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n width: fit-content;\n border-radius: var(--radius-md, 0.375rem);\n /* VI-655: transparent geometry border; the visible resting edge is an inset\n outline from the shared --control-edge-* tokens (as Input does). */\n border: 1px solid transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n /* Match Input's line-height so the trigger's height tracks the text field at\n the same size. Without this the trigger inherited line-height 2.0, rendering\n ~7px taller than a same-size Input. */\n line-height: 1.5;\n white-space: nowrap;\n color: var(--text-primary, #111827);\n transition: outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--select-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.trigger:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.trigger[aria-invalid=\"true\"] {\n /* VI-663: ring and halo widths are theme keys; unset they are the state\n edge and the focus ring, as before. */\n outline: var(--control-invalid-ring-width, var(--control-state-edge-width, 1px)) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-invalid-ring-width, var(--control-state-edge-width, 1px)));\n box-shadow: 0 0 0 var(--control-invalid-halo-width, var(--focus-ring-width, 2px)) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.trigger[data-placeholder] {\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Borderless trigger variant (opt-in via <SelectTrigger variant=\"borderless\">).\n *\n * Closed trigger has no resting edge and sits on --field-control-bg — the same\n * shared form-control surface role Input uses — so a borderless Select matches a\n * borderless Input inside an in-dialog form context that sets\n * --field-control-bg: var(--surface-subtle). Default Select is unchanged. The\n * edge is made transparent (rather than removed) so the trigger's box size\n * stays identical to the default trigger and the focus ring keeps its geometry.\n * Declared after the base .trigger so these overrides win the cascade. */\n.triggerBorderless {\n outline-color: transparent;\n background-color: var(--field-control-bg, var(--select-bg, var(--surface-interactive-default, #f9fafb)));\n /* In-form borderless selects match the Input they sit beside: full width and\n the same md height/padding (the prototype paired select and input controls inside its field rows). */\n width: 100%;\n padding: var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem);\n}\n\n@media (hover: hover) {\n .triggerBorderless:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: transparent;\n }\n}\n\n.triggerBorderless:focus-visible {\n outline-color: transparent;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n:global([data-density=\"editorial\"]) .trigger {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n:global([data-density=\"editorial\"]) .triggerBorderless {\n background-color: var(--field-control-bg, var(--surface-card));\n}\n\n.triggerSizeSm {\n --label-box-height: 2.25rem; /* VI-684: outer height, for the label correction */\n height: var(--label-box-height);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n.triggerSizeMd {\n --label-box-height: var(--select-height, calc(1lh + var(--spacing-3_5, 0.875rem) * 2 + 2px));\n height: var(--select-height, auto);\n padding: var(--select-padding, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n font-size: var(--font-size-sm, 0.875rem);\n border-radius: var(--select-radius, var(--radius-sm, 0.5rem));\n}\n\n.triggerSizeLg {\n --label-box-height: calc(1lh + var(--spacing-4_5, 1.125rem) * 2 + 2px);\n height: auto;\n padding: var(--spacing-4_5, 1.125rem) var(--spacing-5, 1.25rem);\n font-size: var(--font-size-base, 1rem);\n border-radius: var(--radius-sm, 0.5rem);\n}\n\n.triggerIcon {\n pointer-events: none;\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Content */\n.content {\n position: relative;\n z-index: 50;\n max-height: var(--radix-select-content-available-height, 300px);\n min-width: 9rem;\n overflow: hidden;\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n color: var(--text-primary, #111827);\n box-shadow: var(--shadow-lg);\n}\n\n/* Select Viewport */\n.viewport {\n padding: var(--spacing-1, 0.25rem);\n}\n\n.viewportPopper {\n height: var(--radix-select-trigger-height);\n width: 100%;\n min-width: var(--radix-select-trigger-width);\n}\n\n/* Select Label */\n.label {\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--text-secondary, #9ca3af);\n}\n\n/* Select Item */\n.item {\n position: relative;\n display: flex;\n width: 100%;\n cursor: default;\n align-items: center;\n gap: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-sm, 0.25rem);\n padding: var(--spacing-2, 0.5rem) var(--spacing-8, 2rem) var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n outline: none;\n user-select: none;\n}\n\n.item:focus,\n.item[data-highlighted] {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n}\n\n.item[data-disabled] {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.itemIndicatorWrapper {\n position: absolute;\n right: 0.5rem;\n display: flex;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n}\n\n.itemIndicatorIcon {\n width: 0.875rem;\n height: 0.875rem;\n pointer-events: none;\n}\n\n/* Select Separator */\n.separator {\n pointer-events: none;\n margin: var(--spacing-1, 0.25rem) calc(-1 * var(--spacing-1, 0.25rem));\n height: 1px;\n background-color: var(--border-default, #e5e7eb);\n}\n\n/* Scroll Buttons */\n.scrollButton {\n display: flex;\n cursor: default;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-1, 0.25rem);\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\n}\n\n.scrollButtonIcon {\n width: 1rem;\n height: 1rem;\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The value (or\n placeholder) span's line box is trimmed to the capital-to-baseline band, so\n the flex trigger centres the ink and not the font's ascent/descent.\n padding-block hands back the room the trim took, so the trigger's box is\n unchanged. Without text-box (Firefox) rendering is exactly what it was. */\n@supports (text-box: trim-both cap alphabetic) {\n .trigger > span {\n text-box: trim-both cap alphabetic;\n padding-block: calc((1lh - 1cap) / 2);\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took, from the trigger's outer height (--label-box-height, per size\n above). WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .trigger > span {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
|
|
331
331
|
}
|
|
332
332
|
]
|
|
333
333
|
},
|
|
@@ -550,12 +550,12 @@
|
|
|
550
550
|
{
|
|
551
551
|
"path": "components/ui/separator/separator.tsx",
|
|
552
552
|
"type": "registry:ui",
|
|
553
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./separator.module.css\"\n\nconst Separator = React.forwardRef<\n React.ElementRef<typeof SeparatorPrimitive.Root>,\n
|
|
553
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./separator.module.css\"\n\nexport interface SeparatorProps\n extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {\n /**\n * Text drawn centred on a horizontal rule, with the rule on both sides. A\n * labelled separator is never decorative, so the word is exposed. Ignored on\n * a vertical separator.\n */\n label?: React.ReactNode\n}\n\nconst Separator = React.forwardRef<\n React.ElementRef<typeof SeparatorPrimitive.Root>,\n SeparatorProps\n>(\n (\n {\n className,\n orientation = \"horizontal\",\n decorative = true,\n label,\n ...props\n },\n ref\n ) => {\n const labelId = React.useId()\n const hasLabel =\n orientation === \"horizontal\" &&\n label !== undefined &&\n label !== null &&\n label !== false &&\n label !== \"\"\n\n if (hasLabel) {\n return (\n <SeparatorPrimitive.Root\n ref={ref}\n data-slot=\"separator\"\n data-labelled=\"\"\n decorative={false}\n orientation={orientation}\n aria-labelledby={labelId}\n className={cn(styles.separator, styles.labelled, className)}\n {...props}\n >\n <span className={styles.rule} aria-hidden=\"true\" />\n <span id={labelId} className={styles.label}>\n {label}\n </span>\n <span className={styles.rule} aria-hidden=\"true\" />\n </SeparatorPrimitive.Root>\n )\n }\n\n return (\n <SeparatorPrimitive.Root\n ref={ref}\n data-slot=\"separator\"\n decorative={decorative}\n orientation={orientation}\n className={cn(\n styles.separator,\n orientation === \"horizontal\"\n ? styles.horizontal\n : styles.vertical,\n className\n )}\n {...props}\n />\n )\n }\n)\nSeparator.displayName = \"Separator\"\n\nexport { Separator }\n"
|
|
554
554
|
},
|
|
555
555
|
{
|
|
556
556
|
"path": "components/ui/separator/separator.module.css",
|
|
557
557
|
"type": "registry:ui",
|
|
558
|
-
"content": "/* Separator */\n.separator {\n flex-shrink: 0;\n background-color: var(--border-default, #e5e7eb);\n}\n\n.horizontal {\n height: 1px;\n width: 100%;\n}\n\n.vertical {\n width: 1px;\n align-self: stretch;\n}\n"
|
|
558
|
+
"content": "/* Separator */\n.separator {\n flex-shrink: 0;\n background-color: var(--separator-color, var(--border-default, #e5e7eb));\n}\n\n.horizontal {\n height: 1px;\n width: 100%;\n}\n\n.vertical {\n width: 1px;\n align-self: stretch;\n}\n\n/* Labelled: the root becomes a flex row and draws no fill itself; each .rule\n draws the line on the same token, so the word sits centred between two. */\n.labelled {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n width: 100%;\n background-color: transparent;\n}\n\n.rule {\n flex: 1 1 0;\n min-width: 0;\n height: 1px;\n background-color: var(--separator-color, var(--border-default, #e5e7eb));\n}\n\n.label {\n flex-shrink: 0;\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1.5;\n color: var(--text-secondary, #6b7280);\n}\n"
|
|
559
559
|
}
|
|
560
560
|
]
|
|
561
561
|
},
|
|
@@ -935,12 +935,12 @@
|
|
|
935
935
|
{
|
|
936
936
|
"path": "components/ui/save-status/save-status.tsx",
|
|
937
937
|
"type": "registry:ui",
|
|
938
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowClockwise } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./save-status.module.css\"\n\n/**\n * The states `useAutosave` reports, plus `syncing`: a first sync from an outside\n * source is writing rows the user did not type. `useAutosave` never emits it;\n * the consumer sets it.\n */\nexport type SaveStatusState = \"saved\" | \"saving\" | \"syncing\" | \"unsaved\" | \"refused\"\n\nconst STATES: SaveStatusState[] = [\"saved\", \"saving\", \"syncing\", \"unsaved\", \"refused\"]\n\nconst DEFAULT_LABELS: Record<SaveStatusState, string> = {\n saved: \"Saved\",\n saving: \"Saving\",\n syncing: \"Syncing\",\n unsaved: \"Unsaved\",\n refused: \"Couldn't save\",\n}\n\nexport interface SaveStatusProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Where the value stands — pass `useAutosave(...).status`. */\n status: SaveStatusState\n /** Shows a retry mark while `status` is `refused`. Pass `useAutosave(...).retry`. */\n onRetry?: () => void\n /** Override any of the five labels (e.g. to translate them). */\n labels?: Partial<Record<SaveStatusState, string>>\n /**\n * How `unsaved` is drawn. `\"dot\"` draws a dot alone, no word; the label stays\n * as visually hidden text, so the live region still announces it. Defaults to `\"text\"`.\n */\n unsavedAs?: \"text\" | \"dot\"\n /** Accessible name of the retry mark. Defaults to \"Retry saving\". */\n retryLabel?: string\n}\n\n/**\n * The autosave readout: status text in a slot as wide as the longest label\n * (in the theme's own font) plus the retry mark, or `--save-status-width`, so the row it\n * sits in never reflows as the state changes. It is status text, not a\n * button; the only control is the retry mark, shown after a failed save.\n * It draws no edge.\n */\nconst SaveStatus = React.forwardRef<HTMLSpanElement, SaveStatusProps>(\n ({ status, onRetry, labels, unsavedAs = \"text\", retryLabel = \"Retry saving\", className, ...props }, ref) => {\n const label = labels?.[status] ?? DEFAULT_LABELS[status]\n const asDot = status === \"unsaved\" && unsavedAs === \"dot\"\n const showRetry = status === \"refused\" && onRetry\n return (\n <span\n ref={ref}\n data-slot=\"save-status\"\n data-state={status}\n data-as={asDot ? \"dot\" : undefined}\n className={cn(styles.root, className)}\n {...props}\n >\n <span data-slot=\"save-status-cell\" className={styles.cell}>\n {STATES.map((state) => (\n <span key={state} data-slot=\"save-status-sizer\" className={styles.sizer} aria-hidden=\"true\">\n {labels?.[state] ?? DEFAULT_LABELS[state]}\n </span>\n ))}\n {asDot ? <span data-slot=\"save-status-dot\" className={styles.dot} aria-hidden=\"true\" /> : null}\n <span\n data-slot=\"save-status-text\"\n className={cn(styles.text, asDot && styles.hidden)}\n role=\"status\"\n aria-live=\"polite\"\n >\n {label}\n </span>\n </span>\n {showRetry ? (\n <button\n type=\"button\"\n data-slot=\"save-status-retry\"\n className={styles.retry}\n aria-label={retryLabel}\n onClick={onRetry}\n >\n <ArrowClockwise aria-hidden=\"true\" />\n </button>\n ) : (\n <span data-slot=\"save-status-retry-reserve\" className={styles.reserve} aria-hidden=\"true\" />\n )}\n </span>\n )\n }\n)\nSaveStatus.displayName = \"SaveStatus\"\n\nexport { SaveStatus }\n"
|
|
938
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowClockwise } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./save-status.module.css\"\n\n/**\n * The states `useAutosave` reports, plus `syncing`: a first sync from an outside\n * source is writing rows the user did not type. `useAutosave` never emits it;\n * the consumer sets it.\n */\nexport type SaveStatusState = \"saved\" | \"saving\" | \"syncing\" | \"unsaved\" | \"refused\"\n\nconst STATES: SaveStatusState[] = [\"saved\", \"saving\", \"syncing\", \"unsaved\", \"refused\"]\n\nconst DEFAULT_LABELS: Record<SaveStatusState, string> = {\n saved: \"Saved\",\n saving: \"Saving\",\n syncing: \"Syncing\",\n unsaved: \"Unsaved\",\n refused: \"Couldn't save\",\n}\n\nexport interface SaveStatusProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Where the value stands — pass `useAutosave(...).status`. */\n status: SaveStatusState\n /** Shows a retry mark while `status` is `refused`. Pass `useAutosave(...).retry`. */\n onRetry?: () => void\n /** Override any of the five labels (e.g. to translate them). */\n labels?: Partial<Record<SaveStatusState, string>>\n /**\n * How `unsaved` is drawn. `\"dot\"` draws a dot alone, no word; the label stays\n * as visually hidden text, so the live region still announces it. Defaults to `\"text\"`.\n */\n unsavedAs?: \"text\" | \"dot\"\n /**\n * Draws a status dot before the label in every state, in the state's ink.\n * With `unsavedAs=\"dot\"`, Unsaved is this dot alone. Defaults to `false`.\n */\n dot?: boolean\n /** Accessible name of the retry mark. Defaults to \"Retry saving\". */\n retryLabel?: string\n}\n\n/**\n * The autosave readout: status text in a slot as wide as the longest label\n * (in the theme's own font) plus the retry mark, or `--save-status-width`, so the row it\n * sits in never reflows as the state changes. It is status text, not a\n * button; the only control is the retry mark, shown after a failed save.\n * It draws no edge.\n */\nconst SaveStatus = React.forwardRef<HTMLSpanElement, SaveStatusProps>(\n ({ status, onRetry, labels, unsavedAs = \"text\", dot = false, retryLabel = \"Retry saving\", className, ...props }, ref) => {\n const label = labels?.[status] ?? DEFAULT_LABELS[status]\n const asDot = status === \"unsaved\" && unsavedAs === \"dot\"\n const showRetry = status === \"refused\" && onRetry\n return (\n <span\n ref={ref}\n data-slot=\"save-status\"\n data-state={status}\n data-as={asDot ? \"dot\" : undefined}\n className={cn(styles.root, className)}\n {...props}\n >\n {dot ? (\n <span data-slot=\"save-status-lead\" className={styles.lead} aria-hidden=\"true\">\n <span data-slot=\"save-status-status-dot\" className={styles.statusDot} />\n </span>\n ) : null}\n <span data-slot=\"save-status-cell\" className={styles.cell}>\n {STATES.map((state) => (\n <span key={state} data-slot=\"save-status-sizer\" className={styles.sizer} aria-hidden=\"true\">\n {labels?.[state] ?? DEFAULT_LABELS[state]}\n </span>\n ))}\n {asDot && !dot ? <span data-slot=\"save-status-dot\" className={styles.dot} aria-hidden=\"true\" /> : null}\n <span\n data-slot=\"save-status-text\"\n className={cn(styles.text, asDot && styles.hidden)}\n role=\"status\"\n aria-live=\"polite\"\n >\n {label}\n </span>\n </span>\n {showRetry ? (\n <button\n type=\"button\"\n data-slot=\"save-status-retry\"\n className={styles.retry}\n aria-label={retryLabel}\n onClick={onRetry}\n >\n <ArrowClockwise aria-hidden=\"true\" />\n </button>\n ) : (\n <span data-slot=\"save-status-retry-reserve\" className={styles.reserve} aria-hidden=\"true\" />\n )}\n </span>\n )\n }\n)\nSaveStatus.displayName = \"SaveStatus\"\n\nexport { SaveStatus }\n"
|
|
939
939
|
},
|
|
940
940
|
{
|
|
941
941
|
"path": "components/ui/save-status/save-status.module.css",
|
|
942
942
|
"type": "registry:ui",
|
|
943
|
-
"content": "/* SaveStatus (VI-657)\n *\n * A readout, not a control: status text in a fixed-width slot, so the row\n * never reflows as Saved / Saving / Syncing / Unsaved / Couldn't save swap in. It draws\n * no edge at all (no border, no outline, no box). The retry mark is the\n * editor's icon mark (VI-659): the icon Button's size, pad and ink, with its\n * hit target on ::before so it adds no height to the row. */\n.root {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n /* Sized from content: the cell below is as wide as the longest label in\n the current font, and the retry mark's room is always reserved, so the\n slot is one width in every state. The token overrides it. */\n width: var(--save-status-width, auto);\n /* Tall enough for the retry mark in every state, so it adds no height. */\n min-height: var(--button-icon-size, 1rem);\n font-size: var(--font-size-xs, 0.75rem);\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Family, case and tracking are inherited properties, so any unlayered\n fallback would override something: `inherit` beats a layered utility,\n `revert-layer` drops an unlayered consumer rule. In visor-base, the lowest\n Visor layer, every consumer rule still wins, and when a token is unset\n revert-layer leaves the readout as if these declarations were absent. */\n@layer visor-base {\n .root {\n font-family: var(--save-status-font-family, revert-layer);\n text-transform: var(--save-status-text-transform, revert-layer);\n letter-spacing: var(--save-status-letter-spacing, revert-layer);\n }\n}\n\n.root[data-state=\"saved\"] {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.root[data-state=\"syncing\"] {\n color: var(--save-status-syncing-color, var(--text-secondary, #6b7280));\n}\n\n.root[data-state=\"refused\"] {\n color: var(--text-error, #b91c1c);\n}\n\n/* A longer custom label (a translation) truncates inside the slot rather\n than painting past it. */\n.cell {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n align-items: center;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n}\n\n/* Every label, stacked invisibly in the one cell, sets its width. */\n.sizer,\n.text,\n.dot {\n grid-area: 1 / 1;\n}\n\n.sizer {\n visibility: hidden;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Unsaved as a dot alone (unsavedAs=\"dot\"). The dot sits where the word\n would start and takes the unsaved ink; the slot is unchanged, so the row\n does not move. The label stays in the DOM as visually hidden text. */\n.dot {\n justify-self: start;\n width: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n height: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n.hidden {\n width: 1px;\n height: 1px;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/* The retry mark's room, kept when the mark is not shown, so showing it\n never widens the slot. */\n.reserve {\n flex: none;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n}\n\n.retry {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.retry::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.retry > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .retry:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.retry:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
943
|
+
"content": "/* SaveStatus (VI-657)\n *\n * A readout, not a control: status text in a fixed-width slot, so the row\n * never reflows as Saved / Saving / Syncing / Unsaved / Couldn't save swap in. It draws\n * no edge at all (no border, no outline, no box). The retry mark is the\n * editor's icon mark (VI-659): the icon Button's size, pad and ink, with its\n * hit target on ::before so it adds no height to the row. */\n.root {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n /* Sized from content: the cell below is as wide as the longest label in\n the current font, and the retry mark's room is always reserved, so the\n slot is one width in every state. The token overrides it. */\n width: var(--save-status-width, auto);\n /* Tall enough for the retry mark in every state, so it adds no height. */\n min-height: var(--button-icon-size, 1rem);\n font-size: var(--save-status-font-size, var(--font-size-xs, 0.75rem));\n white-space: nowrap;\n color: var(--text-secondary, #6b7280);\n}\n\n/* Family, case and tracking are inherited properties, so any unlayered\n fallback would override something: `inherit` beats a layered utility,\n `revert-layer` drops an unlayered consumer rule. In visor-base, the lowest\n Visor layer, every consumer rule still wins, and when a token is unset\n revert-layer leaves the readout as if these declarations were absent. */\n@layer visor-base {\n .root {\n font-family: var(--save-status-font-family, revert-layer);\n text-transform: var(--save-status-text-transform, revert-layer);\n letter-spacing: var(--save-status-letter-spacing, revert-layer);\n }\n}\n\n.root[data-state=\"saved\"] {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.root[data-state=\"syncing\"] {\n color: var(--save-status-syncing-color, var(--text-secondary, #6b7280));\n}\n\n.root[data-state=\"refused\"] {\n color: var(--text-error, #b91c1c);\n}\n\n/* A longer custom label (a translation) truncates inside the slot rather\n than painting past it. */\n.cell {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n align-items: center;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n}\n\n/* Every label, stacked invisibly in the one cell, sets its width. */\n.sizer,\n.text,\n.dot {\n grid-area: 1 / 1;\n}\n\n.sizer {\n visibility: hidden;\n pointer-events: none;\n white-space: nowrap;\n}\n\n.text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Unsaved as a dot alone (unsavedAs=\"dot\"). The dot sits where the word\n would start and takes the unsaved ink; the slot is unchanged, so the row\n does not move. The label stays in the DOM as visually hidden text. Its\n diameter is the Unsaved one, which follows --save-status-dot-size. */\n.dot {\n justify-self: start;\n width: var(--save-status-unsaved-dot-size, var(--save-status-dot-size, var(--spacing-2, 0.5rem)));\n height: var(--save-status-unsaved-dot-size, var(--save-status-dot-size, var(--spacing-2, 0.5rem)));\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n/* Status dot (dot, VI-663). A dot before the label in every state, in the\n state's ink. Its box is as wide as the larger of the two diameters, so the\n Unsaved dot growing never moves the label. */\n.lead {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: max(var(--save-status-dot-size, var(--spacing-2, 0.5rem)), var(--save-status-unsaved-dot-size, var(--save-status-dot-size, var(--spacing-2, 0.5rem))));\n}\n\n.statusDot {\n width: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n height: var(--save-status-dot-size, var(--spacing-2, 0.5rem));\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n.root[data-state=\"unsaved\"] .statusDot {\n width: var(--save-status-unsaved-dot-size, var(--save-status-dot-size, var(--spacing-2, 0.5rem)));\n height: var(--save-status-unsaved-dot-size, var(--save-status-dot-size, var(--spacing-2, 0.5rem)));\n}\n\n.hidden {\n width: 1px;\n height: 1px;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/* The retry mark's room, kept when the mark is not shown, so showing it\n never widens the slot. */\n.reserve {\n flex: none;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n}\n\n.retry {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--button-icon-size, 1rem);\n height: var(--button-icon-size, 1rem);\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.125rem);\n background: none;\n color: var(--button-icon-ghost-color, var(--text-secondary, #6b7280));\n cursor: pointer;\n transition: color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.retry::before {\n content: \"\";\n position: absolute;\n inset: calc(-1 * var(--button-icon-pad, var(--spacing-2, 0.5rem)));\n}\n\n.retry > svg {\n width: 100%;\n height: 100%;\n}\n\n@media (hover: hover) {\n .retry:hover {\n color: var(--text-primary, #111827);\n }\n}\n\n.retry:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n"
|
|
944
944
|
}
|
|
945
945
|
]
|
|
946
946
|
},
|
|
@@ -1376,12 +1376,38 @@
|
|
|
1376
1376
|
{
|
|
1377
1377
|
"path": "components/ui/toggle-group/toggle-group.tsx",
|
|
1378
1378
|
"type": "registry:ui",
|
|
1379
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./toggle-group.module.css\"\n\n/* ─── Context ───────────────────────────────────────────────────────── */\n\ntype ToggleGroupContextValue = {\n variant?: \"default\" | \"outline\" | null\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | null\n}\n\nconst ToggleGroupContext = React.createContext<ToggleGroupContextValue>({})\n\n/* ─── Sliding indicator hook ────────────────────────────────────────── */\n\nfunction useSlidingIndicator(\n rootRef: React.RefObject<HTMLElement | null>,\n variant: string | null | undefined,\n) {\n const indicatorRef = React.useRef<HTMLSpanElement>(null)\n\n const updateIndicator = React.useCallback(() => {\n const root = rootRef.current\n const indicator = indicatorRef.current\n if (!root || !indicator || variant !== \"outline\") return\n\n const activeItem = root.querySelector(\n '[data-state=\"on\"]',\n ) as HTMLElement | null\n\n if (!activeItem) {\n indicator.style.opacity = \"0\"\n return\n }\n\n indicator.style.opacity = \"1\"\n indicator.style.width = `${activeItem.offsetWidth}px`\n indicator.style.height = `${activeItem.offsetHeight}px`\n indicator.style.transform = `translate(${activeItem.offsetLeft}px, ${activeItem.offsetTop}px)`\n }, [rootRef, variant])\n\n // Update on mount and whenever children change state\n React.useEffect(() => {\n const root = rootRef.current\n if (!root || variant !== \"outline\") return\n\n // Initial position\n updateIndicator()\n\n // Observe data-state changes on children\n const observer = new MutationObserver(updateIndicator)\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n return () => observer.disconnect()\n }, [rootRef, variant, updateIndicator])\n\n return indicatorRef\n}\n\n/* ─── ToggleGroup ───────────────────────────────────────────────────── */\n\nconst toggleGroupVariants = cva(styles.root, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outline: styles.variantOutline,\n },\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type ToggleGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> &\n VariantProps<typeof toggleGroupVariants>\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n ToggleGroupProps\n>(({ className, variant, size, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ variant, size }),\n [variant, size]\n )\n const internalRef = React.useRef<HTMLDivElement>(null)\n // Sliding indicator only works for single-select toggle groups\n const useIndicator = variant === \"outline\" && props.type !== \"multiple\"\n const indicatorRef = useSlidingIndicator(internalRef, useIndicator ? variant : undefined)\n\n // Merge internal ref with forwarded ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-type={props.type ?? \"single\"}\n className={cn(toggleGroupVariants({ variant, size }), className)}\n ref={mergedRef}\n {...props}\n >\n {useIndicator && (\n <span\n ref={indicatorRef}\n className={styles.indicator}\n aria-hidden=\"true\"\n />\n )}\n {props.children}\n </ToggleGroupPrimitive.Root>\n </ToggleGroupContext.Provider>\n )\n})\nToggleGroup.displayName = \"ToggleGroup\"\n\n/* ─── ToggleGroupItem ───────────────────────────────────────────────── */\n\nconst toggleGroupItemVariants = cva(styles.item, {\n variants: {\n variant: {\n default: styles.itemVariantDefault,\n outline: styles.itemVariantOutline,\n },\n size: {\n xs: styles.itemSizeXs,\n sm: styles.itemSizeSm,\n md: styles.itemSizeMd,\n lg: styles.itemSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ToggleGroupItemProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>,\n VariantProps<typeof toggleGroupItemVariants> {}\n\n/**\n * Wrap bare text in a block label so the stylesheet can trim its line box to the\n * capital-to-baseline band (see `.label`), centring the capitals in the item\n * whatever the font (VI-684). Elements (icons) pass through.\n */\nfunction wrapLabel(children: React.ReactNode): React.ReactNode {\n return React.Children.map(children, (child) =>\n typeof child === \"string\" || typeof child === \"number\" ? (\n <span className={styles.label} data-slot=\"toggle-group-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ToggleGroupItemProps\n>(({ className, variant, size, children, ...props }, ref) => {\n const ctx = React.useContext(ToggleGroupContext)\n const resolvedVariant = variant ?? ctx.variant\n const resolvedSize = size ?? ctx.size\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n className={cn(toggleGroupItemVariants({ variant: resolvedVariant, size: resolvedSize }), className)}\n ref={ref}\n {...props}\n >\n {wrapLabel(children)}\n </ToggleGroupPrimitive.Item>\n )\n})\nToggleGroupItem.displayName = \"ToggleGroupItem\"\n\nexport { ToggleGroup, ToggleGroupItem, toggleGroupVariants, toggleGroupItemVariants }\n"
|
|
1379
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./toggle-group.module.css\"\n\n/* ─── Context ───────────────────────────────────────────────────────── */\n\ntype ToggleGroupContextValue = {\n variant?: \"default\" | \"outline\" | null\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | null\n}\n\nconst ToggleGroupContext = React.createContext<ToggleGroupContextValue>({})\n\n/* ─── Sliding indicator hook ────────────────────────────────────────── */\n\nfunction useSlidingIndicator(\n rootRef: React.RefObject<HTMLElement | null>,\n variant: string | null | undefined,\n) {\n const indicatorRef = React.useRef<HTMLSpanElement>(null)\n\n const updateIndicator = React.useCallback(() => {\n const root = rootRef.current\n const indicator = indicatorRef.current\n if (!root || !indicator || variant !== \"outline\") return\n\n const activeItem = root.querySelector(\n '[data-state=\"on\"]',\n ) as HTMLElement | null\n\n if (!activeItem) {\n indicator.style.opacity = \"0\"\n return\n }\n\n indicator.style.opacity = \"1\"\n indicator.style.width = `${activeItem.offsetWidth}px`\n indicator.style.height = `${activeItem.offsetHeight}px`\n indicator.style.transform = `translate(${activeItem.offsetLeft}px, ${activeItem.offsetTop}px)`\n }, [rootRef, variant])\n\n // Update on mount and whenever children change state\n React.useEffect(() => {\n const root = rootRef.current\n if (!root || variant !== \"outline\") return\n\n // Initial position\n updateIndicator()\n\n // Observe data-state changes on children\n const observer = new MutationObserver(updateIndicator)\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n subtree: true,\n })\n\n return () => observer.disconnect()\n }, [rootRef, variant, updateIndicator])\n\n return indicatorRef\n}\n\n/* ─── ToggleGroup ───────────────────────────────────────────────────── */\n\nconst toggleGroupVariants = cva(styles.root, {\n variants: {\n variant: {\n default: styles.variantDefault,\n outline: styles.variantOutline,\n },\n size: {\n xs: styles.sizeXs,\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport type ToggleGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> &\n VariantProps<typeof toggleGroupVariants>\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n ToggleGroupProps\n>(({ className, variant, size, ...props }, ref) => {\n const contextValue = React.useMemo(\n () => ({ variant, size }),\n [variant, size]\n )\n const internalRef = React.useRef<HTMLDivElement>(null)\n // Sliding indicator only works for single-select toggle groups\n const useIndicator = variant === \"outline\" && props.type !== \"multiple\"\n const indicatorRef = useSlidingIndicator(internalRef, useIndicator ? variant : undefined)\n\n // Merge internal ref with forwarded ref\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n data-type={props.type ?? \"single\"}\n // Radix renders role=\"group\" with radio items for single-select; a radio\n // item needs a radiogroup parent (VI-690). Multi-select stays a group of\n // pressed buttons. Spread after, so a caller's own role still wins.\n {...(props.type === \"multiple\" ? {} : { role: \"radiogroup\" })}\n className={cn(toggleGroupVariants({ variant, size }), className)}\n ref={mergedRef}\n {...props}\n >\n {useIndicator && (\n <span\n ref={indicatorRef}\n className={styles.indicator}\n aria-hidden=\"true\"\n />\n )}\n {props.children}\n </ToggleGroupPrimitive.Root>\n </ToggleGroupContext.Provider>\n )\n})\nToggleGroup.displayName = \"ToggleGroup\"\n\n/* ─── ToggleGroupItem ───────────────────────────────────────────────── */\n\nconst toggleGroupItemVariants = cva(styles.item, {\n variants: {\n variant: {\n default: styles.itemVariantDefault,\n outline: styles.itemVariantOutline,\n },\n size: {\n xs: styles.itemSizeXs,\n sm: styles.itemSizeSm,\n md: styles.itemSizeMd,\n lg: styles.itemSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ToggleGroupItemProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>,\n VariantProps<typeof toggleGroupItemVariants> {}\n\n/**\n * Wrap bare text in a block label so the stylesheet can trim its line box to the\n * capital-to-baseline band (see `.label`), centring the capitals in the item\n * whatever the font (VI-684). Elements (icons) pass through.\n */\nfunction wrapLabel(children: React.ReactNode): React.ReactNode {\n return React.Children.map(children, (child) =>\n typeof child === \"string\" || typeof child === \"number\" ? (\n <span className={styles.label} data-slot=\"toggle-group-text\">\n {child}\n </span>\n ) : (\n child\n )\n )\n}\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ToggleGroupItemProps\n>(({ className, variant, size, children, ...props }, ref) => {\n const ctx = React.useContext(ToggleGroupContext)\n const resolvedVariant = variant ?? ctx.variant\n const resolvedSize = size ?? ctx.size\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n className={cn(toggleGroupItemVariants({ variant: resolvedVariant, size: resolvedSize }), className)}\n ref={ref}\n {...props}\n >\n {wrapLabel(children)}\n </ToggleGroupPrimitive.Item>\n )\n})\nToggleGroupItem.displayName = \"ToggleGroupItem\"\n\nexport { ToggleGroup, ToggleGroupItem, toggleGroupVariants, toggleGroupItemVariants }\n"
|
|
1380
1380
|
},
|
|
1381
1381
|
{
|
|
1382
1382
|
"path": "components/ui/toggle-group/toggle-group.module.css",
|
|
1383
1383
|
"type": "registry:ui",
|
|
1384
|
-
"content": "/* ToggleGroup root */\n.root {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.variantDefault {\n /* items have no outer border, rely on item bg */\n}\n\n.variantOutline {\n position: relative;\n border: 1px solid var(--border-default, #e5e7eb);\n gap: 0;\n}\n\n/* Sliding indicator pill */\n.indicator {\n position: absolute;\n top: 0;\n left: 0;\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-radius: var(--radius-md, 0.375rem);\n opacity: 0;\n pointer-events: none;\n z-index: 0;\n transition:\n transform var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Match indicator radius to item radius per size */\n.sizeXs .indicator { border-radius: var(--radius-sm, 0.25rem); }\n.sizeSm .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeMd .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeLg .indicator { border-radius: var(--radius-lg, 0.5rem); }\n\n.sizeXs {\n /* inherited by items */\n}\n\n.variantOutline.sizeXs {\n padding: calc(var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n}\n\n.sizeSm {\n /* inherited by items */\n}\n\n.variantOutline.sizeSm {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeMd {\n /* inherited by items */\n}\n\n.variantOutline.sizeMd {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeLg {\n /* inherited by items */\n}\n\n.variantOutline.sizeLg {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-xl, 0.75rem);\n}\n\n/* ToggleGroup item */\n.item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n
|
|
1384
|
+
"content": "/* ToggleGroup root */\n.root {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1, 0.25rem);\n}\n\n.variantDefault {\n /* items have no outer border, rely on item bg */\n}\n\n.variantOutline {\n position: relative;\n border: 1px solid var(--border-default, #e5e7eb);\n gap: 0;\n}\n\n/* Sliding indicator pill */\n.indicator {\n position: absolute;\n top: 0;\n left: 0;\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n border-radius: var(--radius-md, 0.375rem);\n opacity: 0;\n pointer-events: none;\n z-index: 0;\n transition:\n transform var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n width var(--motion-duration-200, 200ms) var(--motion-easing-default, ease-in-out),\n opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Match indicator radius to item radius per size */\n.sizeXs .indicator { border-radius: var(--radius-sm, 0.25rem); }\n.sizeSm .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeMd .indicator { border-radius: var(--radius-md, 0.375rem); }\n.sizeLg .indicator { border-radius: var(--radius-lg, 0.5rem); }\n\n.sizeXs {\n /* inherited by items */\n}\n\n.variantOutline.sizeXs {\n padding: calc(var(--spacing-1, 0.25rem) / 2);\n border-radius: var(--radius-md, 0.375rem);\n}\n\n.sizeSm {\n /* inherited by items */\n}\n\n.variantOutline.sizeSm {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeMd {\n /* inherited by items */\n}\n\n.variantOutline.sizeMd {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n.sizeLg {\n /* inherited by items */\n}\n\n.variantOutline.sizeLg {\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-xl, 0.75rem);\n}\n\n/* ToggleGroup item */\n.item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n user-select: none;\n white-space: nowrap;\n}\n\n.item:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n/* A solid ring at the focus ring width, cleared of the item by the focus ring offset\n (VI-690). The translucent halo alone measured 1.4:1 on dark planes (WCAG 2.4.7). */\n.item: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.item:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* Default variant items */\n@media (hover: hover) {\n .itemVariantDefault:hover:not(:disabled) {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n }\n}\n\n.itemVariantDefault[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Outline variant items — indicator provides the active background */\n.itemVariantOutline {\n position: relative;\n z-index: 1;\n}\n\n@media (hover: hover) {\n .itemVariantOutline:hover:not(:disabled):not([data-state=\"on\"]) {\n background-color: color-mix(in srgb, var(--surface-interactive-hover, #f3f4f6) 40%, transparent);\n }\n}\n\n.itemVariantOutline[data-state=\"on\"] {\n background-color: transparent;\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n/* Multiple-select outline — items need their own active bg (no sliding indicator) */\n:global([data-type=\"multiple\"]) .itemVariantOutline[data-state=\"on\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Sizes */\n.itemSizeXs {\n --label-box-height: 1.5rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-sm, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeSm {\n --label-box-height: 1.75rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-2, 0.5rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-xs, 0.75rem);\n gap: var(--spacing-1, 0.25rem);\n}\n\n.itemSizeMd {\n --label-box-height: 2.25rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-3, 0.75rem);\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n.itemSizeLg {\n --label-box-height: 2.75rem;\n height: var(--label-box-height);\n padding: 0 var(--spacing-4, 1rem);\n border-radius: var(--radius-lg, 0.5rem);\n font-size: var(--font-size-base, 1rem);\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Label centring (VI-684, shared spec: the label-centering doc under docs/). The label is a\n block span whose line box is trimmed to the capital-to-baseline band, so the\n flex container centres the ink and not the font's ascent/descent. Without\n text-box (Firefox) it is a plain block and rendering is unchanged. */\n.label {\n display: block;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took. WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n"
|
|
1385
|
+
}
|
|
1386
|
+
]
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
"name": "segmented-control",
|
|
1390
|
+
"type": "registry:ui",
|
|
1391
|
+
"description": "A single-select pill group with a sliding active indicator, an options API and an equal-width fullWidth layout. A thin wrapper over toggle-group that never goes empty.",
|
|
1392
|
+
"category": "form",
|
|
1393
|
+
"dependencies": [
|
|
1394
|
+
"class-variance-authority",
|
|
1395
|
+
"@loworbitstudio/visor-core"
|
|
1396
|
+
],
|
|
1397
|
+
"registryDependencies": [
|
|
1398
|
+
"toggle-group",
|
|
1399
|
+
"utils"
|
|
1400
|
+
],
|
|
1401
|
+
"files": [
|
|
1402
|
+
{
|
|
1403
|
+
"path": "components/ui/segmented-control/segmented-control.tsx",
|
|
1404
|
+
"type": "registry:ui",
|
|
1405
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport { ToggleGroup, ToggleGroupItem } from \"../toggle-group/toggle-group\"\nimport type { ToggleGroupProps } from \"../toggle-group/toggle-group\"\nimport styles from \"./segmented-control.module.css\"\n\n/* ─── Types ─────────────────────────────────────────────────────────── */\n\nexport interface SegmentedControlOption {\n /** Value reported by `onValueChange` while this segment is on. */\n value: string\n /** Segment label. Bare text is centred optically (docs/label-centering.md). */\n label: React.ReactNode\n /** Optional leading icon. */\n icon?: React.ReactNode\n disabled?: boolean\n}\n\nexport interface SegmentedControlProps\n extends Omit<\n React.ComponentPropsWithoutRef<\"div\">,\n \"defaultValue\" | \"onChange\" | \"dir\"\n > {\n options: SegmentedControlOption[]\n /** Controlled value. */\n value?: string\n /** Uncontrolled initial value. Falls back to the first enabled option. */\n defaultValue?: string\n /** Fires with the new value. Never fires with an empty string. */\n onValueChange?: (value: string) => void\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\"\n /** Equal-width segments spanning the container (the list-filter treatment). */\n fullWidth?: boolean\n disabled?: boolean\n dir?: ToggleGroupProps[\"dir\"]\n}\n\n/* ─── Variants ──────────────────────────────────────────────────────── */\n\nconst segmentedControlVariants = cva(styles.root, {\n variants: {\n fullWidth: {\n true: styles.fullWidth,\n false: \"\",\n },\n },\n defaultVariants: {\n fullWidth: false,\n },\n})\n\n/* ─── Indicator re-sync ─────────────────────────────────────────────── */\n\n/**\n * ToggleGroup positions its sliding indicator when an item's `data-state`\n * changes, but not when the group is resized (a `fullWidth` control follows\n * its container, and a web font swap changes label widths). Re-setting the\n * active item's own `data-state` queues the mutation record it already\n * listens for, so the indicator re-measures without a fork of ToggleGroup.\n */\nfunction useResyncIndicator(rootRef: React.RefObject<HTMLDivElement | null>) {\n React.useEffect(() => {\n const root = rootRef.current\n if (!root || typeof ResizeObserver === \"undefined\") return\n const observer = new ResizeObserver(() => {\n const active = root.querySelector('[data-state=\"on\"]')\n active?.setAttribute(\"data-state\", \"on\")\n })\n observer.observe(root)\n return () => observer.disconnect()\n }, [rootRef])\n}\n\n/* ─── Label with its width reserved ─────────────────────────────────── */\n\n/**\n * A bare-text label, rendered the way ToggleGroupItem renders one (a\n * `toggle-group-text` block, trimmed to the capital band, see .label) inside a\n * one-cell grid. Two hidden copies of the text share the cell, one at the\n * active weight and one at the inactive weight, so each segment is always as\n * wide as its widest state and selecting a segment moves nothing, even when a\n * theme binds a heavier `active-weight`. The copies are `aria-hidden` and\n * visually hidden: they cost no height and are not read. Elements (icons, rich\n * labels) pass through unchanged.\n */\nfunction renderLabel(label: React.ReactNode): React.ReactNode {\n if (typeof label !== \"string\" && typeof label !== \"number\") return label\n return (\n <span className={styles.labelBox}>\n <span className={styles.label} data-slot=\"toggle-group-text\">\n {label}\n </span>\n <span className={cn(styles.reserve, styles.reserveActive)} aria-hidden=\"true\">\n {label}\n </span>\n <span className={cn(styles.reserve, styles.reserveInactive)} aria-hidden=\"true\">\n {label}\n </span>\n </span>\n )\n}\n\n/* ─── SegmentedControl ──────────────────────────────────────────────── */\n\nconst SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n (\n {\n options,\n value: valueProp,\n defaultValue,\n onValueChange,\n size = \"md\",\n fullWidth = false,\n disabled,\n className,\n ...props\n },\n ref,\n ) => {\n const firstEnabled = options.find((o) => !o.disabled)?.value\n const [inner, setInner] = React.useState<string | undefined>(\n defaultValue ?? firstEnabled,\n )\n const controlled = valueProp !== undefined\n const value = controlled ? valueProp : inner\n\n const rootRef = React.useRef<HTMLDivElement | null>(null)\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n rootRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n useResyncIndicator(rootRef)\n\n // One option is always on. Radix reports \"\" when the active segment is\n // clicked again; the group is always given a value, so ignoring \"\" keeps it.\n const handleValueChange = React.useCallback(\n (next: string) => {\n if (!next) return\n if (!controlled) setInner(next)\n onValueChange?.(next)\n },\n [controlled, onValueChange],\n )\n\n return (\n <ToggleGroup\n {...props}\n ref={mergedRef}\n type=\"single\"\n variant=\"outline\"\n size={size}\n value={value ?? \"\"}\n onValueChange={handleValueChange}\n disabled={disabled}\n data-slot=\"segmented-control\"\n data-full-width={fullWidth ? \"true\" : undefined}\n className={cn(segmentedControlVariants({ fullWidth }), className)}\n >\n {options.map((option) => (\n <ToggleGroupItem\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n >\n {option.icon}\n {renderLabel(option.label)}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n )\n },\n)\nSegmentedControl.displayName = \"SegmentedControl\"\n\nexport { SegmentedControl, segmentedControlVariants }\n"
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
"path": "components/ui/segmented-control/segmented-control.module.css",
|
|
1409
|
+
"type": "registry:ui",
|
|
1410
|
+
"content": "/* SegmentedControl layers on ToggleGroup (outline, single). ToggleGroup owns the\n mechanics, sizes, sliding indicator and label centring; this sheet only\n re-skins the track and indicator and adds the fullWidth layout. ToggleGroup's\n own classes are hashed, so they are reached through the data attributes it\n renders. Specificity here is two classes or a class plus an attribute so the\n re-skin wins whatever order the sheets load in. */\n\n.root[data-variant=\"outline\"] {\n /* VI-655: the border only holds box geometry. The visible resting edge is an\n outline pulled inside the box from the shared --control-edge-* tokens, so\n --control-edge-width: 0 on any ancestor removes it with no layout shift. */\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 /* The well the active pill sits in: the page nudged 12% toward the ink, so it reads as a well\n in light and dark alike (page-relative, unlike --surface-subtle, which is the page in dark). */\n background-color: var(--segmented-control-track-bg, color-mix(in srgb, var(--text-primary, #111827) 12%, var(--surface-page, #ffffff)));\n}\n\n/* Labels are medium weight, never light. */\n.root [data-slot=\"toggle-group-item\"] {\n font-weight: var(--font-weight-medium, 500);\n}\n\n/* Track corner radius: ToggleGroup's per-size radius unless a theme binds\n --segmented-control-radius (a theme that wants 8px, say). Each size carries\n its own fallback, so an unbound theme renders exactly as before. */\n.root[data-variant=\"outline\"][data-size=\"xs\"] {\n border-radius: var(--segmented-control-radius, var(--radius-md, 0.375rem));\n}\n\n.root[data-variant=\"outline\"][data-size=\"sm\"],\n.root[data-variant=\"outline\"][data-size=\"md\"] {\n border-radius: var(--segmented-control-radius, var(--radius-lg, 0.5rem));\n}\n\n.root[data-variant=\"outline\"][data-size=\"lg\"] {\n border-radius: var(--segmented-control-radius, var(--radius-xl, 0.75rem));\n}\n\n/* The sliding pill. A theme can set a lifted dark fill (and the ink on it).\n Its edge is a state edge: an inset ring at --control-state-edge-width, the\n width focus and invalid use, so `edges: off` leaves it and nothing shifts\n (WCAG 1.4.11: the selected state carries a 3:1 boundary of its own). A theme\n whose fill already clears 3:1 against the well binds the edge to transparent. */\n.root > [aria-hidden=\"true\"] {\n background-color: var(--segmented-control-indicator-bg, var(--interactive-primary-bg, var(--primary, #111827)));\n box-shadow: inset 0 0 0 var(--control-state-edge-width, 1px) var(--segmented-control-indicator-edge, var(--text-primary, #111827));\n}\n\n.root [data-slot=\"toggle-group-item\"]:not([data-state=\"on\"]) {\n color: var(--segmented-control-inactive-text, var(--text-primary, #111827));\n}\n\n.root [data-slot=\"toggle-group-item\"][data-state=\"on\"] {\n color: var(--segmented-control-indicator-text, var(--interactive-primary-text, #f9fafb));\n font-weight: var(--segmented-control-active-weight, var(--font-weight-medium, 500));\n}\n\n/* Focus stays visible with edges off: it draws at the focus ring width, which\n the edge switch does not touch. It sits outside the pill, in the track. */\n.root [data-slot=\"toggle-group-item\"]:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: none;\n}\n\n/* Equal-width segments spanning the container (list-filter treatment). */\n.root.fullWidth {\n display: flex;\n width: 100%;\n}\n\n.root.fullWidth > [data-slot=\"toggle-group-item\"] {\n flex: 1 1 0;\n min-width: 0;\n}\n\n/* The indicator snaps instead of sliding. */\n@media (prefers-reduced-motion: reduce) {\n .root > [aria-hidden=\"true\"] {\n transition: none;\n }\n}\n\n/* Label box (see renderLabel in the component file). The label, trimmed to the capital band\n exactly as ToggleGroup trims its own (the label-centering doc under docs/), and two hidden\n copies of the text share one grid cell, so the cell is as wide as the widest of\n the active-weight and inactive-weight text and the visible label stays centred\n in it. The copies have no height. */\n.labelBox {\n display: inline-grid;\n justify-items: center;\n}\n\n.labelBox > * {\n grid-area: 1 / 1;\n}\n\n.label {\n display: block;\n white-space: nowrap;\n}\n\n@supports (text-box: trim-both cap alphabetic) {\n .label {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Blink only: it snaps the baseline to a whole CSS px; this hands back what the\n snap took. WebKit places the baseline exactly, so it is excluded. */\n@supports (text-box: trim-both cap alphabetic) and (not (-apple-pay-button-style: black)) {\n .label {\n transform: translateY(calc((var(--label-box-height) + 1cap) / 2 - round((var(--label-box-height) + 1cap) / 2, 1px)));\n }\n}\n\n.reserve {\n height: 0;\n overflow: hidden;\n visibility: hidden;\n white-space: nowrap;\n pointer-events: none;\n}\n\n.reserveActive {\n font-weight: var(--segmented-control-active-weight, var(--font-weight-medium, 500));\n}\n\n.reserveInactive {\n font-weight: var(--font-weight-medium, 500);\n}\n"
|
|
1385
1411
|
}
|
|
1386
1412
|
]
|
|
1387
1413
|
},
|
|
@@ -1612,12 +1638,12 @@
|
|
|
1612
1638
|
{
|
|
1613
1639
|
"path": "components/ui/date-picker/date-picker.tsx",
|
|
1614
1640
|
"type": "registry:ui",
|
|
1615
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { format } from \"date-fns\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport { CalendarBlank } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Calendar } from \"../calendar/calendar\"\nimport styles from \"./date-picker.module.css\"\n\nexport interface DatePickerProps {\n /** Selected date */\n value?: Date\n /** Called when date changes */\n onChange?: (date: Date | undefined) => void\n /** Placeholder text when no date is selected */\n placeholder?: string\n /** Date format string (date-fns format) */\n dateFormat?: string\n /** Whether the picker is disabled */\n disabled?: boolean\n /** Additional class name for the trigger button */\n className?: string\n}\n\nconst DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(\n (\n {\n value,\n onChange,\n placeholder = \"Pick a date\",\n dateFormat = \"PPP\",\n disabled = false,\n className,\n },\n ref\n ) => {\n const [open, setOpen] = React.useState(
|
|
1641
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { format } from \"date-fns\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport { CalendarBlank } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Calendar } from \"../calendar/calendar\"\nimport styles from \"./date-picker.module.css\"\n\nexport interface DatePickerProps {\n /** Selected date */\n value?: Date\n /** Called when date changes */\n onChange?: (date: Date | undefined) => void\n /** Placeholder text when no date is selected */\n placeholder?: string\n /** Date format string (date-fns format) */\n dateFormat?: string\n /** Whether the picker is disabled */\n disabled?: boolean\n /** Additional class name for the trigger button */\n className?: string\n /** Popover alignment against the trigger (VI-654). Defaults to \"start\". */\n align?: \"start\" | \"center\" | \"end\"\n /** Gap in px between the trigger and the popover. Defaults to 4. */\n sideOffset?: number\n /** Viewport padding in px the popover keeps clear when it collides. Defaults to 0. */\n collisionPadding?: number\n /** Whether the popover starts open (uncontrolled). Defaults to false. */\n defaultOpen?: boolean\n}\n\nconst DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(\n (\n {\n value,\n onChange,\n placeholder = \"Pick a date\",\n dateFormat = \"PPP\",\n disabled = false,\n className,\n align = \"start\",\n sideOffset = 4,\n collisionPadding = 0,\n defaultOpen = false,\n },\n ref\n ) => {\n const [open, setOpen] = React.useState(defaultOpen)\n\n return (\n <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>\n <PopoverPrimitive.Trigger\n ref={ref}\n data-slot=\"date-picker\"\n disabled={disabled}\n className={cn(\n styles.trigger,\n !value && styles.placeholder,\n disabled && styles.disabled,\n className\n )}\n >\n <CalendarBlank size={16} className={styles.icon} />\n <span>{value ? format(value, dateFormat) : placeholder}</span>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"date-picker-content\"\n className={styles.content}\n align={align}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n >\n <Calendar\n mode=\"single\"\n selected={value}\n defaultMonth={value}\n onSelect={(date) => {\n onChange?.(date)\n setOpen(false)\n }}\n autoFocus\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n )\n }\n)\nDatePicker.displayName = \"DatePicker\"\n\nexport { DatePicker }\n"
|
|
1616
1642
|
},
|
|
1617
1643
|
{
|
|
1618
1644
|
"path": "components/ui/date-picker/date-picker.module.css",
|
|
1619
1645
|
"type": "registry:ui",
|
|
1620
|
-
"content": "/* Date Picker Trigger */\n.trigger {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n height: 2.25rem;\n padding: 0 var(--spacing-3, 0.75rem);\n border: 1px solid var(--border-default, #e5e7eb);\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n cursor: pointer;\n transition:
|
|
1646
|
+
"content": "/* Date Picker Trigger */\n.trigger {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n height: 2.25rem;\n padding: 0 var(--spacing-3, 0.75rem);\n /* VI-655: the border only holds box geometry and stays transparent; the\n visible edge is an outline drawn from the shared --control-edge-* tokens. */\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 border-radius: var(--radius-md, 0.375rem);\n background-color: var(--surface-card, #ffffff);\n color: var(--text-primary, #111827);\n font-size: var(--font-size-sm, 0.875rem);\n cursor: pointer;\n transition: outline-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-default, ease);\n white-space: nowrap;\n}\n\n@media (hover: hover) {\n .trigger:hover:not(.disabled) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n.trigger: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.placeholder {\n color: var(--text-tertiary, #9ca3af);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.icon {\n color: var(--text-tertiary, #9ca3af);\n flex-shrink: 0;\n}\n\n/* Popover Content */\n.content {\n z-index: 50;\n background-color: var(--field-menu-bg, var(--surface-popover, var(--surface-card, #ffffff)));\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 border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(\n --shadow-md,\n 0 4px 6px -1px rgba(0, 0, 0, 0.1),\n 0 2px 4px -2px rgba(0, 0, 0, 0.1)\n );\n}\n"
|
|
1621
1647
|
}
|
|
1622
1648
|
]
|
|
1623
1649
|
},
|
|
@@ -2026,12 +2052,12 @@
|
|
|
2026
2052
|
{
|
|
2027
2053
|
"path": "components/ui/number-input/number-input.tsx",
|
|
2028
2054
|
"type": "registry:ui",
|
|
2029
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Minus, Plus } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./number-input.module.css\"\n\nexport interface NumberInputProps\n extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"type\" | \"onChange\" | \"value\" | \"defaultValue\"\n > {\n value?: number\n defaultValue?: number\n onChange?: (value: number | undefined) => void\n min?: number\n max?: number\n step?: number\n}\n\nfunction clamp(value: number, min?: number, max?: number): number {\n if (min !== undefined && value < min) return min\n if (max !== undefined && value > max) return max\n return value\n}\n\nconst NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n (\n {\n className,\n value,\n defaultValue,\n onChange,\n min,\n max,\n step = 1,\n disabled,\n ...props\n },\n ref\n ) => {\n const [internalValue, setInternalValue] = React.useState<string>(\n defaultValue !== undefined ? String(defaultValue) : \"\"\n )\n const isControlled = value !== undefined\n const displayValue = isControlled ? String(value) : internalValue\n\n const updateValue = (newValue: number | undefined) => {\n if (!isControlled) {\n setInternalValue(newValue !== undefined ? String(newValue) : \"\")\n }\n onChange?.(newValue)\n }\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const raw = e.target.value\n if (raw === \"\" || raw === \"-\") {\n if (!isControlled) setInternalValue(raw)\n if (raw === \"\") onChange?.(undefined)\n return\n }\n const parsed = Number(raw)\n if (!Number.isNaN(parsed)) {\n if (!isControlled) setInternalValue(raw)\n onChange?.(parsed)\n }\n }\n\n const handleBlur = () => {\n const parsed = Number(displayValue)\n if (displayValue === \"\" || Number.isNaN(parsed)) {\n updateValue(undefined)\n return\n }\n const clamped = clamp(parsed, min, max)\n updateValue(clamped)\n }\n\n const increment = () => {\n const current = Number(displayValue) || 0\n const next = clamp(current + step, min, max)\n updateValue(next)\n }\n\n const decrement = () => {\n const current = Number(displayValue) || 0\n const next = clamp(current - step, min, max)\n updateValue(next)\n }\n\n return (\n <div\n data-slot=\"number-input\"\n className={cn(styles.wrapper, disabled && styles.wrapperDisabled, className)}\n >\n <button\n type=\"button\"\n className={styles.button}\n onClick={decrement}\n disabled={disabled}\n aria-label=\"Decrease value\"\n tabIndex={-1}\n >\n <Minus className={styles.buttonIcon} aria-hidden=\"true\" />\n </button>\n <input\n ref={
|
|
2055
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Minus, Plus } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./number-input.module.css\"\n\nexport interface NumberInputProps\n extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"type\" | \"onChange\" | \"value\" | \"defaultValue\" | \"prefix\"\n > {\n value?: number\n defaultValue?: number\n onChange?: (value: number | undefined) => void\n min?: number\n max?: number\n step?: number\n /**\n * Fixed text before the value, inside the same well (a currency symbol).\n * Same treatment as `Input`'s `prefix`: secondary ink, never part of the\n * value, clicking it focuses the field, and it is wired to the input with\n * `aria-describedby`. Replaces the native `prefix` HTML attribute, which is\n * not forwarded.\n */\n prefix?: React.ReactNode\n /** Fixed text after the value, inside the same well (a unit: `h`, `m`). See `prefix`. */\n suffix?: React.ReactNode\n}\n\nfunction clamp(value: number, min?: number, max?: number): number {\n if (min !== undefined && value < min) return min\n if (max !== undefined && value > max) return max\n return value\n}\n\nconst NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n (\n {\n className,\n value,\n defaultValue,\n onChange,\n min,\n max,\n step = 1,\n disabled,\n prefix,\n suffix,\n ...props\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement | null>(null)\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 const affixId = React.useId()\n const hasPrefix = prefix !== undefined && prefix !== null && prefix !== false\n const hasSuffix = suffix !== undefined && suffix !== null && suffix !== false\n const prefixId = `${affixId}-prefix`\n const suffixId = `${affixId}-suffix`\n const describedBy =\n [props[\"aria-describedby\"], hasPrefix && prefixId, hasSuffix && suffixId]\n .filter(Boolean)\n .join(\" \") || undefined\n // A press on an affix would blur the input; hand it back instead.\n const focusInput = (e: React.MouseEvent) => {\n if (!hasPrefix && !hasSuffix) return\n const target = e.target as HTMLElement\n if (target === inputRef.current || target.closest(\"button\")) return\n e.preventDefault()\n inputRef.current?.focus()\n }\n const [internalValue, setInternalValue] = React.useState<string>(\n defaultValue !== undefined ? String(defaultValue) : \"\"\n )\n const isControlled = value !== undefined\n const displayValue = isControlled ? String(value) : internalValue\n\n const updateValue = (newValue: number | undefined) => {\n if (!isControlled) {\n setInternalValue(newValue !== undefined ? String(newValue) : \"\")\n }\n onChange?.(newValue)\n }\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const raw = e.target.value\n if (raw === \"\" || raw === \"-\") {\n if (!isControlled) setInternalValue(raw)\n if (raw === \"\") onChange?.(undefined)\n return\n }\n const parsed = Number(raw)\n if (!Number.isNaN(parsed)) {\n if (!isControlled) setInternalValue(raw)\n onChange?.(parsed)\n }\n }\n\n const handleBlur = () => {\n const parsed = Number(displayValue)\n if (displayValue === \"\" || Number.isNaN(parsed)) {\n updateValue(undefined)\n return\n }\n const clamped = clamp(parsed, min, max)\n updateValue(clamped)\n }\n\n const increment = () => {\n const current = Number(displayValue) || 0\n const next = clamp(current + step, min, max)\n updateValue(next)\n }\n\n const decrement = () => {\n const current = Number(displayValue) || 0\n const next = clamp(current - step, min, max)\n updateValue(next)\n }\n\n return (\n <div\n data-slot=\"number-input\"\n className={cn(styles.wrapper, disabled && styles.wrapperDisabled, className)}\n onMouseDown={focusInput}\n >\n <button\n type=\"button\"\n className={styles.button}\n onClick={decrement}\n disabled={disabled}\n aria-label=\"Decrease value\"\n tabIndex={-1}\n >\n <Minus className={styles.buttonIcon} aria-hidden=\"true\" />\n </button>\n {hasPrefix ? (\n <span id={prefixId} className={cn(styles.affix, styles.affixPrefix)} data-slot=\"number-input-prefix\">\n {prefix}\n </span>\n ) : null}\n <input\n ref={setRefs}\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n role=\"spinbutton\"\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={\n displayValue !== \"\" ? Number(displayValue) : undefined\n }\n value={displayValue}\n onChange={handleInputChange}\n onBlur={handleBlur}\n disabled={disabled}\n className={cn(\n styles.input,\n hasPrefix && !hasSuffix && styles.inputAfterPrefix,\n hasSuffix && !hasPrefix && styles.inputBeforeSuffix\n )}\n {...props}\n aria-describedby={describedBy}\n />\n {hasSuffix ? (\n <span id={suffixId} className={cn(styles.affix, styles.affixSuffix)} data-slot=\"number-input-suffix\">\n {suffix}\n </span>\n ) : null}\n <button\n type=\"button\"\n className={styles.button}\n onClick={increment}\n disabled={disabled}\n aria-label=\"Increase value\"\n tabIndex={-1}\n >\n <Plus className={styles.buttonIcon} aria-hidden=\"true\" />\n </button>\n </div>\n )\n }\n)\nNumberInput.displayName = \"NumberInput\"\n\nexport { NumberInput }\n"
|
|
2030
2056
|
},
|
|
2031
2057
|
{
|
|
2032
2058
|
"path": "components/ui/number-input/number-input.module.css",
|
|
2033
2059
|
"type": "registry:ui",
|
|
2034
|
-
"content": "/* Number input wrapper */\n.wrapper {\n display: inline-flex;\n align-items: center;\n border: 1px solid var(--number-input-border, var(--border-default, #e5e7eb));\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--number-input-bg, var(--surface-interactive-default, #f9fafb));\n overflow: hidden;\n transition:
|
|
2060
|
+
"content": "/* Number input wrapper */\n.wrapper {\n display: inline-flex;\n align-items: center;\n /* VI-662: the border only holds box geometry. The resting edge is the shared\n --control-edge-* outline (VI-655), so --control-edge-width: 0 turns it off.\n --number-input-border stays the per-component colour override. */\n border: 1px solid transparent;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--number-input-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--number-input-bg, var(--surface-interactive-default, #f9fafb));\n overflow: hidden;\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 .button:hover {\n color: var(--text-primary, #111827);\n background-color: var(--surface-muted, #f3f4f6);\n }\n}\n\n.wrapper:focus-within {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.wrapperDisabled {\n opacity: 0.5;\n pointer-events: none;\n}\n\n/* Stepper buttons */\n.button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--text-secondary, #9ca3af);\n cursor: pointer;\n flex-shrink: 0;\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}\n\n.button:active {\n background-color: var(--surface-interactive-hover, #e5e7eb);\n transition: none;\n}\n\n.button: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.button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.buttonIcon {\n width: 0.875rem;\n height: 0.875rem;\n}\n\n/* Input */\n.input {\n width: 4rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n background: transparent;\n text-align: center;\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\n/* Prefix and suffix (VI-662). Fixed text inside the same well, between the\n steppers and the value. Both are flex items beside the input, so the value\n never runs under them. The affix reads the same tokens as Input's. */\n.affix {\n flex-shrink: 0;\n white-space: nowrap;\n user-select: none;\n cursor: text;\n font-size: var(--font-size-sm, 0.875rem);\n line-height: 1.5;\n color: var(--input-affix-color, var(--text-secondary, #6b7280));\n}\n\n/* With one affix the value leans toward it, so the pair reads as one figure\n (\"€ 150\", \"2 h\"); with both, or neither, the value stays centred. */\n.inputAfterPrefix {\n text-align: start;\n padding-inline-start: var(--input-affix-gap, var(--spacing-2, 0.5rem));\n}\n\n.inputBeforeSuffix {\n text-align: end;\n padding-inline-end: var(--input-affix-gap, var(--spacing-2, 0.5rem));\n}\n\n.affixPrefix {\n margin-inline-start: var(--spacing-2, 0.5rem);\n}\n\n.affixSuffix {\n margin-inline-end: var(--spacing-2, 0.5rem);\n}\n\n/* Per-font baseline nudge (VI-684 mechanism, shared with Input). The theme engine\n publishes the body face's ascent and descent as unitless em ratios; the value\n box is 2.25rem, the same as Input sm, so it takes the same nudge: the gap\n between the whole px nearest the ideal baseline and the one the font produces.\n The value and both affixes move by it, so they share a baseline. Blink only\n (WebKit places the baseline exactly); no font metrics means no nudge (0px). */\n@supports (padding-top: round(1.4px, 1px)) and (not (-apple-pay-button-style: black)) {\n .wrapper {\n --input-nudge: calc(round((2.25rem + 1cap) / 2, 1px) - (2.25rem + (round(var(--font-ascent) * 1em, 1px) - round(var(--font-descent) * 1em, 1px))) / 2);\n }\n\n .input,\n .affix {\n position: relative;\n top: var(--input-nudge, 0px);\n }\n}\n"
|
|
2035
2061
|
}
|
|
2036
2062
|
]
|
|
2037
2063
|
},
|
|
@@ -2081,12 +2107,12 @@
|
|
|
2081
2107
|
{
|
|
2082
2108
|
"path": "components/ui/otp-input/otp-input.tsx",
|
|
2083
2109
|
"type": "registry:ui",
|
|
2084
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./otp-input.module.css\"\n\nexport interface OTPInputProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n length?: number\n value?: string\n onChange?: (value: string) => void\n disabled?: boolean\n autoFocus?: boolean\n}\n\nconst OTPInput = React.forwardRef<HTMLDivElement, OTPInputProps>(\n (\n {\n className,\n length = 6,\n value,\n onChange,\n disabled = false,\n autoFocus = false,\n ...props\n },\n ref\n ) => {\n const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])\n const [internalValue, setInternalValue] = React.useState(\"\")\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n\n const digits = currentValue.split(\"\").slice(0, length)\n\n const updateValue = (newValue: string) => {\n const cleaned = newValue.replace(/\\D/g, \"\").slice(0, length)\n if (!isControlled) {\n setInternalValue(cleaned)\n }\n onChange?.(cleaned)\n }\n\n const focusInput = (index: number) => {\n const clamped = Math.max(0, Math.min(index, length - 1))\n inputRefs.current[clamped]?.focus()\n }\n\n const handleInput = (index: number, char: string) => {\n const newDigits = [...digits]\n while (newDigits.length < length) newDigits.push(\"\")\n newDigits[index] = char\n const newValue = newDigits.join(\"\")\n updateValue(newValue)\n if (char && index < length - 1) {\n focusInput(index + 1)\n }\n }\n\n const handleKeyDown = (\n index: number,\n e: React.KeyboardEvent<HTMLInputElement>\n ) => {\n if (e.key === \"Backspace\") {\n e.preventDefault()\n if (digits[index]) {\n handleInput(index, \"\")\n } else if (index > 0) {\n handleInput(index - 1, \"\")\n focusInput(index - 1)\n }\n } else if (e.key === \"ArrowLeft\" && index > 0) {\n e.preventDefault()\n focusInput(index - 1)\n } else if (e.key === \"ArrowRight\" && index < length - 1) {\n e.preventDefault()\n focusInput(index + 1)\n }\n }\n\n const handleChange = (\n index: number,\n e: React.ChangeEvent<HTMLInputElement>\n ) => {\n const char = e.target.value.replace(/\\D/g, \"\").slice(-1)\n if (char) {\n handleInput(index, char)\n }\n }\n\n const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\n e.preventDefault()\n const pastedDigits = e.clipboardData\n .getData(\"text\")\n .replace(/\\D/g, \"\")\n .slice(0, length)\n if (pastedDigits) {\n updateValue(pastedDigits)\n focusInput(Math.min(pastedDigits.length, length - 1))\n }\n }\n\n const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {\n e.target.select()\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"otp-input\"\n role=\"group\"\n aria-label=\"Verification code\"\n className={cn(styles.wrapper, className)}\n {...props}\n >\n {Array.from({ length }, (_, i) => (\n <input\n key={i}\n ref={(el) => {\n inputRefs.current[i] = el\n }}\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]\"\n maxLength={1}\n value={digits[i] ?? \"\"}\n onChange={(e) => handleChange(i, e)}\n onKeyDown={(e) => handleKeyDown(i, e)}\n onPaste={handlePaste}\n onFocus={handleFocus}\n disabled={disabled}\n autoFocus={autoFocus && i === 0}\n aria-label={`Digit ${i + 1} of ${length}`}\n className={cn(\n styles.cell,\n digits[i] && styles.cellFilled\n )}\n />\n ))}\n </div>\n )\n }\n)\nOTPInput.displayName = \"OTPInput\"\n\nexport { OTPInput }\n"
|
|
2110
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./otp-input.module.css\"\n\nexport interface OTPInputProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n length?: number\n value?: string\n onChange?: (value: string) => void\n disabled?: boolean\n autoFocus?: boolean\n /** Marks every cell invalid (`aria-invalid`) and draws the invalid edge. The digits are kept so they can be fixed. */\n invalid?: boolean\n /** Shown under the cells while `invalid`, in the error text colour, as a `role=\"alert\"` live region (4.1.3). The component draws the message, so do not render a second error line. */\n errorMessage?: React.ReactNode\n /** Trailing content beside the cells: a working indicator (Spinner) or a resend action. */\n trailing?: React.ReactNode\n}\n\nconst OTPInput = React.forwardRef<HTMLDivElement, OTPInputProps>(\n (\n {\n className,\n length = 6,\n value,\n onChange,\n disabled = false,\n autoFocus = false,\n invalid = false,\n errorMessage,\n trailing,\n ...props\n },\n ref\n ) => {\n const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])\n const [internalValue, setInternalValue] = React.useState(\"\")\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n\n const digits = currentValue.split(\"\").slice(0, length)\n\n const updateValue = (newValue: string) => {\n const cleaned = newValue.replace(/\\D/g, \"\").slice(0, length)\n if (!isControlled) {\n setInternalValue(cleaned)\n }\n onChange?.(cleaned)\n }\n\n const focusInput = (index: number) => {\n const clamped = Math.max(0, Math.min(index, length - 1))\n inputRefs.current[clamped]?.focus()\n }\n\n const handleInput = (index: number, char: string) => {\n const newDigits = [...digits]\n while (newDigits.length < length) newDigits.push(\"\")\n newDigits[index] = char\n const newValue = newDigits.join(\"\")\n updateValue(newValue)\n if (char && index < length - 1) {\n focusInput(index + 1)\n }\n }\n\n const handleKeyDown = (\n index: number,\n e: React.KeyboardEvent<HTMLInputElement>\n ) => {\n if (e.key === \"Backspace\") {\n e.preventDefault()\n if (digits[index]) {\n handleInput(index, \"\")\n } else if (index > 0) {\n handleInput(index - 1, \"\")\n focusInput(index - 1)\n }\n } else if (e.key === \"ArrowLeft\" && index > 0) {\n e.preventDefault()\n focusInput(index - 1)\n } else if (e.key === \"ArrowRight\" && index < length - 1) {\n e.preventDefault()\n focusInput(index + 1)\n }\n }\n\n const errorId = React.useId()\n const hasError = invalid && errorMessage != null && errorMessage !== \"\"\n\n const handleChange = (\n index: number,\n e: React.ChangeEvent<HTMLInputElement>\n ) => {\n // One-time-code autofill writes the whole code into the first cell.\n const all = e.target.value.replace(/\\D/g, \"\")\n if (index === 0 && all.length > 1 && all.length >= length) {\n updateValue(all)\n focusInput(Math.min(all.length, length - 1))\n return\n }\n const char = e.target.value.replace(/\\D/g, \"\").slice(-1)\n if (char) {\n handleInput(index, char)\n }\n }\n\n const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\n e.preventDefault()\n const pastedDigits = e.clipboardData\n .getData(\"text\")\n .replace(/\\D/g, \"\")\n .slice(0, length)\n if (pastedDigits) {\n updateValue(pastedDigits)\n focusInput(Math.min(pastedDigits.length, length - 1))\n }\n }\n\n const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {\n e.target.select()\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"otp-input\"\n role=\"group\"\n aria-label=\"Verification code\"\n className={cn(styles.wrapper, className)}\n {...props}\n >\n <div className={styles.row}>\n {Array.from({ length }, (_, i) => (\n <input\n key={i}\n ref={(el) => {\n inputRefs.current[i] = el\n }}\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]\"\n maxLength={i === 0 ? length : 1}\n autoComplete={i === 0 ? \"one-time-code\" : \"off\"}\n aria-invalid={invalid || undefined}\n aria-describedby={hasError ? errorId : undefined}\n value={digits[i] ?? \"\"}\n onChange={(e) => handleChange(i, e)}\n onKeyDown={(e) => handleKeyDown(i, e)}\n onPaste={handlePaste}\n onFocus={handleFocus}\n disabled={disabled}\n autoFocus={autoFocus && i === 0}\n aria-label={`Digit ${i + 1} of ${length}`}\n className={cn(\n styles.cell,\n digits[i] && styles.cellFilled\n )}\n />\n ))}\n {trailing != null && (\n <span data-slot=\"otp-input-trailing\" className={styles.trailing}>\n {trailing}\n </span>\n )}\n </div>\n {hasError && (\n <span id={errorId} role=\"alert\" className={styles.error}>\n {errorMessage}\n </span>\n )}\n </div>\n )\n }\n)\nOTPInput.displayName = \"OTPInput\"\n\nexport { OTPInput }\n"
|
|
2085
2111
|
},
|
|
2086
2112
|
{
|
|
2087
2113
|
"path": "components/ui/otp-input/otp-input.module.css",
|
|
2088
2114
|
"type": "registry:ui",
|
|
2089
|
-
"content": "/* OTP input wrapper */\n.wrapper {\n display: inline-flex;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Individual digit cell */\n.cell {\n width: 2.5rem;\n height: 2.75rem;\n border: 1px solid var(--otp-border, var(--border-default, #e5e7eb));\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--otp-bg, var(--surface-interactive-default, #f9fafb));\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\n
|
|
2115
|
+
"content": "/* OTP input wrapper */\n.wrapper {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Trailing: a working indicator or a resend action beside the cells. */\n.trailing {\n display: inline-flex;\n align-items: center;\n margin-inline-start: var(--spacing-2, 0.5rem);\n color: var(--text-secondary, #6b7280);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Cells and trailing content share one row; the error sits under it. */\n.row {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n}\n\n/* Visible code error, a live region (4.1.3). Not colour alone (1.4.1). */\n.error {\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-normal, 400);\n color: var(--text-error, #dc2626);\n}\n\n/* Individual digit cell */\n.cell {\n width: 2.5rem;\n height: 2.75rem;\n /* VI-655: the border only holds box geometry. The resting edge is an inset\n outline on the shared --control-edge-* tokens, so --control-edge-width: 0\n turns it off; focus and invalid draw at --control-state-edge-width. */\n border: 1px solid transparent;\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--otp-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--otp-bg, var(--surface-interactive-default, #f9fafb));\n text-align: center;\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--text-primary, #111827);\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.cell::placeholder {\n color: var(--text-secondary, #9ca3af);\n}\n\n@media (hover: hover) {\n .cell:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n outline-color: var(--border-strong, #d1d5db);\n }\n}\n\n.cell: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.cell:disabled {\n pointer-events: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.cellFilled {\n outline-color: var(--border-focus, #111827);\n}\n\n/* Invalid keeps the digits and draws the error ring. The ring is a width, not\n only a colour, and errorMessage is announced, so state is not colour alone. */\n.cell[aria-invalid=\"true\"] {\n outline-color: transparent;\n background-color: var(--otp-bg-invalid, var(--surface-error-subtle, color-mix(in srgb, var(--border-error, #ef4444) 6%, var(--otp-bg, var(--surface-interactive-default, #f9fafb)))));\n box-shadow: inset 0 0 0 var(--control-invalid-ring-width, 1.5px) var(--border-error, #ef4444),\n 0 0 0 var(--control-invalid-halo-width, 0px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.cell[aria-invalid=\"true\"]:focus-visible {\n outline-color: transparent;\n outline-offset: var(--focus-ring-offset, 2px);\n box-shadow: inset 0 0 0 var(--control-invalid-ring-width, 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"
|
|
2090
2116
|
}
|
|
2091
2117
|
]
|
|
2092
2118
|
},
|
|
@@ -2133,12 +2159,12 @@
|
|
|
2133
2159
|
{
|
|
2134
2160
|
"path": "components/ui/tag-input/tag-input.tsx",
|
|
2135
2161
|
"type": "registry:ui",
|
|
2136
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tag-input.module.css\"\n\nexport interface TagInputProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n value?: string[]\n defaultValue?: string[]\n onChange?: (tags: string[]) => void\n placeholder?: string\n disabled?: boolean\n max?: number\n}\n\nconst TagInput = React.forwardRef<HTMLDivElement, TagInputProps>(\n (\n {\n className,\n value,\n defaultValue,\n onChange,\n placeholder = \"Add tag...\",\n disabled = false,\n max,\n ...props\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [internalTags, setInternalTags] = React.useState<string[]>(\n defaultValue ?? []\n )\n const [inputValue, setInputValue] = React.useState(\"\")\n const [focused, setFocused] = React.useState(false)\n\n const isControlled = value !== undefined\n const tags = isControlled ? value : internalTags\n const atMax = max !== undefined && tags.length >= max\n\n const updateTags = (newTags: string[]) => {\n if (!isControlled) {\n setInternalTags(newTags)\n }\n onChange?.(newTags)\n }\n\n const addTag = (text: string) => {\n const trimmed = text.trim()\n if (!trimmed) return\n if (tags.some((t) => t.toLowerCase() === trimmed.toLowerCase())) return\n if (atMax) return\n updateTags([...tags, trimmed])\n setInputValue(\"\")\n }\n\n const removeTag = (index: number) => {\n updateTags(tags.filter((_, i) => i !== index))\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" || e.key === \",\") {\n e.preventDefault()\n addTag(inputValue)\n } else if (e.key === \"Backspace\" && inputValue === \"\" && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const handleWrapperClick = () => {\n inputRef.current?.focus()\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"tag-input\"\n className={cn(\n styles.wrapper,\n focused && styles.wrapperFocused,\n disabled && styles.wrapperDisabled,\n className\n )}\n onClick={handleWrapperClick}\n {...props}\n >\n {tags.map((tag, index) => (\n <span key={`${tag}-${index}`} data-slot=\"tag\" className={styles.tag}>\n <span className={styles.tagLabel}>{tag}</span>\n {!disabled && (\n <button\n type=\"button\"\n className={styles.tagRemove}\n onClick={(e) => {\n e.stopPropagation()\n removeTag(index)\n }}\n aria-label={`Remove ${tag}`}\n >\n <X aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!atMax && (\n <input\n ref={inputRef}\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onFocus={() => setFocused(true)}\n onBlur={(e) => {\n setFocused(false)\n if (inputValue.trim()) {\n addTag(inputValue)\n }\n }}\n placeholder={tags.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n className={styles.input}\n aria-label=\"Add tag\"\n />\n )}\n </div>\n )\n }\n)\nTagInput.displayName = \"TagInput\"\n\nexport { TagInput }\n"
|
|
2162
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./tag-input.module.css\"\n\nexport interface TagInputProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n value?: string[]\n defaultValue?: string[]\n onChange?: (tags: string[]) => void\n placeholder?: string\n disabled?: boolean\n max?: number\n /**\n * Where the next tag is typed. `\"text\"` types after the last tag as bare\n * text. `\"slot\"` types into a dashed slot the shape of a held tag, drawn on\n * the shared drop edge. Defaults to `\"text\"`.\n */\n entryAs?: \"text\" | \"slot\"\n}\n\nconst TagInput = React.forwardRef<HTMLDivElement, TagInputProps>(\n (\n {\n className,\n value,\n defaultValue,\n onChange,\n placeholder = \"Add tag...\",\n disabled = false,\n max,\n entryAs = \"text\",\n ...props\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [internalTags, setInternalTags] = React.useState<string[]>(\n defaultValue ?? []\n )\n const [inputValue, setInputValue] = React.useState(\"\")\n const [focused, setFocused] = React.useState(false)\n\n const isControlled = value !== undefined\n const tags = isControlled ? value : internalTags\n const atMax = max !== undefined && tags.length >= max\n\n const updateTags = (newTags: string[]) => {\n if (!isControlled) {\n setInternalTags(newTags)\n }\n onChange?.(newTags)\n }\n\n const addTag = (text: string) => {\n const trimmed = text.trim()\n if (!trimmed) return\n if (tags.some((t) => t.toLowerCase() === trimmed.toLowerCase())) return\n if (atMax) return\n updateTags([...tags, trimmed])\n setInputValue(\"\")\n }\n\n const removeTag = (index: number) => {\n updateTags(tags.filter((_, i) => i !== index))\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" || e.key === \",\") {\n e.preventDefault()\n addTag(inputValue)\n } else if (e.key === \"Backspace\" && inputValue === \"\" && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const handleWrapperClick = () => {\n inputRef.current?.focus()\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"tag-input\"\n className={cn(\n styles.wrapper,\n focused && styles.wrapperFocused,\n disabled && styles.wrapperDisabled,\n className\n )}\n onClick={handleWrapperClick}\n {...props}\n >\n {tags.map((tag, index) => (\n <span key={`${tag}-${index}`} data-slot=\"tag\" className={styles.tag}>\n <span className={styles.tagLabel}>{tag}</span>\n {!disabled && (\n <button\n type=\"button\"\n className={styles.tagRemove}\n onClick={(e) => {\n e.stopPropagation()\n removeTag(index)\n }}\n aria-label={`Remove ${tag}`}\n >\n <X aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!atMax && (\n <input\n ref={inputRef}\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onFocus={() => setFocused(true)}\n onBlur={(e) => {\n setFocused(false)\n if (inputValue.trim()) {\n addTag(inputValue)\n }\n }}\n placeholder={tags.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n data-slot={entryAs === \"slot\" ? \"tag-input-slot\" : undefined}\n className={cn(styles.input, entryAs === \"slot\" && styles.inputSlot)}\n aria-label=\"Add tag\"\n />\n )}\n </div>\n )\n }\n)\nTagInput.displayName = \"TagInput\"\n\nexport { TagInput }\n"
|
|
2137
2163
|
},
|
|
2138
2164
|
{
|
|
2139
2165
|
"path": "components/ui/tag-input/tag-input.module.css",
|
|
2140
2166
|
"type": "registry:ui",
|
|
2141
|
-
"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"
|
|
2167
|
+
"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: var(--tag-input-min-height, 2.25rem);\n width: 100%;\n border-radius: var(--tag-input-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(--tag-input-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 height: var(--tag-input-tag-height, auto);\n padding: calc(var(--spacing-1, 0.25rem) / 4) var(--tag-input-tag-padding-x, var(--spacing-2, 0.5rem));\n border-radius: var(--tag-input-tag-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(--tag-input-tag-font-size, var(--font-size-xs, 0.75rem));\n line-height: 1.5;\n color: var(--text-primary, #111827);\n}\n\n/* Weight is inherited, so any unlayered fallback would override something:\n `inherit` beats a layered utility, `revert-layer` drops an unlayered consumer\n rule. In visor-base every consumer rule still wins, and while the token is\n unset revert-layer leaves the tag inheriting its row's weight. */\n@layer visor-base {\n .tag {\n font-weight: var(--tag-input-tag-font-weight, revert-layer);\n }\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\n/* Typing slot (entryAs=\"slot\", VI-663). The next tag is typed into a dashed\n slot the shape of a held tag, and the slot grows with what is typed. The\n dashed edge is the shared drop edge (VI-655), so --control-edge-width: 0\n turns it off with every other edge. Declared after .input so it wins.\n 1.375rem is the height a held tag draws while --tag-input-tag-height is\n unset (an 18px line, 1px padding and the 1px geometry border, each side),\n so the unbound slot is the tag's shape too. */\n.inputSlot {\n flex: 0 1 auto;\n box-sizing: border-box;\n field-sizing: content;\n min-width: var(--tag-input-slot-min-width, 4rem);\n max-width: 100%;\n height: var(--tag-input-slot-height, var(--tag-input-tag-height, 1.375rem));\n padding: 0 var(--tag-input-slot-padding-x, var(--tag-input-tag-padding-x, var(--spacing-2, 0.5rem)));\n border-radius: var(--tag-input-slot-radius, var(--tag-input-tag-radius, var(--radius-full, 9999px)));\n font-size: var(--tag-input-tag-font-size, var(--font-size-xs, 0.75rem));\n outline: 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(-1 * var(--control-drop-edge-width, var(--control-edge-width, 1px)));\n}\n"
|
|
2142
2168
|
}
|
|
2143
2169
|
]
|
|
2144
2170
|
},
|
|
@@ -4298,7 +4324,7 @@
|
|
|
4298
4324
|
{
|
|
4299
4325
|
"path": "blocks/dialog-field/dialog-field.module.css",
|
|
4300
4326
|
"type": "registry:block",
|
|
4301
|
-
"content": "/* DialogField block — Animal dialog substrate field (dlg-field, VI-620).\n *\n * flex column, 5px gap · 11px/600 primary-ink label · the standard Visor medium\n * field (md size, themed form-control surface, borderless) hosting a leading\n * icon, the control, and a trailing control/caret. Every value traces to a Visor\n * token; the active theme swaps the whole treatment (no bespoke CSS).\n */\n\n/* Field — flex column. 5px label→control gap (dlg-field spec): the 4px grid\n * has no 5px step, so derive it from --spacing-1 (the same calc idiom the\n * field atom uses for its 6px icon gap). */\n.field {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 4);\n}\n\n/* Label — dlg-label: 11px / 600 / --ink (primary). */\n.label {\n font-size: var(--text-11, 0.6875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n/* Control — the standard Visor field well on the shared form-control surface role\n * (--field-control-bg), borderless. The surface color is theme-driven — a dialog\n * context may lift it with --field-control-bg (the documented in-dialog hook on\n * the input atom); we do not hand-roll it here. Borderless per the dlg-field\n * direction; focus shows a tokenized ring. The well padding is the `size` axis\n * (--controlSm / --controlMd below) so a compact Animal modal field and the\n * default medium field share one well — matching Visor's input-only `size`\n * semantics (padding varies; the label typography does not). */\n.control {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n /* Borderless field → the surface must contrast with the card on its own. A\n * fixed surface token (--surface-subtle) can't guarantee that: some themes\n * pin it almost on the card (e.g. space/light). Instead derive it from the\n * card mixed a few percent toward --text-primary — which is legible on the\n * card by definition, so the field reads as a subtle-but-visible step in\n * every theme and both modes. Overridable per theme via --field-control-bg. */\n background: var(\n --field-control-bg,\n color-mix(in srgb, var(--surface-card, #ffffff), var(--text-primary, #111827) 7%)\n );\n color: var(--text-primary, #111827);\n transition: box-shadow var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.control:focus-within {\n box-shadow: 0 0 0 var(--focus-ring-width, 2px)\n color-mix(in srgb, var(--border-focus, var(--primary, #111827)) 15%, transparent);\n}\n\n/* Size axis — well/control padding only (label typography unchanged). Mirrors the\n * input atom's --input-padding-{sm,md} seams so the field well tracks the input\n * size scale. `md` (default) preserves the VI-620 medium well; `sm` is the\n * compact Animal modal-form well (opted into per-field via size=\"sm\"). */\n.controlSm {\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n}\n\n.controlMd {\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n}\n\n/* Leading icon slot. */\n.icon {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* The control itself sits on the well surface — transparent, borderless, so a\n * plain <input>/<select>/<textarea> dropped in reads as part of the well. */\n.controlInner {\n flex: 1;\n min-width: 0;\n display: flex;\n}\n\n.controlInner :global(input),\n.controlInner :global(select),\n.controlInner :global(textarea) {\n flex: 1;\n min-width: 0;\n border: 0;\n padding: 0;\n background: transparent;\n outline: none;\n color: var(--text-primary, #111827);\n}\n\n/* Trailing control / caret slot. */\n.trailing {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n"
|
|
4327
|
+
"content": "/* DialogField block — Animal dialog substrate field (dlg-field, VI-620).\n *\n * flex column, 5px gap · 11px/600 primary-ink label · the standard Visor medium\n * field (md size, themed form-control surface, borderless) hosting a leading\n * icon, the control, and a trailing control/caret. Every value traces to a Visor\n * token; the active theme swaps the whole treatment (no bespoke CSS).\n */\n\n/* Field — flex column. 5px label→control gap (dlg-field spec): the 4px grid\n * has no 5px step, so derive it from --spacing-1 (the same calc idiom the\n * field atom uses for its 6px icon gap). */\n.field {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 4);\n}\n\n/* Label — dlg-label: 11px / 600 / --ink (primary). */\n.label {\n font-size: var(--text-11, 0.6875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n/* Control — the standard Visor field well on the shared form-control surface role\n * (--field-control-bg), borderless. The surface color is theme-driven — a dialog\n * context may lift it with --field-control-bg (the documented in-dialog hook on\n * the input atom); we do not hand-roll it here. Borderless per the dlg-field\n * direction; focus shows a tokenized ring. The well padding is the `size` axis\n * (--controlSm / --controlMd below) so a compact Animal modal field and the\n * default medium field share one well — matching Visor's input-only `size`\n * semantics (padding varies; the label typography does not). */\n.control {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--input-md-radius, var(--input-radius-md, var(--radius-sm, 0.5rem)));\n /* Borderless field → the surface must contrast with the card on its own. A\n * fixed surface token (--surface-subtle) can't guarantee that: some themes\n * pin it almost on the card (e.g. space/light). Instead derive it from the\n * card mixed a few percent toward --text-primary — which is legible on the\n * card by definition, so the field reads as a subtle-but-visible step in\n * every theme and both modes. Overridable per theme via --field-control-bg. */\n background: var(\n --field-control-bg,\n color-mix(in srgb, var(--surface-card, #ffffff), var(--text-primary, #111827) 7%)\n );\n color: var(--text-primary, #111827);\n transition: box-shadow var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.control:focus-within {\n box-shadow: 0 0 0 var(--focus-ring-width, 2px)\n color-mix(in srgb, var(--border-focus, var(--primary, #111827)) 15%, transparent);\n}\n\n/* Size axis — well/control padding only (label typography unchanged). Mirrors the\n * input atom's --input-padding-{sm,md} seams so the field well tracks the input\n * size scale. `md` (default) preserves the VI-620 medium well; `sm` is the\n * compact Animal modal-form well (opted into per-field via size=\"sm\"). */\n.controlSm {\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n}\n\n.controlMd {\n padding: var(--input-md-padding, var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem)));\n}\n\n/* Leading icon slot. */\n.icon {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* The control itself sits on the well surface — transparent, borderless, so a\n * plain <input>/<select>/<textarea> dropped in reads as part of the well. */\n.controlInner {\n flex: 1;\n min-width: 0;\n display: flex;\n}\n\n.controlInner :global(input),\n.controlInner :global(select),\n.controlInner :global(textarea) {\n flex: 1;\n min-width: 0;\n border: 0;\n padding: 0;\n background: transparent;\n outline: none;\n color: var(--text-primary, #111827);\n}\n\n/* Trailing control / caret slot. */\n.trailing {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n"
|
|
4302
4328
|
}
|
|
4303
4329
|
]
|
|
4304
4330
|
},
|
|
@@ -5317,7 +5343,7 @@
|
|
|
5317
5343
|
{
|
|
5318
5344
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
5319
5345
|
"type": "registry:devtool",
|
|
5320
|
-
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActionRow\",\n \"ActionRowList\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Column\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogBack\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogContext\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogScrim\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadioGroupVariantContext\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetContext\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetScrim\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TimePicker\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
5346
|
+
"content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActionRow\",\n \"ActionRowList\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Column\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogBack\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogContext\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogScrim\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadioGroupVariantContext\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedControl\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetContext\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetScrim\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TimePicker\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
|
|
5321
5347
|
}
|
|
5322
5348
|
]
|
|
5323
5349
|
},
|