@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/pagination/pagination.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Pagination is a row of standard links/buttons inside a labeled navigation landmark (spec §7):\n// there is no APG \"pagination\" widget. Brand and status colors are accents — NEUTRALS carry the\n// surface (spec §3). The one accent the pager paints is on the CURRENT page, and that accent is\n// the primary ACTION (brand) alias, never a status color: a current page reports where you are in\n// the set, not a verification result, so it binds nothing from the status tier (brand != state,\n// G-U2). The set paints from the text, action(primary + ghost), border, and surface aliases only\n// (spec §5).\n\n// The nav landmark wrapping the control set. A neutral canvas surface with logical-property block\n// padding so the row mirrors under dir=\"rtl\" (G-U6). The optional divider above the set (spec §5,\n// border-default) is a caller decision applied via className, not a default binding.\nexport const paginationNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The control list. An inline row of items with the inter-control gap; wraps when narrow. The\n// list carries no text color — each control sets its own.\nexport const paginationListClass =\n \"flex flex-wrap items-center gap-(--space-4)\";\n\n// A single item (the <li>). Inline so the control sits in the row.\nexport const paginationItemClass = \"inline-flex items-center\";\n\n// A page / prev / next control (spec §4). At rest, an enabled non-current control is the label\n// type role in the SECONDARY text color with NO fill; on hover it takes the restrained ghost fill\n// (the only fill a non-current control paints) and the cursor is a pointer. The CURRENT page lifts\n// its label to the primary action FG on the primary action BG and does not navigate — this accent\n// is the BRAND action alias (where you are), never a status. The focus ring is part of the base on\n// every state, never removed. Motion is the fast token transition on the verdify easing, collapsing\n// to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only theatre\n// duration (a page turning is a plain wait, G-U3). A disabled prev/next dims via the disabled TOKEN\n// (DEC-C): native `disabled:` for a button-form control and `aria-disabled:` for a link-form one\n// (an <a> has no native disabled), never a blanket opacity.\nexport const paginationControlVariants = cva(\n [\n // type ROLE + shape + the icon-to-label gap; logical inline padding so it mirrors under RTL\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-label font-medium select-none\",\n // resting state — enabled, non-current: secondary label, no fill, pointer cursor\n \"cursor-pointer text-text-secondary\",\n // hover: the restrained ghost fill (the only fill a non-current control paints)\n \"hover:bg-action-ghost-bg-hover\",\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, on every control (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 prev/next — DEC-C: dim via the disabled TOKEN for BOTH the native-button form\n // (`disabled`) and the link form (`aria-disabled`), never a blanket opacity. The component\n // also strips href + tabindex on a disabled link so it cannot navigate or be tabbed to.\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n {\n variants: {\n // STATE axis (spec §4): the current page is the only control carrying the brand action fill.\n current: {\n true: [\n // the current page: brand action accent (where you are in the set), non-navigating.\n // This is the action(primary) alias, NEVER status-verified (brand != state, G-U2).\n \"bg-action-primary-bg text-action-primary-fg\",\n // the current page is not a control: no hover fill, no pointer\n \"cursor-default hover:bg-action-primary-bg\",\n ],\n false: \"\",\n },\n // SIZE axis (spec §3, DEC-B): both sizes hold the shared target-size floor above; sm differs\n // only by density (tighter vertical padding) below the type role, never a height below the floor.\n size: {\n md: \"py-(--space-2)\",\n sm: \"py-(--space-1)\",\n },\n },\n defaultVariants: { current: false, size: \"md\" },\n },\n);\n\nexport type PaginationControlVariantProps = VariantProps<typeof paginationControlVariants>;\n\n// The prev/next direction icon (spec §5): the sm icon role, decorative (the control names its\n// direction via aria-label, not the glyph). Inherits the control's current text color.\nexport const paginationIconClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The \"Page m of n\" readout for the prev-next variant (spec §3): plain text in the secondary\n// color at the label type role. Not a control — a non-interactive status of position in the set.\nexport const paginationStatusClass =\n \"inline-flex items-center px-(--space-2) text-label text-text-secondary select-none\";\n\n// The ellipsis gap (spec §2/§7): a DECORATIVE, non-interactive stand-in for a run of hidden page\n// controls, in the muted text color at the sm icon role. Removed from the a11y tree + tab order by\n// the component (aria-hidden); it is never a stop.\nexport const paginationEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n"],"mappings":"AAAA,SAAS,WAA8B;AAahC,MAAM,qBAAqB;AAI3B,MAAM,sBACX;AAGK,MAAM,sBAAsB;AAY5B,MAAM,4BAA4B;AAAA,EACvC;AAAA;AAAA,IAEE;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA,MAER,SAAS;AAAA,QACP,MAAM;AAAA;AAAA;AAAA,UAGJ;AAAA;AAAA,UAEA;AAAA,QACF;AAAA,QACA,OAAO;AAAA,MACT;AAAA;AAAA;AAAA,MAGA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,SAAS,OAAO,MAAM,KAAK;AAAA,EAChD;AACF;AAMO,MAAM,sBACX;AAIK,MAAM,wBACX;AAKK,MAAM,0BACX;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/pagination/pagination.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"../../lib/focus-ring\";\n\n// Pagination is a row of standard links/buttons inside a labeled navigation landmark (spec §7):\n// there is no APG \"pagination\" widget. Brand and status colors are accents — NEUTRALS carry the\n// surface (spec §3). The one accent the pager paints is on the CURRENT page, and that accent is\n// the primary ACTION (brand) alias, never a status color: a current page reports where you are in\n// the set, not a verification result, so it binds nothing from the status tier (brand != state,\n// G-U2). The set paints from the text, action(primary + ghost), border, and surface aliases only\n// (spec §5).\n\n// The nav landmark wrapping the control set. A neutral canvas surface with logical-property block\n// padding so the row mirrors under dir=\"rtl\" (G-U6). The optional divider above the set (spec §5,\n// border-default) is a caller decision applied via className, not a default binding.\nexport const paginationNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The control list. An inline row of items with the inter-control gap; wraps when narrow. The\n// list carries no text color — each control sets its own.\nexport const paginationListClass =\n \"flex flex-wrap items-center gap-(--space-4)\";\n\n// A single item (the <li>). Inline so the control sits in the row.\nexport const paginationItemClass = \"inline-flex items-center\";\n\n// A page / prev / next control (spec §4). At rest, an enabled non-current control is the label\n// type role in the SECONDARY text color with NO fill; on hover it takes the restrained ghost fill\n// (the only fill a non-current control paints) and the cursor is a pointer. The CURRENT page lifts\n// its label to the primary action FG on the primary action BG and does not navigate — this accent\n// is the BRAND action alias (where you are), never a status. The focus ring is part of the base on\n// every state, never removed. Motion is the fast token transition on the verdify easing, collapsing\n// to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only theatre\n// duration (a page turning is a plain wait, G-U3). A disabled prev/next dims via the disabled TOKEN\n// (DEC-C): native `disabled:` for a button-form control and `aria-disabled:` for a link-form one\n// (an <a> has no native disabled), never a blanket opacity.\nexport const paginationControlVariants = cva(\n [\n // type ROLE + shape + the icon-to-label gap; logical inline padding so it mirrors under RTL\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-label font-medium select-none\",\n // resting state — enabled, non-current: secondary label, no fill, pointer cursor\n \"cursor-pointer text-text-secondary\",\n // hover: the restrained ghost fill (the only fill a non-current control paints)\n \"hover:bg-action-ghost-bg-hover\",\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, on every control (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 prev/next — DEC-C: dim via the disabled TOKEN for BOTH the native-button form\n // (`disabled`) and the link form (`aria-disabled`), never a blanket opacity. The component\n // also strips href + tabindex on a disabled link so it cannot navigate or be tabbed to.\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n {\n variants: {\n // STATE axis (spec §4): the current page is the only control carrying the brand action fill.\n current: {\n true: [\n // the current page: brand action accent (where you are in the set), non-navigating.\n // This is the action(primary) alias, NEVER status-verified (brand != state, G-U2).\n \"bg-action-primary-bg text-action-primary-fg\",\n // the current page is not a control: no hover fill, no pointer\n \"cursor-default hover:bg-action-primary-bg\",\n ],\n false: \"\",\n },\n // SIZE axis (spec §3, DEC-B): both sizes hold the shared target-size floor above; sm differs\n // only by density (tighter vertical padding) below the type role, never a height below the floor.\n size: {\n md: \"py-(--space-2)\",\n sm: \"py-(--space-1)\",\n },\n },\n defaultVariants: { current: false, size: \"md\" },\n },\n);\n\nexport type PaginationControlVariantProps = VariantProps<typeof paginationControlVariants>;\n\n// The prev/next direction icon (spec §5): the sm icon role, decorative (the control names its\n// direction via aria-label, not the glyph). Inherits the control's current text color.\nexport const paginationIconClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The \"Page m of n\" readout for the prev-next variant (spec §3): plain text in the secondary\n// color at the label type role. Not a control — a non-interactive status of position in the set.\nexport const paginationStatusClass =\n \"inline-flex items-center px-(--space-2) text-label text-text-secondary select-none\";\n\n// The ellipsis gap (spec §2/§7): a DECORATIVE, non-interactive stand-in for a run of hidden page\n// controls, in the muted text color at the sm icon role. Removed from the a11y tree + tab order by\n// the component (aria-hidden); it is never a stop.\nexport const paginationEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n"],"mappings":"AAAA,SAAS,WAA8B;AACvC,SAAS,iBAAiB;AAanB,MAAM,qBAAqB;AAI3B,MAAM,sBACX;AAGK,MAAM,sBAAsB;AAY5B,MAAM,4BAA4B;AAAA,EACvC;AAAA;AAAA,IAEE;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA,MAER,SAAS;AAAA,QACP,MAAM;AAAA;AAAA;AAAA,UAGJ;AAAA;AAAA,UAEA;AAAA,QACF;AAAA,QACA,OAAO;AAAA,MACT;AAAA;AAAA;AAAA,MAGA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,SAAS,OAAO,MAAM,KAAK;AAAA,EAChD;AACF;AAMO,MAAM,sBACX;AAIK,MAAM,wBACX;AAKK,MAAM,0BACX;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"popover.variants.d.ts","sourceRoot":"","sources":["../../../src/components/popover/popover.variants.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,mBAAmB,QAQ4B,CAAC;AAc7D,eAAO,MAAM,iBAAiB,QAOmE,CAAC;AAIlG,eAAO,MAAM,kBAAkB,qDACqB,CAAC;AAKrD,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAK7D,eAAO,MAAM,gBAAgB,kCAAkC,CAAC;AAShE,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAOvD,eAAO,MAAM,iBAAiB,QAOmE,CAAC;AAIlG,eAAO,MAAM,sBAAsB,0CAA0C,CAAC"}
1
+ {"version":3,"file":"popover.variants.d.ts","sourceRoot":"","sources":["../../../src/components/popover/popover.variants.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,mBAAmB,QAQ4B,CAAC;AAc7D,eAAO,MAAM,iBAAiB,QAOnB,CAAC;AAIZ,eAAO,MAAM,kBAAkB,qDACqB,CAAC;AAKrD,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAK7D,eAAO,MAAM,gBAAgB,kCAAkC,CAAC;AAShE,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAOvD,eAAO,MAAM,iBAAiB,QAOnB,CAAC;AAIZ,eAAO,MAAM,sBAAsB,0CAA0C,CAAC"}
@@ -1,10 +1,11 @@
1
- const popoverTriggerClass = "inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-3) text-label text-action-ghost-fg cursor-pointer select-none hover:bg-action-ghost-bg-hover transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 disabled:pointer-events-none disabled:text-text-disabled";
2
- const popoverPanelClass = "z-(--z-index-popover) flex flex-col gap-(--space-3) max-w-(--container-sm) p-(--space-4) bg-surface-raised border border-surface-border rounded-(--radius-md) shadow-(--shadow-md) transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) data-[state=open]:opacity-100 data-[state=closed]:opacity-0 outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2";
1
+ import { focusRing } from "../../lib/focus-ring";
2
+ const popoverTriggerClass = "inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-3) text-label text-action-ghost-fg cursor-pointer select-none hover:bg-action-ghost-bg-hover transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) " + focusRing + " disabled:pointer-events-none disabled:text-text-disabled";
3
+ const popoverPanelClass = "z-(--z-index-popover) flex flex-col gap-(--space-3) max-w-(--container-sm) p-(--space-4) bg-surface-raised border border-surface-border rounded-(--radius-md) shadow-(--shadow-md) transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) data-[state=open]:opacity-100 data-[state=closed]:opacity-0 " + focusRing;
3
4
  const popoverHeaderClass = "flex items-start justify-between gap-(--space-3)";
4
5
  const popoverTitleClass = "text-h3 text-text-primary";
5
6
  const popoverBodyClass = "text-body text-text-secondary";
6
7
  const popoverArrowClass = "fill-surface-raised";
7
- const popoverCloseClass = "inline-flex items-center justify-center rounded-(--radius-md) text-action-ghost-fg hover:bg-action-ghost-bg-hover transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) min-w-(--size-target-mobile) sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2";
8
+ const popoverCloseClass = "inline-flex items-center justify-center rounded-(--radius-md) text-action-ghost-fg hover:bg-action-ghost-bg-hover transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) min-w-(--size-target-mobile) sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) " + focusRing;
8
9
  const popoverCloseGlyphClass = "h-(--size-icon-md) w-(--size-icon-md)";
9
10
  export {
10
11
  popoverArrowClass,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/popover/popover.variants.ts"],"sourcesContent":["// Popover is a small NON-MODAL surface a trigger opens next to itself to hold secondary content —\n// a short form, a definition, a few related controls (spec §1). It is a NEUTRAL surface (spec §3):\n// the panel, the arrow, and the border are neutral, and the brand violet NEVER tints the panel to\n// look \"premium.\" A status color NEVER paints the panel — a verified result is reported by a\n// VerifiedBadge placed INSIDE the body, not by coloring the popover. So NOTHING in this file binds a\n// --color-status-* token or the brand action-primary tier; color enters only through the components\n// the caller places inside it (brand != state, G-U2). This is the ONLY token-binding site (skill §5\n// hard rule). All open/close motion is the FAST token transition on the verdify easing, instant\n// under reduced motion — never the 350ms VerifiedBadge-only theatre duration (G-U3).\n\n// The trigger: the one stop in the page tab order for this control (spec §2 trigger, §4 Focus). A\n// NEUTRAL ghost surface — the label/glyph in the ghost action fg at rest (spec §5\n// --color-action-ghost-fg), the restrained ghost hover fill (spec §5 --color-action-ghost-bg-hover),\n// the md radius, the persistent 2px focus ring (never removed, spec §4 Focus), and the target-size\n// floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the height EMERGING from the floor.\n// A disabled trigger dims via the disabled TOKEN (DEC-C), never a blanket opacity. fast functional\n// hover motion + verdify easing, instant under reduced motion (G-U3). This styles the DEFAULT\n// (non-asChild) trigger; when a Button is passed via `asChild` it carries its own treatment.\nexport const popoverTriggerClass =\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-3) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-[color,background-color] 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 \"disabled:pointer-events-none disabled:text-text-disabled\";\n\n// The panel (spec §2 panel, §5): the floating surface that opens on activation, raised above the\n// page and anchored to the trigger; it repositions to stay in the viewport (Radix). A NEUTRAL raised\n// surface (spec §5 --color-surface-raised) with the outer surface border (spec §5\n// --color-surface-border), the md corner radius (spec §5 --radius-md), and the md elevation shadow\n// above the page (spec §5 --shadow-md), on the POPOVER z-layer (a popover is a non-modal popover\n// layer, NOT the modal layer — there is no scrim, the page behind stays live, spec §1/§7). It NEVER\n// wears a brand or status fill (spec §3/§8). Inset padding + content gaps from --space-*; a column so\n// the header, body, and footer stack. The open/close fade is a PLAIN fast transition + verdify\n// easing, instant under reduced motion — never the 350ms VerifiedBadge-only theatre (spec §4, G-U3).\n// Enter/exit ride Radix's data-state on the content (attribute-selector variants, not arbitrary\n// values). The panel is focusable (Radix sets tabIndex=-1) so focus can land on it when the body has\n// no focusable control; its ring is never removed (spec §7 focus management).\nexport const popoverPanelClass =\n \"z-(--z-index-popover) flex flex-col gap-(--space-3) \" +\n \"max-w-(--container-sm) p-(--space-4) \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-md) shadow-(--shadow-md) \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The header (spec §2 header): the top region holding a short title and an optional close control on\n// the inline-end. Logical-property layout (G-U6) so it mirrors under RTL.\nexport const popoverHeaderClass =\n \"flex items-start justify-between gap-(--space-3)\";\n\n// The title (spec §2 header title, §5): names the panel as a statement, sentence case. The h3 type\n// role in the PRIMARY text color (spec §5 --text-h3 / --color-text-primary). When present, it is the\n// panel's accessible name, wired via aria-labelledby (the panel takes role=\"dialog\", spec §7).\nexport const popoverTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2 body, §5): the content region — text, a short form, or a few controls. The body\n// type role; supporting text in the SECONDARY text color (spec §5 --text-body / --color-text-secondary).\n// Content that needs to scroll usually belongs in a Sheet (spec §2), so the body does not own a scroll.\nexport const popoverBodyClass = \"text-body text-text-secondary\";\n\n// The arrow (spec §2 arrow): a small DECORATIVE pointer joining the panel to its trigger, carrying no\n// meaning of its own (Radix renders it inside an aria-hidden wrapper). It is filled with the SAME\n// neutral raised surface as the panel so it reads as part of the surface, never a brand or status\n// fill (spec §3/§5). `fill-*` is the SVG fill utility for the Radix arrow polygon. (Radix's bare\n// arrow does not carry the panel's outer BORDER edge — a conformant, non-load-bearing deviation from\n// the §5 \"arrow edge\" border, flagged for amendment rather than hand-rolling a bordered polygon, the\n// same deviation the Tooltip arrow pins.)\nexport const popoverArrowClass = \"fill-surface-raised\";\n\n// The close control (spec §2 close, §5): the in-panel dismiss button. A NEUTRAL ghost surface — the\n// glyph in --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 below it.\n// fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const popoverCloseClass =\n \"inline-flex 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 close glyph: a neutral X, --size-icon-md, drawn with currentColor so it inherits the close\n// button's ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const popoverCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n"],"mappings":"AAkBO,MAAM,sBACX;AAqBK,MAAM,oBACX;AAUK,MAAM,qBACX;AAKK,MAAM,oBAAoB;AAK1B,MAAM,mBAAmB;AASzB,MAAM,oBAAoB;AAO1B,MAAM,oBACX;AAUK,MAAM,yBAAyB;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/popover/popover.variants.ts"],"sourcesContent":["import { focusRing } from \"../../lib/focus-ring\";\n// Popover is a small NON-MODAL surface a trigger opens next to itself to hold secondary content —\n// a short form, a definition, a few related controls (spec §1). It is a NEUTRAL surface (spec §3):\n// the panel, the arrow, and the border are neutral, and the brand violet NEVER tints the panel to\n// look \"premium.\" A status color NEVER paints the panel — a verified result is reported by a\n// VerifiedBadge placed INSIDE the body, not by coloring the popover. So NOTHING in this file binds a\n// --color-status-* token or the brand action-primary tier; color enters only through the components\n// the caller places inside it (brand != state, G-U2). This is the ONLY token-binding site (skill §5\n// hard rule). All open/close motion is the FAST token transition on the verdify easing, instant\n// under reduced motion — never the 350ms VerifiedBadge-only theatre duration (G-U3).\n\n// The trigger: the one stop in the page tab order for this control (spec §2 trigger, §4 Focus). A\n// NEUTRAL ghost surface — the label/glyph in the ghost action fg at rest (spec §5\n// --color-action-ghost-fg), the restrained ghost hover fill (spec §5 --color-action-ghost-bg-hover),\n// the md radius, the persistent 2px focus ring (never removed, spec §4 Focus), and the target-size\n// floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the height EMERGING from the floor.\n// A disabled trigger dims via the disabled TOKEN (DEC-C), never a blanket opacity. fast functional\n// hover motion + verdify easing, instant under reduced motion (G-U3). This styles the DEFAULT\n// (non-asChild) trigger; when a Button is passed via `asChild` it carries its own treatment.\nexport const popoverTriggerClass =\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-3) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-[color,background-color] 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 \"disabled:pointer-events-none disabled:text-text-disabled\";\n\n// The panel (spec §2 panel, §5): the floating surface that opens on activation, raised above the\n// page and anchored to the trigger; it repositions to stay in the viewport (Radix). A NEUTRAL raised\n// surface (spec §5 --color-surface-raised) with the outer surface border (spec §5\n// --color-surface-border), the md corner radius (spec §5 --radius-md), and the md elevation shadow\n// above the page (spec §5 --shadow-md), on the POPOVER z-layer (a popover is a non-modal popover\n// layer, NOT the modal layer — there is no scrim, the page behind stays live, spec §1/§7). It NEVER\n// wears a brand or status fill (spec §3/§8). Inset padding + content gaps from --space-*; a column so\n// the header, body, and footer stack. The open/close fade is a PLAIN fast transition + verdify\n// easing, instant under reduced motion — never the 350ms VerifiedBadge-only theatre (spec §4, G-U3).\n// Enter/exit ride Radix's data-state on the content (attribute-selector variants, not arbitrary\n// values). The panel is focusable (Radix sets tabIndex=-1) so focus can land on it when the body has\n// no focusable control; its ring is never removed (spec §7 focus management).\nexport const popoverPanelClass =\n \"z-(--z-index-popover) flex flex-col gap-(--space-3) \" +\n \"max-w-(--container-sm) p-(--space-4) \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-md) shadow-(--shadow-md) \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n focusRing;\n\n// The header (spec §2 header): the top region holding a short title and an optional close control on\n// the inline-end. Logical-property layout (G-U6) so it mirrors under RTL.\nexport const popoverHeaderClass =\n \"flex items-start justify-between gap-(--space-3)\";\n\n// The title (spec §2 header title, §5): names the panel as a statement, sentence case. The h3 type\n// role in the PRIMARY text color (spec §5 --text-h3 / --color-text-primary). When present, it is the\n// panel's accessible name, wired via aria-labelledby (the panel takes role=\"dialog\", spec §7).\nexport const popoverTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2 body, §5): the content region — text, a short form, or a few controls. The body\n// type role; supporting text in the SECONDARY text color (spec §5 --text-body / --color-text-secondary).\n// Content that needs to scroll usually belongs in a Sheet (spec §2), so the body does not own a scroll.\nexport const popoverBodyClass = \"text-body text-text-secondary\";\n\n// The arrow (spec §2 arrow): a small DECORATIVE pointer joining the panel to its trigger, carrying no\n// meaning of its own (Radix renders it inside an aria-hidden wrapper). It is filled with the SAME\n// neutral raised surface as the panel so it reads as part of the surface, never a brand or status\n// fill (spec §3/§5). `fill-*` is the SVG fill utility for the Radix arrow polygon. (Radix's bare\n// arrow does not carry the panel's outer BORDER edge — a conformant, non-load-bearing deviation from\n// the §5 \"arrow edge\" border, flagged for amendment rather than hand-rolling a bordered polygon, the\n// same deviation the Tooltip arrow pins.)\nexport const popoverArrowClass = \"fill-surface-raised\";\n\n// The close control (spec §2 close, §5): the in-panel dismiss button. A NEUTRAL ghost surface — the\n// glyph in --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 below it.\n// fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const popoverCloseClass =\n \"inline-flex 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 close glyph: a neutral X, --size-icon-md, drawn with currentColor so it inherits the close\n// button's ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const popoverCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n"],"mappings":"AAAA,SAAS,iBAAiB;AAmBnB,MAAM,sBACX,kZAMA,YAAY;AAeP,MAAM,oBACX,2XAMA;AAIK,MAAM,qBACX;AAKK,MAAM,oBAAoB;AAK1B,MAAM,mBAAmB;AASzB,MAAM,oBAAoB;AAO1B,MAAM,oBACX,yXAMA;AAIK,MAAM,yBAAyB;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"radio.d.ts","sourceRoot":"","sources":["../../../src/components/radio/radio.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAQL,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAoB1B,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,EAC5D,sBAAsB;IACxB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,UAAU,wFAuFtB,CAAC;AAEF,MAAM,WAAW,UACf,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACpF,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC/B;AAED,eAAO,MAAM,KAAK,qFAuGjB,CAAC"}
1
+ {"version":3,"file":"radio.d.ts","sourceRoot":"","sources":["../../../src/components/radio/radio.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAQL,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAoB1B,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,EAC5D,sBAAsB;IACxB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,UAAU,wFAuFtB,CAAC;AAEF,MAAM,WAAW,UACf,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACpF,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC/B;AAED,eAAO,MAAM,KAAK,qFAuGjB,CAAC"}
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../lib/cn";
5
+ import { focusRing } from "../../lib/focus-ring";
5
6
  import {
6
7
  radioControlVariants,
7
8
  radioDotVariants,
@@ -140,7 +141,7 @@ const Radio = React.forwardRef(
140
141
  onKeyDown,
141
142
  className: cn(
142
143
  "peer sr-only",
143
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2"
144
+ focusRing
144
145
  ),
145
146
  ...props
146
147
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\nimport {\n radioControlVariants,\n radioDotVariants,\n radioTargetVariants,\n radioLabelVariants,\n radioDescriptionVariants,\n radioCardVariants,\n radioGroupVariants,\n type RadioGroupVariantProps,\n} from \"./radio.variants\";\n\ninterface RadioGroupContextValue {\n name: string;\n value: string | undefined;\n variant: NonNullable<RadioGroupVariantProps[\"variant\"]>;\n select: (value: string) => void;\n register: (value: string, el: HTMLInputElement | null, disabled: boolean) => void;\n onArrow: (current: string, dir: 1 | -1) => void;\n rovingValue: string | undefined; // which option owns tabindex=0\n}\n\nconst RadioGroupContext = React.createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroup(): RadioGroupContextValue {\n const ctx = React.useContext(RadioGroupContext);\n if (!ctx) throw new Error(\"<Radio> must be used inside <RadioGroup>\");\n return ctx;\n}\n\nexport interface RadioGroupProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n RadioGroupVariantProps {\n /** Shared `name` for the native radios in this group. */\n name: string;\n /** The group label — the question the options answer. */\n label: React.ReactNode;\n /** Uncontrolled initial selection. */\n defaultValue?: string;\n /** Controlled selection. */\n value?: string;\n /** Fires with the newly selected value. */\n onValueChange?: (value: string) => void;\n}\n\nexport const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(\n function RadioGroup(\n { className, name, label, defaultValue, value: controlled, onValueChange,\n variant = \"default\", children, ...props },\n ref,\n ) {\n const labelId = React.useId();\n const [uncontrolled, setUncontrolled] = React.useState(defaultValue);\n const value = controlled ?? uncontrolled;\n // insertion-ordered registry of options: value → { el, disabled }\n const items = React.useRef<Map<string, { el: HTMLInputElement | null; disabled: boolean }>>(\n new Map(),\n );\n\n const select = React.useCallback(\n (next: string) => {\n if (controlled === undefined) setUncontrolled(next);\n onValueChange?.(next);\n },\n [controlled, onValueChange],\n );\n\n const register = React.useCallback(\n (v: string, el: HTMLInputElement | null, disabled: boolean) => {\n if (el) items.current.set(v, { el, disabled });\n else items.current.delete(v);\n },\n [],\n );\n\n // Arrow navigation: move to next/prev ENABLED option, wrapping, then select + focus.\n // Focus is a post-commit concern, so it legitimately reads the ref Map for the\n // live DOM node; only the render-time roving fallback (below) avoids that Map.\n const onArrow = React.useCallback(\n (current: string, dir: 1 | -1) => {\n const entries = [...items.current.entries()];\n const enabled = entries.filter(([, m]) => !m.disabled);\n if (enabled.length === 0) return;\n const idx = enabled.findIndex(([v]) => v === current);\n const nextIdx = (idx + dir + enabled.length) % enabled.length;\n const [nextValue, nextMeta] = enabled[nextIdx];\n select(nextValue);\n nextMeta.el?.focus();\n },\n [select],\n );\n\n // Roving tabindex owner: the selected option, else the first ENABLED option.\n // Computed from React.Children DURING RENDER, not from the post-commit `items`\n // ref Map — children register via useEffect AFTER the group commits, which would\n // not trigger a recompute, leaving no tab stop when nothing is selected. Reading\n // children at render time recomputes on every render and keeps the defaultValue\n // path working (a real `value` always wins).\n const firstEnabledValue = React.useMemo(() => {\n let first: string | undefined;\n React.Children.forEach(children, (child) => {\n if (first !== undefined) return;\n if (!React.isValidElement<RadioProps>(child)) return;\n if (child.props.disabled) return;\n first = child.props.value;\n });\n return first;\n }, [children]);\n const rovingValue = value ?? firstEnabledValue;\n\n const ctx = React.useMemo<RadioGroupContextValue>(\n () => ({ name, value, variant: variant ?? \"default\", select, register, onArrow, rovingValue }),\n [name, value, variant, select, register, onArrow, rovingValue],\n );\n\n return (\n <RadioGroupContext.Provider value={ctx}>\n <div\n ref={ref}\n role=\"radiogroup\"\n aria-labelledby={labelId}\n className={cn(radioGroupVariants({ variant }), className)}\n {...props}\n >\n <span id={labelId} className=\"text-label text-text-primary\">\n {label}\n </span>\n {children}\n </div>\n </RadioGroupContext.Provider>\n );\n },\n);\n\nexport interface RadioProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"type\" | \"name\" | \"value\"> {\n /** This option's value within the group. */\n value: string;\n /** Secondary text under the option label (With-description variant). */\n description?: React.ReactNode;\n}\n\nexport const Radio = React.forwardRef<HTMLInputElement, RadioProps>(\n function Radio({ className, value, description, disabled = false, children, ...props }, ref) {\n const ctx = useRadioGroup();\n const inputRef = React.useRef<HTMLInputElement | null>(null);\n const descId = React.useId();\n const checked = ctx.value === value;\n const tabbable = ctx.rovingValue === value;\n\n // Register this option with the group so roving + arrow nav can see it.\n React.useEffect(() => {\n ctx.register(value, inputRef.current, disabled);\n return () => ctx.register(value, null, disabled);\n }, [ctx, value, disabled]);\n\n const setRefs = (el: HTMLInputElement | null) => {\n inputRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = el;\n };\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n ctx.onArrow(value, 1);\n } else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n ctx.onArrow(value, -1);\n } else if (e.key === \" \") {\n e.preventDefault();\n if (!checked) ctx.select(value);\n }\n };\n\n // The naming <label> wraps ONLY the input, the control, and the option-label\n // text — never the description. A native <label> contributes its text content to\n // the input's accessible name, so a nested description would pollute the name\n // (spec §7: the name comes from the associated label). The description sits\n // OUTSIDE the label and is linked via aria-describedby (mirrors Checkbox/Select).\n const row = (\n <label className=\"flex items-start gap-2\">\n {/* native radio is the peer; visually hidden but in the a11y tree */}\n <input\n ref={setRefs}\n type=\"radio\"\n name={ctx.name}\n value={value}\n checked={checked}\n disabled={disabled}\n aria-checked={checked}\n aria-disabled={disabled || undefined}\n aria-describedby={description ? descId : undefined}\n tabIndex={tabbable ? 0 : -1}\n onChange={() => ctx.select(value)}\n onKeyDown={onKeyDown}\n className={cn(\n \"peer sr-only\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n )}\n {...props}\n />\n <span\n aria-hidden=\"true\"\n data-testid={`radio-control-${value}`}\n className={cn(radioControlVariants())}\n >\n {/* the dot is a child of the control, not a sibling of the peer input,\n so its visibility is driven by the explicit `selected` variant. */}\n <span\n data-testid={`radio-dot-${value}`}\n className={cn(radioDotVariants({ selected: checked, disabled }))}\n />\n </span>\n {/* disabled colour comes from the explicit cva variant (the label-text span\n is not a sibling of the peer input, so peer-disabled cannot reach it). */}\n <span className={cn(radioLabelVariants({ disabled }))}>{children}</span>\n </label>\n );\n\n // The option container the test queries: it carries the target-size floor once\n // and stacks the naming row above an optional description.\n const option = (\n <div\n data-testid={`radio-target-${value}`}\n className={cn(radioTargetVariants(), \"flex-col\", className)}\n >\n {row}\n {description ? (\n <span id={descId} className={cn(radioDescriptionVariants())}>\n {description}\n </span>\n ) : null}\n </div>\n );\n\n return ctx.variant === \"card\" ? (\n <span data-testid={`radio-card-${value}`} className={cn(radioCardVariants())}>\n {option}\n </span>\n ) : (\n option\n );\n },\n);\n"],"mappings":"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","names":["RadioGroup","Radio"]}
1
+ {"version":3,"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\nimport { focusRing } from \"../../lib/focus-ring\";\nimport {\n radioControlVariants,\n radioDotVariants,\n radioTargetVariants,\n radioLabelVariants,\n radioDescriptionVariants,\n radioCardVariants,\n radioGroupVariants,\n type RadioGroupVariantProps,\n} from \"./radio.variants\";\n\ninterface RadioGroupContextValue {\n name: string;\n value: string | undefined;\n variant: NonNullable<RadioGroupVariantProps[\"variant\"]>;\n select: (value: string) => void;\n register: (value: string, el: HTMLInputElement | null, disabled: boolean) => void;\n onArrow: (current: string, dir: 1 | -1) => void;\n rovingValue: string | undefined; // which option owns tabindex=0\n}\n\nconst RadioGroupContext = React.createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroup(): RadioGroupContextValue {\n const ctx = React.useContext(RadioGroupContext);\n if (!ctx) throw new Error(\"<Radio> must be used inside <RadioGroup>\");\n return ctx;\n}\n\nexport interface RadioGroupProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n RadioGroupVariantProps {\n /** Shared `name` for the native radios in this group. */\n name: string;\n /** The group label — the question the options answer. */\n label: React.ReactNode;\n /** Uncontrolled initial selection. */\n defaultValue?: string;\n /** Controlled selection. */\n value?: string;\n /** Fires with the newly selected value. */\n onValueChange?: (value: string) => void;\n}\n\nexport const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(\n function RadioGroup(\n { className, name, label, defaultValue, value: controlled, onValueChange,\n variant = \"default\", children, ...props },\n ref,\n ) {\n const labelId = React.useId();\n const [uncontrolled, setUncontrolled] = React.useState(defaultValue);\n const value = controlled ?? uncontrolled;\n // insertion-ordered registry of options: value → { el, disabled }\n const items = React.useRef<Map<string, { el: HTMLInputElement | null; disabled: boolean }>>(\n new Map(),\n );\n\n const select = React.useCallback(\n (next: string) => {\n if (controlled === undefined) setUncontrolled(next);\n onValueChange?.(next);\n },\n [controlled, onValueChange],\n );\n\n const register = React.useCallback(\n (v: string, el: HTMLInputElement | null, disabled: boolean) => {\n if (el) items.current.set(v, { el, disabled });\n else items.current.delete(v);\n },\n [],\n );\n\n // Arrow navigation: move to next/prev ENABLED option, wrapping, then select + focus.\n // Focus is a post-commit concern, so it legitimately reads the ref Map for the\n // live DOM node; only the render-time roving fallback (below) avoids that Map.\n const onArrow = React.useCallback(\n (current: string, dir: 1 | -1) => {\n const entries = [...items.current.entries()];\n const enabled = entries.filter(([, m]) => !m.disabled);\n if (enabled.length === 0) return;\n const idx = enabled.findIndex(([v]) => v === current);\n const nextIdx = (idx + dir + enabled.length) % enabled.length;\n const [nextValue, nextMeta] = enabled[nextIdx];\n select(nextValue);\n nextMeta.el?.focus();\n },\n [select],\n );\n\n // Roving tabindex owner: the selected option, else the first ENABLED option.\n // Computed from React.Children DURING RENDER, not from the post-commit `items`\n // ref Map — children register via useEffect AFTER the group commits, which would\n // not trigger a recompute, leaving no tab stop when nothing is selected. Reading\n // children at render time recomputes on every render and keeps the defaultValue\n // path working (a real `value` always wins).\n const firstEnabledValue = React.useMemo(() => {\n let first: string | undefined;\n React.Children.forEach(children, (child) => {\n if (first !== undefined) return;\n if (!React.isValidElement<RadioProps>(child)) return;\n if (child.props.disabled) return;\n first = child.props.value;\n });\n return first;\n }, [children]);\n const rovingValue = value ?? firstEnabledValue;\n\n const ctx = React.useMemo<RadioGroupContextValue>(\n () => ({ name, value, variant: variant ?? \"default\", select, register, onArrow, rovingValue }),\n [name, value, variant, select, register, onArrow, rovingValue],\n );\n\n return (\n <RadioGroupContext.Provider value={ctx}>\n <div\n ref={ref}\n role=\"radiogroup\"\n aria-labelledby={labelId}\n className={cn(radioGroupVariants({ variant }), className)}\n {...props}\n >\n <span id={labelId} className=\"text-label text-text-primary\">\n {label}\n </span>\n {children}\n </div>\n </RadioGroupContext.Provider>\n );\n },\n);\n\nexport interface RadioProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"type\" | \"name\" | \"value\"> {\n /** This option's value within the group. */\n value: string;\n /** Secondary text under the option label (With-description variant). */\n description?: React.ReactNode;\n}\n\nexport const Radio = React.forwardRef<HTMLInputElement, RadioProps>(\n function Radio({ className, value, description, disabled = false, children, ...props }, ref) {\n const ctx = useRadioGroup();\n const inputRef = React.useRef<HTMLInputElement | null>(null);\n const descId = React.useId();\n const checked = ctx.value === value;\n const tabbable = ctx.rovingValue === value;\n\n // Register this option with the group so roving + arrow nav can see it.\n React.useEffect(() => {\n ctx.register(value, inputRef.current, disabled);\n return () => ctx.register(value, null, disabled);\n }, [ctx, value, disabled]);\n\n const setRefs = (el: HTMLInputElement | null) => {\n inputRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = el;\n };\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n ctx.onArrow(value, 1);\n } else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n ctx.onArrow(value, -1);\n } else if (e.key === \" \") {\n e.preventDefault();\n if (!checked) ctx.select(value);\n }\n };\n\n // The naming <label> wraps ONLY the input, the control, and the option-label\n // text — never the description. A native <label> contributes its text content to\n // the input's accessible name, so a nested description would pollute the name\n // (spec §7: the name comes from the associated label). The description sits\n // OUTSIDE the label and is linked via aria-describedby (mirrors Checkbox/Select).\n const row = (\n <label className=\"flex items-start gap-2\">\n {/* native radio is the peer; visually hidden but in the a11y tree */}\n <input\n ref={setRefs}\n type=\"radio\"\n name={ctx.name}\n value={value}\n checked={checked}\n disabled={disabled}\n aria-checked={checked}\n aria-disabled={disabled || undefined}\n aria-describedby={description ? descId : undefined}\n tabIndex={tabbable ? 0 : -1}\n onChange={() => ctx.select(value)}\n onKeyDown={onKeyDown}\n className={cn(\n \"peer sr-only\",\n focusRing,\n )}\n {...props}\n />\n <span\n aria-hidden=\"true\"\n data-testid={`radio-control-${value}`}\n className={cn(radioControlVariants())}\n >\n {/* the dot is a child of the control, not a sibling of the peer input,\n so its visibility is driven by the explicit `selected` variant. */}\n <span\n data-testid={`radio-dot-${value}`}\n className={cn(radioDotVariants({ selected: checked, disabled }))}\n />\n </span>\n {/* disabled colour comes from the explicit cva variant (the label-text span\n is not a sibling of the peer input, so peer-disabled cannot reach it). */}\n <span className={cn(radioLabelVariants({ disabled }))}>{children}</span>\n </label>\n );\n\n // The option container the test queries: it carries the target-size floor once\n // and stacks the naming row above an optional description.\n const option = (\n <div\n data-testid={`radio-target-${value}`}\n className={cn(radioTargetVariants(), \"flex-col\", className)}\n >\n {row}\n {description ? (\n <span id={descId} className={cn(radioDescriptionVariants())}>\n {description}\n </span>\n ) : null}\n </div>\n );\n\n return ctx.variant === \"card\" ? (\n <span data-testid={`radio-card-${value}`} className={cn(radioCardVariants())}>\n {option}\n </span>\n ) : (\n option\n );\n },\n);\n"],"mappings":"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","names":["RadioGroup","Radio"]}
@@ -9,7 +9,7 @@ export declare const optionVariants: (props?: ({
9
9
  size?: "md" | "sm" | "lg" | null | undefined;
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
11
  export declare const checkClass = "absolute end-2 inline-flex text-text-primary";
12
- export declare const groupLabelClass = "px-3 py-1 text-label text-text-muted select-none";
12
+ export declare const groupLabelClass = "px-3 py-1 text-label text-text-secondary select-none";
13
13
  export declare const separatorClass = "my-1 h-px bg-border-default";
14
14
  export declare const errorTextClass = "mt-1 text-label text-status-critical-on-surface";
15
15
  export declare const descriptionClass = "mt-1 text-caption text-text-secondary";
@@ -1 +1 @@
1
- {"version":3,"file":"select.variants.d.ts","sourceRoot":"","sources":["../../../src/components/select/select.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAIlE,eAAO,MAAM,eAAe;;;8EAqD3B,CAAC;AAGF,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAG3D,eAAO,MAAM,YAAY,QAKd,CAAC;AAYZ,eAAO,MAAM,cAAc;;8EAoB1B,CAAC;AAGF,eAAO,MAAM,UAAU,iDAAiD,CAAC;AAGzE,eAAO,MAAM,eAAe,qDAAqD,CAAC;AAGlF,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAG5D,eAAO,MAAM,cAAc,oDAAoD,CAAC;AAIhF,eAAO,MAAM,gBAAgB,0CAA0C,CAAC;AAExE,eAAO,MAAM,UAAU,iCAAiC,CAAC;AAEzD,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC"}
1
+ {"version":3,"file":"select.variants.d.ts","sourceRoot":"","sources":["../../../src/components/select/select.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAKlE,eAAO,MAAM,eAAe;;;8EAqD3B,CAAC;AAGF,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAG3D,eAAO,MAAM,YAAY,QAKd,CAAC;AAYZ,eAAO,MAAM,cAAc;;8EAoB1B,CAAC;AAGF,eAAO,MAAM,UAAU,iDAAiD,CAAC;AAIzE,eAAO,MAAM,eAAe,yDAAyD,CAAC;AAGtF,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAG5D,eAAO,MAAM,cAAc,oDAAoD,CAAC;AAIhF,eAAO,MAAM,gBAAgB,0CAA0C,CAAC;AAExE,eAAO,MAAM,UAAU,iCAAiC,CAAC;AAEzD,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { focusRing } from "../../lib/focus-ring";
2
3
  const triggerVariants = cva(
3
4
  [
4
5
  "inline-flex items-center justify-between gap-2 rounded-(--radius-md) px-3",
@@ -18,7 +19,7 @@ const triggerVariants = cva(
18
19
  "min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)",
19
20
  // visible 2px signal-blue ring at 2px offset; persists while the listbox is open
20
21
  "outline-none",
21
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2",
22
+ focusRing,
22
23
  "data-[state=open]:ring-2 data-[state=open]:ring-border-focus data-[state=open]:ring-offset-2",
23
24
  // error: strong border treatment, driven by aria-invalid
24
25
  "aria-invalid:border-border-strong",
@@ -82,7 +83,7 @@ const optionVariants = cva(
82
83
  }
83
84
  );
84
85
  const checkClass = "absolute end-2 inline-flex text-text-primary";
85
- const groupLabelClass = "px-3 py-1 text-label text-text-muted select-none";
86
+ const groupLabelClass = "px-3 py-1 text-label text-text-secondary select-none";
86
87
  const separatorClass = "my-1 h-px bg-border-default";
87
88
  const errorTextClass = "mt-1 text-label text-status-critical-on-surface";
88
89
  const descriptionClass = "mt-1 text-caption text-text-secondary";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/select/select.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Trigger: control-* tier surface, secondary hover fill, focus ring that persists\n// while open, strong border in error, target-size floor, base+verdify motion.\nexport const triggerVariants = cva(\n [\n \"inline-flex items-center justify-between gap-2 rounded-(--radius-md) px-3\",\n \"bg-control-bg text-control-fg border border-control-border\",\n \"hover:bg-action-secondary-bg-hover cursor-pointer select-none\",\n // DEC-A — the trigger is a form field, so its value SIZE is text-base (16px): the\n // iOS no-zoom reset is a hard floor that holds on EVERY size. The brand type ROLE\n // (line-height + letter-spacing) rides along via the role-suffix vars set per size\n // below. text-body itself (a 15px font-size) is NEVER bound on the trigger — under\n // the role-aware cn it collapses against text-base, and 15px reintroduces the iOS\n // focus zoom the reset exists to prevent. So the font-size stays text-base across\n // sizes; only the leading/tracking role and the padding density shift.\n \"text-base\",\n // open/close transition — base duration + verdify easing, never deliberate theatre\n \"transition-colors duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch / 40px pointer\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset; persists while the listbox is open\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n \"data-[state=open]:ring-2 data-[state=open]:ring-border-focus data-[state=open]:ring-offset-2\",\n // error: strong border treatment, driven by aria-invalid\n \"aria-invalid:border-border-strong\",\n // disabled: out of tab order (Radix sets data-disabled), reduced emphasis\n \"data-[disabled]:pointer-events-none data-[disabled]:cursor-default\",\n \"disabled:text-text-disabled data-[disabled]:text-text-disabled\",\n ],\n {\n variants: {\n // DEC-B: @verdify/tokens exposes only target-size FLOORS (44px / 40px), no\n // height scale. Every size anchors the shared floor (min-h-, in the base) and\n // NEVER sets a fixed height below it (a11y). Each size is TYPE-ROLE + vertical\n // padding (density) ABOVE the floor; the resting height EMERGES from the padding\n // and grows monotonically sm <= md <= lg. Because the trigger is a form field\n // (DEC-A), the value font-SIZE is pinned to text-base in the base — so here the\n // type role shifts only through the brand line-height + letter-spacing role\n // suffix (NOT the font-size), tightening at sm (caption metrics) and loosening at\n // lg (body-lg metrics). The padding ladder is --space-1 (.25rem) <= --space-2\n // (.5rem) <= --space-3 (.75rem), all >= the floor. (An earlier build set fixed\n // h-(--size-target-*), which made lg SHORTER than sm/md on desktop — the inverse\n // of the requirement — and is removed.)\n size: {\n sm: \"leading-(--text-caption--line-height) tracking-(--text-caption--letter-spacing) py-(--space-1)\",\n md: \"leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) py-(--space-2)\",\n lg: \"leading-(--text-body-lg--line-height) tracking-(--text-body-lg--letter-spacing) py-(--space-3)\",\n },\n width: {\n auto: \"w-auto\",\n full: \"w-full\",\n },\n },\n defaultVariants: { size: \"md\", width: \"auto\" },\n },\n);\n\n// Placeholder text: de-emphasised, never the accessible name.\nexport const placeholderClass = \"text-control-placeholder\";\n\n// Listbox: raised surface, outer border, md elevation; opens with base motion only.\nexport const listboxClass = [\n \"z-50 overflow-hidden rounded-(--radius-md) p-1\",\n \"bg-surface-raised border border-surface-border shadow-(--shadow-md)\",\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:transition-none\",\n].join(\" \");\n\n// Option row: primary label, secondary hover/active fill, target-row floor.\n// DEC-B: the option row is parameterized by the SAME size the trigger is, so the\n// listbox density tracks the trigger. It anchors the shared target-row floor\n// (min-h-, in the base) and NEVER sets a fixed height below it; each size is\n// TYPE-ROLE + vertical padding (density) ABOVE the floor, the row height emerging\n// from the padding. Unlike the trigger, an option row is NOT a focused text field —\n// the iOS no-zoom reset does not apply — so here the type role shifts through the\n// actual font-SIZE role: caption (sm) < body (md) < body-lg (lg), paired with the\n// identical --space-1 <= --space-2 <= --space-3 padding ladder as the trigger. Both\n// type role and density therefore climb monotonically sm <= md <= lg, all >= floor.\nexport const optionVariants = cva(\n [\n \"relative flex items-center gap-2 rounded-(--radius-md) pe-8 ps-3\",\n \"text-text-primary outline-none cursor-pointer select-none\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // active (highlighted) option uses the secondary hover fill\n \"data-[highlighted]:bg-action-secondary-bg-hover data-[highlighted]:outline-none\",\n // disabled option: reduced emphasis, not operable\n \"data-[disabled]:text-text-disabled data-[disabled]:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"text-caption py-(--space-1)\",\n md: \"text-body py-(--space-2)\",\n lg: \"text-body-lg py-(--space-3)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The selected-option check — a NEUTRAL mark (text-text-primary), never a status color.\nexport const checkClass = \"absolute end-2 inline-flex text-text-primary\";\n\n// Group heading: non-selectable, muted.\nexport const groupLabelClass = \"px-3 py-1 text-label text-text-muted select-none\";\n\n// Listbox/option dividers.\nexport const separatorClass = \"my-1 h-px bg-border-default\";\n\n// Error-slot text — status critical foreground.\nexport const errorTextClass = \"mt-1 text-label text-status-critical-on-surface\";\n// Non-error helper text — secondary (informational; mirrors Input's help tone, text-caption per\n// the message scale, text-text-secondary rather than text-muted so it reads at the same legibility\n// floor as Input help text — both carry information the user needs to complete the field).\nexport const descriptionClass = \"mt-1 text-caption text-text-secondary\";\n// The visible, associated label.\nexport const labelClass = \"text-label text-text-primary\";\n\nexport type TriggerVariantProps = VariantProps<typeof triggerVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AAIhC,MAAM,kBAAkB;AAAA,EAC7B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAaR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,MAAM,OAAO,OAAO;AAAA,EAC/C;AACF;AAGO,MAAM,mBAAmB;AAGzB,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAYH,MAAM,iBAAiB;AAAA,EAC5B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,KAAK;AAAA,EAChC;AACF;AAGO,MAAM,aAAa;AAGnB,MAAM,kBAAkB;AAGxB,MAAM,iBAAiB;AAGvB,MAAM,iBAAiB;AAIvB,MAAM,mBAAmB;AAEzB,MAAM,aAAa;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/select/select.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"../../lib/focus-ring\";\n\n// Trigger: control-* tier surface, secondary hover fill, focus ring that persists\n// while open, strong border in error, target-size floor, base+verdify motion.\nexport const triggerVariants = cva(\n [\n \"inline-flex items-center justify-between gap-2 rounded-(--radius-md) px-3\",\n \"bg-control-bg text-control-fg border border-control-border\",\n \"hover:bg-action-secondary-bg-hover cursor-pointer select-none\",\n // DEC-A — the trigger is a form field, so its value SIZE is text-base (16px): the\n // iOS no-zoom reset is a hard floor that holds on EVERY size. The brand type ROLE\n // (line-height + letter-spacing) rides along via the role-suffix vars set per size\n // below. text-body itself (a 15px font-size) is NEVER bound on the trigger — under\n // the role-aware cn it collapses against text-base, and 15px reintroduces the iOS\n // focus zoom the reset exists to prevent. So the font-size stays text-base across\n // sizes; only the leading/tracking role and the padding density shift.\n \"text-base\",\n // open/close transition — base duration + verdify easing, never deliberate theatre\n \"transition-colors duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch / 40px pointer\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset; persists while the listbox is open\n \"outline-none\",\n focusRing,\n \"data-[state=open]:ring-2 data-[state=open]:ring-border-focus data-[state=open]:ring-offset-2\",\n // error: strong border treatment, driven by aria-invalid\n \"aria-invalid:border-border-strong\",\n // disabled: out of tab order (Radix sets data-disabled), reduced emphasis\n \"data-[disabled]:pointer-events-none data-[disabled]:cursor-default\",\n \"disabled:text-text-disabled data-[disabled]:text-text-disabled\",\n ],\n {\n variants: {\n // DEC-B: @verdify/tokens exposes only target-size FLOORS (44px / 40px), no\n // height scale. Every size anchors the shared floor (min-h-, in the base) and\n // NEVER sets a fixed height below it (a11y). Each size is TYPE-ROLE + vertical\n // padding (density) ABOVE the floor; the resting height EMERGES from the padding\n // and grows monotonically sm <= md <= lg. Because the trigger is a form field\n // (DEC-A), the value font-SIZE is pinned to text-base in the base — so here the\n // type role shifts only through the brand line-height + letter-spacing role\n // suffix (NOT the font-size), tightening at sm (caption metrics) and loosening at\n // lg (body-lg metrics). The padding ladder is --space-1 (.25rem) <= --space-2\n // (.5rem) <= --space-3 (.75rem), all >= the floor. (An earlier build set fixed\n // h-(--size-target-*), which made lg SHORTER than sm/md on desktop — the inverse\n // of the requirement — and is removed.)\n size: {\n sm: \"leading-(--text-caption--line-height) tracking-(--text-caption--letter-spacing) py-(--space-1)\",\n md: \"leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) py-(--space-2)\",\n lg: \"leading-(--text-body-lg--line-height) tracking-(--text-body-lg--letter-spacing) py-(--space-3)\",\n },\n width: {\n auto: \"w-auto\",\n full: \"w-full\",\n },\n },\n defaultVariants: { size: \"md\", width: \"auto\" },\n },\n);\n\n// Placeholder text: de-emphasised, never the accessible name.\nexport const placeholderClass = \"text-control-placeholder\";\n\n// Listbox: raised surface, outer border, md elevation; opens with base motion only.\nexport const listboxClass = [\n \"z-50 overflow-hidden rounded-(--radius-md) p-1\",\n \"bg-surface-raised border border-surface-border shadow-(--shadow-md)\",\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:transition-none\",\n].join(\" \");\n\n// Option row: primary label, secondary hover/active fill, target-row floor.\n// DEC-B: the option row is parameterized by the SAME size the trigger is, so the\n// listbox density tracks the trigger. It anchors the shared target-row floor\n// (min-h-, in the base) and NEVER sets a fixed height below it; each size is\n// TYPE-ROLE + vertical padding (density) ABOVE the floor, the row height emerging\n// from the padding. Unlike the trigger, an option row is NOT a focused text field —\n// the iOS no-zoom reset does not apply — so here the type role shifts through the\n// actual font-SIZE role: caption (sm) < body (md) < body-lg (lg), paired with the\n// identical --space-1 <= --space-2 <= --space-3 padding ladder as the trigger. Both\n// type role and density therefore climb monotonically sm <= md <= lg, all >= floor.\nexport const optionVariants = cva(\n [\n \"relative flex items-center gap-2 rounded-(--radius-md) pe-8 ps-3\",\n \"text-text-primary outline-none cursor-pointer select-none\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // active (highlighted) option uses the secondary hover fill\n \"data-[highlighted]:bg-action-secondary-bg-hover data-[highlighted]:outline-none\",\n // disabled option: reduced emphasis, not operable\n \"data-[disabled]:text-text-disabled data-[disabled]:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"text-caption py-(--space-1)\",\n md: \"text-body py-(--space-2)\",\n lg: \"text-body-lg py-(--space-3)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The selected-option check — a NEUTRAL mark (text-text-primary), never a status color.\nexport const checkClass = \"absolute end-2 inline-flex text-text-primary\";\n\n// Group heading: non-selectable, essential de-emphasized text — uses secondary (AA), not the\n// decorative-only muted role (accessibility.md).\nexport const groupLabelClass = \"px-3 py-1 text-label text-text-secondary select-none\";\n\n// Listbox/option dividers.\nexport const separatorClass = \"my-1 h-px bg-border-default\";\n\n// Error-slot text — status critical foreground.\nexport const errorTextClass = \"mt-1 text-label text-status-critical-on-surface\";\n// Non-error helper text — secondary (informational; mirrors Input's help tone, text-caption per\n// the message scale, text-text-secondary rather than text-muted so it reads at the same legibility\n// floor as Input help text — both carry information the user needs to complete the field).\nexport const descriptionClass = \"mt-1 text-caption text-text-secondary\";\n// The visible, associated label.\nexport const labelClass = \"text-label text-text-primary\";\n\nexport type TriggerVariantProps = VariantProps<typeof triggerVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AACvC,SAAS,iBAAiB;AAInB,MAAM,kBAAkB;AAAA,EAC7B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAaR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,MAAM,OAAO,OAAO;AAAA,EAC/C;AACF;AAGO,MAAM,mBAAmB;AAGzB,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAYH,MAAM,iBAAiB;AAAA,EAC5B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,KAAK;AAAA,EAChC;AACF;AAGO,MAAM,aAAa;AAInB,MAAM,kBAAkB;AAGxB,MAAM,iBAAiB;AAGvB,MAAM,iBAAiB;AAIvB,MAAM,mBAAmB;AAEzB,MAAM,aAAa;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet.variants.d.ts","sourceRoot":"","sources":["../../../src/components/sheet/sheet.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAclE,eAAO,MAAM,kBAAkB,oFAK7B,CAAC;AA0BH,eAAO,MAAM,kBAAkB;;;8EAyD9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,yGAC2E,CAAC;AAKzG,eAAO,MAAM,eAAe,8BAA8B,CAAC;AAI3D,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAMrE,eAAO,MAAM,cAAc,iEACqC,CAAC;AAMjE,eAAO,MAAM,gBAAgB,sGACwE,CAAC;AAQtG,eAAO,MAAM,kBAAkB,oFAa7B,CAAC;AAIH,eAAO,MAAM,oBAAoB,0CAA0C,CAAC;AAE5E,MAAM,MAAM,sBAAsB,GAAG,YAAY,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"sheet.variants.d.ts","sourceRoot":"","sources":["../../../src/components/sheet/sheet.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAelE,eAAO,MAAM,kBAAkB,oFAK7B,CAAC;AA0BH,eAAO,MAAM,kBAAkB;;;8EAyD9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,yGAC2E,CAAC;AAKzG,eAAO,MAAM,eAAe,8BAA8B,CAAC;AAI3D,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAMrE,eAAO,MAAM,cAAc,iEACqC,CAAC;AAMjE,eAAO,MAAM,gBAAgB,sGACwE,CAAC;AAQtG,eAAO,MAAM,kBAAkB,oFAa7B,CAAC;AAIH,eAAO,MAAM,oBAAoB,0CAA0C,CAAC;AAE5E,MAAM,MAAM,sBAAsB,GAAG,YAAY,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { focusRing } from "../../lib/focus-ring";
2
3
  const sheetScrimVariants = cva([
3
4
  "fixed inset-0 z-(--z-index-modal) bg-scrim-dark",
4
5
  "transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify)",
@@ -21,7 +22,7 @@ const sheetPanelVariants = cva(
21
22
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
22
23
  // the panel takes focus when there is no obvious first control; its ring is never removed
23
24
  "outline-none",
24
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2"
25
+ focusRing
25
26
  ],
26
27
  {
27
28
  variants: {
@@ -80,7 +81,7 @@ const sheetCloseVariants = cva([
80
81
  "sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop)",
81
82
  // visible 2px focus ring at 2px offset; never removed
82
83
  "outline-none",
83
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2"
84
+ focusRing
84
85
  ]);
85
86
  const sheetCloseGlyphClass = "h-(--size-icon-md) w-(--size-icon-md)";
86
87
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/sheet/sheet.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Sheet is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents that enter only through the components placed INSIDE the panel (a primary Button, a\n// VerifiedBadge), never on the PANEL or the SCRIM themselves. Restraint over volume — neutrals\n// carry the surface. So NOTHING in this file binds an --color-action-primary-* or --color-status-*\n// fill (brand != state, G-U2). This is the ONLY token-binding site (skill §5 hard rule).\n\n// The scrim: the dimming layer between the page and the panel; it signals the page behind is inert\n// and gives the focus trap a visible edge (spec §2 scrim, §5 --color-scrim-*). A neutral dim on the\n// modal z-layer, decorative (no role). The fade is a PLAIN base transition + verdify easing,\n// instant under reduced motion — never the 350ms VerifiedBadge-only theatre (G-U3 motion-theatre\n// gate). Enter/exit ride Radix's data-state on the overlay (attribute-selector variants, not\n// arbitrary values). On a light surface the dark scrim token applies (spec §5: scrim-dark).\nexport const sheetScrimVariants = 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 anchored to ONE viewport edge; 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 on its LEADING corners, the lg elevation shadow above the scrim, fixed on\n// the modal z-layer. It never fills the viewport (spec §3: lg never grows to full screen — a sheet\n// that fills the screen should be a route) and scrolls its BODY when content overflows (spec §4\n// scrolled) — the panel is a flex column; the SheetBody owns the scroll. Panel padding/gaps come\n// from --space-*.\n//\n// side (spec §3) = the docking edge AND the slide axis:\n// - inline-end (default) / inline-start: pinned to the full BLOCK extent (inset-y-0), sized by\n// WIDTH, sliding along the inline axis (translate-x). LTR right / left; mirrors under dir=rtl\n// because start/end are logical (G-U6).\n// - block-end: pinned to the full INLINE extent (inset-x-0), sized by HEIGHT, sliding up from the\n// bottom (translate-y). Reads as a bottom sheet on narrow/touch viewports.\n//\n// size (spec §3) = the CROSS-AXIS extent only — a WIDTH for an inline side, a HEIGHT for a block\n// side. md is the default. Resolved per-side by the compoundVariants below (an inline side maps\n// size -> w-(--container-*); a block side maps size -> h-(--container-*)). Bound to the shared\n// --container-* scale, the same scale Dialog caps its width with.\n//\n// The slide+fade open/close is the BASE duration + verdify easing, instant under reduced motion,\n// and rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). The closed translate is a per-side keyword utility\n// (translate-x-full / -translate-x-full / translate-y-full), set in the side variant.\nexport const sheetPanelVariants = cva(\n [\n \"fixed z-(--z-index-modal)\",\n // a flex column: header + footer stay pinned, the body scrolls within (spec §4 scrolled)\n \"flex flex-col gap-(--space-4)\",\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 slide+fade open/close + 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 — the fade is shared across sides (the per-side slide lives\n // in the side variant); attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\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 // side = the docking edge + slide axis (spec §3). Logical inset properties (G-U6): inset-y-0\n // / inset-x-0 pin the full extent along the docked edge; start-0 / end-0 / bottom-0 dock it.\n // The closed-state translate slides the panel fully off its own edge; open returns it to 0.\n side: {\n \"inline-end\": [\n \"inset-y-0 end-0 h-full\",\n \"data-[state=open]:translate-x-0 data-[state=closed]:translate-x-full\",\n ],\n \"inline-start\": [\n \"inset-y-0 start-0 h-full\",\n \"data-[state=open]:translate-x-0 data-[state=closed]:-translate-x-full\",\n ],\n \"block-end\": [\n \"inset-x-0 bottom-0 w-full\",\n \"data-[state=open]:translate-y-0 data-[state=closed]:translate-y-full\",\n ],\n },\n // size = cross-axis extent. The concrete axis (width vs height) is resolved per-side by the\n // compoundVariants below — this key only carries the default for type inference.\n size: {\n sm: \"\",\n md: \"\",\n lg: \"\",\n },\n },\n compoundVariants: [\n // inline sides: size -> WIDTH (the cross-axis of a side that pins the block extent)\n { side: [\"inline-end\", \"inline-start\"], size: \"sm\", class: \"w-(--container-sm)\" },\n { side: [\"inline-end\", \"inline-start\"], size: \"md\", class: \"w-(--container-md)\" },\n { side: [\"inline-end\", \"inline-start\"], size: \"lg\", class: \"w-(--container-lg)\" },\n // block side: size -> HEIGHT (the cross-axis of a side that pins the inline extent)\n { side: \"block-end\", size: \"sm\", class: \"h-(--container-sm)\" },\n { side: \"block-end\", size: \"md\", class: \"h-(--container-md)\" },\n { side: \"block-end\", size: \"lg\", class: \"h-(--container-lg)\" },\n ],\n defaultVariants: { side: \"inline-end\", size: \"md\" },\n },\n);\n\n// The header: the top region holding the title and the close button on the inline-end (spec §2\n// header). Logical-property layout (G-U6); a MUTED surface-border hairline divider under it (spec\n// §5 --color-surface-border-muted) — the panel's inner dividers are muted, distinct from its outer\n// surface-border edge.\nexport const sheetHeaderClass =\n \"flex items-start justify-between gap-(--space-4) border-b border-surface-border-muted pb-(--space-4)\";\n\n// The title: names the task as a statement, sentence case (spec §2). It IS the panel's accessible\n// name (Radix wires aria-labelledby). The h2 type role in primary text (spec §5 --text-h2 /\n// --color-text-primary).\nexport const sheetTitleClass = \"text-h2 text-text-primary\";\n\n// The description: optional supporting text under the title, associated with the panel for screen\n// readers (Radix wires aria-describedby). Body type role in secondary text (spec §5).\nexport const sheetDescriptionClass = \"text-body text-text-secondary\";\n\n// The body: the scrollable content region between header and footer (spec §2 body, §4 scrolled).\n// The panel is a fixed-height flex column; the body takes the remaining space and is the ONLY part\n// that scrolls — the header and footer stay pinned. Body text is the body type role in secondary\n// text (spec §5 --text-body / text-secondary).\nexport const sheetBodyClass =\n \"min-h-0 flex-1 overflow-y-auto text-body text-text-secondary\";\n\n// The footer: the optional action region (spec §2 footer). The primary action sits at the\n// inline-end with a Cancel beside it; the actions are Buttons — the sheet spec does not restate\n// their --color-action-* bindings (spec §5 note). A MUTED surface-border hairline divider above it\n// (spec §5). Logical-property layout (G-U6): actions flow inline-end with a gap.\nexport const sheetFooterClass =\n \"flex items-center justify-end gap-(--space-2) border-t border-surface-border-muted pt-(--space-4)\";\n\n// The close button: the dismiss control in the header, always present and reachable — a sheet is\n// never closable by the scrim alone (spec §2 close, §8). 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 below\n// it. fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const sheetCloseVariants = 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 button's\n// ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const sheetCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type SheetPanelVariantProps = VariantProps<typeof sheetPanelVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AAchC,MAAM,qBAAqB,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA0BM,MAAM,qBAAqB;AAAA,EAChC;AAAA,IACE;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA,MAIR,MAAM;AAAA,QACJ,cAAc;AAAA,UACZ;AAAA,UACA;AAAA,QACF;AAAA,QACA,gBAAgB;AAAA,UACd;AAAA,UACA;AAAA,QACF;AAAA,QACA,aAAa;AAAA,UACX;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA;AAAA;AAAA,MAGA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA;AAAA,MAEhB,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAChF,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAChF,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA;AAAA,MAEhF,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAC7D,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAC7D,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,IAC/D;AAAA,IACA,iBAAiB,EAAE,MAAM,cAAc,MAAM,KAAK;AAAA,EACpD;AACF;AAMO,MAAM,mBACX;AAKK,MAAM,kBAAkB;AAIxB,MAAM,wBAAwB;AAM9B,MAAM,iBACX;AAMK,MAAM,mBACX;AAQK,MAAM,qBAAqB,IAAI;AAAA,EACpC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF,CAAC;AAIM,MAAM,uBAAuB;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/sheet/sheet.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"../../lib/focus-ring\";\n\n// Sheet is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents that enter only through the components placed INSIDE the panel (a primary Button, a\n// VerifiedBadge), never on the PANEL or the SCRIM themselves. Restraint over volume — neutrals\n// carry the surface. So NOTHING in this file binds an --color-action-primary-* or --color-status-*\n// fill (brand != state, G-U2). This is the ONLY token-binding site (skill §5 hard rule).\n\n// The scrim: the dimming layer between the page and the panel; it signals the page behind is inert\n// and gives the focus trap a visible edge (spec §2 scrim, §5 --color-scrim-*). A neutral dim on the\n// modal z-layer, decorative (no role). The fade is a PLAIN base transition + verdify easing,\n// instant under reduced motion — never the 350ms VerifiedBadge-only theatre (G-U3 motion-theatre\n// gate). Enter/exit ride Radix's data-state on the overlay (attribute-selector variants, not\n// arbitrary values). On a light surface the dark scrim token applies (spec §5: scrim-dark).\nexport const sheetScrimVariants = 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 anchored to ONE viewport edge; 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 on its LEADING corners, the lg elevation shadow above the scrim, fixed on\n// the modal z-layer. It never fills the viewport (spec §3: lg never grows to full screen — a sheet\n// that fills the screen should be a route) and scrolls its BODY when content overflows (spec §4\n// scrolled) — the panel is a flex column; the SheetBody owns the scroll. Panel padding/gaps come\n// from --space-*.\n//\n// side (spec §3) = the docking edge AND the slide axis:\n// - inline-end (default) / inline-start: pinned to the full BLOCK extent (inset-y-0), sized by\n// WIDTH, sliding along the inline axis (translate-x). LTR right / left; mirrors under dir=rtl\n// because start/end are logical (G-U6).\n// - block-end: pinned to the full INLINE extent (inset-x-0), sized by HEIGHT, sliding up from the\n// bottom (translate-y). Reads as a bottom sheet on narrow/touch viewports.\n//\n// size (spec §3) = the CROSS-AXIS extent only — a WIDTH for an inline side, a HEIGHT for a block\n// side. md is the default. Resolved per-side by the compoundVariants below (an inline side maps\n// size -> w-(--container-*); a block side maps size -> h-(--container-*)). Bound to the shared\n// --container-* scale, the same scale Dialog caps its width with.\n//\n// The slide+fade open/close is the BASE duration + verdify easing, instant under reduced motion,\n// and rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). The closed translate is a per-side keyword utility\n// (translate-x-full / -translate-x-full / translate-y-full), set in the side variant.\nexport const sheetPanelVariants = cva(\n [\n \"fixed z-(--z-index-modal)\",\n // a flex column: header + footer stay pinned, the body scrolls within (spec §4 scrolled)\n \"flex flex-col gap-(--space-4)\",\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 slide+fade open/close + 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 — the fade is shared across sides (the per-side slide lives\n // in the side variant); attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\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 // side = the docking edge + slide axis (spec §3). Logical inset properties (G-U6): inset-y-0\n // / inset-x-0 pin the full extent along the docked edge; start-0 / end-0 / bottom-0 dock it.\n // The closed-state translate slides the panel fully off its own edge; open returns it to 0.\n side: {\n \"inline-end\": [\n \"inset-y-0 end-0 h-full\",\n \"data-[state=open]:translate-x-0 data-[state=closed]:translate-x-full\",\n ],\n \"inline-start\": [\n \"inset-y-0 start-0 h-full\",\n \"data-[state=open]:translate-x-0 data-[state=closed]:-translate-x-full\",\n ],\n \"block-end\": [\n \"inset-x-0 bottom-0 w-full\",\n \"data-[state=open]:translate-y-0 data-[state=closed]:translate-y-full\",\n ],\n },\n // size = cross-axis extent. The concrete axis (width vs height) is resolved per-side by the\n // compoundVariants below — this key only carries the default for type inference.\n size: {\n sm: \"\",\n md: \"\",\n lg: \"\",\n },\n },\n compoundVariants: [\n // inline sides: size -> WIDTH (the cross-axis of a side that pins the block extent)\n { side: [\"inline-end\", \"inline-start\"], size: \"sm\", class: \"w-(--container-sm)\" },\n { side: [\"inline-end\", \"inline-start\"], size: \"md\", class: \"w-(--container-md)\" },\n { side: [\"inline-end\", \"inline-start\"], size: \"lg\", class: \"w-(--container-lg)\" },\n // block side: size -> HEIGHT (the cross-axis of a side that pins the inline extent)\n { side: \"block-end\", size: \"sm\", class: \"h-(--container-sm)\" },\n { side: \"block-end\", size: \"md\", class: \"h-(--container-md)\" },\n { side: \"block-end\", size: \"lg\", class: \"h-(--container-lg)\" },\n ],\n defaultVariants: { side: \"inline-end\", size: \"md\" },\n },\n);\n\n// The header: the top region holding the title and the close button on the inline-end (spec §2\n// header). Logical-property layout (G-U6); a MUTED surface-border hairline divider under it (spec\n// §5 --color-surface-border-muted) — the panel's inner dividers are muted, distinct from its outer\n// surface-border edge.\nexport const sheetHeaderClass =\n \"flex items-start justify-between gap-(--space-4) border-b border-surface-border-muted pb-(--space-4)\";\n\n// The title: names the task as a statement, sentence case (spec §2). It IS the panel's accessible\n// name (Radix wires aria-labelledby). The h2 type role in primary text (spec §5 --text-h2 /\n// --color-text-primary).\nexport const sheetTitleClass = \"text-h2 text-text-primary\";\n\n// The description: optional supporting text under the title, associated with the panel for screen\n// readers (Radix wires aria-describedby). Body type role in secondary text (spec §5).\nexport const sheetDescriptionClass = \"text-body text-text-secondary\";\n\n// The body: the scrollable content region between header and footer (spec §2 body, §4 scrolled).\n// The panel is a fixed-height flex column; the body takes the remaining space and is the ONLY part\n// that scrolls — the header and footer stay pinned. Body text is the body type role in secondary\n// text (spec §5 --text-body / text-secondary).\nexport const sheetBodyClass =\n \"min-h-0 flex-1 overflow-y-auto text-body text-text-secondary\";\n\n// The footer: the optional action region (spec §2 footer). The primary action sits at the\n// inline-end with a Cancel beside it; the actions are Buttons — the sheet spec does not restate\n// their --color-action-* bindings (spec §5 note). A MUTED surface-border hairline divider above it\n// (spec §5). Logical-property layout (G-U6): actions flow inline-end with a gap.\nexport const sheetFooterClass =\n \"flex items-center justify-end gap-(--space-2) border-t border-surface-border-muted pt-(--space-4)\";\n\n// The close button: the dismiss control in the header, always present and reachable — a sheet is\n// never closable by the scrim alone (spec §2 close, §8). 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 below\n// it. fast functional hover motion + verdify easing, instant under reduced motion (G-U3).\nexport const sheetCloseVariants = 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 button's\n// ghost-fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nexport const sheetCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type SheetPanelVariantProps = VariantProps<typeof sheetPanelVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AACvC,SAAS,iBAAiB;AAcnB,MAAM,qBAAqB,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA0BM,MAAM,qBAAqB;AAAA,EAChC;AAAA,IACE;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA,MAIR,MAAM;AAAA,QACJ,cAAc;AAAA,UACZ;AAAA,UACA;AAAA,QACF;AAAA,QACA,gBAAgB;AAAA,UACd;AAAA,UACA;AAAA,QACF;AAAA,QACA,aAAa;AAAA,UACX;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA;AAAA;AAAA,MAGA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA;AAAA,MAEhB,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAChF,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAChF,EAAE,MAAM,CAAC,cAAc,cAAc,GAAG,MAAM,MAAM,OAAO,qBAAqB;AAAA;AAAA,MAEhF,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAC7D,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,MAC7D,EAAE,MAAM,aAAa,MAAM,MAAM,OAAO,qBAAqB;AAAA,IAC/D;AAAA,IACA,iBAAiB,EAAE,MAAM,cAAc,MAAM,KAAK;AAAA,EACpD;AACF;AAMO,MAAM,mBACX;AAKK,MAAM,kBAAkB;AAIxB,MAAM,wBAAwB;AAM9B,MAAM,iBACX;AAMK,MAAM,mBACX;AAQK,MAAM,qBAAqB,IAAI;AAAA,EACpC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF,CAAC;AAIM,MAAM,uBAAuB;","names":[]}
@@ -5,7 +5,7 @@ export declare const sidebarRailVariants: (props?: ({
5
5
  export type SidebarRailVariantProps = VariantProps<typeof sidebarRailVariants>;
6
6
  export declare const sidebarHeaderClass = "flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted";
7
7
  export declare const sidebarGroupClass = "flex flex-col gap-(--space-1) py-(--space-2)";
8
- export declare const sidebarGroupLabelClass = "px-(--space-2) py-(--space-1) text-caption text-text-muted select-none";
8
+ export declare const sidebarGroupLabelClass = "px-(--space-2) py-(--space-1) text-caption text-text-secondary select-none";
9
9
  export declare const sidebarListClass = "flex flex-col gap-(--space-1) m-0 p-0 list-none";
10
10
  export declare const sidebarItemVariants: (props?: ({} & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
11
  export type SidebarItemVariantProps = VariantProps<typeof sidebarItemVariants>;
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar.variants.d.ts","sourceRoot":"","sources":["../../../src/components/sidebar/sidebar.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAgBlE,eAAO,MAAM,mBAAmB;;8EAsB/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAK/E,eAAO,MAAM,kBAAkB,yGACyE,CAAC;AAIzG,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;AAIhF,eAAO,MAAM,sBAAsB,2EACuC,CAAC;AAI3E,eAAO,MAAM,gBAAgB,oDAAoD,CAAC;AAsBlF,eAAO,MAAM,mBAAmB,2FA8B/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAM/E,eAAO,MAAM,oBAAoB,gHAC8E,CAAC;AAKhH,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAI/D,eAAO,MAAM,wBAAwB,sEACgC,CAAC;AAItE,eAAO,MAAM,kBAAkB,8FAC8D,CAAC;AAM9F,eAAO,MAAM,0BAA0B,2FAatC,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG,YAAY,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAIhG,eAAO,MAAM,wBAAwB,oOAC8L,CAAC"}
1
+ {"version":3,"file":"sidebar.variants.d.ts","sourceRoot":"","sources":["../../../src/components/sidebar/sidebar.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAiBlE,eAAO,MAAM,mBAAmB;;8EAsB/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAK/E,eAAO,MAAM,kBAAkB,yGACyE,CAAC;AAIzG,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;AAKhF,eAAO,MAAM,sBAAsB,+EAC2C,CAAC;AAI/E,eAAO,MAAM,gBAAgB,oDAAoD,CAAC;AAsBlF,eAAO,MAAM,mBAAmB,2FA8B/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAM/E,eAAO,MAAM,oBAAoB,gHAC8E,CAAC;AAKhH,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAI/D,eAAO,MAAM,wBAAwB,sEACgC,CAAC;AAItE,eAAO,MAAM,kBAAkB,8FAC8D,CAAC;AAM9F,eAAO,MAAM,0BAA0B,2FAatC,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG,YAAY,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAIhG,eAAO,MAAM,wBAAwB,oOAC8L,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { focusRing } from "../../lib/focus-ring";
2
3
  const sidebarRailVariants = cva(
3
4
  [
4
5
  // surface + elevation + column layout with the rail insets
@@ -24,7 +25,7 @@ const sidebarRailVariants = cva(
24
25
  );
25
26
  const sidebarHeaderClass = "flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted";
26
27
  const sidebarGroupClass = "flex flex-col gap-(--space-1) py-(--space-2)";
27
- const sidebarGroupLabelClass = "px-(--space-2) py-(--space-1) text-caption text-text-muted select-none";
28
+ const sidebarGroupLabelClass = "px-(--space-2) py-(--space-1) text-caption text-text-secondary select-none";
28
29
  const sidebarListClass = "flex flex-col gap-(--space-1) m-0 p-0 list-none";
29
30
  const sidebarItemVariants = cva(
30
31
  [
@@ -50,7 +51,7 @@ const sidebarItemVariants = cva(
50
51
  "min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)",
51
52
  // focus ring — identical on every state, never removed; persists expanded and collapsed
52
53
  "outline-none",
53
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2",
54
+ focusRing,
54
55
  // disabled (non-operable) item — DEC-C: dim via the disabled TOKEN, never opacity. aria-disabled
55
56
  // drives it because an <a> has no native disabled; the component also strips href + tabindex.
56
57
  "aria-disabled:pointer-events-none aria-disabled:text-text-disabled"
@@ -70,7 +71,7 @@ const sidebarCollapseToggleClass = cva(
70
71
  "motion-reduce:duration-(--motion-duration-instant)",
71
72
  "min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)",
72
73
  "outline-none",
73
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2",
74
+ focusRing,
74
75
  "disabled:pointer-events-none disabled:text-text-disabled"
75
76
  ],
76
77
  { variants: {}, defaultVariants: {} }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/sidebar/sidebar.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Sidebar is the primary navigation rail down one edge of an app shell (spec §1). It is a\n// NEUTRAL surface (spec §3): the rail does not wear the brand. The only accent is on the CURRENT\n// item, which takes the primary ACTION (brand) alias — where you are — never a status color: a\n// current item reports your location, not a verification result, so it binds nothing from the\n// status tier (brand != state, G-U2). A verified meaning belongs to VerifiedBadge. The rail\n// paints from the surface, text, action(primary + ghost), and border aliases only (spec §5).\n\n// The nav landmark wrapping the rail. The raised neutral surface against the page canvas, an edge\n// against the content area (a logical inline-end border when docked inline-start, inline-start when\n// docked inline-end — G-U6, so it mirrors under dir=\"rtl\"), the sm elevation where it floats above\n// content, and the expand/collapse WIDTH transition. Motion is the BASE token transition on the\n// verdify easing, collapsing to the instant endpoint under reduced motion — never the 350ms\n// VerifiedBadge-only theatre duration (toggling the rail is a plain transition, not theatre — spec\n// §4 Collapsed, G-U3). The rail is NEVER tinted with the brand or a status fill (spec §3/§8).\nexport const sidebarRailVariants = cva(\n [\n // surface + elevation + column layout with the rail insets\n \"flex flex-col gap-(--space-2) bg-surface-raised shadow-sm\",\n \"p-(--space-2)\",\n // the expand/collapse width transition — base duration + verdify easing, instant under\n // reduced motion (NEVER the deliberate verified-check theatre, G-U3)\n \"transition-[width] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n // SIDE axis (spec §3): the rail docks at a logical edge and carries its border on the edge\n // that faces the content area — inline-end when docked inline-start, inline-start when docked\n // inline-end. Logical properties only (border-e / border-s), so it mirrors under dir=\"rtl\".\n side: {\n \"inline-start\": \"border-e border-surface-border\",\n \"inline-end\": \"border-s border-surface-border\",\n },\n },\n defaultVariants: { side: \"inline-start\" },\n },\n);\n\nexport type SidebarRailVariantProps = VariantProps<typeof sidebarRailVariants>;\n\n// The header slot (spec §2): the top slot for a product mark or workspace switcher. It is NOT a\n// navigation item and is skipped by item arrow movement. A separator from the list below it via a\n// muted hairline (spec §5, surface-border-muted). Plain structural slot — no variant axis.\nexport const sidebarHeaderClass =\n \"flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted\";\n\n// A labeled cluster of items (spec §2 group). Restraint over volume keeps the rail scannable, so a\n// group is plain structure; its dividers use the muted hairline (spec §5). No variant axis.\nexport const sidebarGroupClass = \"flex flex-col gap-(--space-1) py-(--space-2)\";\n\n// The group heading (spec §2/§5): the cluster label in the MUTED text color at the CAPTION type\n// role. Decorative-weight wayfinding, not a navigation item.\nexport const sidebarGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-caption text-text-muted select-none\";\n\n// The item list (the <ul>): a flush column of items. The list carries no text color — each item\n// sets its own. The roving arrow-key handler is wired by the component on this element.\nexport const sidebarListClass = \"flex flex-col gap-(--space-1) m-0 p-0 list-none\";\n\n// One navigation item (spec §2 item, §4 states). A native <a href> link.\n//\n// RESTING (default): the LABEL in the SECONDARY text color and the icon in the GHOST action fg, on\n// the rail surface with NO fill (spec §4 Default).\n// HOVER: a restrained ghost-action hover fill; the cursor is a pointer (spec §4 Hover). The only\n// fill a resting item ever paints.\n// CURRENT (aria-current=page): the leading INDICATOR BAR is painted in the primary ACTION (brand)\n// alias — where you are — and the label lifts to the PRIMARY text color. This spec offers two\n// indicator treatments (spec §5: an indicator \"bar OR fill\"); this component uses the restrained\n// BAR (a logical inline-start border accent) so the rail never wears a brand SURFACE (spec §3/§8).\n// The current state is never carried by color alone: the indicator bar shape AND aria-current also\n// encode it, so it survives a contrast or color-blind reading (spec §4 Current / 1.4.1, brand !=\n// state — NEVER status-verified).\n// FOCUS: the visible 2px focus ring, part of the base on every state, never removed; it persists in\n// both the expanded and collapsed rail (spec §4 Focus / 2.4.7).\n// DISABLED (aria-disabled): dims via the disabled TOKEN (DEC-C), never a blanket opacity; the\n// component also strips href + tabindex and skips it in arrow movement, while the label stays\n// readable to AT (spec §4 Disabled / §7).\n// Motion is the fast token transition on the verdify easing, instant under reduced motion — never\n// the 350ms VerifiedBadge-only theatre duration (G-U3).\nexport const sidebarItemVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL; the leading\n // indicator bar is a left/start accent rendered via a logical inline-start border that is\n // transparent at rest and painted in the action alias when current\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-3)\",\n \"border-s-2 border-transparent\",\n // type ROLE + resting label color, no fill, pointer cursor\n \"text-label text-text-secondary no-underline cursor-pointer select-none\",\n // hover: the restrained ghost-action fill (the only fill a resting item paints)\n \"hover:bg-action-ghost-bg-hover\",\n // CURRENT: the leading indicator BAR is painted in the brand action alias and the label lifts to\n // the primary text color. This is the action(primary) alias (where you are), NEVER status-verified\n // (brand != state, G-U2). The indicator bar SHAPE + aria-current carry the state alongside the\n // color, so it survives a color-blind read; the rail itself stays a neutral surface (no brand fill).\n \"aria-[current=page]:border-s-action-primary-bg aria-[current=page]:text-text-primary\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre, G-U3)\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer, on every item (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 ring — identical on every state, never removed; persists expanded and collapsed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled (non-operable) item — DEC-C: dim via the disabled TOKEN, never opacity. aria-disabled\n // drives it because an <a> has no native disabled; the component also strips href + tabindex.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarItemVariantProps = VariantProps<typeof sidebarItemVariants>;\n\n// The leading item icon (spec §5): the md icon role, decorative (the item names itself by its label\n// text, not the glyph). At rest it is the GHOST action fg; it inherits the disabled token when the\n// item is disabled (aria-disabled on the parent link). When the item is current it lifts with the\n// label to the primary text color. shrink-0 so it never collapses when labels are hidden.\nexport const sidebarItemIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-action-ghost-fg\";\n\n// The item label text (spec §5): the part hidden in the collapsed rail. When collapsed the label is\n// visually hidden but stays in the accessibility tree (sr-only), so the link keeps its accessible\n// name without depending on a tooltip being open (spec §4 Collapsed / §7).\nexport const sidebarItemLabelClass = \"min-w-0 flex-1 truncate\";\n\n// A trailing count or status on an item (spec §2): text or an aria-label, never color alone (spec\n// §7). Plain secondary caption text, hidden with the label when collapsed.\nexport const sidebarItemTrailingClass =\n \"ms-auto inline-flex items-center text-caption text-text-secondary\";\n\n// The footer slot (spec §2): a bottom slot for an account or settings entry; its items follow the\n// same item rules. Pushed to the bottom; separated from the list above by the muted hairline.\nexport const sidebarFooterClass =\n \"mt-auto flex flex-col gap-(--space-1) pt-(--space-2) border-t border-surface-border-muted\";\n\n// The collapse-toggle (spec §2/§4/§6/§7): a native <button>, NOT an item, reachable in the rail's\n// tab order. It carries the ghost-action glyph and the same neutral hover fill, focus ring, and\n// target-size floor as an item. Its glyph is decorative (aria-hidden); aria-expanded + aria-label\n// carry the action. Same fast motion, never the deliberate theatre.\nexport const sidebarCollapseToggleClass = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-action-ghost-fg cursor-pointer select-none\",\n \"hover:bg-action-ghost-bg-hover\",\n \"transition-[color,background-color] 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\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarCollapseToggleVariantProps = VariantProps<typeof sidebarCollapseToggleClass>;\n\n// The collapse-toggle glyph (spec §5): the md icon role, decorative. It rotates to mirror the rail\n// width (collapsed vs expanded) as a reinforcement; aria-expanded carries the state, not the glyph.\nexport const sidebarCollapseIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant)\";\n"],"mappings":"AAAA,SAAS,WAA8B;AAgBhC,MAAM,sBAAsB;AAAA,EACjC;AAAA;AAAA,IAEE;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA,MAIR,MAAM;AAAA,QACJ,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,eAAe;AAAA,EAC1C;AACF;AAOO,MAAM,qBACX;AAIK,MAAM,oBAAoB;AAI1B,MAAM,yBACX;AAIK,MAAM,mBAAmB;AAsBzB,MAAM,sBAAsB;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,IAIE;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA,EACF;AAAA,EACA,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAAC,EAAE;AACtC;AAQO,MAAM,uBACX;AAKK,MAAM,wBAAwB;AAI9B,MAAM,2BACX;AAIK,MAAM,qBACX;AAMK,MAAM,6BAA6B;AAAA,EACxC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAAC,EAAE;AACtC;AAMO,MAAM,2BACX;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/sidebar/sidebar.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"../../lib/focus-ring\";\n\n// A Sidebar is the primary navigation rail down one edge of an app shell (spec §1). It is a\n// NEUTRAL surface (spec §3): the rail does not wear the brand. The only accent is on the CURRENT\n// item, which takes the primary ACTION (brand) alias — where you are — never a status color: a\n// current item reports your location, not a verification result, so it binds nothing from the\n// status tier (brand != state, G-U2). A verified meaning belongs to VerifiedBadge. The rail\n// paints from the surface, text, action(primary + ghost), and border aliases only (spec §5).\n\n// The nav landmark wrapping the rail. The raised neutral surface against the page canvas, an edge\n// against the content area (a logical inline-end border when docked inline-start, inline-start when\n// docked inline-end — G-U6, so it mirrors under dir=\"rtl\"), the sm elevation where it floats above\n// content, and the expand/collapse WIDTH transition. Motion is the BASE token transition on the\n// verdify easing, collapsing to the instant endpoint under reduced motion — never the 350ms\n// VerifiedBadge-only theatre duration (toggling the rail is a plain transition, not theatre — spec\n// §4 Collapsed, G-U3). The rail is NEVER tinted with the brand or a status fill (spec §3/§8).\nexport const sidebarRailVariants = cva(\n [\n // surface + elevation + column layout with the rail insets\n \"flex flex-col gap-(--space-2) bg-surface-raised shadow-sm\",\n \"p-(--space-2)\",\n // the expand/collapse width transition — base duration + verdify easing, instant under\n // reduced motion (NEVER the deliberate verified-check theatre, G-U3)\n \"transition-[width] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n // SIDE axis (spec §3): the rail docks at a logical edge and carries its border on the edge\n // that faces the content area — inline-end when docked inline-start, inline-start when docked\n // inline-end. Logical properties only (border-e / border-s), so it mirrors under dir=\"rtl\".\n side: {\n \"inline-start\": \"border-e border-surface-border\",\n \"inline-end\": \"border-s border-surface-border\",\n },\n },\n defaultVariants: { side: \"inline-start\" },\n },\n);\n\nexport type SidebarRailVariantProps = VariantProps<typeof sidebarRailVariants>;\n\n// The header slot (spec §2): the top slot for a product mark or workspace switcher. It is NOT a\n// navigation item and is skipped by item arrow movement. A separator from the list below it via a\n// muted hairline (spec §5, surface-border-muted). Plain structural slot — no variant axis.\nexport const sidebarHeaderClass =\n \"flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted\";\n\n// A labeled cluster of items (spec §2 group). Restraint over volume keeps the rail scannable, so a\n// group is plain structure; its dividers use the muted hairline (spec §5). No variant axis.\nexport const sidebarGroupClass = \"flex flex-col gap-(--space-1) py-(--space-2)\";\n\n// The group heading (spec §2/§5): the cluster label in the SECONDARY text color at the CAPTION type\n// role. It is essential de-emphasized text — it names the cluster of items — so it uses\n// --color-text-secondary (AA), not the decorative-only muted role (accessibility.md).\nexport const sidebarGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-caption text-text-secondary select-none\";\n\n// The item list (the <ul>): a flush column of items. The list carries no text color — each item\n// sets its own. The roving arrow-key handler is wired by the component on this element.\nexport const sidebarListClass = \"flex flex-col gap-(--space-1) m-0 p-0 list-none\";\n\n// One navigation item (spec §2 item, §4 states). A native <a href> link.\n//\n// RESTING (default): the LABEL in the SECONDARY text color and the icon in the GHOST action fg, on\n// the rail surface with NO fill (spec §4 Default).\n// HOVER: a restrained ghost-action hover fill; the cursor is a pointer (spec §4 Hover). The only\n// fill a resting item ever paints.\n// CURRENT (aria-current=page): the leading INDICATOR BAR is painted in the primary ACTION (brand)\n// alias — where you are — and the label lifts to the PRIMARY text color. This spec offers two\n// indicator treatments (spec §5: an indicator \"bar OR fill\"); this component uses the restrained\n// BAR (a logical inline-start border accent) so the rail never wears a brand SURFACE (spec §3/§8).\n// The current state is never carried by color alone: the indicator bar shape AND aria-current also\n// encode it, so it survives a contrast or color-blind reading (spec §4 Current / 1.4.1, brand !=\n// state — NEVER status-verified).\n// FOCUS: the visible 2px focus ring, part of the base on every state, never removed; it persists in\n// both the expanded and collapsed rail (spec §4 Focus / 2.4.7).\n// DISABLED (aria-disabled): dims via the disabled TOKEN (DEC-C), never a blanket opacity; the\n// component also strips href + tabindex and skips it in arrow movement, while the label stays\n// readable to AT (spec §4 Disabled / §7).\n// Motion is the fast token transition on the verdify easing, instant under reduced motion — never\n// the 350ms VerifiedBadge-only theatre duration (G-U3).\nexport const sidebarItemVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL; the leading\n // indicator bar is a left/start accent rendered via a logical inline-start border that is\n // transparent at rest and painted in the action alias when current\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-3)\",\n \"border-s-2 border-transparent\",\n // type ROLE + resting label color, no fill, pointer cursor\n \"text-label text-text-secondary no-underline cursor-pointer select-none\",\n // hover: the restrained ghost-action fill (the only fill a resting item paints)\n \"hover:bg-action-ghost-bg-hover\",\n // CURRENT: the leading indicator BAR is painted in the brand action alias and the label lifts to\n // the primary text color. This is the action(primary) alias (where you are), NEVER status-verified\n // (brand != state, G-U2). The indicator bar SHAPE + aria-current carry the state alongside the\n // color, so it survives a color-blind read; the rail itself stays a neutral surface (no brand fill).\n \"aria-[current=page]:border-s-action-primary-bg aria-[current=page]:text-text-primary\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre, G-U3)\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer, on every item (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 ring — identical on every state, never removed; persists expanded and collapsed\n \"outline-none\",\n focusRing,\n // disabled (non-operable) item — DEC-C: dim via the disabled TOKEN, never opacity. aria-disabled\n // drives it because an <a> has no native disabled; the component also strips href + tabindex.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarItemVariantProps = VariantProps<typeof sidebarItemVariants>;\n\n// The leading item icon (spec §5): the md icon role, decorative (the item names itself by its label\n// text, not the glyph). At rest it is the GHOST action fg; it inherits the disabled token when the\n// item is disabled (aria-disabled on the parent link). When the item is current it lifts with the\n// label to the primary text color. shrink-0 so it never collapses when labels are hidden.\nexport const sidebarItemIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-action-ghost-fg\";\n\n// The item label text (spec §5): the part hidden in the collapsed rail. When collapsed the label is\n// visually hidden but stays in the accessibility tree (sr-only), so the link keeps its accessible\n// name without depending on a tooltip being open (spec §4 Collapsed / §7).\nexport const sidebarItemLabelClass = \"min-w-0 flex-1 truncate\";\n\n// A trailing count or status on an item (spec §2): text or an aria-label, never color alone (spec\n// §7). Plain secondary caption text, hidden with the label when collapsed.\nexport const sidebarItemTrailingClass =\n \"ms-auto inline-flex items-center text-caption text-text-secondary\";\n\n// The footer slot (spec §2): a bottom slot for an account or settings entry; its items follow the\n// same item rules. Pushed to the bottom; separated from the list above by the muted hairline.\nexport const sidebarFooterClass =\n \"mt-auto flex flex-col gap-(--space-1) pt-(--space-2) border-t border-surface-border-muted\";\n\n// The collapse-toggle (spec §2/§4/§6/§7): a native <button>, NOT an item, reachable in the rail's\n// tab order. It carries the ghost-action glyph and the same neutral hover fill, focus ring, and\n// target-size floor as an item. Its glyph is decorative (aria-hidden); aria-expanded + aria-label\n// carry the action. Same fast motion, never the deliberate theatre.\nexport const sidebarCollapseToggleClass = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-action-ghost-fg cursor-pointer select-none\",\n \"hover:bg-action-ghost-bg-hover\",\n \"transition-[color,background-color] 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\",\n focusRing,\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarCollapseToggleVariantProps = VariantProps<typeof sidebarCollapseToggleClass>;\n\n// The collapse-toggle glyph (spec §5): the md icon role, decorative. It rotates to mirror the rail\n// width (collapsed vs expanded) as a reinforcement; aria-expanded carries the state, not the glyph.\nexport const sidebarCollapseIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant)\";\n"],"mappings":"AAAA,SAAS,WAA8B;AACvC,SAAS,iBAAiB;AAgBnB,MAAM,sBAAsB;AAAA,EACjC;AAAA;AAAA,IAEE;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA;AAAA,MAIR,MAAM;AAAA,QACJ,gBAAgB;AAAA,QAChB,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,eAAe;AAAA,EAC1C;AACF;AAOO,MAAM,qBACX;AAIK,MAAM,oBAAoB;AAK1B,MAAM,yBACX;AAIK,MAAM,mBAAmB;AAsBzB,MAAM,sBAAsB;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,IAIE;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA,EACF;AAAA,EACA,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAAC,EAAE;AACtC;AAQO,MAAM,uBACX;AAKK,MAAM,wBAAwB;AAI9B,MAAM,2BACX;AAIK,MAAM,qBACX;AAMK,MAAM,6BAA6B;AAAA,EACxC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAAC,EAAE;AACtC;AAMO,MAAM,2BACX;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"switch.variants.d.ts","sourceRoot":"","sources":["../../../src/components/switch/switch.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AASlE,eAAO,MAAM,mBAAmB;;8EA8B/B,CAAC;AAUF,eAAO,MAAM,mBAAmB;;;8EAgB/B,CAAC;AAMF,eAAO,MAAM,qBAAqB,oFAGhC,CAAC;AAMH,eAAO,MAAM,mBAAmB;;8EAQ/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
1
+ {"version":3,"file":"switch.variants.d.ts","sourceRoot":"","sources":["../../../src/components/switch/switch.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAUlE,eAAO,MAAM,mBAAmB;;8EA8B/B,CAAC;AAUF,eAAO,MAAM,mBAAmB;;;8EAgB/B,CAAC;AAMF,eAAO,MAAM,qBAAqB,oFAGhC,CAAC;AAMH,eAAO,MAAM,mBAAmB;;8EAQ/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { focusRing } from "../../lib/focus-ring";
2
3
  const switchTrackVariants = cva(
3
4
  [
4
5
  "relative inline-flex shrink-0 items-center rounded-full border",
@@ -11,7 +12,7 @@ const switchTrackVariants = cva(
11
12
  "transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)",
12
13
  // visible 2px signal-blue ring at 2px offset, on every state, never removed
13
14
  "outline-none",
14
- "focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2",
15
+ focusRing,
15
16
  // disabled: no pointer; the checked value still reads to AT. DEC-C / spec §4:
16
17
  // reduced emphasis comes from dimming the thumb (the indicator) to the disabled
17
18
  // TOKEN — see switchThumbVariants — NOT a blanket opacity-60 dim of the track.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/switch/switch.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The track: a native <button role=\"switch\">. Off binds control-*; the\n// aria-checked: state-variant binds the on (action-primary) track — never a\n// status-* utility (brand ≠ state). The visible 2px focus ring is never removed.\n// The target-size floor lives on the hit-area wrapper (switchHitAreaVariants), NOT\n// here: a min-height floor on the SAME node as the h-5/h-6 track would force both\n// sizes to the 44px floor (used height = max(min-height, height)), erasing the\n// size variant. The track keeps only its own h-5/h-6 visible height.\nexport const switchTrackVariants = cva(\n [\n \"relative inline-flex shrink-0 items-center rounded-full border\",\n // off (resting) track + resting border\n \"bg-control-bg border-control-border\",\n // on track (state-variant) + on hover / pressed\n \"aria-checked:bg-action-primary-bg aria-checked:border-action-primary-bg\",\n \"aria-checked:hover:bg-action-primary-bg-hover aria-checked:active:bg-action-primary-bg-active\",\n // fast, functional track-tint — no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // visible 2px signal-blue ring at 2px offset, on every state, never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled: no pointer; the checked value still reads to AT. DEC-C / spec §4:\n // reduced emphasis comes from dimming the thumb (the indicator) to the disabled\n // TOKEN — see switchThumbVariants — NOT a blanket opacity-60 dim of the track.\n \"disabled:pointer-events-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n // visible track height/width only — the hit-area floor is a separate wrapper,\n // so sm renders a genuinely shorter visible track than md\n size: {\n md: \"h-6 w-11 px-0.5\",\n sm: \"h-5 w-9 px-0.5\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The thumb: control-fg fill, slides start→end. Position (not color) carries on/off\n// state, via the aria-checked: translate variant on the parent button.\n//\n// DEC-C / spec §4: a disabled switch dims the thumb (its indicator) to\n// --color-text-disabled — the SAME token the label dims to — NOT a blanket\n// opacity-60 dim of the track. The thumb is a child of the button, not a sibling\n// of a peer input, so the colour is driven by an explicit `disabled` cva variant\n// the component passes (mirrors switchLabelVariants); enabled keeps control-fg.\nexport const switchThumbVariants = cva(\n [\n \"pointer-events-none block rounded-full shadow-sm\",\n \"translate-x-0 aria-checked:translate-x-full\",\n \"transition-transform duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n ],\n {\n variants: {\n size: {\n md: \"h-5 w-5\",\n sm: \"h-4 w-4\",\n },\n disabled: { true: \"bg-text-disabled\", false: \"bg-control-fg\" },\n },\n defaultVariants: { size: \"md\", disabled: false },\n },\n);\n\n// The hit-area wrapper around the track. This — NOT the visible track — carries the\n// target-size floor (44px touch / 40px pointer) so the touch target always meets the\n// minimum while the track keeps its smaller h-5/h-6 visible height. inline-flex +\n// items-center vertically centres the shorter track within the taller hit area.\nexport const switchHitAreaVariants = cva([\n \"inline-flex shrink-0 items-center justify-center\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n]);\n\n// The label naming the setting. The <label> element is nested in a column wrapper,\n// not a sibling of the button, so peer-disabled never reaches it; the disabled\n// colour is driven by the explicit `disabled` variant the component passes (mirrors\n// checkboxLabelVariants / radioLabelVariants).\nexport const switchLabelVariants = cva(\n [\"text-label text-text-primary select-none\"],\n {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n },\n);\n\nexport type SwitchVariantProps = VariantProps<typeof switchTrackVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AAShC,MAAM,sBAAsB;AAAA,EACjC;AAAA,IACE;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA,MAGR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,KAAK;AAAA,EAChC;AACF;AAUO,MAAM,sBAAsB;AAAA,EACjC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,UAAU,EAAE,MAAM,oBAAoB,OAAO,gBAAgB;AAAA,IAC/D;AAAA,IACA,iBAAiB,EAAE,MAAM,MAAM,UAAU,MAAM;AAAA,EACjD;AACF;AAMO,MAAM,wBAAwB,IAAI;AAAA,EACvC;AAAA,EACA;AACF,CAAC;AAMM,MAAM,sBAAsB;AAAA,EACjC,CAAC,0CAA0C;AAAA,EAC3C;AAAA,IACE,UAAU;AAAA,MACR,UAAU,EAAE,MAAM,sBAAsB,OAAO,GAAG;AAAA,IACpD;AAAA,IACA,iBAAiB,EAAE,UAAU,MAAM;AAAA,EACrC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/switch/switch.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"../../lib/focus-ring\";\n\n// The track: a native <button role=\"switch\">. Off binds control-*; the\n// aria-checked: state-variant binds the on (action-primary) track — never a\n// status-* utility (brand ≠ state). The visible 2px focus ring is never removed.\n// The target-size floor lives on the hit-area wrapper (switchHitAreaVariants), NOT\n// here: a min-height floor on the SAME node as the h-5/h-6 track would force both\n// sizes to the 44px floor (used height = max(min-height, height)), erasing the\n// size variant. The track keeps only its own h-5/h-6 visible height.\nexport const switchTrackVariants = cva(\n [\n \"relative inline-flex shrink-0 items-center rounded-full border\",\n // off (resting) track + resting border\n \"bg-control-bg border-control-border\",\n // on track (state-variant) + on hover / pressed\n \"aria-checked:bg-action-primary-bg aria-checked:border-action-primary-bg\",\n \"aria-checked:hover:bg-action-primary-bg-hover aria-checked:active:bg-action-primary-bg-active\",\n // fast, functional track-tint — no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // visible 2px signal-blue ring at 2px offset, on every state, never removed\n \"outline-none\",\n focusRing,\n // disabled: no pointer; the checked value still reads to AT. DEC-C / spec §4:\n // reduced emphasis comes from dimming the thumb (the indicator) to the disabled\n // TOKEN — see switchThumbVariants — NOT a blanket opacity-60 dim of the track.\n \"disabled:pointer-events-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n // visible track height/width only — the hit-area floor is a separate wrapper,\n // so sm renders a genuinely shorter visible track than md\n size: {\n md: \"h-6 w-11 px-0.5\",\n sm: \"h-5 w-9 px-0.5\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The thumb: control-fg fill, slides start→end. Position (not color) carries on/off\n// state, via the aria-checked: translate variant on the parent button.\n//\n// DEC-C / spec §4: a disabled switch dims the thumb (its indicator) to\n// --color-text-disabled — the SAME token the label dims to — NOT a blanket\n// opacity-60 dim of the track. The thumb is a child of the button, not a sibling\n// of a peer input, so the colour is driven by an explicit `disabled` cva variant\n// the component passes (mirrors switchLabelVariants); enabled keeps control-fg.\nexport const switchThumbVariants = cva(\n [\n \"pointer-events-none block rounded-full shadow-sm\",\n \"translate-x-0 aria-checked:translate-x-full\",\n \"transition-transform duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n ],\n {\n variants: {\n size: {\n md: \"h-5 w-5\",\n sm: \"h-4 w-4\",\n },\n disabled: { true: \"bg-text-disabled\", false: \"bg-control-fg\" },\n },\n defaultVariants: { size: \"md\", disabled: false },\n },\n);\n\n// The hit-area wrapper around the track. This — NOT the visible track — carries the\n// target-size floor (44px touch / 40px pointer) so the touch target always meets the\n// minimum while the track keeps its smaller h-5/h-6 visible height. inline-flex +\n// items-center vertically centres the shorter track within the taller hit area.\nexport const switchHitAreaVariants = cva([\n \"inline-flex shrink-0 items-center justify-center\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n]);\n\n// The label naming the setting. The <label> element is nested in a column wrapper,\n// not a sibling of the button, so peer-disabled never reaches it; the disabled\n// colour is driven by the explicit `disabled` variant the component passes (mirrors\n// checkboxLabelVariants / radioLabelVariants).\nexport const switchLabelVariants = cva(\n [\"text-label text-text-primary select-none\"],\n {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n },\n);\n\nexport type SwitchVariantProps = VariantProps<typeof switchTrackVariants>;\n"],"mappings":"AAAA,SAAS,WAA8B;AACvC,SAAS,iBAAiB;AASnB,MAAM,sBAAsB;AAAA,EACjC;AAAA,IACE;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA;AAAA;AAAA,MAGR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,MAAM,KAAK;AAAA,EAChC;AACF;AAUO,MAAM,sBAAsB;AAAA,EACjC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,UAAU,EAAE,MAAM,oBAAoB,OAAO,gBAAgB;AAAA,IAC/D;AAAA,IACA,iBAAiB,EAAE,MAAM,MAAM,UAAU,MAAM;AAAA,EACjD;AACF;AAMO,MAAM,wBAAwB,IAAI;AAAA,EACvC;AAAA,EACA;AACF,CAAC;AAMM,MAAM,sBAAsB;AAAA,EACjC,CAAC,0CAA0C;AAAA,EAC3C;AAAA,IACE,UAAU;AAAA,MACR,UAAU,EAAE,MAAM,sBAAsB,OAAO,GAAG;AAAA,IACpD;AAAA,IACA,iBAAiB,EAAE,UAAU,MAAM;AAAA,EACrC;AACF;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"table.variants.d.ts","sourceRoot":"","sources":["../../../src/components/table/table.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAclE,eAAO,MAAM,aAAa,oFAGxB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;AAInE,eAAO,MAAM,iBAAiB,6DAA6D,CAAC;AAM5F,eAAO,MAAM,mBAAmB;;8EAQ9B,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAQ/E,eAAO,MAAM,iBAAiB;;8EAW5B,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAU3E,eAAO,MAAM,gBAAgB;;8EAiB5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC;AA4BzE,eAAO,MAAM,iBAAiB;;;;;;8EAiC7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAK3E,eAAO,MAAM,sBAAsB;;;8EAMlC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,YAAY,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAKrF,eAAO,MAAM,iBAAiB;;;8EAM7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAU3E,eAAO,MAAM,oBAAoB,QAOgE,CAAC;AAKlG,eAAO,MAAM,mBAAmB,2FAC0D,CAAC;AAI3F,eAAO,MAAM,eAAe,4EAC+C,CAAC;AAM5E,eAAO,MAAM,sBAAsB,kCAAkC,CAAC"}
1
+ {"version":3,"file":"table.variants.d.ts","sourceRoot":"","sources":["../../../src/components/table/table.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAelE,eAAO,MAAM,aAAa,oFAGxB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;AAInE,eAAO,MAAM,iBAAiB,6DAA6D,CAAC;AAM5F,eAAO,MAAM,mBAAmB;;8EAQ9B,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAQ/E,eAAO,MAAM,iBAAiB;;8EAW5B,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAU3E,eAAO,MAAM,gBAAgB;;8EAiB5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC;AA4BzE,eAAO,MAAM,iBAAiB;;;;;;8EAkC7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAK3E,eAAO,MAAM,sBAAsB;;;8EAMlC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,YAAY,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAKrF,eAAO,MAAM,iBAAiB;;;8EAM7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAU3E,eAAO,MAAM,oBAAoB,QAOtB,CAAC;AAKZ,eAAO,MAAM,mBAAmB,2FAC0D,CAAC;AAI3F,eAAO,MAAM,eAAe,4EAC+C,CAAC;AAM5E,eAAO,MAAM,sBAAsB,kCAAkC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { focusRing } from "../../lib/focus-ring";
2
3
  const tableVariants = cva([
3
4
  "w-full border-collapse text-start",
4
5
  "bg-surface-canvas text-body text-text-primary"
@@ -66,8 +67,9 @@ const tableCellVariants = cva(
66
67
  false: ""
67
68
  },
68
69
  auxiliary: {
69
- // de-emphasized auxiliary cell text — a timestamp, a unit (spec §5 --color-text-muted)
70
- true: "text-text-muted",
70
+ // de-emphasized auxiliary cell text — a timestamp, a unit. Essential (it conveys data), so
71
+ // it uses secondary (AA), not the decorative-only muted role (accessibility.md).
72
+ true: "text-text-secondary",
71
73
  false: ""
72
74
  },
73
75
  status: {
@@ -103,7 +105,7 @@ const tableHeadVariants = cva(
103
105
  defaultVariants: { density: "comfortable", rule: "horizontal" }
104
106
  }
105
107
  );
106
- const tableSortButtonClass = "inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) text-label text-action-ghost-fg cursor-pointer select-none hover:bg-action-ghost-bg-hover transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2";
108
+ const tableSortButtonClass = "inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) text-label text-action-ghost-fg cursor-pointer select-none hover:bg-action-ghost-bg-hover transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant) min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) " + focusRing;
107
109
  const tableSortCaretClass = "inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center";
108
110
  const tableEmptyClass = "px-(--space-3) py-(--space-4) text-center text-body text-text-secondary";
109
111
  const tableSkeletonCellClass = "px-(--space-3) py-(--space-3)";