@verdify/ui 0.2.2 → 0.3.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.
Files changed (124) hide show
  1. package/dist/components/accordion/accordion.variants.d.ts.map +1 -1
  2. package/dist/components/accordion/accordion.variants.js +2 -1
  3. package/dist/components/accordion/accordion.variants.js.map +1 -1
  4. package/dist/components/alert/alert.variants.d.ts.map +1 -1
  5. package/dist/components/alert/alert.variants.js +3 -2
  6. package/dist/components/alert/alert.variants.js.map +1 -1
  7. package/dist/components/breadcrumb/breadcrumb.variants.d.ts.map +1 -1
  8. package/dist/components/breadcrumb/breadcrumb.variants.js +2 -1
  9. package/dist/components/breadcrumb/breadcrumb.variants.js.map +1 -1
  10. package/dist/components/button/button.variants.d.ts.map +1 -1
  11. package/dist/components/button/button.variants.js +2 -1
  12. package/dist/components/button/button.variants.js.map +1 -1
  13. package/dist/components/card/card.variants.d.ts.map +1 -1
  14. package/dist/components/card/card.variants.js +2 -1
  15. package/dist/components/card/card.variants.js.map +1 -1
  16. package/dist/components/checkbox/checkbox.variants.d.ts.map +1 -1
  17. package/dist/components/checkbox/checkbox.variants.js +2 -1
  18. package/dist/components/checkbox/checkbox.variants.js.map +1 -1
  19. package/dist/components/command-palette/command-palette.variants.d.ts +1 -1
  20. package/dist/components/command-palette/command-palette.variants.d.ts.map +1 -1
  21. package/dist/components/command-palette/command-palette.variants.js +5 -3
  22. package/dist/components/command-palette/command-palette.variants.js.map +1 -1
  23. package/dist/components/credential-card/credential-card.variants.d.ts +2 -2
  24. package/dist/components/credential-card/credential-card.variants.d.ts.map +1 -1
  25. package/dist/components/credential-card/credential-card.variants.js +2 -2
  26. package/dist/components/credential-card/credential-card.variants.js.map +1 -1
  27. package/dist/components/data-grid/data-grid.variants.d.ts +1 -1
  28. package/dist/components/data-grid/data-grid.variants.d.ts.map +1 -1
  29. package/dist/components/data-grid/data-grid.variants.js +7 -6
  30. package/dist/components/data-grid/data-grid.variants.js.map +1 -1
  31. package/dist/components/dialog/dialog.variants.d.ts.map +1 -1
  32. package/dist/components/dialog/dialog.variants.js +3 -2
  33. package/dist/components/dialog/dialog.variants.js.map +1 -1
  34. package/dist/components/identity-chip/identity-chip.variants.d.ts.map +1 -1
  35. package/dist/components/identity-chip/identity-chip.variants.js +3 -2
  36. package/dist/components/identity-chip/identity-chip.variants.js.map +1 -1
  37. package/dist/components/input/input.variants.d.ts.map +1 -1
  38. package/dist/components/input/input.variants.js +2 -1
  39. package/dist/components/input/input.variants.js.map +1 -1
  40. package/dist/components/menu/menu.d.ts.map +1 -1
  41. package/dist/components/menu/menu.js +1 -1
  42. package/dist/components/menu/menu.js.map +1 -1
  43. package/dist/components/menu/menu.variants.d.ts +1 -1
  44. package/dist/components/menu/menu.variants.d.ts.map +1 -1
  45. package/dist/components/menu/menu.variants.js +3 -2
  46. package/dist/components/menu/menu.variants.js.map +1 -1
  47. package/dist/components/pagination/pagination.variants.d.ts.map +1 -1
  48. package/dist/components/pagination/pagination.variants.js +2 -1
  49. package/dist/components/pagination/pagination.variants.js.map +1 -1
  50. package/dist/components/popover/popover.variants.d.ts.map +1 -1
  51. package/dist/components/popover/popover.variants.js +4 -3
  52. package/dist/components/popover/popover.variants.js.map +1 -1
  53. package/dist/components/radio/radio.d.ts.map +1 -1
  54. package/dist/components/radio/radio.js +2 -1
  55. package/dist/components/radio/radio.js.map +1 -1
  56. package/dist/components/select/select.variants.d.ts +1 -1
  57. package/dist/components/select/select.variants.d.ts.map +1 -1
  58. package/dist/components/select/select.variants.js +3 -2
  59. package/dist/components/select/select.variants.js.map +1 -1
  60. package/dist/components/sheet/sheet.variants.d.ts.map +1 -1
  61. package/dist/components/sheet/sheet.variants.js +3 -2
  62. package/dist/components/sheet/sheet.variants.js.map +1 -1
  63. package/dist/components/sidebar/sidebar.variants.d.ts +1 -1
  64. package/dist/components/sidebar/sidebar.variants.d.ts.map +1 -1
  65. package/dist/components/sidebar/sidebar.variants.js +4 -3
  66. package/dist/components/sidebar/sidebar.variants.js.map +1 -1
  67. package/dist/components/switch/switch.variants.d.ts.map +1 -1
  68. package/dist/components/switch/switch.variants.js +2 -1
  69. package/dist/components/switch/switch.variants.js.map +1 -1
  70. package/dist/components/table/table.variants.d.ts.map +1 -1
  71. package/dist/components/table/table.variants.js +5 -3
  72. package/dist/components/table/table.variants.js.map +1 -1
  73. package/dist/components/tabs/tabs.variants.d.ts.map +1 -1
  74. package/dist/components/tabs/tabs.variants.js +3 -2
  75. package/dist/components/tabs/tabs.variants.js.map +1 -1
  76. package/dist/components/textarea/textarea.js +1 -1
  77. package/dist/components/textarea/textarea.js.map +1 -1
  78. package/dist/components/textarea/textarea.variants.d.ts.map +1 -1
  79. package/dist/components/textarea/textarea.variants.js +2 -1
  80. package/dist/components/textarea/textarea.variants.js.map +1 -1
  81. package/dist/components/toast/toast.variants.d.ts.map +1 -1
  82. package/dist/components/toast/toast.variants.js +3 -2
  83. package/dist/components/toast/toast.variants.js.map +1 -1
  84. package/dist/components/trust-score/trust-score.variants.d.ts +1 -1
  85. package/dist/components/trust-score/trust-score.variants.d.ts.map +1 -1
  86. package/dist/components/trust-score/trust-score.variants.js +1 -1
  87. package/dist/components/trust-score/trust-score.variants.js.map +1 -1
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +3 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/lib/focus-ring.d.ts +2 -0
  93. package/dist/lib/focus-ring.d.ts.map +1 -0
  94. package/dist/lib/focus-ring.js +5 -0
  95. package/dist/lib/focus-ring.js.map +1 -0
  96. package/package.json +3 -3
  97. package/registry/accordion.json +3 -2
  98. package/registry/alert.json +3 -2
  99. package/registry/breadcrumb.json +3 -2
  100. package/registry/button.json +3 -2
  101. package/registry/card.json +3 -2
  102. package/registry/checkbox.json +3 -2
  103. package/registry/command-palette.json +3 -2
  104. package/registry/credential-card.json +1 -1
  105. package/registry/data-grid.json +2 -1
  106. package/registry/dialog.json +3 -2
  107. package/registry/focus-ring.json +16 -0
  108. package/registry/identity-chip.json +2 -1
  109. package/registry/init.json +1 -1
  110. package/registry/input.json +3 -2
  111. package/registry/menu.json +4 -3
  112. package/registry/pagination.json +3 -2
  113. package/registry/popover.json +3 -2
  114. package/registry/radio.json +3 -2
  115. package/registry/select.json +3 -2
  116. package/registry/sheet.json +3 -2
  117. package/registry/sidebar.json +3 -2
  118. package/registry/switch.json +3 -2
  119. package/registry/table.json +2 -1
  120. package/registry/tabs.json +3 -2
  121. package/registry/textarea.json +3 -2
  122. package/registry/toast.json +3 -2
  123. package/registry/trust-score.json +1 -1
  124. package/registry.json +4 -0
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Accordion is a NEUTRAL layout container (spec §1/§3/§8): brand violet and Verified Green\n// are accents, neutrals carry the surface, so NOTHING here binds a --color-action-primary-*\n// or --color-status-* fill. An expanded section is not \"selected\" or \"verified\" — the open\n// item is shown by the indicator + aria-expanded, never by a brand/status fill. The only\n// token-binding site is this file (skill §5 hard rule).\n\n// The item: one collapsible section. A neutral hairline divider (border-border-default) and\n// the item radius bound the header + its panel. The root stacks items; the item draws the box.\nexport const accordionItemVariants = cva([\n // overflow-hidden so the panel reveal clips to the item radius; logical text start (G-U6)\n \"overflow-hidden rounded-(--radius-md) border border-border-default\",\n \"text-start\",\n]);\n\n// The trigger: the focusable <button> inside the heading. A control-* tier surface at rest;\n// the neutral secondary hover fill on hover AND on press (pressed is acknowledged without\n// theatre — restraint over volume, spec §4 Pressed); the trigger title in the label type\n// role; the persistent focus ring; the target-size floor; base reveal motion (NEVER the\n// deliberate verified-check theatre); DEC-C disabled via the disabled TOKEN, never opacity.\nexport const accordionTriggerVariants = cva([\n // layout: full-width header row, title on the inline-start, indicator on the inline-end.\n // `group` anchors the trigger so the child indicator can react to its data-disabled state\n // (Radix sets data-disabled + native disabled on this button) without prop plumbing.\n \"group flex w-full items-center justify-between gap-(--space-2) px-(--space-4)\",\n // rest surface: control-* tier (neutral), with the at-rest control border\n \"bg-control-bg text-control-fg border-control-border\",\n // trigger title type role + medium weight; cursor affordance\n \"text-label font-medium cursor-pointer select-none\",\n // hover AND pressed lift to the neutral secondary hover fill (spec §4) — never a brand fill\n \"hover:bg-action-secondary-bg-hover active:bg-action-secondary-bg-hover\",\n // expand/collapse is a PLAIN reveal — base duration + verdify easing, instant under reduced\n // motion. Never the 350ms VerifiedBadge-only theatre duration (G-U3 motion-theatre gate).\n \"transition-colors duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8), padding density above it; the\n // resting height EMERGES from the floor + py, never a fixed height below the a11y floor (DEC-B)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) py-(--space-2)\",\n // visible 2px signal-blue ring at 2px offset; persists whether the item is open or closed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // focused: the trigger border emphasises to the focus color (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus\",\n // disabled — DEC-C: out of the tab order (native disabled), reduced emphasis via the disabled\n // TOKEN on the title AND the indicator (below), never a blanket opacity on the control\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n]);\n\n// The indicator glyph: a neutral chevron (ghost fg), --size-icon-md, rotating to mirror the\n// expanded state. Decorative (aria-hidden); aria-expanded carries the open state, not the glyph.\n// It rotates 180deg when the item is open (data-state=open on the trigger), with the same base\n// reveal motion.\n//\n// DEC-C / spec §4·§5: the glyph is drawn with stroke=\"currentColor\", so its color is whatever\n// `text-*` resolves to on the SVG. At rest that is the distinct indicator color\n// --color-action-ghost-fg (spec §5 assigns ghost-fg to the indicator, NOT the trigger title's\n// control-fg). When the item is disabled it must flip to --color-text-disabled — the SAME token\n// the title dims to (spec §4 \"reduced emphasis\", §5 \"Disabled item's title AND indicator\"). We\n// flip the glyph color via the trigger's live data-disabled state (Radix mirrors the item's\n// disabled onto the trigger button as data-disabled + native disabled). This realizes the\n// Checkbox color-flip principle (text-action-ghost-fg -> text-text-disabled) using Select's\n// data-[disabled] mechanism (select.variants.ts) — appropriate here because the disabled state\n// is Radix-context-driven, not a prop on AccordionTrigger, so a prop-fed cva boolean can't see it.\nexport const accordionIndicatorVariants = cva([\n \"h-(--size-icon-md) w-(--size-icon-md) shrink-0 text-action-ghost-fg\",\n \"group-data-[disabled]:text-text-disabled\",\n \"transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // rotate the chevron when the parent trigger is open (Radix sets data-state on the trigger)\n \"data-[state=open]:rotate-180\",\n]);\n\n// The panel: the collapsible region. The canvas surface, the primary body text at the body\n// type role, panel insets from --space-4. A divider above it (border-t) continues the neutral\n// hairline from the item. The reveal animates the Radix content-height var (a STRUCTURAL keyword\n// arbitrary property, not a raw value), base duration, collapsed to its endpoints under reduced\n// motion.\nexport const accordionContentVariants = cva([\n \"overflow-hidden bg-surface-canvas text-text-primary text-body\",\n \"border-t border-border-default\",\n]);\n\n// The panel inner padding wrapper — the actual content insets (Radix Content clips height, so\n// the padding lives on an inner element to avoid jumpy collapse).\nexport const accordionContentInnerClass = \"px-(--space-4) py-(--space-4)\";\n\nexport type AccordionItemVariantProps = VariantProps<typeof accordionItemVariants>;\nexport type AccordionTriggerVariantProps = VariantProps<typeof accordionTriggerVariants>;\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// Accordion is a NEUTRAL layout container (spec §1/§3/§8): brand violet and Verified Green\n// are accents, neutrals carry the surface, so NOTHING here binds a --color-action-primary-*\n// or --color-status-* fill. An expanded section is not \"selected\" or \"verified\" — the open\n// item is shown by the indicator + aria-expanded, never by a brand/status fill. The only\n// token-binding site is this file (skill §5 hard rule).\n\n// The item: one collapsible section. A neutral hairline divider (border-border-default) and\n// the item radius bound the header + its panel. The root stacks items; the item draws the box.\nexport const accordionItemVariants = cva([\n // overflow-hidden so the panel reveal clips to the item radius; logical text start (G-U6)\n \"overflow-hidden rounded-(--radius-md) border border-border-default\",\n \"text-start\",\n]);\n\n// The trigger: the focusable <button> inside the heading. A control-* tier surface at rest;\n// the neutral secondary hover fill on hover AND on press (pressed is acknowledged without\n// theatre — restraint over volume, spec §4 Pressed); the trigger title in the label type\n// role; the persistent focus ring; the target-size floor; base reveal motion (NEVER the\n// deliberate verified-check theatre); DEC-C disabled via the disabled TOKEN, never opacity.\nexport const accordionTriggerVariants = cva([\n // layout: full-width header row, title on the inline-start, indicator on the inline-end.\n // `group` anchors the trigger so the child indicator can react to its data-disabled state\n // (Radix sets data-disabled + native disabled on this button) without prop plumbing.\n \"group flex w-full items-center justify-between gap-(--space-2) px-(--space-4)\",\n // rest surface: control-* tier (neutral), with the at-rest control border\n \"bg-control-bg text-control-fg border-control-border\",\n // trigger title type role + medium weight; cursor affordance\n \"text-label font-medium cursor-pointer select-none\",\n // hover AND pressed lift to the neutral secondary hover fill (spec §4) — never a brand fill\n \"hover:bg-action-secondary-bg-hover active:bg-action-secondary-bg-hover\",\n // expand/collapse is a PLAIN reveal — base duration + verdify easing, instant under reduced\n // motion. Never the 350ms VerifiedBadge-only theatre duration (G-U3 motion-theatre gate).\n \"transition-colors duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8), padding density above it; the\n // resting height EMERGES from the floor + py, never a fixed height below the a11y floor (DEC-B)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) py-(--space-2)\",\n // visible 2px signal-blue ring at 2px offset; persists whether the item is open or closed\n \"outline-none\",\n focusRing,\n // focused: the trigger border emphasises to the focus color (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus\",\n // disabled — DEC-C: out of the tab order (native disabled), reduced emphasis via the disabled\n // TOKEN on the title AND the indicator (below), never a blanket opacity on the control\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n]);\n\n// The indicator glyph: a neutral chevron (ghost fg), --size-icon-md, rotating to mirror the\n// expanded state. Decorative (aria-hidden); aria-expanded carries the open state, not the glyph.\n// It rotates 180deg when the item is open (data-state=open on the trigger), with the same base\n// reveal motion.\n//\n// DEC-C / spec §4·§5: the glyph is drawn with stroke=\"currentColor\", so its color is whatever\n// `text-*` resolves to on the SVG. At rest that is the distinct indicator color\n// --color-action-ghost-fg (spec §5 assigns ghost-fg to the indicator, NOT the trigger title's\n// control-fg). When the item is disabled it must flip to --color-text-disabled — the SAME token\n// the title dims to (spec §4 \"reduced emphasis\", §5 \"Disabled item's title AND indicator\"). We\n// flip the glyph color via the trigger's live data-disabled state (Radix mirrors the item's\n// disabled onto the trigger button as data-disabled + native disabled). This realizes the\n// Checkbox color-flip principle (text-action-ghost-fg -> text-text-disabled) using Select's\n// data-[disabled] mechanism (select.variants.ts) — appropriate here because the disabled state\n// is Radix-context-driven, not a prop on AccordionTrigger, so a prop-fed cva boolean can't see it.\nexport const accordionIndicatorVariants = cva([\n \"h-(--size-icon-md) w-(--size-icon-md) shrink-0 text-action-ghost-fg\",\n \"group-data-[disabled]:text-text-disabled\",\n \"transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // rotate the chevron when the parent trigger is open (Radix sets data-state on the trigger)\n \"data-[state=open]:rotate-180\",\n]);\n\n// The panel: the collapsible region. The canvas surface, the primary body text at the body\n// type role, panel insets from --space-4. A divider above it (border-t) continues the neutral\n// hairline from the item. The reveal animates the Radix content-height var (a STRUCTURAL keyword\n// arbitrary property, not a raw value), base duration, collapsed to its endpoints under reduced\n// motion.\nexport const accordionContentVariants = cva([\n \"overflow-hidden bg-surface-canvas text-text-primary text-body\",\n \"border-t border-border-default\",\n]);\n\n// The panel inner padding wrapper — the actual content insets (Radix Content clips height, so\n// the padding lives on an inner element to avoid jumpy collapse).\nexport const accordionContentInnerClass = \"px-(--space-4) py-(--space-4)\";\n\nexport type AccordionItemVariantProps = VariantProps<typeof accordionItemVariants>;\nexport type AccordionTriggerVariantProps = VariantProps<typeof accordionTriggerVariants>;\n",
16
16
  "path": "accordion/accordion.variants.ts",
17
17
  "target": "@ui/accordion/accordion.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "accordion",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "accordion",
32
33
  "type": "registry:ui"
@@ -11,7 +11,7 @@
11
11
  "type": "registry:ui"
12
12
  },
13
13
  {
14
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An Alert is an inline, in-page feedback surface — a message that reports the status of a\n// region or task and, where one exists, what to do next (spec §1). It is NOT the brand: its\n// color comes from the status it reports, never from Sovereign Violet, so this file binds\n// nothing from the --color-action-* tier (brand != state, G-U2). This is the ONLY token-binding\n// site (skill §5 hard rule).\n//\n// The alert is a message surface, not a control (spec §4): the container has no hover, pressed,\n// loading, or disabled state — those belong to the controls inside it (the actions and the\n// dismiss button, which carry their own tokens). The container's only own state is FOCUS, shown\n// only when it is programmatically focused to move a reader to a blocking error; the focus ring\n// is in the base and is never removed (spec §4/§7).\n//\n// Container fill (spec §3/§5): every variant paints the SAME one neutral raised surface — the\n// status trio's `-bg` resolves to that surface — so the status color is a quiet TINT and EDGE,\n// not a flood. The meaning is carried by the border (the status -border) and the decorative\n// leading icon (the BRIGHT status accent, tokens 0.6.0), never a saturated fill; restraint over\n// volume. Status is paired with the fixed per-variant icon and the readable title/body text so it\n// survives for a reader who cannot perceive color (WCAG 1.4.1, spec §8).\nexport const alertVariants = cva(\n [\n // layout: the bordered container is a row — leading status icon, then the stacked\n // title/body/actions content; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-2)\",\n // the bordered container: internal padding off the edge, the md corner radius, a 1px edge\n \"rounded-(--radius-md) border p-(--space-3)\",\n // logical-property text alignment so the alert mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // appearance + dismiss motion: the FAST functional duration on verdify easing, collapsing to\n // the instant endpoint under reduced motion (spec §5). Even a `verified` alert fades in on\n // the fast duration — the 350ms VerifiedBadge-only theatre is NEVER borrowed by an alert\n // appearing (G-U3 motion-theatre gate).\n \"transition-[opacity,transform] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // FOCUS: the container takes focus only when programmatically focused to move a reader to a\n // blocking error (spec §4/§6/§7). It is NOT a tab stop by default (no tabIndex set in tsx);\n // the caller passes tabIndex={-1} for that move. When it is focused it shows the visible 2px\n // ring at a 2px offset, and the ring is never removed (2.4.7).\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the status the alert reports. The four map one-to-one to the\n // --color-status-* trios; there is no neutral or brand-colored alert. Each trio tints the\n // surface (-bg = the one neutral raised surface) and edges it (-border); the decorative\n // leading icon takes the matching -accent (the readable title/body keep their AA text\n // tokens). NONE binds --color-action-* (brand != state, spec §3/§8).\n variant: {\n // a verification succeeded / a claim is proven — the in-product Verified Green status,\n // NEVER the brand and NEVER generic success (spec §3/§8)\n verified: \"bg-status-verified-bg border-status-verified-border\",\n // neutral, informational, needs no action — the lowest-urgency variant (spec §3)\n signal: \"bg-status-signal-bg border-status-signal-border\",\n // needs attention but not yet broken — a soft limit, a pending step (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border\",\n // something failed or blocks progress — a rejected proof, a failed validation (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder status is spent only when warranted\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The leading status glyph (spec §2/§5): one fixed per-variant icon at the sm icon role. It\n// pairs with the color AND the text so status is never carried by color alone (1.4.1). Because\n// the glyph is DECORATIVE (aria-hidden in tsx) and the message TITLE/body text carries the\n// status meaning, the icon is exempt from the AA text floor and takes the BRIGHT variant ACCENT\n// via the matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an\n// emphasis mark on the quiet surface, while the readable title/body keep their AA text tokens.\nexport const alertIconVariants = cva(\"inline-flex shrink-0 h-(--size-icon-sm) w-(--size-icon-sm)\", {\n variants: {\n variant: {\n verified: \"text-status-verified-accent\",\n signal: \"text-status-signal-accent\",\n caution: \"text-status-caution-accent\",\n critical: \"text-status-critical-accent\",\n },\n },\n defaultVariants: { variant: \"signal\" },\n});\n\n// The stacked content column (spec §2): title, body, then actions, at the small stacked gap.\n// min-w-0 lets long body text wrap instead of overflowing the row.\nexport const alertContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The title (spec §2/§5): a short heading stating the status as a sentence-case statement, in\n// the h3 type role + primary text color. The type role already avoids label tracking — no\n// all-caps, no exclamation mark. Brand violet never paints the title.\nexport const alertTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2/§5): the message, in the body type role + primary text color. It names what\n// happened and, for a caution or critical alert, what to do next — honest about hard things,\n// never blaming the reader (spec §1/§8). Supporting/secondary lines use --color-text-secondary\n// (text-text-secondary) where the caller needs them.\nexport const alertBodyClass = \"text-body text-text-primary\";\n\n// The actions slot (spec §2): at most one or two controls closing the message (a retry, a link\n// to the failing step), holding at most one primary action (restraint over volume). The controls\n// are Buttons — the alert does not restate their --color-action-* bindings. Logical-property row\n// with the small gap; a little top margin separates it from the body.\nexport const alertActionsClass = \"flex items-center gap-(--space-2) mt-(--space-1)\";\n\n// The dismiss control (spec §2/§6/§7): a close button on the inline-end edge, present only on the\n// dismissible variant. A NEUTRAL ghost surface — the glyph in --color-action-ghost-fg at rest,\n// the restrained ghost hover fill (no bg/border at rest) — so the close affordance is neutral and\n// never competes with the status. It is a real focus stop with the target-size floor (44px touch\n// / 40px pointer, spec §7 / DEC-B; the height EMERGES from the floor, never fixed below it), the\n// persistent focus ring, and the fast functional hover motion + verdify easing, instant under\n// reduced motion (G-U3). The ghost fg/hover is the control's OWN action treatment, not the\n// alert's status (the brand != state gate scopes status keys to the container variant only).\nexport const alertDismissClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) \" +\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile) \" +\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The dismiss glyph (spec §7): a neutral X at the sm icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the\n// accessible name.\nexport const alertDismissGlyphClass = \"h-(--size-icon-sm) w-(--size-icon-sm)\";\n\nexport type AlertVariantProps = VariantProps<typeof alertVariants>;\n",
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// An Alert is an inline, in-page feedback surface — a message that reports the status of a\n// region or task and, where one exists, what to do next (spec §1). It is NOT the brand: its\n// color comes from the status it reports, never from Sovereign Violet, so this file binds\n// nothing from the --color-action-* tier (brand != state, G-U2). This is the ONLY token-binding\n// site (skill §5 hard rule).\n//\n// The alert is a message surface, not a control (spec §4): the container has no hover, pressed,\n// loading, or disabled state — those belong to the controls inside it (the actions and the\n// dismiss button, which carry their own tokens). The container's only own state is FOCUS, shown\n// only when it is programmatically focused to move a reader to a blocking error; the focus ring\n// is in the base and is never removed (spec §4/§7).\n//\n// Container fill (spec §3/§5): every variant paints the SAME one neutral raised surface — the\n// status trio's `-bg` resolves to that surface — so the status color is a quiet TINT and EDGE,\n// not a flood. The meaning is carried by the border (the status -border) and the decorative\n// leading icon (the BRIGHT status accent, tokens 0.6.0), never a saturated fill; restraint over\n// volume. Status is paired with the fixed per-variant icon and the readable title/body text so it\n// survives for a reader who cannot perceive color (WCAG 1.4.1, spec §8).\nexport const alertVariants = cva(\n [\n // layout: the bordered container is a row — leading status icon, then the stacked\n // title/body/actions content; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-2)\",\n // the bordered container: internal padding off the edge, the md corner radius, a 1px edge\n \"rounded-(--radius-md) border p-(--space-3)\",\n // logical-property text alignment so the alert mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // appearance + dismiss motion: the FAST functional duration on verdify easing, collapsing to\n // the instant endpoint under reduced motion (spec §5). Even a `verified` alert fades in on\n // the fast duration — the 350ms VerifiedBadge-only theatre is NEVER borrowed by an alert\n // appearing (G-U3 motion-theatre gate).\n \"transition-[opacity,transform] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // FOCUS: the container takes focus only when programmatically focused to move a reader to a\n // blocking error (spec §4/§6/§7). It is NOT a tab stop by default (no tabIndex set in tsx);\n // the caller passes tabIndex={-1} for that move. When it is focused it shows the visible 2px\n // ring at a 2px offset, and the ring is never removed (2.4.7).\n \"outline-none\",\n focusRing,\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the status the alert reports. The four map one-to-one to the\n // --color-status-* trios; there is no neutral or brand-colored alert. Each trio tints the\n // surface (-bg = the one neutral raised surface) and edges it (-border); the decorative\n // leading icon takes the matching -accent (the readable title/body keep their AA text\n // tokens). NONE binds --color-action-* (brand != state, spec §3/§8).\n variant: {\n // a verification succeeded / a claim is proven — the in-product Verified Green status,\n // NEVER the brand and NEVER generic success (spec §3/§8)\n verified: \"bg-status-verified-bg border-status-verified-border\",\n // neutral, informational, needs no action — the lowest-urgency variant (spec §3)\n signal: \"bg-status-signal-bg border-status-signal-border\",\n // needs attention but not yet broken — a soft limit, a pending step (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border\",\n // something failed or blocks progress — a rejected proof, a failed validation (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder status is spent only when warranted\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The leading status glyph (spec §2/§5): one fixed per-variant icon at the sm icon role. It\n// pairs with the color AND the text so status is never carried by color alone (1.4.1). Because\n// the glyph is DECORATIVE (aria-hidden in tsx) and the message TITLE/body text carries the\n// status meaning, the icon is exempt from the AA text floor and takes the BRIGHT variant ACCENT\n// via the matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an\n// emphasis mark on the quiet surface, while the readable title/body keep their AA text tokens.\nexport const alertIconVariants = cva(\"inline-flex shrink-0 h-(--size-icon-sm) w-(--size-icon-sm)\", {\n variants: {\n variant: {\n verified: \"text-status-verified-accent\",\n signal: \"text-status-signal-accent\",\n caution: \"text-status-caution-accent\",\n critical: \"text-status-critical-accent\",\n },\n },\n defaultVariants: { variant: \"signal\" },\n});\n\n// The stacked content column (spec §2): title, body, then actions, at the small stacked gap.\n// min-w-0 lets long body text wrap instead of overflowing the row.\nexport const alertContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The title (spec §2/§5): a short heading stating the status as a sentence-case statement, in\n// the h3 type role + primary text color. The type role already avoids label tracking — no\n// all-caps, no exclamation mark. Brand violet never paints the title.\nexport const alertTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2/§5): the message, in the body type role + primary text color. It names what\n// happened and, for a caution or critical alert, what to do next — honest about hard things,\n// never blaming the reader (spec §1/§8). Supporting/secondary lines use --color-text-secondary\n// (text-text-secondary) where the caller needs them.\nexport const alertBodyClass = \"text-body text-text-primary\";\n\n// The actions slot (spec §2): at most one or two controls closing the message (a retry, a link\n// to the failing step), holding at most one primary action (restraint over volume). The controls\n// are Buttons — the alert does not restate their --color-action-* bindings. Logical-property row\n// with the small gap; a little top margin separates it from the body.\nexport const alertActionsClass = \"flex items-center gap-(--space-2) mt-(--space-1)\";\n\n// The dismiss control (spec §2/§6/§7): a close button on the inline-end edge, present only on the\n// dismissible variant. A NEUTRAL ghost surface — the glyph in --color-action-ghost-fg at rest,\n// the restrained ghost hover fill (no bg/border at rest) — so the close affordance is neutral and\n// never competes with the status. It is a real focus stop with the target-size floor (44px touch\n// / 40px pointer, spec §7 / DEC-B; the height EMERGES from the floor, never fixed below it), the\n// persistent focus ring, and the fast functional hover motion + verdify easing, instant under\n// reduced motion (G-U3). The ghost fg/hover is the control's OWN action treatment, not the\n// alert's status (the brand != state gate scopes status keys to the container variant only).\nexport const alertDismissClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) \" +\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile) \" +\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) \" +\n focusRing;\n\n// The dismiss glyph (spec §7): a neutral X at the sm icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the\n// accessible name.\nexport const alertDismissGlyphClass = \"h-(--size-icon-sm) w-(--size-icon-sm)\";\n\nexport type AlertVariantProps = VariantProps<typeof alertVariants>;\n",
15
15
  "path": "alert/alert.variants.ts",
16
16
  "target": "@ui/alert/alert.variants.ts",
17
17
  "type": "registry:ui"
@@ -25,7 +25,8 @@
25
25
  ],
26
26
  "name": "alert",
27
27
  "registryDependencies": [
28
- "@verdify/cn"
28
+ "@verdify/cn",
29
+ "@verdify/focus-ring"
29
30
  ],
30
31
  "title": "alert",
31
32
  "type": "registry:ui"
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Breadcrumb is structural wayfinding in NEUTRAL text (spec §3): it names the trail from a\n// root to the current page. It has no status-colored and no brand-accented variant at rest —\n// a crumb reports a location in the hierarchy, not a verification result, so it binds nothing\n// from the status tier and nothing from the action(brand) tier. The trail paints from the\n// text, ghost-action, border, and surface aliases only (spec §5).\n\n// The nav landmark wrapping the trail. A neutral canvas surface with logical-property block\n// padding so the trail mirrors under dir=\"rtl\" (G-U6). The optional hairline separating the\n// breadcrumb from the content below it (spec §5, border-default) is a caller decision, not a\n// default binding — apply it via className where the surface needs the divide.\nexport const breadcrumbNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The ordered list: root -> current. Inline row of items + separators, wrapping when narrow,\n// with the small inter-item gap. The list itself carries no text color — each item sets its own.\nexport const breadcrumbListClass =\n \"flex flex-wrap items-center gap-(--space-1) text-caption\";\n\n// A single trail item (the <li>). Inline so the item and its trailing separator sit on one row.\nexport const breadcrumbItemClass = \"inline-flex items-center gap-(--space-1)\";\n\n// The link item (spec §4). At rest it is the trail label type role in the SECONDARY text color;\n// on hover it takes a restrained ghost fill and the label lifts to the PRIMARY text color (the\n// ghost-action hover fill is the only fill a crumb ever paints). The focus ring is part of the\n// base, on every state, never removed. Motion is the fast token transition on the verdify easing,\n// collapsing to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only\n// theatre duration. A disabled (non-returnable) ancestor dims via the disabled TOKEN, not a\n// blanket opacity (DEC-C), and is taken out of the tab order + pointer flow by the component.\nexport const breadcrumbLinkClass = cva(\n [\n // type ROLE + resting color; logical inline padding + the small icon gap for an optional glyph\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-sm) px-(--space-1)\",\n \"text-caption text-text-secondary\",\n // hover: the restrained ghost fill, label lifts to the primary text color, pointer cursor\n \"cursor-pointer hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n // active text where the ghost treatment applies (spec §5)\n \"active:text-action-ghost-fg\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre)\n \"transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer (spec §7, 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus ring — identical on every state, never removed (spec §4 / 2.4.7)\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled (non-returnable) ancestor — DEC-C: dim via the disabled TOKEN, never opacity.\n // aria-disabled drives it because a breadcrumb ancestor is an <a>, which has no native\n // disabled; the component also strips href + tabindex so it cannot navigate or be tabbed to.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\n// The current page (spec §2/§4/§5): the last item. PLAIN text in the PRIMARY color, NOT a link —\n// no focus ring, no target-size floor, not focusable. The non-interactive guidance: a current\n// crumb is a label, not a control.\nexport const breadcrumbPageClass =\n \"inline-flex items-center gap-(--space-1) px-(--space-1) text-caption text-text-primary\";\n\n// The separator glyph between two items (spec §2/§4): decoration only, in the MUTED text color,\n// at the sm icon role. Removed from the a11y tree by the component (aria-hidden + role=presentation)\n// so the trail is not announced as \"Home slash Billing slash Invoice\".\nexport const breadcrumbSeparatorClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\n// The overflow indicator for the collapsed variant (spec §2): the ellipsis standing in for the\n// middle of a long trail. It renders here as a DECORATIVE, non-interactive glyph in the muted\n// text color — the interactive overflow Menu (a Menu button revealing the hidden ancestors)\n// defers to the Menu primitive, which the library has not built yet (see component TSDoc).\nexport const breadcrumbEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\nexport type BreadcrumbLinkVariantProps = VariantProps<typeof breadcrumbLinkClass>;\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A Breadcrumb is structural wayfinding in NEUTRAL text (spec §3): it names the trail from a\n// root to the current page. It has no status-colored and no brand-accented variant at rest —\n// a crumb reports a location in the hierarchy, not a verification result, so it binds nothing\n// from the status tier and nothing from the action(brand) tier. The trail paints from the\n// text, ghost-action, border, and surface aliases only (spec §5).\n\n// The nav landmark wrapping the trail. A neutral canvas surface with logical-property block\n// padding so the trail mirrors under dir=\"rtl\" (G-U6). The optional hairline separating the\n// breadcrumb from the content below it (spec §5, border-default) is a caller decision, not a\n// default binding — apply it via className where the surface needs the divide.\nexport const breadcrumbNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The ordered list: root -> current. Inline row of items + separators, wrapping when narrow,\n// with the small inter-item gap. The list itself carries no text color — each item sets its own.\nexport const breadcrumbListClass =\n \"flex flex-wrap items-center gap-(--space-1) text-caption\";\n\n// A single trail item (the <li>). Inline so the item and its trailing separator sit on one row.\nexport const breadcrumbItemClass = \"inline-flex items-center gap-(--space-1)\";\n\n// The link item (spec §4). At rest it is the trail label type role in the SECONDARY text color;\n// on hover it takes a restrained ghost fill and the label lifts to the PRIMARY text color (the\n// ghost-action hover fill is the only fill a crumb ever paints). The focus ring is part of the\n// base, on every state, never removed. Motion is the fast token transition on the verdify easing,\n// collapsing to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only\n// theatre duration. A disabled (non-returnable) ancestor dims via the disabled TOKEN, not a\n// blanket opacity (DEC-C), and is taken out of the tab order + pointer flow by the component.\nexport const breadcrumbLinkClass = cva(\n [\n // type ROLE + resting color; logical inline padding + the small icon gap for an optional glyph\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-sm) px-(--space-1)\",\n \"text-caption text-text-secondary\",\n // hover: the restrained ghost fill, label lifts to the primary text color, pointer cursor\n \"cursor-pointer hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n // active text where the ghost treatment applies (spec §5)\n \"active:text-action-ghost-fg\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre)\n \"transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer (spec §7, 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus ring — identical on every state, never removed (spec §4 / 2.4.7)\n \"outline-none\",\n focusRing,\n // disabled (non-returnable) ancestor — DEC-C: dim via the disabled TOKEN, never opacity.\n // aria-disabled drives it because a breadcrumb ancestor is an <a>, which has no native\n // disabled; the component also strips href + tabindex so it cannot navigate or be tabbed to.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\n// The current page (spec §2/§4/§5): the last item. PLAIN text in the PRIMARY color, NOT a link —\n// no focus ring, no target-size floor, not focusable. The non-interactive guidance: a current\n// crumb is a label, not a control.\nexport const breadcrumbPageClass =\n \"inline-flex items-center gap-(--space-1) px-(--space-1) text-caption text-text-primary\";\n\n// The separator glyph between two items (spec §2/§4): decoration only, in the MUTED text color,\n// at the sm icon role. Removed from the a11y tree by the component (aria-hidden + role=presentation)\n// so the trail is not announced as \"Home slash Billing slash Invoice\".\nexport const breadcrumbSeparatorClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\n// The overflow indicator for the collapsed variant (spec §2): the ellipsis standing in for the\n// middle of a long trail. It renders here as a DECORATIVE, non-interactive glyph in the muted\n// text color — the interactive overflow Menu (a Menu button revealing the hidden ancestors)\n// defers to the Menu primitive, which the library has not built yet (see component TSDoc).\nexport const breadcrumbEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\nexport type BreadcrumbLinkVariantProps = VariantProps<typeof breadcrumbLinkClass>;\n",
16
16
  "path": "breadcrumb/breadcrumb.variants.ts",
17
17
  "target": "@ui/breadcrumb/breadcrumb.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "breadcrumb",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "breadcrumb",
32
33
  "type": "registry:ui"
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\nexport const buttonVariants = cva(\n [\n // shape, type role, layout, icon-to-label gap, horizontal padding, elevation\n \"inline-flex items-center justify-center gap-2 rounded-md px-4 shadow-sm\",\n \"text-label font-medium whitespace-nowrap select-none\",\n // hover/pressed transition — fast + verdify easing, no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch, 40px pointer — logical block-size\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset, on every variant, never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled: reduced-emphasis label, no pointer, no hover/pressed response\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border\",\n \"hover:bg-action-primary-bg-hover active:bg-action-primary-bg-active\",\n ],\n secondary: [\n \"bg-action-secondary-bg text-action-secondary-fg border border-action-secondary-border\",\n \"hover:bg-action-secondary-bg-hover\",\n ],\n ghost: [\n \"bg-transparent text-action-ghost-fg border border-transparent\",\n \"hover:bg-action-ghost-bg-hover\",\n ],\n destructive: [\n \"bg-action-destructive-bg text-action-destructive-fg\",\n \"border border-action-destructive-border\",\n ],\n },\n },\n defaultVariants: { variant: \"primary\" },\n },\n);\n\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>;\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\nexport const buttonVariants = cva(\n [\n // shape, type role, layout, icon-to-label gap, horizontal padding, elevation\n \"inline-flex items-center justify-center gap-2 rounded-md px-4 shadow-sm\",\n \"text-label font-medium whitespace-nowrap select-none\",\n // hover/pressed transition — fast + verdify easing, no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch, 40px pointer — logical block-size\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset, on every variant, never removed\n \"outline-none\",\n focusRing,\n // disabled: reduced-emphasis label, no pointer, no hover/pressed response\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border\",\n \"hover:bg-action-primary-bg-hover active:bg-action-primary-bg-active\",\n ],\n secondary: [\n \"bg-action-secondary-bg text-action-secondary-fg border border-action-secondary-border\",\n \"hover:bg-action-secondary-bg-hover\",\n ],\n ghost: [\n \"bg-transparent text-action-ghost-fg border border-transparent\",\n \"hover:bg-action-ghost-bg-hover\",\n ],\n destructive: [\n \"bg-action-destructive-bg text-action-destructive-fg\",\n \"border border-action-destructive-border\",\n ],\n },\n },\n defaultVariants: { variant: \"primary\" },\n },\n);\n\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>;\n",
16
16
  "path": "button/button.variants.ts",
17
17
  "target": "@ui/button/button.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "button",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "button",
32
33
  "type": "registry:ui"
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The card container. A card is a NEUTRAL layout surface (spec §1/§5): it paints from the\n// surface-* roles only — never from a --color-action-* or --color-status-* token, which belong\n// to the controls and badges it holds. The structural axis is the container BEHAVIOR (spec §3):\n// `static` (a non-interactive grouping, the common case) vs `interactive` (the whole surface is\n// one link/button). A second `emphasis` axis flips the resting hairline between the default and\n// the quieter muted border for a low-emphasis grouping (spec §4). The `media` slot composes via\n// the CardMedia subcomponent, so it is not a container-behavior variant here.\nexport const cardVariants = cva(\n [\n // raised surface, rounded container, resting elevation, internal padding from --space-4\n \"block bg-surface-raised border rounded-lg shadow-sm p-(--space-4)\",\n // logical-property text alignment so the card mirrors under dir=rtl (G-U6)\n \"text-start text-text-primary\",\n ],\n {\n variants: {\n variant: {\n // a non-interactive grouping: the whole card does nothing; the controls inside it do\n static: \"\",\n // the whole surface is one control: a restrained hover lift (fast + verdify easing,\n // NEVER the deliberate verified-check theatre), a stronger hover border, and the\n // settle-back on press; the focus ring + target-size floor live in the interactive base\n interactive: [\n \"cursor-pointer\",\n \"transition-[color,box-shadow,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // hover lift: the resting hairline (border-surface-border / -muted, set by `emphasis`)\n // strengthens to the higher-contrast border-border-strong, alongside the shadow-sm->md\n // elevation. Two DISTINCT tokens, so the §4 \"slightly stronger border\" is a real delta,\n // not the no-op that mapping both rest+hover to --color-surface-border would produce.\n \"hover:shadow-md hover:border-border-strong\",\n \"active:shadow-sm\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring around the whole card, on every state, never removed (§4)\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled — DEC-C: dim via the disabled TOKEN, never a blanket opacity; remove the\n // pointer + hover/pressed response. aria-disabled covers the <a> path (native disabled\n // already blocks the <button> path).\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:shadow-none\",\n ],\n },\n emphasis: {\n // the default container hairline\n default: \"border-surface-border\",\n // the quieter hairline for a low-emphasis grouping (spec §4)\n muted: \"border-surface-border-muted\",\n },\n },\n defaultVariants: { variant: \"static\", emphasis: \"default\" },\n },\n);\n\n// The media slot: a leading image/illustration/chart spanning the inline edges above the header.\n// It negates the container's --space-4 padding on the top + inline edges so the media bleeds to\n// the card edge, and re-establishes the --space-2 stacked-slot gap below it.\nexport const cardMediaClass =\n \"-mt-(--space-4) -mx-(--space-4) mb-(--space-2) overflow-hidden rounded-t-lg\";\n\n// The header slot: title + supporting text on the inline-start, an optional control set on the\n// inline-end. --space-2 stacked-slot gap below it.\nexport const cardHeaderClass = \"flex items-start justify-between gap-(--space-2) mb-(--space-2)\";\n\n// The title: a statement in the h3 type role + the primary text color. Sentence case, no all-caps\n// (the type role already avoids the label tracking); brand violet never paints the title.\nexport const cardTitleClass = \"text-h3 text-text-primary\";\n\n// The header-actions slot: a small control set aligned to the header's inline-end edge.\nexport const cardHeaderActionsClass =\n \"flex shrink-0 items-center gap-(--space-2) -mt-(--space-1) -me-(--space-1)\";\n\n// The body slot: the card's primary content, in the body type role + secondary text color.\nexport const cardBodyClass = \"text-body text-text-secondary\";\n\n// The footer slot: actions or closing metadata, in the label type role + secondary text color.\n// --space-2 stacked-slot gap above it.\nexport const cardFooterClass =\n \"flex items-center justify-between gap-(--space-2) mt-(--space-2) text-label text-text-secondary\";\n\nexport type CardVariantProps = VariantProps<typeof cardVariants>;\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The card container. A card is a NEUTRAL layout surface (spec §1/§5): it paints from the\n// surface-* roles only — never from a --color-action-* or --color-status-* token, which belong\n// to the controls and badges it holds. The structural axis is the container BEHAVIOR (spec §3):\n// `static` (a non-interactive grouping, the common case) vs `interactive` (the whole surface is\n// one link/button). A second `emphasis` axis flips the resting hairline between the default and\n// the quieter muted border for a low-emphasis grouping (spec §4). The `media` slot composes via\n// the CardMedia subcomponent, so it is not a container-behavior variant here.\nexport const cardVariants = cva(\n [\n // raised surface, rounded container, resting elevation, internal padding from --space-4\n \"block bg-surface-raised border rounded-lg shadow-sm p-(--space-4)\",\n // logical-property text alignment so the card mirrors under dir=rtl (G-U6)\n \"text-start text-text-primary\",\n ],\n {\n variants: {\n variant: {\n // a non-interactive grouping: the whole card does nothing; the controls inside it do\n static: \"\",\n // the whole surface is one control: a restrained hover lift (fast + verdify easing,\n // NEVER the deliberate verified-check theatre), a stronger hover border, and the\n // settle-back on press; the focus ring + target-size floor live in the interactive base\n interactive: [\n \"cursor-pointer\",\n \"transition-[color,box-shadow,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // hover lift: the resting hairline (border-surface-border / -muted, set by `emphasis`)\n // strengthens to the higher-contrast border-border-strong, alongside the shadow-sm->md\n // elevation. Two DISTINCT tokens, so the §4 \"slightly stronger border\" is a real delta,\n // not the no-op that mapping both rest+hover to --color-surface-border would produce.\n \"hover:shadow-md hover:border-border-strong\",\n \"active:shadow-sm\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring around the whole card, on every state, never removed (§4)\n \"outline-none\",\n focusRing,\n // disabled — DEC-C: dim via the disabled TOKEN, never a blanket opacity; remove the\n // pointer + hover/pressed response. aria-disabled covers the <a> path (native disabled\n // already blocks the <button> path).\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:shadow-none\",\n ],\n },\n emphasis: {\n // the default container hairline\n default: \"border-surface-border\",\n // the quieter hairline for a low-emphasis grouping (spec §4)\n muted: \"border-surface-border-muted\",\n },\n },\n defaultVariants: { variant: \"static\", emphasis: \"default\" },\n },\n);\n\n// The media slot: a leading image/illustration/chart spanning the inline edges above the header.\n// It negates the container's --space-4 padding on the top + inline edges so the media bleeds to\n// the card edge, and re-establishes the --space-2 stacked-slot gap below it.\nexport const cardMediaClass =\n \"-mt-(--space-4) -mx-(--space-4) mb-(--space-2) overflow-hidden rounded-t-lg\";\n\n// The header slot: title + supporting text on the inline-start, an optional control set on the\n// inline-end. --space-2 stacked-slot gap below it.\nexport const cardHeaderClass = \"flex items-start justify-between gap-(--space-2) mb-(--space-2)\";\n\n// The title: a statement in the h3 type role + the primary text color. Sentence case, no all-caps\n// (the type role already avoids the label tracking); brand violet never paints the title.\nexport const cardTitleClass = \"text-h3 text-text-primary\";\n\n// The header-actions slot: a small control set aligned to the header's inline-end edge.\nexport const cardHeaderActionsClass =\n \"flex shrink-0 items-center gap-(--space-2) -mt-(--space-1) -me-(--space-1)\";\n\n// The body slot: the card's primary content, in the body type role + secondary text color.\nexport const cardBodyClass = \"text-body text-text-secondary\";\n\n// The footer slot: actions or closing metadata, in the label type role + secondary text color.\n// --space-2 stacked-slot gap above it.\nexport const cardFooterClass =\n \"flex items-center justify-between gap-(--space-2) mt-(--space-2) text-label text-text-secondary\";\n\nexport type CardVariantProps = VariantProps<typeof cardVariants>;\n",
16
16
  "path": "card/card.variants.ts",
17
17
  "target": "@ui/card/card.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "card",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "card",
32
33
  "type": "registry:ui"
@@ -11,7 +11,7 @@
11
11
  "type": "registry:ui"
12
12
  },
13
13
  {
14
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The square box that carries the visual state. Token binding lives here ONLY.\n// `peer` so the overlaid indicator glyphs can react to :checked / :indeterminate.\nexport const checkboxBoxVariants = cva(\n [\n // shape + neutral control tier at rest (unchecked)\n \"peer shrink-0 appearance-none rounded-sm border bg-control-bg text-control-fg\",\n \"border-border-default\",\n // hover: the box border strengthens; box + label read as one target\n \"hover:border-border-strong\",\n // selection accent (Sovereign Violet) — the brand, NOT a status — when set\n \"checked:bg-action-primary-bg checked:text-action-primary-fg\",\n \"checked:border-action-primary-bg\",\n \"indeterminate:bg-action-primary-bg indeterminate:text-action-primary-fg\",\n \"indeterminate:border-action-primary-bg\",\n // functional toggle: fast + verdify easing, instant under reduced motion.\n // Never the deliberate theatre — a checkbox toggle is not the verified-check moment.\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // visible 2px signal-blue ring at 2px offset, never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // error: critical border marks the FIELD; the fill stays the selection accent\n \"aria-invalid:border-status-critical-border\",\n // disabled: not interactive; state still reads (a disabled-checked box reads checked).\n // DEC-C / spec §4: a disabled control is dimmed via the disabled TOKEN on the\n // indicator (text-text-disabled, below), NOT a blanket opacity-60 on the box.\n \"disabled:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--size-icon-sm) w-(--size-icon-sm)\",\n md: \"h-(--size-icon-md) w-(--size-icon-md)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The overlaid indicator glyphs sit absolutely over the box and are colored by\n// the action-primary FOREGROUND — the glyph on the brand accent fill (spec §5),\n// never a status color. `peer-*` drives visibility from the sibling input's\n// native :checked / :indeterminate state; both glyphs are pointer-transparent\n// and aria-hidden (the input carries role/state). Each glyph is centered over\n// the box and drawn at three-quarters of the box so it never touches the edge.\n//\n// DEC-C / spec §4·§5: when the control is disabled the indicator (check / bar)\n// renders in --color-text-disabled — the SAME token the label dims to — NOT a\n// blanket opacity-60 dim of the whole box. The glyph is a sibling AFTER the peer\n// input, so a `disabled` cva variant (driven by the explicit prop, mirroring\n// checkboxLabelVariants) flips text-action-primary-fg → text-text-disabled.\nconst indicatorBase = [\n \"pointer-events-none absolute inset-0 m-auto hidden h-3/4 w-3/4\",\n];\nconst indicatorColorVariants = {\n variants: {\n disabled: {\n true: \"text-text-disabled\",\n false: \"text-action-primary-fg\",\n },\n },\n defaultVariants: { disabled: false },\n} as const;\n// Check glyph: shown when the peer input is :checked. The mixed (parent-only)\n// case never renders this glyph (it is omitted in JSX when isMixed), so the\n// check and the bar stay mutually exclusive without brittle variant stacking.\nexport const checkboxCheckGlyphVariants = cva(\n [...indicatorBase, \"peer-checked:block\"],\n indicatorColorVariants,\n);\n// Horizontal-bar glyph: shown only when the peer input is :indeterminate.\nexport const checkboxBarGlyphVariants = cva(\n [...indicatorBase, \"peer-indeterminate:block\"],\n indicatorColorVariants,\n);\n\n// The label naming the choice; part of the hit area.\nexport const checkboxLabelVariants = cva(\"text-text-primary select-none\", {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n});\n\nexport type CheckboxBoxVariantProps = VariantProps<typeof checkboxBoxVariants>;\n",
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The square box that carries the visual state. Token binding lives here ONLY.\n// `peer` so the overlaid indicator glyphs can react to :checked / :indeterminate.\nexport const checkboxBoxVariants = cva(\n [\n // shape + neutral control tier at rest (unchecked)\n \"peer shrink-0 appearance-none rounded-sm border bg-control-bg text-control-fg\",\n \"border-border-default\",\n // hover: the box border strengthens; box + label read as one target\n \"hover:border-border-strong\",\n // selection accent (Sovereign Violet) — the brand, NOT a status — when set\n \"checked:bg-action-primary-bg checked:text-action-primary-fg\",\n \"checked:border-action-primary-bg\",\n \"indeterminate:bg-action-primary-bg indeterminate:text-action-primary-fg\",\n \"indeterminate:border-action-primary-bg\",\n // functional toggle: fast + verdify easing, instant under reduced motion.\n // Never the deliberate theatre — a checkbox toggle is not the verified-check moment.\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // visible 2px signal-blue ring at 2px offset, never removed\n \"outline-none\",\n focusRing,\n // error: critical border marks the FIELD; the fill stays the selection accent\n \"aria-invalid:border-status-critical-border\",\n // disabled: not interactive; state still reads (a disabled-checked box reads checked).\n // DEC-C / spec §4: a disabled control is dimmed via the disabled TOKEN on the\n // indicator (text-text-disabled, below), NOT a blanket opacity-60 on the box.\n \"disabled:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--size-icon-sm) w-(--size-icon-sm)\",\n md: \"h-(--size-icon-md) w-(--size-icon-md)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The overlaid indicator glyphs sit absolutely over the box and are colored by\n// the action-primary FOREGROUND — the glyph on the brand accent fill (spec §5),\n// never a status color. `peer-*` drives visibility from the sibling input's\n// native :checked / :indeterminate state; both glyphs are pointer-transparent\n// and aria-hidden (the input carries role/state). Each glyph is centered over\n// the box and drawn at three-quarters of the box so it never touches the edge.\n//\n// DEC-C / spec §4·§5: when the control is disabled the indicator (check / bar)\n// renders in --color-text-disabled — the SAME token the label dims to — NOT a\n// blanket opacity-60 dim of the whole box. The glyph is a sibling AFTER the peer\n// input, so a `disabled` cva variant (driven by the explicit prop, mirroring\n// checkboxLabelVariants) flips text-action-primary-fg → text-text-disabled.\nconst indicatorBase = [\n \"pointer-events-none absolute inset-0 m-auto hidden h-3/4 w-3/4\",\n];\nconst indicatorColorVariants = {\n variants: {\n disabled: {\n true: \"text-text-disabled\",\n false: \"text-action-primary-fg\",\n },\n },\n defaultVariants: { disabled: false },\n} as const;\n// Check glyph: shown when the peer input is :checked. The mixed (parent-only)\n// case never renders this glyph (it is omitted in JSX when isMixed), so the\n// check and the bar stay mutually exclusive without brittle variant stacking.\nexport const checkboxCheckGlyphVariants = cva(\n [...indicatorBase, \"peer-checked:block\"],\n indicatorColorVariants,\n);\n// Horizontal-bar glyph: shown only when the peer input is :indeterminate.\nexport const checkboxBarGlyphVariants = cva(\n [...indicatorBase, \"peer-indeterminate:block\"],\n indicatorColorVariants,\n);\n\n// The label naming the choice; part of the hit area.\nexport const checkboxLabelVariants = cva(\"text-text-primary select-none\", {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n});\n\nexport type CheckboxBoxVariantProps = VariantProps<typeof checkboxBoxVariants>;\n",
15
15
  "path": "checkbox/checkbox.variants.ts",
16
16
  "target": "@ui/checkbox/checkbox.variants.ts",
17
17
  "type": "registry:ui"
@@ -25,7 +25,8 @@
25
25
  ],
26
26
  "name": "checkbox",
27
27
  "registryDependencies": [
28
- "@verdify/cn"
28
+ "@verdify/cn",
29
+ "@verdify/focus-ring"
29
30
  ],
30
31
  "title": "checkbox",
31
32
  "type": "registry:ui"
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The command palette is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified\n// Green are accents, neutrals carry the surface. The SCRIM, the PANEL, the INPUT, and the RESULT\n// ROWS are neutral; the active-row highlight is the SECONDARY hover fill, NEVER a status or brand\n// color — the active row reports WHERE you are in the list, not a verified result, so it never\n// takes Verified Green, and Sovereign Violet never marks a row (brand != state, G-U2). A status\n// color appears only when a result row legitimately carries a status (a verified entity in its\n// subtitle), and then it is the --color-status-* accent on that row's OWN affordance, never a flood\n// of the palette — that affordance is the caller's (VerifiedBadge), not bound here. So NOTHING in\n// this file binds an --color-action-primary-* or --color-status-* fill. This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim (spec §2 scrim, §5 --color-scrim-*): the dimming layer behind the panel that separates\n// the palette from the page and absorbs outside clicks. A neutral dim on the modal z-layer,\n// decorative (no role). The fade is a PLAIN base transition + verdify easing, instant under reduced\n// motion — never the 350ms VerifiedBadge-only theatre (G-U3). Enter/exit ride Radix's data-state on\n// the overlay (attribute-selector variants, not arbitrary values). On a light surface the dark\n// scrim token applies (spec §5: scrim-dark behind the panel on a light surface; the committed\n// overlays bind scrim-dark with no auto light-surface inversion).\nexport const commandPaletteScrimClass =\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark \" +\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\";\n\n// The panel (spec §2 panel, §5): the raised container holding the input and results; it takes\n// role=dialog + the focus trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the\n// outer surface border, the lg corner radius, the lg elevation shadow above the scrim, anchored near\n// the top of the modal z-layer. It never exceeds the viewport — the listbox owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\nexport const commandPalettePanelClass =\n // Horizontally centered, anchored in the UPPER region of the viewport (a palette sits high, not\n // dead-center like a Dialog): top-1/4 is a Tailwind FRACTION utility (no arbitrary raw value), so\n // it stays clean against the token-binding gate while keeping the panel near the top.\n \"fixed left-1/2 top-1/4 z-(--z-index-modal) -translate-x-1/2 \" +\n // never exceeds the viewport — the listbox owns the scroll. calc()-bodied brackets are structural\n // (not raw-value-leading), gate-legitimate, and mirror the Dialog panel's viewport cap.\n \"flex w-[calc(100%-var(--space-8))] max-w-(--container-md) flex-col gap-(--space-3) \" +\n \"max-h-[calc(75dvh)] \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg) \" +\n \"p-(--space-3) \" +\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95 \" +\n // the panel owns no focus stop itself (focus lives in the input); its ring is never removed\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The input (spec §2 input, §4 Focus, §5): the single-line search field at the top, focused on\n// open. It IS the combobox. A control-tier field — control bg/border/fg + the placeholder token\n// (spec §5 --color-control-*). The query text is the body type role in the control fg. While focused\n// the input border takes the focus color and the visible 2px focus ring shows (never removed, spec\n// §4 Focus). The target-size floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the\n// height EMERGING from the floor, never fixed below it. The body metrics ride along\n// (leading/tracking) without binding the text-body SIZE — DEC-A: a form field's value size stays\n// text-base (the iOS no-zoom floor), the body type role contributes only its leading + tracking.\nexport const commandPaletteInputClass =\n \"w-full rounded-(--radius-md) ps-(--space-9) pe-(--space-3) \" +\n \"bg-control-bg text-control-fg border border-control-border \" +\n \"placeholder:text-control-placeholder \" +\n // DEC-A: form-field value size is text-base (iOS no-zoom floor); body type role rides via metrics\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n // the input border takes the focus color while focused (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The leading search icon (spec §2 input, §5 --size-icon-md): decorative (aria-hidden) — the input\n// names itself by its aria-label, not the glyph. Positioned at the inline-start of the input via a\n// logical offset so it mirrors under RTL (G-U6); inherits the placeholder color via currentColor.\nexport const commandPaletteSearchIconClass =\n \"pointer-events-none absolute start-(--space-3) top-1/2 -translate-y-1/2 \" +\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) items-center justify-center text-control-placeholder\";\n\n// The listbox (spec §2 listbox, §5): the results region below the input. A neutral raised surface\n// region with the input-to-results divider above it (a neutral hairline in the default border\n// color), scrolling within the panel when results overflow. Logical inline padding so it mirrors\n// under RTL (G-U6). No motion of its own — a wait is a plain wait (spec §4 Loading), the busy state\n// rides aria-busy, not theatre.\nexport const commandPaletteListboxClass =\n \"min-h-0 flex-1 overflow-y-auto border-t border-border-default pt-(--space-2)\";\n\n// The group label (spec §2 group-label, §5): a non-selectable heading that partitions results by\n// kind; it is NOT a result and is skipped by arrow movement. The MUTED text color at the LABEL type\n// role (spec §5 --color-text-muted / --text-label). Logical inline padding (G-U6).\nexport const commandPaletteGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-label text-text-muted select-none\";\n\n// One result row (spec §2 option, §4 states). A neutral row at rest; the active (highlighted) AND\n// the pointer-hovered row share ONE state — aria-selected — painted with the SECONDARY hover fill\n// (spec §4 Hover, §5 --color-action-secondary-bg-hover). Pointer hover and keyboard active are kept\n// in sync by setting aria-selected on whichever the user touched, so Enter always runs what is\n// highlighted. The active row reports WHERE you are, NOT a verified result — never Verified Green,\n// never the brand (spec §3/§8, G-U2). The label is the PRIMARY text color at the BODY type role\n// (spec §5 --color-text-primary / --text-body).\n//\n// FOCUS (spec §4 Focus): DOM focus stays in the INPUT the whole time; the active row is conveyed by\n// aria-activedescendant, not by moving focus into the list (spec §8 Don't). So a row does NOT paint\n// its own focus-visible ring — the active fill is the affordance, and the visible focus ring lives\n// on the input.\n//\n// DISABLED (spec §4 Disabled): a non-runnable row dims via the disabled TOKEN (DEC-C), never a\n// blanket opacity; it is aria-disabled, skipped by arrow movement (handled in the roving logic), and\n// not activatable. Its short reason sits in the secondary line.\nexport const commandPaletteOptionVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL (G-U6)\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-2)\",\n // the resting (neutral) label color; pointer cursor; no underline\n \"text-text-primary cursor-pointer select-none\",\n // the shared pointer+keyboard highlight: the secondary hover fill on the ACTIVE row\n // (aria-selected). NEVER a status/brand fill — the active row is location, not a verified result.\n \"aria-selected:bg-action-secondary-bg-hover\",\n // target-size floor — 44px touch / 40px pointer, on every row (spec §7 2.5.8), never a fixed\n // height below the floor\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus stays in the input (spec §4 Focus / §8) — the row never paints its own focus ring\n \"outline-none\",\n // disabled (non-runnable) row — DEC-C: dim via the disabled TOKEN, never opacity; not operable\n \"aria-disabled:text-text-disabled aria-disabled:pointer-events-none aria-disabled:cursor-default\",\n ],\n {\n variants: {\n // The result label/secondary type role. There is one row treatment; the axis only exists to\n // keep the binding file shaped like the other compounds. md is the only value the spec needs.\n density: {\n md: \"py-(--space-2)\",\n },\n },\n defaultVariants: { density: \"md\" },\n },\n);\n\nexport type CommandPaletteOptionVariantProps = VariantProps<typeof commandPaletteOptionVariants>;\n\n// The result label (spec §2 option, §5): the primary line, body type role in the primary text color,\n// truncating when long so the row keeps its shape (--color-text-primary / --text-body).\nexport const commandPaletteLabelClass = \"min-w-0 flex-1 truncate text-body text-text-primary\";\n\n// The result secondary line (spec §2 option, §5): the optional second line under/after the label, in\n// the SECONDARY text color (spec §5 --color-text-secondary). Truncates when long.\nexport const commandPaletteSecondaryClass = \"truncate text-label text-text-secondary\";\n\n// The leading result icon (spec §2 option, §5 --size-icon-md): decorative — the row names itself by\n// its label text, not the glyph. Inherits the row color via currentColor; never collapses.\nexport const commandPaletteOptionIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center\";\n\n// The trailing keyboard-shortcut hint (spec §2 option, §5): pushed to the inline-end, in the MUTED\n// text color at the LABEL type role (spec §5 --color-text-muted / --text-label). Decorative\n// wayfinding, never a focus stop; logical inline-end placement (G-U6). The monospace ID/shortcut\n// stays isolated LTR inside RTL text (G-U6).\nexport const commandPaletteShortcutClass =\n \"ms-auto ps-(--space-4) text-label text-text-muted\";\n\n// The empty (no-match) state (spec §2 empty, §4): plain text that says nothing matched and what to\n// try — never a dead end, never a blamed query (principles voice). The SECONDARY text color at the\n// body type role, with breathing room (spec §5 --color-text-secondary). Announced as text, not\n// silence (spec §7 4.1.3).\nexport const commandPaletteEmptyClass =\n \"px-(--space-2) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The footer (spec §2 footer, §5): a thin hint row showing the active keys (move, run, dismiss) so\n// the keyboard model is discoverable in place. The SECONDARY text color at the LABEL type role with\n// a neutral hairline divider above it (spec §5 --color-text-secondary / --text-label /\n// --color-border-default). Logical inline layout (G-U6).\nexport const commandPaletteFooterClass =\n \"flex items-center gap-(--space-4) border-t border-border-default pt-(--space-2) \" +\n \"text-label text-text-secondary\";\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The command palette is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified\n// Green are accents, neutrals carry the surface. The SCRIM, the PANEL, the INPUT, and the RESULT\n// ROWS are neutral; the active-row highlight is the SECONDARY hover fill, NEVER a status or brand\n// color — the active row reports WHERE you are in the list, not a verified result, so it never\n// takes Verified Green, and Sovereign Violet never marks a row (brand != state, G-U2). A status\n// color appears only when a result row legitimately carries a status (a verified entity in its\n// subtitle), and then it is the --color-status-* accent on that row's OWN affordance, never a flood\n// of the palette — that affordance is the caller's (VerifiedBadge), not bound here. So NOTHING in\n// this file binds an --color-action-primary-* or --color-status-* fill. This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim (spec §2 scrim, §5 --color-scrim-*): the dimming layer behind the panel that separates\n// the palette from the page and absorbs outside clicks. A neutral dim on the modal z-layer,\n// decorative (no role). The fade is a PLAIN base transition + verdify easing, instant under reduced\n// motion — never the 350ms VerifiedBadge-only theatre (G-U3). Enter/exit ride Radix's data-state on\n// the overlay (attribute-selector variants, not arbitrary values). On a light surface the dark\n// scrim token applies (spec §5: scrim-dark behind the panel on a light surface; the committed\n// overlays bind scrim-dark with no auto light-surface inversion).\nexport const commandPaletteScrimClass =\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark \" +\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\";\n\n// The panel (spec §2 panel, §5): the raised container holding the input and results; it takes\n// role=dialog + the focus trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the\n// outer surface border, the lg corner radius, the lg elevation shadow above the scrim, anchored near\n// the top of the modal z-layer. It never exceeds the viewport — the listbox owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\nexport const commandPalettePanelClass =\n // Horizontally centered, anchored in the UPPER region of the viewport (a palette sits high, not\n // dead-center like a Dialog): top-1/4 is a Tailwind FRACTION utility (no arbitrary raw value), so\n // it stays clean against the token-binding gate while keeping the panel near the top.\n \"fixed left-1/2 top-1/4 z-(--z-index-modal) -translate-x-1/2 \" +\n // never exceeds the viewport — the listbox owns the scroll. calc()-bodied brackets are structural\n // (not raw-value-leading), gate-legitimate, and mirror the Dialog panel's viewport cap.\n \"flex w-[calc(100%-var(--space-8))] max-w-(--container-md) flex-col gap-(--space-3) \" +\n \"max-h-[calc(75dvh)] \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg) \" +\n \"p-(--space-3) \" +\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95 \" +\n // the panel owns no focus stop itself (focus lives in the input); its ring is never removed\n focusRing;\n\n// The input (spec §2 input, §4 Focus, §5): the single-line search field at the top, focused on\n// open. It IS the combobox. A control-tier field — control bg/border/fg + the placeholder token\n// (spec §5 --color-control-*). The query text is the body type role in the control fg. While focused\n// the input border takes the focus color and the visible 2px focus ring shows (never removed, spec\n// §4 Focus). The target-size floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the\n// height EMERGING from the floor, never fixed below it. The body metrics ride along\n// (leading/tracking) without binding the text-body SIZE — DEC-A: a form field's value size stays\n// text-base (the iOS no-zoom floor), the body type role contributes only its leading + tracking.\nexport const commandPaletteInputClass =\n \"w-full rounded-(--radius-md) ps-(--space-9) pe-(--space-3) \" +\n \"bg-control-bg text-control-fg border border-control-border \" +\n \"placeholder:text-control-placeholder \" +\n // DEC-A: form-field value size is text-base (iOS no-zoom floor); body type role rides via metrics\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n // the input border takes the focus color while focused (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus \" +\n focusRing;\n\n// The leading search icon (spec §2 input, §5 --size-icon-md): decorative (aria-hidden) — the input\n// names itself by its aria-label, not the glyph. Positioned at the inline-start of the input via a\n// logical offset so it mirrors under RTL (G-U6); inherits the placeholder color via currentColor.\nexport const commandPaletteSearchIconClass =\n \"pointer-events-none absolute start-(--space-3) top-1/2 -translate-y-1/2 \" +\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) items-center justify-center text-control-placeholder\";\n\n// The listbox (spec §2 listbox, §5): the results region below the input. A neutral raised surface\n// region with the input-to-results divider above it (a neutral hairline in the default border\n// color), scrolling within the panel when results overflow. Logical inline padding so it mirrors\n// under RTL (G-U6). No motion of its own — a wait is a plain wait (spec §4 Loading), the busy state\n// rides aria-busy, not theatre.\nexport const commandPaletteListboxClass =\n \"min-h-0 flex-1 overflow-y-auto border-t border-border-default pt-(--space-2)\";\n\n// The group label (spec §2 group-label, §5): a non-selectable heading that partitions results by\n// kind; it is NOT a result and is skipped by arrow movement. It is essential de-emphasized text (it\n// names the group), so it uses the SECONDARY text color (AA) at the LABEL type role (spec §5\n// --color-text-secondary / --text-label) — not the decorative-only muted role (accessibility.md).\nexport const commandPaletteGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-label text-text-secondary select-none\";\n\n// One result row (spec §2 option, §4 states). A neutral row at rest; the active (highlighted) AND\n// the pointer-hovered row share ONE state — aria-selected — painted with the SECONDARY hover fill\n// (spec §4 Hover, §5 --color-action-secondary-bg-hover). Pointer hover and keyboard active are kept\n// in sync by setting aria-selected on whichever the user touched, so Enter always runs what is\n// highlighted. The active row reports WHERE you are, NOT a verified result — never Verified Green,\n// never the brand (spec §3/§8, G-U2). The label is the PRIMARY text color at the BODY type role\n// (spec §5 --color-text-primary / --text-body).\n//\n// FOCUS (spec §4 Focus): DOM focus stays in the INPUT the whole time; the active row is conveyed by\n// aria-activedescendant, not by moving focus into the list (spec §8 Don't). So a row does NOT paint\n// its own focus-visible ring — the active fill is the affordance, and the visible focus ring lives\n// on the input.\n//\n// DISABLED (spec §4 Disabled): a non-runnable row dims via the disabled TOKEN (DEC-C), never a\n// blanket opacity; it is aria-disabled, skipped by arrow movement (handled in the roving logic), and\n// not activatable. Its short reason sits in the secondary line.\nexport const commandPaletteOptionVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL (G-U6)\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-2)\",\n // the resting (neutral) label color; pointer cursor; no underline\n \"text-text-primary cursor-pointer select-none\",\n // the shared pointer+keyboard highlight: the secondary hover fill on the ACTIVE row\n // (aria-selected). NEVER a status/brand fill — the active row is location, not a verified result.\n \"aria-selected:bg-action-secondary-bg-hover\",\n // target-size floor — 44px touch / 40px pointer, on every row (spec §7 2.5.8), never a fixed\n // height below the floor\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus stays in the input (spec §4 Focus / §8) — the row never paints its own focus ring\n \"outline-none\",\n // disabled (non-runnable) row — DEC-C: dim via the disabled TOKEN, never opacity; not operable\n \"aria-disabled:text-text-disabled aria-disabled:pointer-events-none aria-disabled:cursor-default\",\n ],\n {\n variants: {\n // The result label/secondary type role. There is one row treatment; the axis only exists to\n // keep the binding file shaped like the other compounds. md is the only value the spec needs.\n density: {\n md: \"py-(--space-2)\",\n },\n },\n defaultVariants: { density: \"md\" },\n },\n);\n\nexport type CommandPaletteOptionVariantProps = VariantProps<typeof commandPaletteOptionVariants>;\n\n// The result label (spec §2 option, §5): the primary line, body type role in the primary text color,\n// truncating when long so the row keeps its shape (--color-text-primary / --text-body).\nexport const commandPaletteLabelClass = \"min-w-0 flex-1 truncate text-body text-text-primary\";\n\n// The result secondary line (spec §2 option, §5): the optional second line under/after the label, in\n// the SECONDARY text color (spec §5 --color-text-secondary). Truncates when long.\nexport const commandPaletteSecondaryClass = \"truncate text-label text-text-secondary\";\n\n// The leading result icon (spec §2 option, §5 --size-icon-md): decorative — the row names itself by\n// its label text, not the glyph. Inherits the row color via currentColor; never collapses.\nexport const commandPaletteOptionIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center\";\n\n// The trailing keyboard-shortcut hint (spec §2 option, §5): pushed to the inline-end, in the MUTED\n// text color at the LABEL type role (spec §5 --color-text-muted / --text-label). Decorative\n// wayfinding, never a focus stop; logical inline-end placement (G-U6). The monospace ID/shortcut\n// stays isolated LTR inside RTL text (G-U6).\nexport const commandPaletteShortcutClass =\n \"ms-auto ps-(--space-4) text-label text-text-muted\";\n\n// The empty (no-match) state (spec §2 empty, §4): plain text that says nothing matched and what to\n// try — never a dead end, never a blamed query (principles voice). The SECONDARY text color at the\n// body type role, with breathing room (spec §5 --color-text-secondary). Announced as text, not\n// silence (spec §7 4.1.3).\nexport const commandPaletteEmptyClass =\n \"px-(--space-2) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The footer (spec §2 footer, §5): a thin hint row showing the active keys (move, run, dismiss) so\n// the keyboard model is discoverable in place. The SECONDARY text color at the LABEL type role with\n// a neutral hairline divider above it (spec §5 --color-text-secondary / --text-label /\n// --color-border-default). Logical inline layout (G-U6).\nexport const commandPaletteFooterClass =\n \"flex items-center gap-(--space-4) border-t border-border-default pt-(--space-2) \" +\n \"text-label text-text-secondary\";\n",
16
16
  "path": "command-palette/command-palette.variants.ts",
17
17
  "target": "@ui/command-palette/command-palette.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "command-palette",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "command-palette",
32
33
  "type": "registry:ui"
@@ -11,7 +11,7 @@
11
11
  "type": "registry:ui"
12
12
  },
13
13
  {
14
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// CredentialCard is one ROW in the list of credentials ATTACHED to an identity — a single way\n// to reach that identity (an email, phone, passkey, wallet, or enterprise SSO connection). It\n// encodes the platform's first invariant — identity is not credentials — in its UI contract: a\n// card is a LINK to an identity, never the identity itself, so it never reads as \"you\" and never\n// stands in for the account (spec §1/§8).\n//\n// It COMPOSES the committed primitives rather than reinventing them — a Card-like neutral surface\n// (the surface-* roles, bound here), the destructive Button for `remove`, the secondary Button\n// for an optional `action`, the Badge for a `status`, the Checkbox for `selectable`, and the\n// Button's in-place loading spinner for a resolving removal. So the focus ring, the target-size\n// floor, the keyboard model, and the control motion all come from those proven primitives; this\n// file binds ONLY the surrounding neutral surface + text-role + layout classes.\n//\n// brand != state (spec §3/§5/§8). The card SURFACE consumes NO action or status token of its own\n// — neutrals carry the card. The brand violet (Sovereign Violet, the action accent) is never a\n// card fill, never the icon, never the identifier, and never marks a credential as special. The\n// verified-status green is reserved for the `verified` status Badge and is never spent on the\n// card surface, the icon, or the identifier — coloring the card green would imply the IDENTITY is\n// verified, which is the one misreport this molecule forbids. The card therefore binds nothing\n// from the action tier and nothing from the status tier; those colors live on the controls and\n// Badges it holds (asserted positively in their own primitives' tests), never on this surface.\n//\n// The motion the card adds is none of its own: control hover/press uses the composed primitive's\n// fast functional transition on verdify easing, collapsing to the instant endpoint under reduced\n// motion. A resolving removal is a plain wait on the Button's ambient spinner — never the 350ms\n// VerifiedBadge-only theatre duration: a removal is not a verification (G-U3).\n\n// The card container (spec §2/§4/§5): a NEUTRAL raised surface, the same Card-static surface the\n// committed Card binds, but it is a LIST ROW (`<li role=\"listitem\">`, set in the tsx) so it sits\n// in the credential list's `<ul role=\"list\">` (spec §7). It is a static container, NOT a single\n// clickable control — the whole row does not map to one action — so it carries no focus ring and\n// no target-size floor of its own (those live on its controls, spec §4/§5). The `kind` axis is\n// which credential the card represents; it is carried by the `icon` + `label` TEXT, never by\n// color, so NONE of the kinds recolors the surface — every kind is the identical neutral surface\n// (spec §3). `selectable` composes with any kind and changes layout (a leading Checkbox), not\n// color, so it is not a color variant here.\nexport const credentialCardVariants = cva(\n [\n // raised neutral surface, rounded container, resting elevation, internal padding from --space-4\n \"relative flex items-start gap-(--space-2) rounded-lg border bg-surface-raised shadow-sm p-(--space-4)\",\n // the default container hairline (spec §5)\n \"border-surface-border\",\n // logical-property text alignment so the row mirrors under dir=rtl (G-U6); list rows carry\n // no bullet marker\n \"text-start text-text-primary list-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the credential KIND the card represents). The kind is carried\n // by the icon + label text, never color, so every kind is the SAME neutral surface.\n kind: {\n email: \"\",\n phone: \"\",\n passkey: \"\",\n wallet: \"\",\n \"enterprise-sso\": \"\",\n },\n },\n defaultVariants: { kind: \"email\" },\n },\n);\n\n// The kind icon (spec §2/§5/§7): one small glyph for the credential kind at the md icon role. It\n// reinforces the kind shown in the label and is decorative (aria-hidden, set in the tsx) — the\n// label text still carries the kind if the icon is dropped, so the kind never rests on the icon\n// or color alone. Its fill is the SECONDARY text role (a neutral role), never a status color and\n// never the brand (spec §2/§3).\nexport const credentialCardIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-text-secondary\";\n\n// The label + identifier block (spec §2): the human-readable kind name above the value that\n// identifies this specific credential. Takes the remaining inline space between the icon and the\n// trailing status/controls; stacks the two lines at the --space-1 gap.\nexport const credentialCardBodyClass = \"flex min-w-0 flex-1 flex-col gap-(--space-1)\";\n\n// The label (spec §2/§5): the human-readable name of the credential KIND, a statement in\n// sentence case (for example \"Email\", \"Passkey\", \"Wallet\"), in the label type role + the PRIMARY\n// text color. The label says what kind of link this is, not who the identity is.\nexport const credentialCardLabelClass = \"text-label text-text-primary\";\n\n// The identifier (spec §2/§5/G-U6): the value that identifies this specific credential — the\n// email, the masked phone, the passkey device name, the truncated wallet address, or the SSO\n// provider + domain. It renders in the MONOSPACE type role (never the UI font for credential\n// strings) in the SECONDARY text color, isolated left-to-right so addresses, hashes, and wallet\n// strings stay readable even inside RTL text, and truncates rather than wrapping.\nexport const credentialCardIdentifierClass =\n \"text-mono text-text-secondary [direction:ltr] truncate\";\n\n// The status row (spec §2): at most one or two small Badges stating a fact about the credential —\n// `verified` (the green status, on the composed Badge's verified variant) or `primary` (the\n// credential you currently sign in with, a NEUTRAL Badge — \"primary\" is which credential is used,\n// a fact, not a status color and never the brand). The status describes the credential, not the\n// identity. This row only positions the composed Badges; their color lives in badge.variants.ts.\nexport const credentialCardStatusClass = \"flex shrink-0 flex-wrap items-center gap-(--space-1)\";\n\n// The meta line (spec §2/§5): quiet secondary text such as when the credential was added or last\n// used, in the MUTED text color at the caption role. De-emphasized; never an alarm color.\nexport const credentialCardMetaClass = \"text-caption text-text-muted\";\n\n// The trailing controls cluster (spec §2): the optional `action` beside the required `remove`,\n// aligned to the inline-end edge at the --space-2 gap. Keep the card to one primary action\n// (`remove`) plus at most one further low-emphasis control (restraint over volume). It only lays\n// the controls out; each control's treatment + focus ring + target-size floor live on the\n// composed Button.\nexport const credentialCardControlsClass = \"flex shrink-0 items-center gap-(--space-2)\";\n\n// The disabled-reason note (spec §4/§5/§7): when a control is disabled (for example `remove` on\n// the last sign-in credential), the reason is given as adjacent text in the MUTED text color at\n// the caption role — wired to the control as its accessible description (so the reason is heard,\n// not just seen), never communicated by graying alone.\nexport const credentialCardReasonClass = \"text-caption text-text-muted\";\n\n// The removal-failure message (spec §4/§7): on a FAILED removal the credential stays in the list\n// and the failure is stated plainly where it happened, in the CRITICAL status foreground at the\n// caption role — naming what failed and what to do next, never blaming the reader and never an\n// exclamation mark. It is the ONLY status color this component binds, and only here (never on the\n// card surface, brand != state). It is announced through an assertive live region (role=alert,\n// set in the tsx) per 4.1.3.\nexport const credentialCardErrorClass = \"text-caption text-status-critical-on-surface\";\n\nexport type CredentialCardVariantProps = VariantProps<typeof credentialCardVariants>;\n",
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// CredentialCard is one ROW in the list of credentials ATTACHED to an identity — a single way\n// to reach that identity (an email, phone, passkey, wallet, or enterprise SSO connection). It\n// encodes the platform's first invariant — identity is not credentials — in its UI contract: a\n// card is a LINK to an identity, never the identity itself, so it never reads as \"you\" and never\n// stands in for the account (spec §1/§8).\n//\n// It COMPOSES the committed primitives rather than reinventing them — a Card-like neutral surface\n// (the surface-* roles, bound here), the destructive Button for `remove`, the secondary Button\n// for an optional `action`, the Badge for a `status`, the Checkbox for `selectable`, and the\n// Button's in-place loading spinner for a resolving removal. So the focus ring, the target-size\n// floor, the keyboard model, and the control motion all come from those proven primitives; this\n// file binds ONLY the surrounding neutral surface + text-role + layout classes.\n//\n// brand != state (spec §3/§5/§8). The card SURFACE consumes NO action or status token of its own\n// — neutrals carry the card. The brand violet (Sovereign Violet, the action accent) is never a\n// card fill, never the icon, never the identifier, and never marks a credential as special. The\n// verified-status green is reserved for the `verified` status Badge and is never spent on the\n// card surface, the icon, or the identifier — coloring the card green would imply the IDENTITY is\n// verified, which is the one misreport this molecule forbids. The card therefore binds nothing\n// from the action tier and nothing from the status tier; those colors live on the controls and\n// Badges it holds (asserted positively in their own primitives' tests), never on this surface.\n//\n// The motion the card adds is none of its own: control hover/press uses the composed primitive's\n// fast functional transition on verdify easing, collapsing to the instant endpoint under reduced\n// motion. A resolving removal is a plain wait on the Button's ambient spinner — never the 350ms\n// VerifiedBadge-only theatre duration: a removal is not a verification (G-U3).\n\n// The card container (spec §2/§4/§5): a NEUTRAL raised surface, the same Card-static surface the\n// committed Card binds, but it is a LIST ROW (`<li role=\"listitem\">`, set in the tsx) so it sits\n// in the credential list's `<ul role=\"list\">` (spec §7). It is a static container, NOT a single\n// clickable control — the whole row does not map to one action — so it carries no focus ring and\n// no target-size floor of its own (those live on its controls, spec §4/§5). The `kind` axis is\n// which credential the card represents; it is carried by the `icon` + `label` TEXT, never by\n// color, so NONE of the kinds recolors the surface — every kind is the identical neutral surface\n// (spec §3). `selectable` composes with any kind and changes layout (a leading Checkbox), not\n// color, so it is not a color variant here.\nexport const credentialCardVariants = cva(\n [\n // raised neutral surface, rounded container, resting elevation, internal padding from --space-4\n \"relative flex items-start gap-(--space-2) rounded-lg border bg-surface-raised shadow-sm p-(--space-4)\",\n // the default container hairline (spec §5)\n \"border-surface-border\",\n // logical-property text alignment so the row mirrors under dir=rtl (G-U6); list rows carry\n // no bullet marker\n \"text-start text-text-primary list-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the credential KIND the card represents). The kind is carried\n // by the icon + label text, never color, so every kind is the SAME neutral surface.\n kind: {\n email: \"\",\n phone: \"\",\n passkey: \"\",\n wallet: \"\",\n \"enterprise-sso\": \"\",\n },\n },\n defaultVariants: { kind: \"email\" },\n },\n);\n\n// The kind icon (spec §2/§5/§7): one small glyph for the credential kind at the md icon role. It\n// reinforces the kind shown in the label and is decorative (aria-hidden, set in the tsx) — the\n// label text still carries the kind if the icon is dropped, so the kind never rests on the icon\n// or color alone. Its fill is the SECONDARY text role (a neutral role), never a status color and\n// never the brand (spec §2/§3).\nexport const credentialCardIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-text-secondary\";\n\n// The label + identifier block (spec §2): the human-readable kind name above the value that\n// identifies this specific credential. Takes the remaining inline space between the icon and the\n// trailing status/controls; stacks the two lines at the --space-1 gap.\nexport const credentialCardBodyClass = \"flex min-w-0 flex-1 flex-col gap-(--space-1)\";\n\n// The label (spec §2/§5): the human-readable name of the credential KIND, a statement in\n// sentence case (for example \"Email\", \"Passkey\", \"Wallet\"), in the label type role + the PRIMARY\n// text color. The label says what kind of link this is, not who the identity is.\nexport const credentialCardLabelClass = \"text-label text-text-primary\";\n\n// The identifier (spec §2/§5/G-U6): the value that identifies this specific credential — the\n// email, the masked phone, the passkey device name, the truncated wallet address, or the SSO\n// provider + domain. It renders in the MONOSPACE type role (never the UI font for credential\n// strings) in the SECONDARY text color, isolated left-to-right so addresses, hashes, and wallet\n// strings stay readable even inside RTL text, and truncates rather than wrapping.\nexport const credentialCardIdentifierClass =\n \"text-mono text-text-secondary [direction:ltr] truncate\";\n\n// The status row (spec §2): at most one or two small Badges stating a fact about the credential —\n// `verified` (the green status, on the composed Badge's verified variant) or `primary` (the\n// credential you currently sign in with, a NEUTRAL Badge — \"primary\" is which credential is used,\n// a fact, not a status color and never the brand). The status describes the credential, not the\n// identity. This row only positions the composed Badges; their color lives in badge.variants.ts.\nexport const credentialCardStatusClass = \"flex shrink-0 flex-wrap items-center gap-(--space-1)\";\n\n// The meta line (spec §2/§5): quiet secondary text such as when the credential was added or last\n// used, at the caption role. Essential de-emphasized text (it conveys when), so it uses the SECONDARY\n// text color (AA), not the decorative-only muted role (accessibility.md). Never an alarm color.\nexport const credentialCardMetaClass = \"text-caption text-text-secondary\";\n\n// The trailing controls cluster (spec §2): the optional `action` beside the required `remove`,\n// aligned to the inline-end edge at the --space-2 gap. Keep the card to one primary action\n// (`remove`) plus at most one further low-emphasis control (restraint over volume). It only lays\n// the controls out; each control's treatment + focus ring + target-size floor live on the\n// composed Button.\nexport const credentialCardControlsClass = \"flex shrink-0 items-center gap-(--space-2)\";\n\n// The disabled-reason note (spec §4/§5/§7): when a control is disabled (for example `remove` on\n// the last sign-in credential), the reason is given as adjacent text at the caption role — wired to\n// the control as its accessible description (so the reason is heard, not just seen). It is essential\n// text, so it uses the SECONDARY text color (AA), not the decorative-only muted role\n// (accessibility.md), and is never communicated by graying alone.\nexport const credentialCardReasonClass = \"text-caption text-text-secondary\";\n\n// The removal-failure message (spec §4/§7): on a FAILED removal the credential stays in the list\n// and the failure is stated plainly where it happened, in the CRITICAL status foreground at the\n// caption role — naming what failed and what to do next, never blaming the reader and never an\n// exclamation mark. It is the ONLY status color this component binds, and only here (never on the\n// card surface, brand != state). It is announced through an assertive live region (role=alert,\n// set in the tsx) per 4.1.3.\nexport const credentialCardErrorClass = \"text-caption text-status-critical-on-surface\";\n\nexport type CredentialCardVariantProps = VariantProps<typeof credentialCardVariants>;\n",
15
15
  "path": "credential-card/credential-card.variants.ts",
16
16
  "target": "@ui/credential-card/credential-card.variants.ts",
17
17
  "type": "registry:ui"
@@ -11,7 +11,7 @@
11
11
  "type": "registry:ui"
12
12
  },
13
13
  {
14
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A DataGrid shows many rows of structured records in a scrollable, operable, two-dimensional grid\n// you navigate one cell at a time (spec §1). It is a NEUTRAL data surface (spec §1/§3): neutrals\n// carry roughly 90% of it, and a dense grid earns its legibility from restraint. It paints from the\n// surface, text, and border roles; it reaches the --color-action-* tier only for the controls it\n// hosts (the sortable-header ghost accent, the row-hover affordance, the bulk-bar actions, and the\n// NEUTRAL selection accent) and the --color-status-* tier ONLY inside a cell that reports a real\n// state, paired with text — never as a row tint, a header fill, or the selection accent, and NEVER\n// a brand token as a status (brand != state, G-U2). Selection and the active cell are NEUTRAL action\n// states; a verified/trust state is a status cell (spec §3/§4/§8).\n\n// The scroll container <div role=\"grid\"> (spec §2 grid / §5). The neutral canvas surface and the\n// default cell text role, framed by the outer surface border at the md radius, with a fixed-height\n// scroll viewport (the caller sizes it via className — the token set has no grid-height scale, the\n// caller-owned-dimension precedent J). It NEVER wears the brand violet or a status fill (spec §3/§8)\n// — those belong to the controls and the status cells inside it. The active cell is kept scrolled\n// clear of the sticky header and pinned columns by scroll-margin (2.4.11 Focus Not Obscured).\nexport const dataGridVariants = cva([\n \"relative w-full overflow-auto\",\n \"bg-surface-canvas text-body text-text-primary\",\n \"border border-surface-border rounded-(--radius-md)\",\n]);\n\nexport type DataGridVariantProps = VariantProps<typeof dataGridVariants>;\n\n// The inner table element. The grid is a real <table> for the row/column relationship (1.3.1),\n// border-collapsed so the gridlines read as single hairlines, and start-aligned so it mirrors under\n// dir=\"rtl\" (G-U6).\nexport const dataGridTableClass = \"w-full border-collapse text-start\";\n\n// The column-header row <tr> (spec §2 column-header-row / §4 Default / §5). It is STICKY — it stays\n// pinned to the top of the scroll viewport while rows scroll under it — on the raised neutral surface\n// with the sm elevation so it reads ABOVE the scrolling rows (spec §4/§5). z on the sticky layer so a\n// scrolled cell never paints over the pinned header. A header row NEVER wears a status or brand tint\n// (spec §3/§8).\nexport const dataGridHeaderRowClass =\n \"sticky top-0 z-(--z-index-sticky) bg-surface-raised shadow-sm \" +\n \"border-b border-border-default\";\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — any in-cell control keeps its own --size-target-* floor\n// (DEC-B: never a fixed height below the floor). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n} as const;\n\n// A data row <tr> (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained GHOST fill to track the eye across a wide row — an AFFORDANCE, never the sole carrier of\n// meaning, and never a selection (nothing is selected until you act, spec §4 Hover). SELECTED\n// (aria-selected): the NEUTRAL secondary-action selection accent — selection is a neutral action\n// state, NOT verified green and NOT the brand violet (selecting a row never implies it is verified;\n// brand != state, G-U2, spec §4/§8). Selection is encoded by the row checkbox + aria-selected, so a\n// grayscale reader reads it from the checkbox, not the fill (1.4.1). Motion is the fast token\n// transition on the verdify easing, instant under reduced motion — never the 350ms VerifiedBadge-only\n// theatre (a row hover/select is a plain transition, G-U3).\nexport const dataGridRowClass =\n \"border-b border-border-default \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"aria-selected:bg-action-secondary-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// A data cell <td role=\"gridcell\"> (spec §2 cell, §4/§5). It is ONE focusable unit in the roving\n// grid: exactly one cell is the active cell (tabindex=0) and shows the visible 2px focus ring; every\n// other cell is tabindex=-1. The ring is part of the base and is NEVER removed (spec §4 Focus /\n// 2.4.7). scroll-margin keeps the active cell clear of the sticky header + pinned columns before it\n// takes focus (spec §7, 2.4.11 Focus Not Obscured). Default: the primary text color at the body type\n// role. A `mono` cell takes the monospace role and is isolated LTR so an identifier/key/timestamp\n// stays readable inside an RTL layout (spec §3/§5, G-U6). A `secondary` cell is de-emphasized\n// auxiliary text. A `status` cell carries the status fg paired with the cell's words — the status\n// color lives in the CELL only, never the row or header (spec §3), and NEVER a brand token (brand !=\n// state, G-U2); status-*-bg is the one neutral raised surface, so meaning is carried by the fg + the\n// text, not a saturated fill.\nexport const dataGridCellVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-body text-text-primary\",\n // the roving active cell's focus ring — always visible, never removed (2.4.7)\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // keep the active cell clear of the sticky header + a pinned column before it takes focus (2.4.11)\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: {\n ...cellPaddingVariants,\n mono: {\n // identifier/key/timestamp: the monospace role, isolated LTR inside RTL text (G-U6)\n true: \"text-mono [direction:ltr]\",\n false: \"\",\n },\n secondary: {\n // de-emphasized secondary cell text (spec §5 --color-text-secondary)\n true: \"text-text-secondary\",\n false: \"\",\n },\n status: {\n none: \"\",\n // each status is the fg only, paired with the cell's text — the words carry the meaning,\n // the fg reinforces it (spec §3/§5); never a saturated -bg fill, never the brand\n verified: \"text-status-verified-on-surface\",\n signal: \"text-status-signal-on-surface\",\n caution: \"text-status-caution-on-surface\",\n critical: \"text-status-critical-on-surface\",\n },\n },\n defaultVariants: {\n density: \"comfortable\",\n mono: false,\n secondary: false,\n status: \"none\",\n },\n },\n);\n\nexport type DataGridCellVariantProps = VariantProps<typeof dataGridCellVariants>;\n\n// A column-header cell <th role=\"columnheader\"> (spec §2 column-header / §4/§5). The header LABEL is\n// the SECONDARY text color at the label type role (the quiet, tracked column label) on the raised\n// header surface — a header NEVER wears a status or brand tint (spec §3/§8). It is also a roving\n// active-cell target, so it carries the same focus ring + scroll-margin as a data cell.\nexport const dataGridColumnHeaderVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-label text-text-secondary\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\" },\n },\n);\n\nexport type DataGridColumnHeaderVariantProps = VariantProps<typeof dataGridColumnHeaderVariants>;\n\n// The SORTABLE-header control (spec §2/§4 Sorted/§6/§7): a real <button> inside the columnheader, so\n// it reads as the control it is and toggles the sort on Enter. It is the GHOST action accent — the\n// label + caret in the ghost fg with the restrained ghost hover fill (spec §4 sortable-header hover /\n// §5) — the action tier is legitimate here because it is a control the grid HOSTS, not a header tint.\n// It carries the target-size floor (40px desktop / 44px touch, spec §7) and inherits the active\n// cell's focus ring from the columnheader. Motion is the fast token transition, never the deliberate\n// verified-check theatre (G-U3). aria-sort lives on the parent th, and the caret encodes direction\n// alongside it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const dataGridSortButtonClass =\n \"inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The sort-direction caret (spec §4 Sorted / §5): the sm icon role, decorative (the direction is also\n// encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on color\n// alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const dataGridSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The selection cell <td role=\"gridcell\"> and the select-all header cell (spec §2 selection-cell /\n// §5): the leading cell that holds the row's Checkbox, carrying the active-cell focus ring +\n// scroll-margin like any other cell. The checkbox is the committed Checkbox component (reused, not\n// re-rolled), which already binds the control surface + border tokens (spec §5) and the brand action\n// accent on the CHECKED box — a checked checkbox is the brand accent, never status-verified (G-U2):\n// selection is a neutral action state. This wrapper is just the cell padding + the roving focus ring.\nexport const dataGridSelectionCellClass =\n \"px-(--space-3) align-middle text-start \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 \" +\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\";\n\n// The bulk-action bar (spec §2 bulk-action-bar / §4/§5): a toolbar that appears when rows are\n// selected, on the raised neutral surface with the sm elevation and a hairline top border, holding\n// the selection actions + a clear-selection control. It is a NEUTRAL surface — the COLOR lives on the\n// action buttons it holds, never on the bar (spec §3/§8). Motion is the fast token transition (the\n// bar's appear/transition), never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkBarClass =\n \"flex items-center gap-(--space-3) px-(--space-3) py-(--space-2) \" +\n \"bg-surface-raised border-t border-border-default \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The selected-count label in the bulk-action bar (spec §2): the secondary text at the label role —\n// the count of selected rows, in words, so selection is announced, never color alone (1.4.1 / 4.1.3).\nexport const dataGridBulkCountClass = \"text-label text-text-secondary\";\n\n// A bulk-action button (spec §2/§5): the PRIMARY selection action is the primary ACTION accent; a\n// `destructive` action (for example, revoke a key) is the destructive ACTION accent — a risk signal\n// named in TEXT, never a status color (spec §5/§8). Both carry the visible focus ring + target-size\n// floor. Motion is the fast token transition, never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkActionVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-1) rounded-(--radius-md) px-(--space-3)\",\n \"text-label font-medium cursor-pointer\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n ],\n {\n variants: {\n destructive: {\n // a destructive bulk action — a risk signal in the ACTION tier, NEVER a status token\n true: \"bg-action-destructive-bg text-action-destructive-fg border border-action-destructive-border\",\n // the default bulk action — the primary action accent\n false: \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border hover:bg-action-primary-bg-hover\",\n },\n },\n defaultVariants: { destructive: false },\n },\n);\n\nexport type DataGridBulkActionVariantProps = VariantProps<typeof dataGridBulkActionVariants>;\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full grid width, in the muted\n// text color — an empty grid is NOT an error and never reads as one (no status color). Its copy says\n// why it is empty and what to do next, in plain words ending in a period (spec §4 Empty / voice).\nexport const dataGridEmptyClass =\n \"px-(--space-3) py-(--space-6) text-center text-body text-text-muted\";\n\n// The off-screen-capable live region (spec §2 status-region / §7 4.1.3): announces the resolved row\n// count, sort + filter changes, and the selection count politely; a blocking row-load error\n// assertively. Always sr-only (it never paints — the visual state carries the same information for\n// sighted users), so it reaches assistive tech as TEXT, never color alone (1.4.1 / 4.1.3).\nexport const dataGridStatusRegionClass = \"sr-only\";\n",
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A DataGrid shows many rows of structured records in a scrollable, operable, two-dimensional grid\n// you navigate one cell at a time (spec §1). It is a NEUTRAL data surface (spec §1/§3): neutrals\n// carry roughly 90% of it, and a dense grid earns its legibility from restraint. It paints from the\n// surface, text, and border roles; it reaches the --color-action-* tier only for the controls it\n// hosts (the sortable-header ghost accent, the row-hover affordance, the bulk-bar actions, and the\n// NEUTRAL selection accent) and the --color-status-* tier ONLY inside a cell that reports a real\n// state, paired with text — never as a row tint, a header fill, or the selection accent, and NEVER\n// a brand token as a status (brand != state, G-U2). Selection and the active cell are NEUTRAL action\n// states; a verified/trust state is a status cell (spec §3/§4/§8).\n\n// The scroll container <div role=\"grid\"> (spec §2 grid / §5). The neutral canvas surface and the\n// default cell text role, framed by the outer surface border at the md radius, with a fixed-height\n// scroll viewport (the caller sizes it via className — the token set has no grid-height scale, the\n// caller-owned-dimension precedent J). It NEVER wears the brand violet or a status fill (spec §3/§8)\n// — those belong to the controls and the status cells inside it. The active cell is kept scrolled\n// clear of the sticky header and pinned columns by scroll-margin (2.4.11 Focus Not Obscured).\nexport const dataGridVariants = cva([\n \"relative w-full overflow-auto\",\n \"bg-surface-canvas text-body text-text-primary\",\n \"border border-surface-border rounded-(--radius-md)\",\n]);\n\nexport type DataGridVariantProps = VariantProps<typeof dataGridVariants>;\n\n// The inner table element. The grid is a real <table> for the row/column relationship (1.3.1),\n// border-collapsed so the gridlines read as single hairlines, and start-aligned so it mirrors under\n// dir=\"rtl\" (G-U6).\nexport const dataGridTableClass = \"w-full border-collapse text-start\";\n\n// The column-header row <tr> (spec §2 column-header-row / §4 Default / §5). It is STICKY — it stays\n// pinned to the top of the scroll viewport while rows scroll under it — on the raised neutral surface\n// with the sm elevation so it reads ABOVE the scrolling rows (spec §4/§5). z on the sticky layer so a\n// scrolled cell never paints over the pinned header. A header row NEVER wears a status or brand tint\n// (spec §3/§8).\nexport const dataGridHeaderRowClass =\n \"sticky top-0 z-(--z-index-sticky) bg-surface-raised shadow-sm \" +\n \"border-b border-border-default\";\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — any in-cell control keeps its own --size-target-* floor\n// (DEC-B: never a fixed height below the floor). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n} as const;\n\n// A data row <tr> (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained GHOST fill to track the eye across a wide row — an AFFORDANCE, never the sole carrier of\n// meaning, and never a selection (nothing is selected until you act, spec §4 Hover). SELECTED\n// (aria-selected): the NEUTRAL secondary-action selection accent — selection is a neutral action\n// state, NOT verified green and NOT the brand violet (selecting a row never implies it is verified;\n// brand != state, G-U2, spec §4/§8). Selection is encoded by the row checkbox + aria-selected, so a\n// grayscale reader reads it from the checkbox, not the fill (1.4.1). Motion is the fast token\n// transition on the verdify easing, instant under reduced motion — never the 350ms VerifiedBadge-only\n// theatre (a row hover/select is a plain transition, G-U3).\nexport const dataGridRowClass =\n \"border-b border-border-default \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"aria-selected:bg-action-secondary-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// A data cell <td role=\"gridcell\"> (spec §2 cell, §4/§5). It is ONE focusable unit in the roving\n// grid: exactly one cell is the active cell (tabindex=0) and shows the visible 2px focus ring; every\n// other cell is tabindex=-1. The ring is part of the base and is NEVER removed (spec §4 Focus /\n// 2.4.7). scroll-margin keeps the active cell clear of the sticky header + pinned columns before it\n// takes focus (spec §7, 2.4.11 Focus Not Obscured). Default: the primary text color at the body type\n// role. A `mono` cell takes the monospace role and is isolated LTR so an identifier/key/timestamp\n// stays readable inside an RTL layout (spec §3/§5, G-U6). A `secondary` cell is de-emphasized\n// auxiliary text. A `status` cell carries the status fg paired with the cell's words — the status\n// color lives in the CELL only, never the row or header (spec §3), and NEVER a brand token (brand !=\n// state, G-U2); status-*-bg is the one neutral raised surface, so meaning is carried by the fg + the\n// text, not a saturated fill.\nexport const dataGridCellVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-body text-text-primary\",\n // the roving active cell's focus ring — always visible, never removed (2.4.7)\n focusRing,\n // keep the active cell clear of the sticky header + a pinned column before it takes focus (2.4.11)\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: {\n ...cellPaddingVariants,\n mono: {\n // identifier/key/timestamp: the monospace role, isolated LTR inside RTL text (G-U6)\n true: \"text-mono [direction:ltr]\",\n false: \"\",\n },\n secondary: {\n // de-emphasized secondary cell text (spec §5 --color-text-secondary)\n true: \"text-text-secondary\",\n false: \"\",\n },\n status: {\n none: \"\",\n // each status is the fg only, paired with the cell's text — the words carry the meaning,\n // the fg reinforces it (spec §3/§5); never a saturated -bg fill, never the brand\n verified: \"text-status-verified-on-surface\",\n signal: \"text-status-signal-on-surface\",\n caution: \"text-status-caution-on-surface\",\n critical: \"text-status-critical-on-surface\",\n },\n },\n defaultVariants: {\n density: \"comfortable\",\n mono: false,\n secondary: false,\n status: \"none\",\n },\n },\n);\n\nexport type DataGridCellVariantProps = VariantProps<typeof dataGridCellVariants>;\n\n// A column-header cell <th role=\"columnheader\"> (spec §2 column-header / §4/§5). The header LABEL is\n// the SECONDARY text color at the label type role (the quiet, tracked column label) on the raised\n// header surface — a header NEVER wears a status or brand tint (spec §3/§8). It is also a roving\n// active-cell target, so it carries the same focus ring + scroll-margin as a data cell.\nexport const dataGridColumnHeaderVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-label text-text-secondary\",\n focusRing,\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\" },\n },\n);\n\nexport type DataGridColumnHeaderVariantProps = VariantProps<typeof dataGridColumnHeaderVariants>;\n\n// The SORTABLE-header control (spec §2/§4 Sorted/§6/§7): a real <button> inside the columnheader, so\n// it reads as the control it is and toggles the sort on Enter. It is the GHOST action accent — the\n// label + caret in the ghost fg with the restrained ghost hover fill (spec §4 sortable-header hover /\n// §5) — the action tier is legitimate here because it is a control the grid HOSTS, not a header tint.\n// It carries the target-size floor (40px desktop / 44px touch, spec §7) and inherits the active\n// cell's focus ring from the columnheader. Motion is the fast token transition, never the deliberate\n// verified-check theatre (G-U3). aria-sort lives on the parent th, and the caret encodes direction\n// alongside it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const dataGridSortButtonClass =\n \"inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n focusRing;\n\n// The sort-direction caret (spec §4 Sorted / §5): the sm icon role, decorative (the direction is also\n// encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on color\n// alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const dataGridSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The selection cell <td role=\"gridcell\"> and the select-all header cell (spec §2 selection-cell /\n// §5): the leading cell that holds the row's Checkbox, carrying the active-cell focus ring +\n// scroll-margin like any other cell. The checkbox is the committed Checkbox component (reused, not\n// re-rolled), which already binds the control surface + border tokens (spec §5) and the brand action\n// accent on the CHECKED box — a checked checkbox is the brand accent, never status-verified (G-U2):\n// selection is a neutral action state. This wrapper is just the cell padding + the roving focus ring.\nexport const dataGridSelectionCellClass =\n \"px-(--space-3) align-middle text-start \" +\n focusRing + \" \" +\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\";\n\n// The bulk-action bar (spec §2 bulk-action-bar / §4/§5): a toolbar that appears when rows are\n// selected, on the raised neutral surface with the sm elevation and a hairline top border, holding\n// the selection actions + a clear-selection control. It is a NEUTRAL surface — the COLOR lives on the\n// action buttons it holds, never on the bar (spec §3/§8). Motion is the fast token transition (the\n// bar's appear/transition), never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkBarClass =\n \"flex items-center gap-(--space-3) px-(--space-3) py-(--space-2) \" +\n \"bg-surface-raised border-t border-border-default \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The selected-count label in the bulk-action bar (spec §2): the secondary text at the label role —\n// the count of selected rows, in words, so selection is announced, never color alone (1.4.1 / 4.1.3).\nexport const dataGridBulkCountClass = \"text-label text-text-secondary\";\n\n// A bulk-action button (spec §2/§5): the PRIMARY selection action is the primary ACTION accent; a\n// `destructive` action (for example, revoke a key) is the destructive ACTION accent — a risk signal\n// named in TEXT, never a status color (spec §5/§8). Both carry the visible focus ring + target-size\n// floor. Motion is the fast token transition, never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkActionVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-1) rounded-(--radius-md) px-(--space-3)\",\n \"text-label font-medium cursor-pointer\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n focusRing,\n ],\n {\n variants: {\n destructive: {\n // a destructive bulk action — a risk signal in the ACTION tier, NEVER a status token\n true: \"bg-action-destructive-bg text-action-destructive-fg border border-action-destructive-border\",\n // the default bulk action — the primary action accent\n false: \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border hover:bg-action-primary-bg-hover\",\n },\n },\n defaultVariants: { destructive: false },\n },\n);\n\nexport type DataGridBulkActionVariantProps = VariantProps<typeof dataGridBulkActionVariants>;\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full grid width — an empty grid\n// is NOT an error and never reads as one (no status color). Its copy says why it is empty and what to\n// do next (spec §4 Empty / voice). Essential text, so it uses the SECONDARY text color (AA), not the\n// decorative-only muted role (accessibility.md).\nexport const dataGridEmptyClass =\n \"px-(--space-3) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The off-screen-capable live region (spec §2 status-region / §7 4.1.3): announces the resolved row\n// count, sort + filter changes, and the selection count politely; a blocking row-load error\n// assertively. Always sr-only (it never paints — the visual state carries the same information for\n// sighted users), so it reaches assistive tech as TEXT, never color alone (1.4.1 / 4.1.3).\nexport const dataGridStatusRegionClass = \"sr-only\";\n",
15
15
  "path": "data-grid/data-grid.variants.ts",
16
16
  "target": "@ui/data-grid/data-grid.variants.ts",
17
17
  "type": "registry:ui"
@@ -26,6 +26,7 @@
26
26
  "name": "data-grid",
27
27
  "registryDependencies": [
28
28
  "@verdify/cn",
29
+ "@verdify/focus-ring",
29
30
  "@verdify/checkbox"
30
31
  ],
31
32
  "title": "data-grid",
@@ -12,7 +12,7 @@
12
12
  "type": "registry:ui"
13
13
  },
14
14
  {
15
- "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Dialog is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents, neutrals carry the surface. The PANEL and the SCRIM are neutral; Sovereign Violet\n// appears only on a footer PRIMARY action — through Button, not here — and Verified Green never\n// appears on the dialog as decoration. A destructive confirm uses the destructive ACTION\n// treatment on its confirm button (Button), never a red panel. So NOTHING in this file binds an\n// --color-action-primary-* or --color-status-* fill (brand != state, G-U2). This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim: the dimming layer behind the panel that separates the dialog from the page and\n// absorbs outside clicks (spec §2 scrim, §5 --color-scrim-*). It is a neutral dim on the modal\n// z-layer, decorative (no role). The fade is a PLAIN base transition + verdify easing, instant\n// under reduced motion — never the 350ms VerifiedBadge-only theatre (G-U3 motion-theatre gate).\n// Enter/exit ride Radix's data-state on the overlay (attribute-selector variants, not arbitrary\n// values). On a light surface the dark scrim token applies (spec §5: scrim-dark on light).\nexport const dialogScrimVariants = cva([\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark\",\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n]);\n\n// The panel: the raised container holding the dialog content; it takes role=dialog + the focus\n// trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the outer surface border,\n// the lg corner radius, the lg elevation shadow above the scrim, centered on the modal z-layer.\n// It never exceeds the viewport and scrolls its BODY when content overflows (spec §3) — the\n// panel caps its own height to the viewport less the gutter; the DialogBody owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\n//\n// size = the panel's MAX WIDTH only (spec §3 sm/md/lg, md default), bound to the --container-*\n// scale; the panel is full-width up to that cap and centered. There is no fixed height — the\n// height emerges from the content up to the viewport cap.\nexport const dialogPanelVariants = cva(\n [\n // centered on the modal layer; full available width up to the size cap, with side gutters\n \"fixed left-1/2 top-1/2 z-(--z-index-modal) -translate-x-1/2 -translate-y-1/2\",\n \"flex w-[calc(100%-var(--space-8))] flex-col gap-(--space-4)\",\n // never taller than the viewport less the gutter; the body scrolls within (spec §3)\n \"max-h-[calc(100dvh-var(--space-8))]\",\n // neutral raised surface + outer border + lg radius + lg elevation; panel inset padding\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg)\",\n \"p-(--space-6)\",\n // base open/close transition + verdify easing, instant under reduced motion (NEVER deliberate)\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // enter/exit ride Radix data-state — attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95\",\n // the panel takes focus when there is no obvious first control; its ring is never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n ],\n {\n variants: {\n // size = max width only (spec §3). md is the default. Bound to the --container-* scale.\n size: {\n sm: \"max-w-(--container-sm)\",\n md: \"max-w-(--container-md)\",\n lg: \"max-w-(--container-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The header: the top region holding the title and the optional close button on the inline-end.\n// Logical-property layout (G-U6); a neutral hairline divider under it continues the surface.\nexport const dialogHeaderClass =\n \"flex items-start justify-between gap-(--space-4) border-b border-border-default pb-(--space-4)\";\n\n// The title: names the dialog in one short statement; it is the accessible name (Radix wires\n// aria-labelledby). The h2 type role in primary text (spec §5 --text-h2 / --color-text-primary).\nexport const dialogTitleClass = \"text-h2 text-text-primary\";\n\n// The description: optional supporting text under the title, associated with the panel for\n// screen readers (Radix wires aria-describedby). Body type role in secondary text (spec §5).\nexport const dialogDescriptionClass = \"text-body text-text-secondary\";\n\n// The body: the scrollable content region between header and footer. The panel caps its height\n// to the viewport; the body takes the remaining space and scrolls when content overflows (spec\n// §3). Body text is the body type role in secondary text (spec §5 --text-body / text-secondary).\nexport const dialogBodyClass =\n \"min-h-0 flex-1 overflow-y-auto text-body text-text-secondary\";\n\n// The footer: the action region holding the primary and any secondary/cancel action, aligned to\n// the inline-end with a neutral hairline divider above it. The actions themselves are Buttons —\n// the dialog spec does not restate their --color-action-* bindings (spec §5 note). Logical-\n// property layout (G-U6): actions flow inline-end with a gap.\nexport const dialogFooterClass =\n \"flex items-center justify-end gap-(--space-2) border-t border-border-default pt-(--space-4)\";\n\n// The close button: the dismiss control in the header. A NEUTRAL ghost surface — the glyph in\n// --color-action-ghost-fg at rest, the restrained ghost hover fill (spec §5 ghost-fg /\n// ghost-bg-hover), the md radius, the persistent focus ring, the target-size floor (44px touch /\n// 40px pointer, spec §7 2.5.8 / DEC-B) with the height EMERGING from the floor, never fixed\n// below it. fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const dialogCloseVariants = cva([\n \"inline-flex items-center justify-center rounded-(--radius-md)\",\n // neutral ghost surface: glyph color at rest + restrained hover fill (no bg/border at rest)\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover\",\n // fast functional hover transition + verdify easing, instant under reduced motion (NEVER deliberate)\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer; the close button is square at the floor (DEC-B)\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile)\",\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop)\",\n // visible 2px focus ring at 2px offset; never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n]);\n\n// The close glyph: a neutral X, --size-icon-md, drawn with currentColor so it inherits the\n// button's ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const dialogCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type DialogPanelVariantProps = VariantProps<typeof dialogPanelVariants>;\n",
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// Dialog is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents, neutrals carry the surface. The PANEL and the SCRIM are neutral; Sovereign Violet\n// appears only on a footer PRIMARY action — through Button, not here — and Verified Green never\n// appears on the dialog as decoration. A destructive confirm uses the destructive ACTION\n// treatment on its confirm button (Button), never a red panel. So NOTHING in this file binds an\n// --color-action-primary-* or --color-status-* fill (brand != state, G-U2). This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim: the dimming layer behind the panel that separates the dialog from the page and\n// absorbs outside clicks (spec §2 scrim, §5 --color-scrim-*). It is a neutral dim on the modal\n// z-layer, decorative (no role). The fade is a PLAIN base transition + verdify easing, instant\n// under reduced motion — never the 350ms VerifiedBadge-only theatre (G-U3 motion-theatre gate).\n// Enter/exit ride Radix's data-state on the overlay (attribute-selector variants, not arbitrary\n// values). On a light surface the dark scrim token applies (spec §5: scrim-dark on light).\nexport const dialogScrimVariants = cva([\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark\",\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n]);\n\n// The panel: the raised container holding the dialog content; it takes role=dialog + the focus\n// trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the outer surface border,\n// the lg corner radius, the lg elevation shadow above the scrim, centered on the modal z-layer.\n// It never exceeds the viewport and scrolls its BODY when content overflows (spec §3) — the\n// panel caps its own height to the viewport less the gutter; the DialogBody owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\n//\n// size = the panel's MAX WIDTH only (spec §3 sm/md/lg, md default), bound to the --container-*\n// scale; the panel is full-width up to that cap and centered. There is no fixed height — the\n// height emerges from the content up to the viewport cap.\nexport const dialogPanelVariants = cva(\n [\n // centered on the modal layer; full available width up to the size cap, with side gutters\n \"fixed left-1/2 top-1/2 z-(--z-index-modal) -translate-x-1/2 -translate-y-1/2\",\n \"flex w-[calc(100%-var(--space-8))] flex-col gap-(--space-4)\",\n // never taller than the viewport less the gutter; the body scrolls within (spec §3)\n \"max-h-[calc(100dvh-var(--space-8))]\",\n // neutral raised surface + outer border + lg radius + lg elevation; panel inset padding\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg)\",\n \"p-(--space-6)\",\n // base open/close transition + verdify easing, instant under reduced motion (NEVER deliberate)\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // enter/exit ride Radix data-state — attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95\",\n // the panel takes focus when there is no obvious first control; its ring is never removed\n \"outline-none\",\n focusRing,\n ],\n {\n variants: {\n // size = max width only (spec §3). md is the default. Bound to the --container-* scale.\n size: {\n sm: \"max-w-(--container-sm)\",\n md: \"max-w-(--container-md)\",\n lg: \"max-w-(--container-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The header: the top region holding the title and the optional close button on the inline-end.\n// Logical-property layout (G-U6); a neutral hairline divider under it continues the surface.\nexport const dialogHeaderClass =\n \"flex items-start justify-between gap-(--space-4) border-b border-border-default pb-(--space-4)\";\n\n// The title: names the dialog in one short statement; it is the accessible name (Radix wires\n// aria-labelledby). The h2 type role in primary text (spec §5 --text-h2 / --color-text-primary).\nexport const dialogTitleClass = \"text-h2 text-text-primary\";\n\n// The description: optional supporting text under the title, associated with the panel for\n// screen readers (Radix wires aria-describedby). Body type role in secondary text (spec §5).\nexport const dialogDescriptionClass = \"text-body text-text-secondary\";\n\n// The body: the scrollable content region between header and footer. The panel caps its height\n// to the viewport; the body takes the remaining space and scrolls when content overflows (spec\n// §3). Body text is the body type role in secondary text (spec §5 --text-body / text-secondary).\nexport const dialogBodyClass =\n \"min-h-0 flex-1 overflow-y-auto text-body text-text-secondary\";\n\n// The footer: the action region holding the primary and any secondary/cancel action, aligned to\n// the inline-end with a neutral hairline divider above it. The actions themselves are Buttons —\n// the dialog spec does not restate their --color-action-* bindings (spec §5 note). Logical-\n// property layout (G-U6): actions flow inline-end with a gap.\nexport const dialogFooterClass =\n \"flex items-center justify-end gap-(--space-2) border-t border-border-default pt-(--space-4)\";\n\n// The close button: the dismiss control in the header. A NEUTRAL ghost surface — the glyph in\n// --color-action-ghost-fg at rest, the restrained ghost hover fill (spec §5 ghost-fg /\n// ghost-bg-hover), the md radius, the persistent focus ring, the target-size floor (44px touch /\n// 40px pointer, spec §7 2.5.8 / DEC-B) with the height EMERGING from the floor, never fixed\n// below it. fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const dialogCloseVariants = cva([\n \"inline-flex items-center justify-center rounded-(--radius-md)\",\n // neutral ghost surface: glyph color at rest + restrained hover fill (no bg/border at rest)\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover\",\n // fast functional hover transition + verdify easing, instant under reduced motion (NEVER deliberate)\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer; the close button is square at the floor (DEC-B)\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile)\",\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop)\",\n // visible 2px focus ring at 2px offset; never removed\n \"outline-none\",\n focusRing,\n]);\n\n// The close glyph: a neutral X, --size-icon-md, drawn with currentColor so it inherits the\n// button's ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const dialogCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type DialogPanelVariantProps = VariantProps<typeof dialogPanelVariants>;\n",
16
16
  "path": "dialog/dialog.variants.ts",
17
17
  "target": "@ui/dialog/dialog.variants.ts",
18
18
  "type": "registry:ui"
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "name": "dialog",
28
28
  "registryDependencies": [
29
- "@verdify/cn"
29
+ "@verdify/cn",
30
+ "@verdify/focus-ring"
30
31
  ],
31
32
  "title": "dialog",
32
33
  "type": "registry:ui"
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "content": "// Two-tone focus ring (WCAG 1.4.11). Single source of truth — every interactive\n// component composes this constant instead of inlining the ring utilities.\n//\n// The indicator is a three-part composite, innermost-first:\n// 1. `focus-visible:ring-offset-2` — a 2px gap between the target and the ring,\n// so the ring renders on the page surface rather than on a filled control.\n// 2. `focus-visible:ring-2 ring-border-focus` — the 2px signal-blue identity\n// layer (`--color-border-focus` = `--focus-ring-color`, #4D9DFF). This layer\n// is ~2.55:1 on light surfaces alone — below the WCAG 1.4.11 ≥3:1 floor — so\n// it CANNOT carry the contrast obligation by itself.\n// 3. `focus-visible:outline-(--focus-ring-casing)` — the contrast (casing)\n// layer, bound to the per-band `--focus-ring-casing` CSS var (= the band's\n// AAA `text.primary`: dark ink on light bands, light ink on dark bands).\n// Sat at `outline-offset-4` (4px = the blue ring's 2px offset + 2px width) it\n// hugs the OUTER edge of the blue ring, so the composite always has a ≥3:1\n// edge against every surface and against any\n// filled control the ring sits over. Tailwind v4 `outline-(<custom-property>)`\n// maps to `outline-color: var(<custom-property>)`.\n//\n// `outline-none` suppresses the browser default so the two-tone composite is the\n// only indicator; it is never removed (removing it with no replacement is a defect).\nexport const focusRing =\n \"outline-none \" +\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 \" +\n \"focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-4 \" +\n \"focus-visible:outline-(--focus-ring-casing)\";\n",
7
+ "path": "lib/focus-ring.ts",
8
+ "target": "@lib/focus-ring.ts",
9
+ "type": "registry:lib"
10
+ }
11
+ ],
12
+ "name": "focus-ring",
13
+ "registryDependencies": [],
14
+ "title": "focusRing (Verdify two-tone focus ring)",
15
+ "type": "registry:lib"
16
+ }