@ecoma-io/loom 0.4.0 → 0.5.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 (251) hide show
  1. package/README.md +11 -9
  2. package/dist/a11y.js +2 -2
  3. package/dist/index.js +36 -1
  4. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/CalendarDate.js +1 -1
  5. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/CalendarDate.js.map +1 -1
  6. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +1 -1
  7. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js.map +1 -1
  8. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/conversion.js +1 -1
  9. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/conversion.js.map +1 -1
  10. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/manipulation.js +1 -1
  11. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/manipulation.js.map +1 -1
  12. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/queries.js +1 -1
  13. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/queries.js.map +1 -1
  14. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/string.js +1 -1
  15. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/string.js.map +1 -1
  16. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/utils.js +1 -1
  17. package/dist/node_modules/.pnpm/{@internationalized_date@3.12.3 → @internationalized_date@3.12.4}/node_modules/@internationalized/date/dist/private/utils.js.map +1 -1
  18. package/dist/packages/blocks/empty-state/src/EmptyState.js.map +1 -1
  19. package/dist/packages/blocks/empty-state/src/EmptyState.vue.d.ts.map +1 -1
  20. package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js +5 -6
  21. package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/packages/blocks/error-summary/src/ErrorSummary.js +7 -0
  23. package/dist/packages/blocks/error-summary/src/ErrorSummary.js.map +1 -0
  24. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue.d.ts +31 -0
  25. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js +90 -0
  26. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js.map +1 -0
  27. package/dist/packages/blocks/error-summary/src/index.d.ts +4 -0
  28. package/dist/packages/blocks/error-summary/src/index.js +3 -0
  29. package/dist/packages/composition/scroll-reel/src/ScrollReel.js.map +1 -1
  30. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js +1 -0
  31. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  32. package/dist/packages/composition/sidebar/src/Sidebar.js.map +1 -1
  33. package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts +4 -0
  34. package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts.map +1 -1
  35. package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js +13 -4
  36. package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  37. package/dist/packages/composition/split/src/Split.js.map +1 -1
  38. package/dist/packages/composition/split/src/Split.vue.d.ts +11 -12
  39. package/dist/packages/composition/split/src/Split.vue.d.ts.map +1 -1
  40. package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js +15 -5
  41. package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js.map +1 -1
  42. package/dist/packages/core/src/a11y-scope.d.ts +71 -5
  43. package/dist/packages/core/src/a11y-scope.d.ts.map +1 -1
  44. package/dist/packages/core/src/a11y-scope.js +126 -6
  45. package/dist/packages/core/src/a11y-scope.js.map +1 -1
  46. package/dist/packages/core/src/index.d.ts +1 -1
  47. package/dist/packages/core/src/index.d.ts.map +1 -1
  48. package/dist/packages/core/src/index.js +2 -2
  49. package/dist/packages/labels/src/component-shapes.d.ts +163 -1
  50. package/dist/packages/labels/src/component-shapes.d.ts.map +1 -1
  51. package/dist/packages/labels/src/date-labels.d.ts +82 -5
  52. package/dist/packages/labels/src/date-labels.d.ts.map +1 -1
  53. package/dist/packages/labels/src/date-labels.js +54 -7
  54. package/dist/packages/labels/src/date-labels.js.map +1 -1
  55. package/dist/packages/labels/src/field-context.d.ts +2 -2
  56. package/dist/packages/labels/src/field-context.js.map +1 -1
  57. package/dist/packages/labels/src/index.d.ts +3 -3
  58. package/dist/packages/labels/src/index.d.ts.map +1 -1
  59. package/dist/packages/labels/src/index.js +2 -2
  60. package/dist/packages/labels/src/label-registry.d.ts +9 -1
  61. package/dist/packages/labels/src/label-registry.d.ts.map +1 -1
  62. package/dist/packages/layouts/dashboard/src/Dashboard.js.map +1 -1
  63. package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts +4 -4
  64. package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts.map +1 -1
  65. package/dist/packages/layouts/dashboard/src/Dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  66. package/dist/packages/layouts/settings/src/Settings.js.map +1 -1
  67. package/dist/packages/layouts/settings/src/Settings.vue.d.ts +3 -3
  68. package/dist/packages/layouts/settings/src/Settings.vue.d.ts.map +1 -1
  69. package/dist/packages/layouts/settings/src/Settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  70. package/dist/packages/layouts/split-layout/src/SplitLayout.js.map +1 -1
  71. package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts +4 -6
  72. package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts.map +1 -1
  73. package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js +20 -5
  74. package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js.map +1 -1
  75. package/dist/packages/layouts/split-layout/src/index.d.ts +1 -1
  76. package/dist/packages/layouts/split-layout/src/index.d.ts.map +1 -1
  77. package/dist/packages/loom/src/a11y.d.ts +1 -1
  78. package/dist/packages/loom/src/index.d.ts +27 -1
  79. package/dist/packages/primitives/alert/src/Alert.js.map +1 -1
  80. package/dist/packages/primitives/alert/src/Alert.vue.d.ts +18 -0
  81. package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js +10 -7
  82. package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js.map +1 -1
  83. package/dist/packages/primitives/avatar/src/Avatar.js.map +1 -1
  84. package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts +2 -2
  85. package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts.map +1 -1
  86. package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js +1 -1
  87. package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  88. package/dist/packages/primitives/calendar/src/Calendar.js +7 -0
  89. package/dist/packages/primitives/calendar/src/Calendar.js.map +1 -0
  90. package/dist/packages/primitives/calendar/src/Calendar.vue.d.ts +95 -0
  91. package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js +204 -0
  92. package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  93. package/dist/packages/primitives/calendar/src/index.d.ts +3 -0
  94. package/dist/packages/primitives/calendar/src/index.js +3 -0
  95. package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts +1 -1
  96. package/dist/packages/primitives/command/src/Command.js +7 -0
  97. package/dist/packages/primitives/command/src/Command.js.map +1 -0
  98. package/dist/packages/primitives/command/src/Command.vue.d.ts +77 -0
  99. package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js +333 -0
  100. package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js.map +1 -0
  101. package/dist/packages/primitives/command/src/index.d.ts +4 -0
  102. package/dist/packages/primitives/command/src/index.js +3 -0
  103. package/dist/packages/primitives/copy-button/src/CopyButton.js +7 -0
  104. package/dist/packages/primitives/copy-button/src/CopyButton.js.map +1 -0
  105. package/dist/packages/primitives/copy-button/src/CopyButton.vue.d.ts +64 -0
  106. package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js +113 -0
  107. package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js.map +1 -0
  108. package/dist/packages/primitives/copy-button/src/index.d.ts +4 -0
  109. package/dist/packages/primitives/copy-button/src/index.js +3 -0
  110. package/dist/packages/primitives/data-grid/src/DataGrid.js +7 -0
  111. package/dist/packages/primitives/data-grid/src/DataGrid.js.map +1 -0
  112. package/dist/packages/primitives/data-grid/src/DataGrid.vue.d.ts +128 -0
  113. package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js +347 -0
  114. package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js.map +1 -0
  115. package/dist/packages/primitives/data-grid/src/index.d.ts +4 -0
  116. package/dist/packages/primitives/data-grid/src/index.js +3 -0
  117. package/dist/packages/primitives/date-picker/src/DatePicker.js.map +1 -1
  118. package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js +13 -13
  119. package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  120. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.js.map +1 -1
  121. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js +21 -18
  122. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  123. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.js.map +1 -1
  124. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js +17 -16
  125. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  126. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.js.map +1 -1
  127. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js +17 -16
  128. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  129. package/dist/packages/primitives/dialog/src/Dialog.js.map +1 -1
  130. package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts +15 -7
  131. package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js +5 -3
  132. package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  133. package/dist/packages/primitives/drawer/src/Drawer.js.map +1 -1
  134. package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts +10 -2
  135. package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js +11 -3
  136. package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  137. package/dist/packages/primitives/hover-card/src/index.d.ts.map +1 -1
  138. package/dist/packages/primitives/live-region/src/LiveRegion.js +7 -0
  139. package/dist/packages/primitives/live-region/src/LiveRegion.js.map +1 -0
  140. package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts +36 -0
  141. package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts.map +1 -0
  142. package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js +59 -0
  143. package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js.map +1 -0
  144. package/dist/packages/primitives/live-region/src/context.d.ts +40 -0
  145. package/dist/packages/primitives/live-region/src/context.d.ts.map +1 -0
  146. package/dist/packages/primitives/live-region/src/context.js +85 -0
  147. package/dist/packages/primitives/live-region/src/context.js.map +1 -0
  148. package/dist/packages/primitives/live-region/src/index.d.ts +4 -0
  149. package/dist/packages/primitives/live-region/src/index.d.ts.map +1 -0
  150. package/dist/packages/primitives/live-region/src/index.js +3 -0
  151. package/dist/packages/primitives/live-region/src/useAnnounce.d.ts +17 -0
  152. package/dist/packages/primitives/live-region/src/useAnnounce.d.ts.map +1 -0
  153. package/dist/packages/primitives/live-region/src/useAnnounce.js +25 -0
  154. package/dist/packages/primitives/live-region/src/useAnnounce.js.map +1 -0
  155. package/dist/packages/primitives/menubar/src/Menubar.js.map +1 -1
  156. package/dist/packages/primitives/menubar/src/Menubar.vue.d.ts.map +1 -1
  157. package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js +33 -0
  158. package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  159. package/dist/packages/primitives/meter/src/Meter.js +7 -0
  160. package/dist/packages/primitives/meter/src/Meter.js.map +1 -0
  161. package/dist/packages/primitives/meter/src/Meter.vue.d.ts +87 -0
  162. package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js +236 -0
  163. package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js.map +1 -0
  164. package/dist/packages/primitives/meter/src/index.d.ts +4 -0
  165. package/dist/packages/primitives/meter/src/index.js +3 -0
  166. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js +7 -0
  167. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js.map +1 -0
  168. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts +69 -0
  169. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts.map +1 -0
  170. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js +118 -0
  171. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  172. package/dist/packages/primitives/navigation-menu/src/index.d.ts +3 -0
  173. package/dist/packages/primitives/navigation-menu/src/index.d.ts.map +1 -0
  174. package/dist/packages/primitives/navigation-menu/src/index.js +2 -0
  175. package/dist/packages/primitives/segmented-control/src/SegmentedControl.js.map +1 -1
  176. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue.d.ts.map +1 -1
  177. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js +1 -1
  178. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/packages/primitives/skip-link/src/SkipLink.js +7 -0
  180. package/dist/packages/primitives/skip-link/src/SkipLink.js.map +1 -0
  181. package/dist/packages/primitives/skip-link/src/SkipLink.vue.d.ts +62 -0
  182. package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js +64 -0
  183. package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js.map +1 -0
  184. package/dist/packages/primitives/skip-link/src/index.d.ts +3 -0
  185. package/dist/packages/primitives/skip-link/src/index.js +3 -0
  186. package/dist/packages/primitives/stepper/src/Stepper.js.map +1 -1
  187. package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js +7 -5
  188. package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js.map +1 -1
  189. package/dist/packages/primitives/switch/src/Switch.js.map +1 -1
  190. package/dist/packages/primitives/switch/src/Switch.vue.d.ts.map +1 -1
  191. package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js +1 -1
  192. package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  193. package/dist/packages/primitives/table/src/index.d.ts +1 -1
  194. package/dist/packages/primitives/table/src/index.js +2 -2
  195. package/dist/packages/primitives/time-picker/src/TimePicker.js.map +1 -1
  196. package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js +10 -8
  197. package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  198. package/dist/packages/primitives/toast/src/ToastItem.js.map +1 -1
  199. package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js +3 -1
  200. package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  201. package/dist/packages/primitives/toggle-group/src/ToggleGroup.js +7 -0
  202. package/dist/packages/primitives/toggle-group/src/ToggleGroup.js.map +1 -0
  203. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts +53 -0
  204. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts.map +1 -0
  205. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js +111 -0
  206. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js.map +1 -0
  207. package/dist/packages/primitives/toggle-group/src/index.d.ts +3 -0
  208. package/dist/packages/primitives/toggle-group/src/index.d.ts.map +1 -0
  209. package/dist/packages/primitives/toggle-group/src/index.js +2 -0
  210. package/dist/packages/primitives/toolbar/src/Toolbar.js +7 -0
  211. package/dist/packages/primitives/toolbar/src/Toolbar.js.map +1 -0
  212. package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts +31 -0
  213. package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts.map +1 -0
  214. package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js +149 -0
  215. package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  216. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js +7 -0
  217. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js.map +1 -0
  218. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts +24 -0
  219. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts.map +1 -0
  220. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js +40 -0
  221. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/packages/primitives/toolbar/src/index.d.ts +4 -0
  223. package/dist/packages/primitives/toolbar/src/index.d.ts.map +1 -0
  224. package/dist/packages/primitives/toolbar/src/index.js +3 -0
  225. package/dist/packages/primitives/tree-view/src/TreeView.js +7 -0
  226. package/dist/packages/primitives/tree-view/src/TreeView.js.map +1 -0
  227. package/dist/packages/primitives/tree-view/src/TreeView.vue.d.ts +99 -0
  228. package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js +310 -0
  229. package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  230. package/dist/packages/primitives/tree-view/src/TreeViewNode.js +7 -0
  231. package/dist/packages/primitives/tree-view/src/TreeViewNode.js.map +1 -0
  232. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts +15 -0
  233. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts.map +1 -0
  234. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js +103 -0
  235. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js.map +1 -0
  236. package/dist/packages/primitives/tree-view/src/context.d.ts +39 -0
  237. package/dist/packages/primitives/tree-view/src/context.d.ts.map +1 -0
  238. package/dist/packages/primitives/tree-view/src/context.js +6 -0
  239. package/dist/packages/primitives/tree-view/src/context.js.map +1 -0
  240. package/dist/packages/primitives/tree-view/src/index.d.ts +4 -0
  241. package/dist/packages/primitives/tree-view/src/index.js +3 -0
  242. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js +7 -0
  243. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js.map +1 -0
  244. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts +21 -0
  245. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts.map +1 -0
  246. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js +27 -0
  247. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js.map +1 -0
  248. package/dist/packages/primitives/visually-hidden/src/index.d.ts +3 -0
  249. package/dist/packages/primitives/visually-hidden/src/index.d.ts.map +1 -0
  250. package/dist/packages/primitives/visually-hidden/src/index.js +2 -0
  251. package/package.json +29 -28
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedControl.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/segmented-control/src/SegmentedControl.vue"],"names":[],"mappings":"AAuQA,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAuBd,qCAAqC;iBACxB,MAAM;IACnB,wCAAwC;aAC/B,sBAAsB,EAAE;IACjC,wHAAwH;eAC7G,OAAO,GAAG,SAAS;IAC9B,wFAAwF;WACjF,SAAS,GAAG,IAAI;;;;IAPvB,qCAAqC;iBACxB,MAAM;IACnB,wCAAwC;aAC/B,sBAAsB,EAAE;IACjC,wHAAwH;eAC7G,OAAO,GAAG,SAAS;IAC9B,wFAAwF;WACjF,SAAS,GAAG,IAAI;;;;UAAhB,SAAS,GAAG,IAAI;cAFZ,OAAO,GAAG,SAAS;6EA4U9B,CAAC"}
1
+ {"version":3,"file":"SegmentedControl.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/segmented-control/src/SegmentedControl.vue"],"names":[],"mappings":"AAyQA,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;wBACoB,OAAO,YAAY;AAAxC,wBAAyC;AACzC,QAAA,MAAM,YAAY;IAuBd,qCAAqC;iBACxB,MAAM;IACnB,wCAAwC;aAC/B,sBAAsB,EAAE;IACjC,wHAAwH;eAC7G,OAAO,GAAG,SAAS;IAC9B,wFAAwF;WACjF,SAAS,GAAG,IAAI;;;;IAPvB,qCAAqC;iBACxB,MAAM;IACnB,wCAAwC;aAC/B,sBAAsB,EAAE;IACjC,wHAAwH;eAC7G,OAAO,GAAG,SAAS;IAC9B,wFAAwF;WACjF,SAAS,GAAG,IAAI;;;;UAAhB,SAAS,GAAG,IAAI;cAFZ,OAAO,GAAG,SAAS;6EAgV9B,CAAC"}
@@ -108,7 +108,7 @@ var SegmentedControl_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
108
108
  disabled: opt.disabled ?? false
109
109
  }, { ref_for: true }, unref(optional)({ "data-testid": opt.testId }), {
110
110
  style: { "transition": "transform var(--duration-fast) var(--ease-spring),\n color var(--duration-fast) var(--ease-out)" },
111
- class: unref(cn)("group/segment relative z-10 inline-flex min-h-6 items-center justify-center rounded-sm text-muted-foreground", __props.size === "sm" ? "px-1.5 py-px text-micro" : "px-3 py-1 text-sm", "data-[state=checked]:font-medium data-[state=checked]:text-foreground", "data-[state=unchecked]:hover:bg-subtle", "active:scale-press", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo", "disabled:pointer-events-none data-[disabled]:bg-background")
111
+ class: unref(cn)("group/segment relative z-10 inline-flex min-h-6 min-w-6 items-center justify-center rounded-sm text-muted-foreground", __props.size === "sm" ? "px-1.5 py-px text-micro" : "px-3 py-1 text-sm", "data-[state=checked]:font-medium data-[state=checked]:text-foreground", "data-[state=unchecked]:hover:bg-subtle", "active:scale-press", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo", "disabled:pointer-events-none data-[disabled]:bg-background")
112
112
  }), {
113
113
  default: withCtx(() => [createElementVNode("span", _hoisted_1, toDisplayString(opt.label), 1)]),
114
114
  _: 2
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedControl.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/segmented-control/src/SegmentedControl.vue"],"sourcesContent":["<script lang=\"ts\">\n/** One segment in a {@link SegmentedControl}. */\nexport interface SegmentedControlOption {\n /** Matched against `modelValue`. */\n value: string;\n /** Visible label, rendered inside the segment. */\n label: string;\n /** Disables this segment alone, leaving its neighbours pickable. */\n disabled?: boolean;\n /** Forwarded to the segment's DOM node — lets hosts keep stable test hooks. */\n testId?: string;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { nextTick, onMounted, onUnmounted, reactive, ref, useTemplateRef, watch } from \"vue\";\nimport { RadioGroupRoot, RadioGroupItem } from \"reka-ui\";\nimport { cn, optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * SegmentedControl — pick one of 2-5 mutually exclusive options, all visible\n * at once; one is always active. Built on Reka UI's RadioGroup (single\n * choice, roving tabindex — the group is one Tab stop, not N — with\n * arrow-key navigation). Styled with Loom tokens.\n *\n * Inside a [Field](../Field/Field.vue) the row's description, `name`,\n * `required` and `invalid` reach the group through `useFieldControl()`.\n *\n * **A row's label does not name this group.** `<label for>` names a labelable\n * element and this renders a `div[role=\"radiogroup\"]`, so the row's label\n * resolves to the element and names nobody. Name it with a\n * [Fieldset](../Fieldset/Fieldset.vue)'s real `<legend>`, or with an\n * `aria-label`/`aria-labelledby` of its own — which is what every example\n * already does, because the control carries no label text.\n *\n * There is no `readonly`, and a row's is ignored rather than approximated: each\n * segment is a `<button role=\"radio\">` with no native read-only state to put it\n * in, and one segment is always active, so a read-only control here would be a\n * disabled one showing an answer.\n */\nconst props = withDefaults(\n defineProps<{\n /** The selected option's `value`. */\n modelValue?: string;\n /** The segments to render, in order. */\n options: SegmentedControlOption[];\n /** Disables every segment at once, distinct from a single option's own `disabled`. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** `\"sm\"` is the compressed form for dense chrome — e.g. a status bar density level. */\n size?: \"default\" | \"sm\";\n }>(),\n {\n // Not `false`: absent has to stay tellable from `false` for a Field above\n // to disable the control and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n size: \"default\",\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: groupAttrs } = useSplitAttrs();\n\n/**\n * The segments are real `<button>`s, so a `<fieldset disabled>` above this\n * control already makes each of them inert — but Reka's roving focus is what\n * decides where Tab lands, and it is driven by the group's own `disabled`,\n * which the fieldset never reaches. Measured on this branch: every segment went\n * `:disabled` while the `role=\"radiogroup\"` container kept `tabindex=\"0\"`, so\n * Tab still stopped on an unavailable control and left the reader nowhere. Its\n * own `disabled` prop sets that to `-1`.\n *\n * Reading the group state here rather than painting it with a CSS variant is\n * also what keeps the appearance honest, and this is the control where the\n * distinction bites: one option may carry `disabled` of its own, so \"a\n * `:disabled` button is inside this track\" and \"this track is unavailable\" are\n * different facts. Only the fieldset says the second.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `groupAttrs`, and that position is the contract (see TextField.vue).\n//\n// `name`, `required` and `invalid` are omitted because this component has no\n// prop for any of them — the row's answer stands unopposed, and its `name`\n// reaches RadioGroupRoot's own prop, which mints the hidden input a `<form>`\n// submits. `readonly` is omitted for the reason the docblock gives.\n//\n// The resolved `required` then reaches the group through Reka's own prop rather\n// than the `aria-required` in `field.attrs`: RadioGroupRoot writes that\n// attribute itself, so leaving it to the bag is a race with a `false` — see\n// Checkbox.vue, where the merge order makes the same race a certainty.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // segments themselves.\n disabled: groupDisabled.value ? true : props.disabled,\n}));\n\n// Shared sliding indicator behind the checked segment. Measured off the live\n// DOM (offsetLeft/offsetWidth of the [data-state=checked] item), not derived\n// from index/width math, so \"sm\"/\"default\" sizing and variable-width labels\n// stay correct without per-size indicator code.\nconst indicatorEl = ref<HTMLElement | null>(null);\nconst indicatorStyle = reactive({ left: \"0px\", width: \"0px\", opacity: \"0\" });\n\nlet resizeObserver: ResizeObserver | undefined;\n\n// Re-targets onto the checked item itself (not the container) so a late\n// width settle on exactly that element — e.g. the `font-medium` weight swap\n// on data-[state=checked] reflowing after this function's own read — is\n// caught by the observer and re-measured, instead of going unnoticed because\n// the container's total box happened not to change.\nfunction updateIndicator() {\n const container = indicatorEl.value?.parentElement;\n const active = container?.querySelector<HTMLElement>('[role=\"radio\"][data-state=\"checked\"]');\n if (!active) {\n indicatorStyle.opacity = \"0\";\n resizeObserver?.disconnect();\n return;\n }\n indicatorStyle.left = `${active.offsetLeft}px`;\n indicatorStyle.width = `${active.offsetWidth}px`;\n indicatorStyle.opacity = \"1\";\n resizeObserver?.disconnect();\n resizeObserver?.observe(active);\n}\n\nonMounted(() => {\n resizeObserver = new ResizeObserver(updateIndicator);\n updateIndicator();\n});\nonUnmounted(() => resizeObserver?.disconnect());\n\n// One watcher for both re-measure triggers, and deliberately **not** deep on\n// `options`. The two separate watches each scheduled the same\n// `nextTick(updateIndicator)`, so one host tick that changed both inputs paid\n// the measurement twice; merging them costs nothing and removes the double.\n// Dropping `deep:` is not a regression: an in-place mutation of an option\n// object only matters to the indicator if it changes a *width* — a label edit,\n// say — and the ResizeObserver above is already watching exactly the checked\n// element for that, so the deep traversal bought nothing the observer does not\n// deliver sooner. A new array identity or a new selection still fires this.\nwatch([() => props.modelValue, () => props.options], () => nextTick(updateIndicator));\n</script>\n\n<template>\n <RadioGroupRoot\n ref=\"root\"\n v-bind=\"{ ...optional({ modelValue }), ...groupAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n orientation=\"horizontal\"\n :class=\"\n cn(\n 'relative inline-flex items-center gap-0.5 rounded-md border border-input bg-muted p-0.5',\n // The one thing the *track* has left to say. Its fill is already\n // `bg-muted` — the well every other unavailable control drains to — so\n // the fill channel is spent before the state arrives, and the segments\n // inside carry the rest. The rim slackening from `input` to the lighter\n // `border` is what makes a wholly unavailable control tellable from an\n // available one at the group's own edge rather than only cell by cell.\n field.disabled && 'cursor-not-allowed border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <span\n ref=\"indicatorEl\"\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute inset-y-0.5 rounded-sm bg-card shadow-sm\"\n style=\"\n transition:\n left var(--duration-fast) var(--ease-spring),\n width var(--duration-fast) var(--ease-spring);\n \"\n :style=\"{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n opacity: indicatorStyle.opacity,\n }\"\n />\n <RadioGroupItem\n v-for=\"opt in options\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n :disabled=\"opt.disabled ?? false\"\n v-bind=\"optional({ 'data-testid': opt.testId })\"\n style=\"\n transition:\n transform var(--duration-fast) var(--ease-spring),\n color var(--duration-fast) var(--ease-out);\n \"\n :class=\"\n cn(\n // Named, not the bare `group`: a segmented control is dropped into\n // application chrome, and a bare `group-*` would also answer to any\n // ancestor a host happened to mark up as one.\n // `min-h-6` is the WCAG 2.5.8 target floor made explicit: the sm\n // segment's box was previously a product of whatever line-height the\n // surrounding text happened to hand down, and swapping to the named\n // `text-micro` (line-height 1.4) dropped it to 23px. A floor that\n // holds regardless of type metrics is the honest encoding.\n 'group/segment relative z-10 inline-flex min-h-6 items-center justify-center rounded-sm text-muted-foreground',\n size === 'sm' ? 'px-1.5 py-px text-micro' : 'px-3 py-1 text-sm',\n 'data-[state=checked]:font-medium data-[state=checked]:text-foreground',\n 'data-[state=unchecked]:hover:bg-subtle',\n 'active:scale-press',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // No `opacity` here. A segment *is* its label — there is no glyph and\n // no fill of its own to dim — so `disabled:opacity-50` had nothing to\n // act on but the text, taking an unchecked segment to 1.96:1 and a\n // chosen one to 3.13:1 on its thumb.\n //\n // Unavailability is a fill instead, and it is the one step this\n // system already treats as meaningful: the cell drains up a rung of\n // the elevation rhythm, from the track's `bg-muted` to the page's own\n // `bg-background`, so it reads as a hole in the track rather than as\n // part of it. On a *chosen* segment that same fill covers the raised\n // white thumb, which is the whole signal — the value is still shown\n // and it is visibly no longer settable.\n 'disabled:pointer-events-none data-[disabled]:bg-background',\n )\n \"\n >\n <!-- The mute lives on this span rather than on the segment, and it has\n to. `data-[disabled]:` sorts *before* `data-[state=checked]:` in\n Tailwind's variant order, so a disabled segment that is also the\n chosen one would take `text-foreground` from the button — and a\n colour set on the button is only inherited here. Declared on the span\n it wins whatever the segment resolved to: 5.25:1 over the drained\n fill, against the 1.96:1 the dim used to leave. -->\n <span class=\"group-data-[disabled]/segment:text-muted-foreground\">{{ opt.label }}</span>\n </RadioGroupItem>\n </RadioGroupRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;EA2CA,MAAM,QAAQ;EAwBd,MAAM,EAAE,OAAO,MAAM,eAAe,cAAc;;;;;;;;;;;;;;;;;;;EAoBlD,MAAM,OAAO,eAAkC,MAAM;EACrD,MAAM,gBAAgB,0BAA0B,KAAK,OAAO,GAAG;EAkB/D,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,MAAM;GAGnB,UAAU,cAAc,QAAQ,OAAO,MAAM;EAC/C,EAAE;EAMF,MAAM,cAAc,IAAwB,IAAI;EAChD,MAAM,iBAAiB,SAAS;GAAE,MAAM;GAAO,OAAO;GAAO,SAAS;EAAI,CAAC;EAE3E,IAAI;EAOJ,SAAS,kBAAkB;GAEzB,MAAM,UADY,YAAY,OAAO,cAAA,EACX,cAA2B,0CAAsC;GAC3F,IAAI,CAAC,QAAQ;IACX,eAAe,UAAU;IACzB,gBAAgB,WAAW;IAC3B;GACF;GACA,eAAe,OAAO,GAAG,OAAO,WAAW;GAC3C,eAAe,QAAQ,GAAG,OAAO,YAAY;GAC7C,eAAe,UAAU;GACzB,gBAAgB,WAAW;GAC3B,gBAAgB,QAAQ,MAAM;EAChC;EAEA,gBAAgB;GACd,iBAAiB,IAAI,eAAe,eAAe;GACnD,gBAAgB;EAClB,CAAC;EACD,kBAAkB,gBAAgB,WAAW,CAAC;EAW9C,MAAM,OAAO,MAAM,kBAAkB,MAAM,OAAO,SAAS,SAAS,eAAe,CAAC;;GAIlF,OAAA,UAAA,GAAA,YAwFiB,MAAA,cAAA,GAxFjB,WAwFiB;IAvFX,SAAA;IAAJ,KAAI;GACS,GAAA;IAAA,GAAA,MAAA,QAAA,CAAQ,CAAA,EAAA,YAAG,QAAA,WAAU,CAAA;IAAA,GAAQ,MAAA,UAAA;IAAU,GAAK,MAAA,KAAA,CAAK,CAAC;GAAK,GAAA;IACnE,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,UAAU,MAAA,KAAA,CAAK,CAAC;IACjB,aAAY;IACX,OAAc,MAAA,EAAA,CAAE,CAAA,2FAAwkB,MAAA,KAAA,CAAK,CAAC,YAAQ,oCAAgD,MAAA,KAAA,CAAK,CAAC,KAAA;IAa5pB,uBAAkB,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,qBAAsB,OAAO,MAAM,CAAA;;IAE7D,SAAA,cAcE,CAdF,mBAcE,QAAA;KAbI,SAAA;KAAJ,KAAI;KACJ,eAAY;KACZ,OAAM;KACN,OAIC,eAAA,CAJD,EAAA,cAAA,yGAAA,GAIC;MACuB,MAAA,eAAe;MAAqB,OAAA,eAAe;MAAwB,SAAA,eAAe;;IAMpH,GAAA,MAAA,CAAA,IAAA,UAAA,IAAA,GAAA,mBAmDiB,UAAA,MAAA,WAlDD,QAAA,UAAP,QAAG;KADZ,OAAA,UAAA,GAAA,YAmDiB,MAAA,cAAA,GAnDjB,WAmDiB;MAjDd,KAAK,IAAI;MACT,OAAO,IAAI;MACX,UAAU,IAAI,YAAQ;KACf,GAAA,EAAA,SAAA,KAAA,GAAA,MAAA,QAAA,CAAQ,CAAA,EAAA,eAAkB,IAAI,OAAM,CAAA,GAAA;MAC5C,OAAA,EAAA,cAAA,2GAAA;MAKC,OAAgB,MAAA,EAAA,CAAE,CAAA,gHAA0tB,QAAA,SAAI,OAAA,4BAAA,qBAAA,yEAAA,0CAAA,sBAAA,+GAAA,4DAAA;;MAuCjvB,SAAA,cAAwF,CAAxF,mBAAwF,QAAxF,YAAwF,gBAAnB,IAAI,KAAK,GAAA,CAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"SegmentedControl.vue_vue_type_script_setup_true_lang.js","names":["$emit"],"sources":["../../../../../packages/primitives/segmented-control/src/SegmentedControl.vue"],"sourcesContent":["<script lang=\"ts\">\n/** One segment in a {@link SegmentedControl}. */\nexport interface SegmentedControlOption {\n /** Matched against `modelValue`. */\n value: string;\n /** Visible label, rendered inside the segment. */\n label: string;\n /** Disables this segment alone, leaving its neighbours pickable. */\n disabled?: boolean;\n /** Forwarded to the segment's DOM node — lets hosts keep stable test hooks. */\n testId?: string;\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { nextTick, onMounted, onUnmounted, reactive, ref, useTemplateRef, watch } from \"vue\";\nimport { RadioGroupRoot, RadioGroupItem } from \"reka-ui\";\nimport { cn, optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * SegmentedControl — pick one of 2-5 mutually exclusive options, all visible\n * at once; one is always active. Built on Reka UI's RadioGroup (single\n * choice, roving tabindex — the group is one Tab stop, not N — with\n * arrow-key navigation). Styled with Loom tokens.\n *\n * Inside a [Field](../Field/Field.vue) the row's description, `name`,\n * `required` and `invalid` reach the group through `useFieldControl()`.\n *\n * **A row's label does not name this group.** `<label for>` names a labelable\n * element and this renders a `div[role=\"radiogroup\"]`, so the row's label\n * resolves to the element and names nobody. Name it with a\n * [Fieldset](../Fieldset/Fieldset.vue)'s real `<legend>`, or with an\n * `aria-label`/`aria-labelledby` of its own — which is what every example\n * already does, because the control carries no label text.\n *\n * There is no `readonly`, and a row's is ignored rather than approximated: each\n * segment is a `<button role=\"radio\">` with no native read-only state to put it\n * in, and one segment is always active, so a read-only control here would be a\n * disabled one showing an answer.\n */\nconst props = withDefaults(\n defineProps<{\n /** The selected option's `value`. */\n modelValue?: string;\n /** The segments to render, in order. */\n options: SegmentedControlOption[];\n /** Disables every segment at once, distinct from a single option's own `disabled`. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** `\"sm\"` is the compressed form for dense chrome — e.g. a status bar density level. */\n size?: \"default\" | \"sm\";\n }>(),\n {\n // Not `false`: absent has to stay tellable from `false` for a Field above\n // to disable the control and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n size: \"default\",\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: string] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: groupAttrs } = useSplitAttrs();\n\n/**\n * The segments are real `<button>`s, so a `<fieldset disabled>` above this\n * control already makes each of them inert — but Reka's roving focus is what\n * decides where Tab lands, and it is driven by the group's own `disabled`,\n * which the fieldset never reaches. Measured on this branch: every segment went\n * `:disabled` while the `role=\"radiogroup\"` container kept `tabindex=\"0\"`, so\n * Tab still stopped on an unavailable control and left the reader nowhere. Its\n * own `disabled` prop sets that to `-1`.\n *\n * Reading the group state here rather than painting it with a CSS variant is\n * also what keeps the appearance honest, and this is the control where the\n * distinction bites: one option may carry `disabled` of its own, so \"a\n * `:disabled` button is inside this track\" and \"this track is unavailable\" are\n * different facts. Only the fieldset says the second.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `groupAttrs`, and that position is the contract (see TextField.vue).\n//\n// `name`, `required` and `invalid` are omitted because this component has no\n// prop for any of them — the row's answer stands unopposed, and its `name`\n// reaches RadioGroupRoot's own prop, which mints the hidden input a `<form>`\n// submits. `readonly` is omitted for the reason the docblock gives.\n//\n// The resolved `required` then reaches the group through Reka's own prop rather\n// than the `aria-required` in `field.attrs`: RadioGroupRoot writes that\n// attribute itself, so leaving it to the bag is a race with a `false` — see\n// Checkbox.vue, where the merge order makes the same race a certainty.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // segments themselves.\n disabled: groupDisabled.value ? true : props.disabled,\n}));\n\n// Shared sliding indicator behind the checked segment. Measured off the live\n// DOM (offsetLeft/offsetWidth of the [data-state=checked] item), not derived\n// from index/width math, so \"sm\"/\"default\" sizing and variable-width labels\n// stay correct without per-size indicator code.\nconst indicatorEl = ref<HTMLElement | null>(null);\nconst indicatorStyle = reactive({ left: \"0px\", width: \"0px\", opacity: \"0\" });\n\nlet resizeObserver: ResizeObserver | undefined;\n\n// Re-targets onto the checked item itself (not the container) so a late\n// width settle on exactly that element — e.g. the `font-medium` weight swap\n// on data-[state=checked] reflowing after this function's own read — is\n// caught by the observer and re-measured, instead of going unnoticed because\n// the container's total box happened not to change.\nfunction updateIndicator() {\n const container = indicatorEl.value?.parentElement;\n const active = container?.querySelector<HTMLElement>('[role=\"radio\"][data-state=\"checked\"]');\n if (!active) {\n indicatorStyle.opacity = \"0\";\n resizeObserver?.disconnect();\n return;\n }\n indicatorStyle.left = `${active.offsetLeft}px`;\n indicatorStyle.width = `${active.offsetWidth}px`;\n indicatorStyle.opacity = \"1\";\n resizeObserver?.disconnect();\n resizeObserver?.observe(active);\n}\n\nonMounted(() => {\n resizeObserver = new ResizeObserver(updateIndicator);\n updateIndicator();\n});\nonUnmounted(() => resizeObserver?.disconnect());\n\n// One watcher for both re-measure triggers, and deliberately **not** deep on\n// `options`. The two separate watches each scheduled the same\n// `nextTick(updateIndicator)`, so one host tick that changed both inputs paid\n// the measurement twice; merging them costs nothing and removes the double.\n// Dropping `deep:` is not a regression: an in-place mutation of an option\n// object only matters to the indicator if it changes a *width* — a label edit,\n// say — and the ResizeObserver above is already watching exactly the checked\n// element for that, so the deep traversal bought nothing the observer does not\n// deliver sooner. A new array identity or a new selection still fires this.\nwatch([() => props.modelValue, () => props.options], () => nextTick(updateIndicator));\n</script>\n\n<template>\n <RadioGroupRoot\n ref=\"root\"\n v-bind=\"{ ...optional({ modelValue }), ...groupAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n orientation=\"horizontal\"\n :class=\"\n cn(\n 'relative inline-flex items-center gap-0.5 rounded-md border border-input bg-muted p-0.5',\n // The one thing the *track* has left to say. Its fill is already\n // `bg-muted` — the well every other unavailable control drains to — so\n // the fill channel is spent before the state arrives, and the segments\n // inside carry the rest. The rim slackening from `input` to the lighter\n // `border` is what makes a wholly unavailable control tellable from an\n // available one at the group's own edge rather than only cell by cell.\n field.disabled && 'cursor-not-allowed border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', String($event))\"\n >\n <span\n ref=\"indicatorEl\"\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute inset-y-0.5 rounded-sm bg-card shadow-sm\"\n style=\"\n transition:\n left var(--duration-fast) var(--ease-spring),\n width var(--duration-fast) var(--ease-spring);\n \"\n :style=\"{\n left: indicatorStyle.left,\n width: indicatorStyle.width,\n opacity: indicatorStyle.opacity,\n }\"\n />\n <RadioGroupItem\n v-for=\"opt in options\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n :disabled=\"opt.disabled ?? false\"\n v-bind=\"optional({ 'data-testid': opt.testId })\"\n style=\"\n transition:\n transform var(--duration-fast) var(--ease-spring),\n color var(--duration-fast) var(--ease-out);\n \"\n :class=\"\n cn(\n // Named, not the bare `group`: a segmented control is dropped into\n // application chrome, and a bare `group-*` would also answer to any\n // ancestor a host happened to mark up as one.\n // `min-h-6`/`min-w-6` are the WCAG 2.5.8 target floors made\n // explicit: the sm segment's box was previously a product of whatever\n // line-height the surrounding text happened to hand down — swapping\n // to the named `text-micro` (line-height 1.4) dropped it to 23px,\n // and a micro label (a one-character density toggle) collapses the\n // width under 24 too. Floors that hold regardless of type metrics\n // are the honest encoding.\n 'group/segment relative z-10 inline-flex min-h-6 min-w-6 items-center justify-center rounded-sm text-muted-foreground',\n size === 'sm' ? 'px-1.5 py-px text-micro' : 'px-3 py-1 text-sm',\n 'data-[state=checked]:font-medium data-[state=checked]:text-foreground',\n 'data-[state=unchecked]:hover:bg-subtle',\n 'active:scale-press',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // No `opacity` here. A segment *is* its label — there is no glyph and\n // no fill of its own to dim — so `disabled:opacity-50` had nothing to\n // act on but the text, taking an unchecked segment to 1.96:1 and a\n // chosen one to 3.13:1 on its thumb.\n //\n // Unavailability is a fill instead, and it is the one step this\n // system already treats as meaningful: the cell drains up a rung of\n // the elevation rhythm, from the track's `bg-muted` to the page's own\n // `bg-background`, so it reads as a hole in the track rather than as\n // part of it. On a *chosen* segment that same fill covers the raised\n // white thumb, which is the whole signal — the value is still shown\n // and it is visibly no longer settable.\n 'disabled:pointer-events-none data-[disabled]:bg-background',\n )\n \"\n >\n <!-- The mute lives on this span rather than on the segment, and it has\n to. `data-[disabled]:` sorts *before* `data-[state=checked]:` in\n Tailwind's variant order, so a disabled segment that is also the\n chosen one would take `text-foreground` from the button — and a\n colour set on the button is only inherited here. Declared on the span\n it wins whatever the segment resolved to: 5.25:1 over the drained\n fill, against the 1.96:1 the dim used to leave. -->\n <span class=\"group-data-[disabled]/segment:text-muted-foreground\">{{ opt.label }}</span>\n </RadioGroupItem>\n </RadioGroupRoot>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;EA2CA,MAAM,QAAQ;EAwBd,MAAM,EAAE,OAAO,MAAM,eAAe,cAAc;;;;;;;;;;;;;;;;;;;EAoBlD,MAAM,OAAO,eAAkC,MAAM;EACrD,MAAM,gBAAgB,0BAA0B,KAAK,OAAO,GAAG;EAkB/D,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,MAAM;GAGnB,UAAU,cAAc,QAAQ,OAAO,MAAM;EAC/C,EAAE;EAMF,MAAM,cAAc,IAAwB,IAAI;EAChD,MAAM,iBAAiB,SAAS;GAAE,MAAM;GAAO,OAAO;GAAO,SAAS;EAAI,CAAC;EAE3E,IAAI;EAOJ,SAAS,kBAAkB;GAEzB,MAAM,UADY,YAAY,OAAO,cAAA,EACX,cAA2B,0CAAsC;GAC3F,IAAI,CAAC,QAAQ;IACX,eAAe,UAAU;IACzB,gBAAgB,WAAW;IAC3B;GACF;GACA,eAAe,OAAO,GAAG,OAAO,WAAW;GAC3C,eAAe,QAAQ,GAAG,OAAO,YAAY;GAC7C,eAAe,UAAU;GACzB,gBAAgB,WAAW;GAC3B,gBAAgB,QAAQ,MAAM;EAChC;EAEA,gBAAgB;GACd,iBAAiB,IAAI,eAAe,eAAe;GACnD,gBAAgB;EAClB,CAAC;EACD,kBAAkB,gBAAgB,WAAW,CAAC;EAW9C,MAAM,OAAO,MAAM,kBAAkB,MAAM,OAAO,SAAS,SAAS,eAAe,CAAC;;GAIlF,OAAA,UAAA,GAAA,YA0FiB,MAAA,cAAA,GA1FjB,WA0FiB;IAzFX,SAAA;IAAJ,KAAI;GACS,GAAA;IAAA,GAAA,MAAA,QAAA,CAAQ,CAAA,EAAA,YAAG,QAAA,WAAU,CAAA;IAAA,GAAQ,MAAA,UAAA;IAAU,GAAK,MAAA,KAAA,CAAK,CAAC;GAAK,GAAA;IACnE,UAAU,MAAA,KAAA,CAAK,CAAC;IAChB,UAAU,MAAA,KAAA,CAAK,CAAC;IACjB,aAAY;IACX,OAAc,MAAA,EAAA,CAAE,CAAA,2FAAwkB,MAAA,KAAA,CAAK,CAAC,YAAQ,oCAAgD,MAAA,KAAA,CAAK,CAAC,KAAA;IAa5pB,uBAAkB,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,qBAAsB,OAAO,MAAM,CAAA;;IAE7D,SAAA,cAcE,CAdF,mBAcE,QAAA;KAbI,SAAA;KAAJ,KAAI;KACJ,eAAY;KACZ,OAAM;KACN,OAIC,eAAA,CAJD,EAAA,cAAA,yGAAA,GAIC;MACuB,MAAA,eAAe;MAAqB,OAAA,eAAe;MAAwB,SAAA,eAAe;;IAMpH,GAAA,MAAA,CAAA,IAAA,UAAA,IAAA,GAAA,mBAqDiB,UAAA,MAAA,WApDD,QAAA,UAAP,QAAG;KADZ,OAAA,UAAA,GAAA,YAqDiB,MAAA,cAAA,GArDjB,WAqDiB;MAnDd,KAAK,IAAI;MACT,OAAO,IAAI;MACX,UAAU,IAAI,YAAQ;KACf,GAAA,EAAA,SAAA,KAAA,GAAA,MAAA,QAAA,CAAQ,CAAA,EAAA,eAAkB,IAAI,OAAM,CAAA,GAAA;MAC5C,OAAA,EAAA,cAAA,2GAAA;MAKC,OAAgB,MAAA,EAAA,CAAE,CAAA,wHAAy1B,QAAA,SAAI,OAAA,4BAAA,qBAAA,yEAAA,0CAAA,sBAAA,+GAAA,4DAAA;;MAyCh3B,SAAA,cAAwF,CAAxF,mBAAwF,QAAxF,YAAwF,gBAAnB,IAAI,KAAK,GAAA,CAAA,CAAA,CAAA"}
@@ -0,0 +1,7 @@
1
+ import SkipLink_vue_vue_type_script_setup_true_lang_default, { SKIP_LINK_LABELS, skipLinkClass } from "./SkipLink.vue_vue_type_script_setup_true_lang.js";
2
+ //#region packages/primitives/skip-link/src/SkipLink.vue
3
+ var SkipLink_default = SkipLink_vue_vue_type_script_setup_true_lang_default;
4
+ //#endregion
5
+ export { SKIP_LINK_LABELS, SkipLink_default as default, skipLinkClass };
6
+
7
+ //# sourceMappingURL=SkipLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkipLink.js","names":[],"sources":["../../../../../packages/primitives/skip-link/src/SkipLink.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { SkipLinkLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * SkipLink — the WCAG 2.4.1 (Bypass Blocks) affordance.\n *\n * A link to the main content that is the page's first focusable element and\n * stays visually hidden until something focuses it. A keyboard user tabs\n * once from the top of the document and lands on it; a pointer user never\n * meets it. It is a primitive beside `Link` rather than a variant of it\n * because the two answer different questions: `Link` navigates, `SkipLink`\n * bypasses — and its entire presentation *is* the hidden-until-focus\n * contract, which is exactly what no consumer should re-answer per product.\n *\n * Placement is the consumer's half of the contract: render it first in the\n * DOM, before the header and navigation it bypasses (next to the app shell),\n * and give the destination `tabindex=\"-1\"` so following the link moves focus\n * there instead of only scrolling.\n */\nexport const SKIP_LINK_LABELS: SkipLinkLabels = {\n label: \"Skip to main content\",\n};\n\n/**\n * Visually hidden until focused. `absolute` takes the link out of flow so\n * being first in the document reserves no space; the 1px clipped box is the\n * visually-hidden pattern — out of sight but focusable, so DOM order keeps\n * it the first tab stop.\n *\n * The reveal keys on `:focus`, deliberately not `:focus-visible`: focus\n * arriving from a skip target's own activation, from programmatic handoff\n * or from an assistive technology is real focus, and a link that stays\n * hidden for it would be a trap. Every reveal utility carries the `:focus`\n * variant, so each out-specifies its hidden twin and the pair cannot\n * reorder regardless of stylesheet order.\n *\n * The revealed state speaks tokens only — `bg-primary`/`text-primary-text`\n * is the theme's pinned contrast pair and `outline-ring` + `shadow-halo`\n * are the shared focus markers — and carries no transition, so\n * `prefers-reduced-motion` needs no exception here.\n */\nexport const skipLinkClass = [\n \"absolute h-px w-px overflow-hidden whitespace-nowrap [clip:rect(0,0,0,0)]\",\n \"focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:h-auto focus:w-auto focus:overflow-visible focus:whitespace-normal focus:[clip:auto] focus:p-4 focus:rounded-md focus:bg-primary focus:text-primary-text focus:text-sm focus:font-medium focus:underline focus:underline-offset-4 focus:shadow-halo focus:outline-2 focus:outline-offset-2 focus:outline-ring\",\n].join(\" \");\n</script>\n\n<script setup lang=\"ts\">\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\nconst props = withDefaults(\n defineProps<{\n /** The in-page destination. Defaults to `#main`, the id the documentation names as the main-content landmark. */\n href?: string;\n /**\n * Names for what the link says, as any subset of `SkipLinkLabels` — the\n * per-instance correction over `SKIP_LINK_LABELS`, in the same shape\n * every component's `labels` prop takes.\n */\n labels?: LabelOverrides<SkipLinkLabels>;\n }>(),\n { href: \"#main\" },\n);\n\n// `text`, not `labels`: the prop of that name is one of the three sources\n// this resolves (own prop, then the host vocabulary from\n// `provideLoomLabels`, then these English defaults), and a template reading\n// the raw prop would be reading the overrides rather than the answer.\nconst text = useLabels(\"skipLink\", SKIP_LINK_LABELS, () => props.labels);\n</script>\n\n<template>\n <a :href=\"href\" :class=\"cn(skipLinkClass, $attrs.class)\">{{ text.label }}</a>\n</template>\n"],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { type LabelOverrides } from "../../../labels/src/index";
2
+ import type { SkipLinkLabels } from "../../../labels/src/index";
3
+ /**
4
+ * SkipLink — the WCAG 2.4.1 (Bypass Blocks) affordance.
5
+ *
6
+ * A link to the main content that is the page's first focusable element and
7
+ * stays visually hidden until something focuses it. A keyboard user tabs
8
+ * once from the top of the document and lands on it; a pointer user never
9
+ * meets it. It is a primitive beside `Link` rather than a variant of it
10
+ * because the two answer different questions: `Link` navigates, `SkipLink`
11
+ * bypasses — and its entire presentation *is* the hidden-until-focus
12
+ * contract, which is exactly what no consumer should re-answer per product.
13
+ *
14
+ * Placement is the consumer's half of the contract: render it first in the
15
+ * DOM, before the header and navigation it bypasses (next to the app shell),
16
+ * and give the destination `tabindex="-1"` so following the link moves focus
17
+ * there instead of only scrolling.
18
+ */
19
+ export declare const SKIP_LINK_LABELS: SkipLinkLabels;
20
+ /**
21
+ * Visually hidden until focused. `absolute` takes the link out of flow so
22
+ * being first in the document reserves no space; the 1px clipped box is the
23
+ * visually-hidden pattern — out of sight but focusable, so DOM order keeps
24
+ * it the first tab stop.
25
+ *
26
+ * The reveal keys on `:focus`, deliberately not `:focus-visible`: focus
27
+ * arriving from a skip target's own activation, from programmatic handoff
28
+ * or from an assistive technology is real focus, and a link that stays
29
+ * hidden for it would be a trap. Every reveal utility carries the `:focus`
30
+ * variant, so each out-specifies its hidden twin and the pair cannot
31
+ * reorder regardless of stylesheet order.
32
+ *
33
+ * The revealed state speaks tokens only — `bg-primary`/`text-primary-text`
34
+ * is the theme's pinned contrast pair and `outline-ring` + `shadow-halo`
35
+ * are the shared focus markers — and carries no transition, so
36
+ * `prefers-reduced-motion` needs no exception here.
37
+ */
38
+ export declare const skipLinkClass: string;
39
+ declare const _default: typeof __VLS_export;
40
+ export default _default;
41
+ declare const __VLS_export: import("vue").DefineComponent<{
42
+ /** The in-page destination. Defaults to `#main`, the id the documentation names as the main-content landmark. */
43
+ href?: string;
44
+ /**
45
+ * Names for what the link says, as any subset of `SkipLinkLabels` — the
46
+ * per-instance correction over `SKIP_LINK_LABELS`, in the same shape
47
+ * every component's `labels` prop takes.
48
+ */
49
+ labels?: LabelOverrides<SkipLinkLabels>;
50
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
51
+ /** The in-page destination. Defaults to `#main`, the id the documentation names as the main-content landmark. */
52
+ href?: string;
53
+ /**
54
+ * Names for what the link says, as any subset of `SkipLinkLabels` — the
55
+ * per-instance correction over `SKIP_LINK_LABELS`, in the same shape
56
+ * every component's `labels` prop takes.
57
+ */
58
+ labels?: LabelOverrides<SkipLinkLabels>;
59
+ }> & Readonly<{}>, {
60
+ href: string;
61
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
62
+ //# sourceMappingURL=SkipLink.vue.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { cn } from "../../../core/src/cn.js";
2
+ import "../../../core/src/index.js";
3
+ import { useLabels } from "../../../labels/src/labels.js";
4
+ import "../../../labels/src/index.js";
5
+ import { createElementBlock, defineComponent, normalizeClass, openBlock, toDisplayString, unref } from "vue";
6
+ //#region packages/primitives/skip-link/src/SkipLink.vue?vue&type=script&setup=true&lang.ts
7
+ var _hoisted_1 = ["href"];
8
+ /**
9
+ * SkipLink — the WCAG 2.4.1 (Bypass Blocks) affordance.
10
+ *
11
+ * A link to the main content that is the page's first focusable element and
12
+ * stays visually hidden until something focuses it. A keyboard user tabs
13
+ * once from the top of the document and lands on it; a pointer user never
14
+ * meets it. It is a primitive beside `Link` rather than a variant of it
15
+ * because the two answer different questions: `Link` navigates, `SkipLink`
16
+ * bypasses — and its entire presentation *is* the hidden-until-focus
17
+ * contract, which is exactly what no consumer should re-answer per product.
18
+ *
19
+ * Placement is the consumer's half of the contract: render it first in the
20
+ * DOM, before the header and navigation it bypasses (next to the app shell),
21
+ * and give the destination `tabindex="-1"` so following the link moves focus
22
+ * there instead of only scrolling.
23
+ */
24
+ var SKIP_LINK_LABELS = { label: "Skip to main content" };
25
+ /**
26
+ * Visually hidden until focused. `absolute` takes the link out of flow so
27
+ * being first in the document reserves no space; the 1px clipped box is the
28
+ * visually-hidden pattern — out of sight but focusable, so DOM order keeps
29
+ * it the first tab stop.
30
+ *
31
+ * The reveal keys on `:focus`, deliberately not `:focus-visible`: focus
32
+ * arriving from a skip target's own activation, from programmatic handoff
33
+ * or from an assistive technology is real focus, and a link that stays
34
+ * hidden for it would be a trap. Every reveal utility carries the `:focus`
35
+ * variant, so each out-specifies its hidden twin and the pair cannot
36
+ * reorder regardless of stylesheet order.
37
+ *
38
+ * The revealed state speaks tokens only — `bg-primary`/`text-primary-text`
39
+ * is the theme's pinned contrast pair and `outline-ring` + `shadow-halo`
40
+ * are the shared focus markers — and carries no transition, so
41
+ * `prefers-reduced-motion` needs no exception here.
42
+ */
43
+ var skipLinkClass = ["absolute h-px w-px overflow-hidden whitespace-nowrap [clip:rect(0,0,0,0)]", "focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:h-auto focus:w-auto focus:overflow-visible focus:whitespace-normal focus:[clip:auto] focus:p-4 focus:rounded-md focus:bg-primary focus:text-primary-text focus:text-sm focus:font-medium focus:underline focus:underline-offset-4 focus:shadow-halo focus:outline-2 focus:outline-offset-2 focus:outline-ring"].join(" ");
44
+ var SkipLink_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
45
+ __name: "SkipLink",
46
+ props: {
47
+ href: { default: "#main" },
48
+ labels: {}
49
+ },
50
+ setup(__props) {
51
+ const props = __props;
52
+ const text = useLabels("skipLink", SKIP_LINK_LABELS, () => props.labels);
53
+ return (_ctx, _cache) => {
54
+ return openBlock(), createElementBlock("a", {
55
+ href: __props.href,
56
+ class: normalizeClass(unref(cn)(unref(skipLinkClass), _ctx.$attrs.class))
57
+ }, toDisplayString(unref(text).label), 11, _hoisted_1);
58
+ };
59
+ }
60
+ });
61
+ //#endregion
62
+ export { SKIP_LINK_LABELS, SkipLink_vue_vue_type_script_setup_true_lang_default as default, skipLinkClass };
63
+
64
+ //# sourceMappingURL=SkipLink.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkipLink.vue_vue_type_script_setup_true_lang.js","names":["$attrs"],"sources":["../../../../../packages/primitives/skip-link/src/SkipLink.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { SkipLinkLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * SkipLink — the WCAG 2.4.1 (Bypass Blocks) affordance.\n *\n * A link to the main content that is the page's first focusable element and\n * stays visually hidden until something focuses it. A keyboard user tabs\n * once from the top of the document and lands on it; a pointer user never\n * meets it. It is a primitive beside `Link` rather than a variant of it\n * because the two answer different questions: `Link` navigates, `SkipLink`\n * bypasses — and its entire presentation *is* the hidden-until-focus\n * contract, which is exactly what no consumer should re-answer per product.\n *\n * Placement is the consumer's half of the contract: render it first in the\n * DOM, before the header and navigation it bypasses (next to the app shell),\n * and give the destination `tabindex=\"-1\"` so following the link moves focus\n * there instead of only scrolling.\n */\nexport const SKIP_LINK_LABELS: SkipLinkLabels = {\n label: \"Skip to main content\",\n};\n\n/**\n * Visually hidden until focused. `absolute` takes the link out of flow so\n * being first in the document reserves no space; the 1px clipped box is the\n * visually-hidden pattern — out of sight but focusable, so DOM order keeps\n * it the first tab stop.\n *\n * The reveal keys on `:focus`, deliberately not `:focus-visible`: focus\n * arriving from a skip target's own activation, from programmatic handoff\n * or from an assistive technology is real focus, and a link that stays\n * hidden for it would be a trap. Every reveal utility carries the `:focus`\n * variant, so each out-specifies its hidden twin and the pair cannot\n * reorder regardless of stylesheet order.\n *\n * The revealed state speaks tokens only — `bg-primary`/`text-primary-text`\n * is the theme's pinned contrast pair and `outline-ring` + `shadow-halo`\n * are the shared focus markers — and carries no transition, so\n * `prefers-reduced-motion` needs no exception here.\n */\nexport const skipLinkClass = [\n \"absolute h-px w-px overflow-hidden whitespace-nowrap [clip:rect(0,0,0,0)]\",\n \"focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:h-auto focus:w-auto focus:overflow-visible focus:whitespace-normal focus:[clip:auto] focus:p-4 focus:rounded-md focus:bg-primary focus:text-primary-text focus:text-sm focus:font-medium focus:underline focus:underline-offset-4 focus:shadow-halo focus:outline-2 focus:outline-offset-2 focus:outline-ring\",\n].join(\" \");\n</script>\n\n<script setup lang=\"ts\">\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\nconst props = withDefaults(\n defineProps<{\n /** The in-page destination. Defaults to `#main`, the id the documentation names as the main-content landmark. */\n href?: string;\n /**\n * Names for what the link says, as any subset of `SkipLinkLabels` — the\n * per-instance correction over `SKIP_LINK_LABELS`, in the same shape\n * every component's `labels` prop takes.\n */\n labels?: LabelOverrides<SkipLinkLabels>;\n }>(),\n { href: \"#main\" },\n);\n\n// `text`, not `labels`: the prop of that name is one of the three sources\n// this resolves (own prop, then the host vocabulary from\n// `provideLoomLabels`, then these English defaults), and a template reading\n// the raw prop would be reading the overrides rather than the answer.\nconst text = useLabels(\"skipLink\", SKIP_LINK_LABELS, () => props.labels);\n</script>\n\n<template>\n <a :href=\"href\" :class=\"cn(skipLinkClass, $attrs.class)\">{{ text.label }}</a>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmBA,IAAa,mBAAmC,EAC9C,OAAO,uBACT;;;;;;;;;;;;;;;;;;;AAoBA,IAAa,gBAAgB,CAC3B,6EACA,qWACF,CAAC,CAAC,KAAK,GAAG;;;;;;;;EAOV,MAAM,QAAQ;EAkBd,MAAM,OAAO,UAAU,YAAY,wBAAwB,MAAM,MAAM;;GAIrE,OAAA,UAAA,GAAA,mBAA6E,KAAA;IAAzE,MAAM,QAAA;IAAO,OAAK,eAAE,MAAA,EAAA,CAAE,CAAC,MAAA,aAAA,GAAeA,KAAAA,OAAO,KAAK,CAAA;GAAM,GAAA,gBAAA,MAAA,IAAA,CAAI,CAAC,KAAK,GAAA,IAAA,UAAA"}
@@ -0,0 +1,3 @@
1
+ export { SKIP_LINK_LABELS, default } from "./SkipLink.vue";
2
+ export type { SkipLinkLabels } from "../../../labels/src/index";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SKIP_LINK_LABELS } from "./SkipLink.vue_vue_type_script_setup_true_lang.js";
2
+ import SkipLink_default from "./SkipLink.js";
3
+ export { SKIP_LINK_LABELS, SkipLink_default as default };
@@ -1 +1 @@
1
- {"version":3,"file":"Stepper.js","names":[],"sources":["../../../../../packages/primitives/stepper/src/Stepper.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { StepperLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n *\n * `group` is capitalised where Reka's is not, which is what lets\n * `Stepper.test.ts` assert whose string reached the DOM rather than merely\n * what it said.\n */\nexport const STEPPER_LABELS: StepperLabels = {\n group: \"Progress\",\n position: ({ step, stepCount }) => `Step ${String(step)} of ${String(stepCount)}`,\n completed: ({ title }) => `${title} (completed)`,\n};\n\n/** One step in the spine. Its `title` is also the accessible name of that step's trigger. */\nexport interface StepperStep {\n /** The step's label, shown next to its indicator and read out as the trigger's name. */\n title: string;\n /** A line under the title saying what the step asks for. Omit it for a bare spine. */\n description?: string;\n /** Present but unreachable: the indicator greys and the title mutes, and neither pointer nor keyboard can select it. */\n disabled?: boolean;\n}\n\n/** The axis the spine runs along, which also decides which arrow keys move between steps. */\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport {\n StepperDescription,\n StepperIndicator,\n StepperItem,\n StepperRoot,\n StepperSeparator,\n StepperTitle,\n StepperTrigger,\n} from \"reka-ui\";\nimport { Check } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * Stepper — the progress spine of a multi-step flow: a checkout, an\n * onboarding sequence, a wizard. It says how many steps there are, which one\n * a reader is on, and which are behind them; the step's own content is the\n * host's, rendered next to or under the spine.\n *\n * It is **not** a number stepper. A numeric value with increment and\n * decrement buttons is NumberField. The word is genuinely ambiguous and both\n * controls answer to it, so the two are worth naming together: this one is\n * about position in a flow, that one is about a quantity.\n *\n * Progress shows one bar for one task whose extent is a percentage — reach\n * for it when the stages have no names worth showing. Tabs switches between\n * panels that have no order at all; a stepper's steps are ordered by\n * definition, which is why it carries a connector and Tabs carries none.\n *\n * Built on Reka UI's Stepper, which supplies the item state machine\n * (completed / active / inactive), the `linear` reachability rule and\n * arrow-key navigation along the orientation's axis.\n *\n * Three things are added on top of it.\n *\n * The first is the roving tab stop. Reka gives *every* focusable trigger\n * `tabindex=\"0\"`, so a five-step spine would be five Tab stops — the defect\n * the accessibility bar names outright. The `tabindex` bound below overrides\n * Reka's, leaving exactly one trigger tabbable at a time (the one last\n * focused, else the current step); Reka's own arrow-key handler still moves\n * real focus between them, which `tabindex=\"-1\"` does not prevent.\n *\n * The second is where `aria-current` sits. Reka puts `aria-current=\"true\"` on\n * the item wrapper, a `div` with no role that a reader never lands on, so the\n * marking is not conveyed at the moment it matters. It is moved here onto the\n * trigger — the element that takes focus — and given the value the WAI-ARIA\n * pattern asks for on a step in a process.\n *\n * The third is the live region. Reka renders one of its own on the root and\n * writes \"Step N of M\" into it as a **text node inside its render function** —\n * not a prop, not a slot, not an attribute — which makes it the one string in\n * this library that no binding on Loom's side can replace. So Loom renders its\n * own alongside it and takes Reka's out of the accessibility tree from\n * `src/styles/global.css`, which carries that reasoning and the coupling it\n * creates. `e2e/stepper.e2e.ts` is what keeps the coupling honest: it asserts\n * that a Loom stepper has exactly one live region and that its words are\n * Loom's.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current step, 1-based. Omit it and Stepper owns its own position. */\n modelValue?: number;\n /** The ordered steps. Their index decides their number; there is no id to keep. */\n steps: StepperStep[];\n /** The axis the spine runs along; vertical moves the titles beside the indicators. */\n orientation?: StepperOrientation;\n /** Locks the flow to its furthest point: a reader may go back, or one step on, never jump ahead. */\n linear?: boolean;\n /**\n * Names for everything the spine says out loud, as any subset of\n * `StepperLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * A whole application's language belongs in that vocabulary rather than\n * here. This is the per-instance correction, and the key it exists for is\n * `group`: a page with two flows on it needs each named for what it steps\n * through.\n */\n labels?: LabelOverrides<StepperLabels>;\n }>(),\n { orientation: \"horizontal\", linear: false },\n);\n\nconst emit = defineEmits<{\n /** The step chosen, 1-based. Fires for a trigger click and for Enter/Space on one. */\n \"update:modelValue\": [value: number];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"stepper\", STEPPER_LABELS, () => props.labels);\n\n// The root renders a real element and it is the one every fallthrough\n// attribute belongs on: it is the `role=\"group\"` the whole spine lives in, so\n// an `aria-label` naming the flow lands there. It is spread *after*\n// `text.group` in the template for that reason — the vocabulary supplies the\n// name a spine has when nobody names it, and a caller who does name one still\n// wins. `class` is pulled out of the spread and merged through `cn()` instead,\n// so a caller's layout utility resolves against the root's own rather than\n// merely being concatenated after it.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: rootAttrs } = useSplitAttrs();\n\n/**\n * The two layouts, side by side rather than as template branches that drift\n * apart. `satisfies` is what stops an orientation being added to the type and\n * forgotten here.\n *\n * Horizontal hangs the connector off the item box (`relative`) rather than\n * off the rail: `left-1/2 w-full` then spans exactly the distance between two\n * indicator centres, since the items are equal-width flex children. It is\n * drawn behind them — the trigger is `relative`, so it and every later item's\n * trigger paint over a line that runs under the circles.\n */\nconst LAYOUTS = {\n horizontal: {\n root: \"flex w-full items-start\",\n item: \"relative flex flex-1 flex-col items-center gap-2 text-center\",\n rail: \"\",\n separator: \"absolute left-1/2 top-4 h-0.5 w-full -translate-y-1/2\",\n fill: \"origin-left scale-x-0\",\n filled: \"scale-x-100\",\n text: \"flex flex-col gap-0.5\",\n },\n vertical: {\n root: \"flex flex-col\",\n item: \"flex gap-3\",\n rail: \"flex flex-col items-center self-stretch\",\n // `order-1` rather than a second template branch: the connector has to be\n // declared before the trigger so the trigger paints over it, and has to sit\n // below the trigger once the rail is a column.\n separator: \"order-1 my-1 w-0.5 flex-1\",\n fill: \"origin-top scale-y-0\",\n filled: \"scale-y-100\",\n text: \"flex flex-col gap-0.5\",\n },\n} satisfies Record<StepperOrientation, Record<string, string>>;\n\nconst layout = computed(() => LAYOUTS[props.orientation]);\n\n/** The step that last held focus, and so owns the spine's single tab stop. */\nconst focused = ref<number | null>(null);\n\n/**\n * Reka's own focusability rule, mirrored: a linear stepper refuses focus to\n * anything past one step ahead of where the reader is. It has to be repeated\n * rather than read off the DOM, because the tab stop is decided while the\n * `tabindex` attributes are still being rendered — and a tab stop parked on a\n * step Reka will not focus leaves the whole spine unreachable by Tab.\n */\nfunction focusable(step: number, current: number): boolean {\n if (props.steps[step - 1]?.disabled) return false;\n return props.linear ? step <= current + 1 : true;\n}\n\n/**\n * The one step whose trigger is tabbable. Everything else is `tabindex=\"-1\"`.\n *\n * Reka types the value it hands its own slot as possibly absent even though\n * `StepperRoot` defaults it to 1, so the fallback here is that declaration\n * being honoured rather than a state this can reach.\n */\nfunction tabStop(current: number | undefined): number {\n const at = current ?? 1;\n if (focused.value !== null && focusable(focused.value, at)) return focused.value;\n if (focusable(at, at)) return at;\n return props.steps.findIndex((_, index) => focusable(index + 1, at)) + 1;\n}\n\n/**\n * Reka points every trigger's `aria-describedby` at a description id whether\n * or not that description was rendered, and a reference resolving to nothing\n * is worse than no reference: it claims a description exists. Binding the key\n * as `undefined` is what takes it back off, since an attribute set to\n * `undefined` is removed rather than merged.\n */\nfunction describedBy(step: StepperStep): Record<string, undefined> {\n return step.description === undefined ? { \"aria-describedby\": undefined } : {};\n}\n\n/** Reka reports its position as possibly absent; this component's model is a step number or nothing at all. */\nfunction onUpdate(value: number | undefined): void {\n if (value !== undefined) emit(\"update:modelValue\", value);\n}\n</script>\n\n<template>\n <StepperRoot\n v-slot=\"{ modelValue: current }\"\n v-bind=\"{ 'aria-label': text.group, ...rootAttrs, ...optional({ modelValue }) }\"\n data-loom-stepper\n :orientation=\"orientation\"\n :linear=\"linear\"\n :class=\"cn(layout.root, attrs.class as string)\"\n @update:model-value=\"onUpdate\"\n >\n <StepperItem\n v-for=\"(step, index) in steps\"\n :key=\"index\"\n v-slot=\"{ state }\"\n :step=\"index + 1\"\n :disabled=\"step.disabled ?? false\"\n :aria-current=\"undefined\"\n :class=\"layout.item\"\n >\n <div :class=\"layout.rail\">\n <!-- Declared before the trigger so the trigger paints over it: the\n connector runs from this indicator's centre to the next one's. -->\n <StepperSeparator\n v-if=\"index < steps.length - 1\"\n :class=\"cn('overflow-hidden rounded-full bg-border', layout.separator)\"\n >\n <span\n :class=\"\n cn(\n 'block h-full w-full bg-primary transition-transform ease-out',\n layout.fill,\n state === 'completed' && layout.filled,\n )\n \"\n />\n </StepperSeparator>\n\n <StepperTrigger\n v-bind=\"describedBy(step)\"\n :tabindex=\"tabStop(current) === index + 1 ? 0 : -1\"\n :aria-current=\"state === 'active' ? 'step' : undefined\"\n :class=\"\n cn(\n 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-xs font-medium',\n 'transition-colors ease-out',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Unavailable is a *colour*, and the whole distinction lives on\n // the indicator. It used to be `opacity-50` on the item, which\n // took the title and the description down with it:\n // `--color-muted-foreground` is 5.76:1 on white and 2.11:1 once\n // composited at half alpha, so the one thing that had to stay\n // readable was the one thing the fade broke. A grey-filled disc\n // against the inactive step's white one says the same thing and\n // costs nothing, and every string below is a measured pair.\n //\n // Keyed off the prop and NOT off `data-[disabled]:`, because in\n // a linear flow Reka marks every step past the next one disabled\n // — painting all of them unavailable reads as broken rather than\n // as later, and a step that is merely ahead already says so by\n // being inactive. The branch also keeps the two apart in the\n // cascade: the inactive rules carry an attribute selector, so a\n // plain `bg-muted` alongside them would lose.\n step.disabled\n ? 'border-border-strong bg-muted text-muted-foreground'\n : [\n 'data-[state=inactive]:border-input data-[state=inactive]:bg-background data-[state=inactive]:text-muted-foreground',\n 'data-[state=inactive]:enabled:hover:bg-subtle',\n ],\n 'data-[state=active]:border-primary data-[state=active]:bg-primary-muted data-[state=active]:text-primary-text',\n 'data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground',\n 'disabled:cursor-not-allowed',\n )\n \"\n @focus=\"focused = index + 1\"\n >\n <StepperIndicator class=\"flex items-center justify-center\">\n <!-- Three states, none of them colour alone: the check is a shape\n change, the number is text, and the current step carries\n `aria-current` above. -->\n <Check v-if=\"state === 'completed'\" class=\"h-4 w-4 animate-scale-in\" />\n <template v-else>{{ index + 1 }}</template>\n </StepperIndicator>\n </StepperTrigger>\n </div>\n\n <div\n :class=\"cn(layout.text, orientation === 'vertical' && index < steps.length - 1 && 'pb-6')\"\n >\n <!-- `as=\"span\"`, not Reka's default `h4`: a step label is not a\n section heading, and a component cannot know what heading level\n the page it lands in has room for. -->\n <!-- The title mutes rather than fades: `--color-muted-foreground` is a\n measured 5.76:1 on the card it sits on, two steps back from\n `--color-foreground` and still comfortably readable. The\n description below is already that colour and stays there — an\n unreachable step's description is the one line a reader most needs\n in order to find out why it is unreachable. -->\n <StepperTitle\n as=\"span\"\n :class=\"\n cn('text-sm font-medium', step.disabled ? 'text-muted-foreground' : 'text-foreground')\n \"\n >\n <!-- ARIA has a value for \"you are here\" and none for \"done\", and the\n check glyph is invisible to a screen reader. This element is what\n `aria-labelledby` names the trigger with, so the word has to join\n the name from inside it.\n Two nodes rather than the title plus a trailing \"(completed)\"\n span, because a suffix is Loom deciding where a qualifier sits\n relative to the thing it qualifies, and that is a property of the\n language. The visible half is `aria-hidden` and excluded from the\n name computation; the hidden half carries the whole finished\n sentence the vocabulary returned. -->\n <template v-if=\"state === 'completed'\">\n <span aria-hidden=\"true\">{{ step.title }}</span>\n <span class=\"sr-only\">{{ text.completed({ title: step.title }) }}</span>\n </template>\n <template v-else>{{ step.title }}</template>\n </StepperTitle>\n <StepperDescription v-if=\"step.description\" class=\"text-xs text-muted-foreground\">\n {{ step.description }}\n </StepperDescription>\n </div>\n </StepperItem>\n\n <!-- Loom's own position announcement, standing in for the one `StepperRoot`\n renders as a text node its render function hard-codes in English.\n `data-loom-live` is what the rule in `global.css` selects *around* when\n it removes Reka's from the accessibility tree, so the two are told\n apart by what Loom marked rather than by DOM order.\n The count comes from `steps` rather than from Reka's registry, which\n fills in over a tick as each trigger mounts — the same number, one\n render sooner, which matters for a region a screen reader reads on\n arrival. -->\n <span data-loom-live role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"sr-only\">{{\n text.position({ step: current ?? 1, stepCount: steps.length })\n }}</span>\n </StepperRoot>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"Stepper.js","names":[],"sources":["../../../../../packages/primitives/stepper/src/Stepper.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { StepperLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n *\n * `group` is capitalised where Reka's is not, which is what lets\n * `Stepper.test.ts` assert whose string reached the DOM rather than merely\n * what it said.\n */\nexport const STEPPER_LABELS: StepperLabels = {\n group: \"Progress\",\n position: ({ step, stepCount }) => `Step ${String(step)} of ${String(stepCount)}`,\n step: ({ step }) => `Step ${String(step)}:`,\n completed: ({ title }) => `${title} (completed)`,\n};\n\n/** One step in the spine. Its `title` is also the accessible name of that step's trigger. */\nexport interface StepperStep {\n /** The step's label, shown next to its indicator and read out as the trigger's name. */\n title: string;\n /** A line under the title saying what the step asks for. Omit it for a bare spine. */\n description?: string;\n /** Present but unreachable: the indicator greys and the title mutes, and neither pointer nor keyboard can select it. */\n disabled?: boolean;\n}\n\n/** The axis the spine runs along, which also decides which arrow keys move between steps. */\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport {\n StepperDescription,\n StepperIndicator,\n StepperItem,\n StepperRoot,\n StepperSeparator,\n StepperTitle,\n StepperTrigger,\n} from \"reka-ui\";\nimport { Check } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * Stepper — the progress spine of a multi-step flow: a checkout, an\n * onboarding sequence, a wizard. It says how many steps there are, which one\n * a reader is on, and which are behind them; the step's own content is the\n * host's, rendered next to or under the spine.\n *\n * It is **not** a number stepper. A numeric value with increment and\n * decrement buttons is NumberField. The word is genuinely ambiguous and both\n * controls answer to it, so the two are worth naming together: this one is\n * about position in a flow, that one is about a quantity.\n *\n * Progress shows one bar for one task whose extent is a percentage — reach\n * for it when the stages have no names worth showing. Tabs switches between\n * panels that have no order at all; a stepper's steps are ordered by\n * definition, which is why it carries a connector and Tabs carries none.\n *\n * Built on Reka UI's Stepper, which supplies the item state machine\n * (completed / active / inactive), the `linear` reachability rule and\n * arrow-key navigation along the orientation's axis.\n *\n * Three things are added on top of it.\n *\n * The first is the roving tab stop. Reka gives *every* focusable trigger\n * `tabindex=\"0\"`, so a five-step spine would be five Tab stops — the defect\n * the accessibility bar names outright. The `tabindex` bound below overrides\n * Reka's, leaving exactly one trigger tabbable at a time (the one last\n * focused, else the current step); Reka's own arrow-key handler still moves\n * real focus between them, which `tabindex=\"-1\"` does not prevent.\n *\n * The second is where `aria-current` sits. Reka puts `aria-current=\"true\"` on\n * the item wrapper, a `div` with no role that a reader never lands on, so the\n * marking is not conveyed at the moment it matters. It is moved here onto the\n * trigger — the element that takes focus — and given the value the WAI-ARIA\n * pattern asks for on a step in a process.\n *\n * The third is the live region. Reka renders one of its own on the root and\n * writes \"Step N of M\" into it as a **text node inside its render function** —\n * not a prop, not a slot, not an attribute — which makes it the one string in\n * this library that no binding on Loom's side can replace. So Loom renders its\n * own alongside it and takes Reka's out of the accessibility tree from\n * `src/styles/global.css`, which carries that reasoning and the coupling it\n * creates. `e2e/stepper.e2e.ts` is what keeps the coupling honest: it asserts\n * that a Loom stepper has exactly one live region and that its words are\n * Loom's.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current step, 1-based. Omit it and Stepper owns its own position. */\n modelValue?: number;\n /** The ordered steps. Their index decides their number; there is no id to keep. */\n steps: StepperStep[];\n /** The axis the spine runs along; vertical moves the titles beside the indicators. */\n orientation?: StepperOrientation;\n /** Locks the flow to its furthest point: a reader may go back, or one step on, never jump ahead. */\n linear?: boolean;\n /**\n * Names for everything the spine says out loud, as any subset of\n * `StepperLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * A whole application's language belongs in that vocabulary rather than\n * here. This is the per-instance correction, and the key it exists for is\n * `group`: a page with two flows on it needs each named for what it steps\n * through.\n */\n labels?: LabelOverrides<StepperLabels>;\n }>(),\n { orientation: \"horizontal\", linear: false },\n);\n\nconst emit = defineEmits<{\n /** The step chosen, 1-based. Fires for a trigger click and for Enter/Space on one. */\n \"update:modelValue\": [value: number];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"stepper\", STEPPER_LABELS, () => props.labels);\n\n// The root renders a real element and it is the one every fallthrough\n// attribute belongs on: it is the `role=\"group\"` the whole spine lives in, so\n// an `aria-label` naming the flow lands there. It is spread *after*\n// `text.group` in the template for that reason — the vocabulary supplies the\n// name a spine has when nobody names it, and a caller who does name one still\n// wins. `class` is pulled out of the spread and merged through `cn()` instead,\n// so a caller's layout utility resolves against the root's own rather than\n// merely being concatenated after it.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: rootAttrs } = useSplitAttrs();\n\n/**\n * The two layouts, side by side rather than as template branches that drift\n * apart. `satisfies` is what stops an orientation being added to the type and\n * forgotten here.\n *\n * Horizontal hangs the connector off the item box (`relative`) rather than\n * off the rail: `left-1/2 w-full` then spans exactly the distance between two\n * indicator centres, since the items are equal-width flex children. It is\n * drawn behind them — the trigger is `relative`, so it and every later item's\n * trigger paint over a line that runs under the circles.\n */\nconst LAYOUTS = {\n horizontal: {\n root: \"flex w-full items-start\",\n item: \"relative flex flex-1 flex-col items-center gap-2 text-center\",\n rail: \"\",\n separator: \"absolute left-1/2 top-4 h-0.5 w-full -translate-y-1/2\",\n fill: \"origin-left scale-x-0\",\n filled: \"scale-x-100\",\n text: \"flex flex-col gap-0.5\",\n },\n vertical: {\n root: \"flex flex-col\",\n item: \"flex gap-3\",\n rail: \"flex flex-col items-center self-stretch\",\n // `order-1` rather than a second template branch: the connector has to be\n // declared before the trigger so the trigger paints over it, and has to sit\n // below the trigger once the rail is a column.\n separator: \"order-1 my-1 w-0.5 flex-1\",\n fill: \"origin-top scale-y-0\",\n filled: \"scale-y-100\",\n text: \"flex flex-col gap-0.5\",\n },\n} satisfies Record<StepperOrientation, Record<string, string>>;\n\nconst layout = computed(() => LAYOUTS[props.orientation]);\n\n/** The step that last held focus, and so owns the spine's single tab stop. */\nconst focused = ref<number | null>(null);\n\n/**\n * Reka's own focusability rule, mirrored: a linear stepper refuses focus to\n * anything past one step ahead of where the reader is. It has to be repeated\n * rather than read off the DOM, because the tab stop is decided while the\n * `tabindex` attributes are still being rendered — and a tab stop parked on a\n * step Reka will not focus leaves the whole spine unreachable by Tab.\n */\nfunction focusable(step: number, current: number): boolean {\n if (props.steps[step - 1]?.disabled) return false;\n return props.linear ? step <= current + 1 : true;\n}\n\n/**\n * The one step whose trigger is tabbable. Everything else is `tabindex=\"-1\"`.\n *\n * Reka types the value it hands its own slot as possibly absent even though\n * `StepperRoot` defaults it to 1, so the fallback here is that declaration\n * being honoured rather than a state this can reach.\n */\nfunction tabStop(current: number | undefined): number {\n const at = current ?? 1;\n if (focused.value !== null && focusable(focused.value, at)) return focused.value;\n if (focusable(at, at)) return at;\n return props.steps.findIndex((_, index) => focusable(index + 1, at)) + 1;\n}\n\n/**\n * Reka points every trigger's `aria-describedby` at a description id whether\n * or not that description was rendered, and a reference resolving to nothing\n * is worse than no reference: it claims a description exists. Binding the key\n * as `undefined` is what takes it back off, since an attribute set to\n * `undefined` is removed rather than merged.\n */\nfunction describedBy(step: StepperStep): Record<string, undefined> {\n return step.description === undefined ? { \"aria-describedby\": undefined } : {};\n}\n\n/** Reka reports its position as possibly absent; this component's model is a step number or nothing at all. */\nfunction onUpdate(value: number | undefined): void {\n if (value !== undefined) emit(\"update:modelValue\", value);\n}\n</script>\n\n<template>\n <StepperRoot\n v-slot=\"{ modelValue: current }\"\n v-bind=\"{ 'aria-label': text.group, ...rootAttrs, ...optional({ modelValue }) }\"\n data-loom-stepper\n :orientation=\"orientation\"\n :linear=\"linear\"\n :class=\"cn(layout.root, attrs.class as string)\"\n @update:model-value=\"onUpdate\"\n >\n <StepperItem\n v-for=\"(step, index) in steps\"\n :key=\"index\"\n v-slot=\"{ state }\"\n :step=\"index + 1\"\n :disabled=\"step.disabled ?? false\"\n :aria-current=\"undefined\"\n :class=\"layout.item\"\n >\n <div :class=\"layout.rail\">\n <!-- Declared before the trigger so the trigger paints over it: the\n connector runs from this indicator's centre to the next one's. -->\n <StepperSeparator\n v-if=\"index < steps.length - 1\"\n :class=\"cn('overflow-hidden rounded-full bg-border', layout.separator)\"\n >\n <span\n :class=\"\n cn(\n 'block h-full w-full bg-primary transition-transform ease-out',\n layout.fill,\n state === 'completed' && layout.filled,\n )\n \"\n />\n </StepperSeparator>\n\n <StepperTrigger\n v-bind=\"describedBy(step)\"\n :tabindex=\"tabStop(current) === index + 1 ? 0 : -1\"\n :aria-current=\"state === 'active' ? 'step' : undefined\"\n :class=\"\n cn(\n 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-xs font-medium',\n 'transition-colors ease-out',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Unavailable is a *colour*, and the whole distinction lives on\n // the indicator. It used to be `opacity-50` on the item, which\n // took the title and the description down with it:\n // `--color-muted-foreground` is 5.76:1 on white and 2.11:1 once\n // composited at half alpha, so the one thing that had to stay\n // readable was the one thing the fade broke. A grey-filled disc\n // against the inactive step's white one says the same thing and\n // costs nothing, and every string below is a measured pair.\n //\n // Keyed off the prop and NOT off `data-[disabled]:`, because in\n // a linear flow Reka marks every step past the next one disabled\n // — painting all of them unavailable reads as broken rather than\n // as later, and a step that is merely ahead already says so by\n // being inactive. The branch also keeps the two apart in the\n // cascade: the inactive rules carry an attribute selector, so a\n // plain `bg-muted` alongside them would lose.\n step.disabled\n ? 'border-border-strong bg-muted text-muted-foreground'\n : [\n 'data-[state=inactive]:border-input data-[state=inactive]:bg-background data-[state=inactive]:text-muted-foreground',\n 'data-[state=inactive]:enabled:hover:bg-subtle',\n ],\n 'data-[state=active]:border-primary data-[state=active]:bg-primary-muted data-[state=active]:text-primary-text',\n 'data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground',\n 'disabled:cursor-not-allowed',\n )\n \"\n @focus=\"focused = index + 1\"\n >\n <StepperIndicator class=\"flex items-center justify-center\">\n <!-- Three states, none of them colour alone: the check is a shape\n change, the number is text, and the current step carries\n `aria-current` above. -->\n <Check v-if=\"state === 'completed'\" class=\"h-4 w-4 animate-scale-in\" />\n <template v-else>{{ index + 1 }}</template>\n </StepperIndicator>\n </StepperTrigger>\n </div>\n\n <div\n :class=\"cn(layout.text, orientation === 'vertical' && index < steps.length - 1 && 'pb-6')\"\n >\n <!-- `as=\"span\"`, not Reka's default `h4`: a step label is not a\n section heading, and a component cannot know what heading level\n the page it lands in has room for. -->\n <!-- The title mutes rather than fades: `--color-muted-foreground` is a\n measured 5.76:1 on the card it sits on, two steps back from\n `--color-foreground` and still comfortably readable. The\n description below is already that colour and stays there — an\n unreachable step's description is the one line a reader most needs\n in order to find out why it is unreachable. -->\n <StepperTitle\n as=\"span\"\n :class=\"\n cn('text-sm font-medium', step.disabled ? 'text-muted-foreground' : 'text-foreground')\n \"\n >\n <!-- ARIA has a value for \"you are here\" and none for \"done\", and the\n check glyph is invisible to a screen reader. This element is what\n `aria-labelledby` names the trigger with, so the word has to join\n the name from inside it.\n Two nodes rather than the title plus a trailing \"(completed)\"\n span, because a suffix is Loom deciding where a qualifier sits\n relative to the thing it qualifies, and that is a property of the\n language. The visible half is `aria-hidden` and excluded from the\n name computation; the hidden half carries the whole finished\n sentence the vocabulary returned. -->\n <!-- The position leads the name for the same reason: the trigger's\n visible text is the step's number, and WCAG 2.5.3 (Label in\n Name) makes a control's visible text part of its accessible\n name. Measured 2026-08-26, `label-content-name-mismatch` fired\n on every non-completed trigger until the number joined the\n name — a voice user saying \"Step 2\" has to land on the same\n trigger one saying \"Shipping\" does. -->\n <span class=\"sr-only\">{{ text.step({ step: index + 1 }) }}</span>\n <template v-if=\"state === 'completed'\">\n <span aria-hidden=\"true\">{{ step.title }}</span>\n <span class=\"sr-only\">{{ text.completed({ title: step.title }) }}</span>\n </template>\n <template v-else>{{ step.title }}</template>\n </StepperTitle>\n <StepperDescription v-if=\"step.description\" class=\"text-xs text-muted-foreground\">\n {{ step.description }}\n </StepperDescription>\n </div>\n </StepperItem>\n\n <!-- Loom's own position announcement, standing in for the one `StepperRoot`\n renders as a text node its render function hard-codes in English.\n `data-loom-live` is what the rule in `global.css` selects *around* when\n it removes Reka's from the accessibility tree, so the two are told\n apart by what Loom marked rather than by DOM order.\n The count comes from `steps` rather than from Reka's registry, which\n fills in over a tick as each trigger mounts — the same number, one\n render sooner, which matters for a region a screen reader reads on\n arrival. -->\n <span data-loom-live role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"sr-only\">{{\n text.position({ step: current ?? 1, stepCount: steps.length })\n }}</span>\n </StepperRoot>\n</template>\n"],"mappings":""}
@@ -8,9 +8,10 @@ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock
8
8
  import { Check } from "@lucide/vue";
9
9
  import { StepperDescription, StepperIndicator, StepperItem, StepperRoot, StepperSeparator, StepperTitle, StepperTrigger } from "reka-ui";
10
10
  //#region packages/primitives/stepper/src/Stepper.vue?vue&type=script&setup=true&lang.ts
11
- var _hoisted_1 = { "aria-hidden": "true" };
12
- var _hoisted_2 = { class: "sr-only" };
13
- var _hoisted_3 = {
11
+ var _hoisted_1 = { class: "sr-only" };
12
+ var _hoisted_2 = { "aria-hidden": "true" };
13
+ var _hoisted_3 = { class: "sr-only" };
14
+ var _hoisted_4 = {
14
15
  "data-loom-live": "",
15
16
  role: "status",
16
17
  "aria-live": "polite",
@@ -30,6 +31,7 @@ var _hoisted_3 = {
30
31
  var STEPPER_LABELS = {
31
32
  group: "Progress",
32
33
  position: ({ step, stepCount }) => `Step ${String(step)} of ${String(stepCount)}`,
34
+ step: ({ step }) => `Step ${String(step)}:`,
33
35
  completed: ({ title }) => `${title} (completed)`
34
36
  };
35
37
  var Stepper_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
@@ -172,7 +174,7 @@ var Stepper_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
172
174
  as: "span",
173
175
  class: normalizeClass(unref(cn)("text-sm font-medium", step.disabled ? "text-muted-foreground" : "text-foreground"))
174
176
  }, {
175
- default: withCtx(() => [state === "completed" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", _hoisted_1, toDisplayString(step.title), 1), createElementVNode("span", _hoisted_2, toDisplayString(unref(text).completed({ title: step.title })), 1)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(step.title), 1)], 64))]),
177
+ default: withCtx(() => [createElementVNode("span", _hoisted_1, toDisplayString(unref(text).step({ step: index + 1 })), 1), state === "completed" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", _hoisted_2, toDisplayString(step.title), 1), createElementVNode("span", _hoisted_3, toDisplayString(unref(text).completed({ title: step.title })), 1)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(step.title), 1)], 64))]),
176
178
  _: 2
177
179
  }, 1032, ["class"]), step.description ? (openBlock(), createBlock(unref(StepperDescription), {
178
180
  key: 0,
@@ -187,7 +189,7 @@ var Stepper_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCo
187
189
  "disabled",
188
190
  "class"
189
191
  ]);
190
- }), 128)), createElementVNode("span", _hoisted_3, toDisplayString(unref(text).position({
192
+ }), 128)), createElementVNode("span", _hoisted_4, toDisplayString(unref(text).position({
191
193
  step: current ?? 1,
192
194
  stepCount: __props.steps.length
193
195
  })), 1)]),
@@ -1 +1 @@
1
- {"version":3,"file":"Stepper.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/stepper/src/Stepper.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { StepperLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n *\n * `group` is capitalised where Reka's is not, which is what lets\n * `Stepper.test.ts` assert whose string reached the DOM rather than merely\n * what it said.\n */\nexport const STEPPER_LABELS: StepperLabels = {\n group: \"Progress\",\n position: ({ step, stepCount }) => `Step ${String(step)} of ${String(stepCount)}`,\n completed: ({ title }) => `${title} (completed)`,\n};\n\n/** One step in the spine. Its `title` is also the accessible name of that step's trigger. */\nexport interface StepperStep {\n /** The step's label, shown next to its indicator and read out as the trigger's name. */\n title: string;\n /** A line under the title saying what the step asks for. Omit it for a bare spine. */\n description?: string;\n /** Present but unreachable: the indicator greys and the title mutes, and neither pointer nor keyboard can select it. */\n disabled?: boolean;\n}\n\n/** The axis the spine runs along, which also decides which arrow keys move between steps. */\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport {\n StepperDescription,\n StepperIndicator,\n StepperItem,\n StepperRoot,\n StepperSeparator,\n StepperTitle,\n StepperTrigger,\n} from \"reka-ui\";\nimport { Check } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * Stepper — the progress spine of a multi-step flow: a checkout, an\n * onboarding sequence, a wizard. It says how many steps there are, which one\n * a reader is on, and which are behind them; the step's own content is the\n * host's, rendered next to or under the spine.\n *\n * It is **not** a number stepper. A numeric value with increment and\n * decrement buttons is NumberField. The word is genuinely ambiguous and both\n * controls answer to it, so the two are worth naming together: this one is\n * about position in a flow, that one is about a quantity.\n *\n * Progress shows one bar for one task whose extent is a percentage — reach\n * for it when the stages have no names worth showing. Tabs switches between\n * panels that have no order at all; a stepper's steps are ordered by\n * definition, which is why it carries a connector and Tabs carries none.\n *\n * Built on Reka UI's Stepper, which supplies the item state machine\n * (completed / active / inactive), the `linear` reachability rule and\n * arrow-key navigation along the orientation's axis.\n *\n * Three things are added on top of it.\n *\n * The first is the roving tab stop. Reka gives *every* focusable trigger\n * `tabindex=\"0\"`, so a five-step spine would be five Tab stops — the defect\n * the accessibility bar names outright. The `tabindex` bound below overrides\n * Reka's, leaving exactly one trigger tabbable at a time (the one last\n * focused, else the current step); Reka's own arrow-key handler still moves\n * real focus between them, which `tabindex=\"-1\"` does not prevent.\n *\n * The second is where `aria-current` sits. Reka puts `aria-current=\"true\"` on\n * the item wrapper, a `div` with no role that a reader never lands on, so the\n * marking is not conveyed at the moment it matters. It is moved here onto the\n * trigger — the element that takes focus — and given the value the WAI-ARIA\n * pattern asks for on a step in a process.\n *\n * The third is the live region. Reka renders one of its own on the root and\n * writes \"Step N of M\" into it as a **text node inside its render function** —\n * not a prop, not a slot, not an attribute — which makes it the one string in\n * this library that no binding on Loom's side can replace. So Loom renders its\n * own alongside it and takes Reka's out of the accessibility tree from\n * `src/styles/global.css`, which carries that reasoning and the coupling it\n * creates. `e2e/stepper.e2e.ts` is what keeps the coupling honest: it asserts\n * that a Loom stepper has exactly one live region and that its words are\n * Loom's.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current step, 1-based. Omit it and Stepper owns its own position. */\n modelValue?: number;\n /** The ordered steps. Their index decides their number; there is no id to keep. */\n steps: StepperStep[];\n /** The axis the spine runs along; vertical moves the titles beside the indicators. */\n orientation?: StepperOrientation;\n /** Locks the flow to its furthest point: a reader may go back, or one step on, never jump ahead. */\n linear?: boolean;\n /**\n * Names for everything the spine says out loud, as any subset of\n * `StepperLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * A whole application's language belongs in that vocabulary rather than\n * here. This is the per-instance correction, and the key it exists for is\n * `group`: a page with two flows on it needs each named for what it steps\n * through.\n */\n labels?: LabelOverrides<StepperLabels>;\n }>(),\n { orientation: \"horizontal\", linear: false },\n);\n\nconst emit = defineEmits<{\n /** The step chosen, 1-based. Fires for a trigger click and for Enter/Space on one. */\n \"update:modelValue\": [value: number];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"stepper\", STEPPER_LABELS, () => props.labels);\n\n// The root renders a real element and it is the one every fallthrough\n// attribute belongs on: it is the `role=\"group\"` the whole spine lives in, so\n// an `aria-label` naming the flow lands there. It is spread *after*\n// `text.group` in the template for that reason — the vocabulary supplies the\n// name a spine has when nobody names it, and a caller who does name one still\n// wins. `class` is pulled out of the spread and merged through `cn()` instead,\n// so a caller's layout utility resolves against the root's own rather than\n// merely being concatenated after it.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: rootAttrs } = useSplitAttrs();\n\n/**\n * The two layouts, side by side rather than as template branches that drift\n * apart. `satisfies` is what stops an orientation being added to the type and\n * forgotten here.\n *\n * Horizontal hangs the connector off the item box (`relative`) rather than\n * off the rail: `left-1/2 w-full` then spans exactly the distance between two\n * indicator centres, since the items are equal-width flex children. It is\n * drawn behind them — the trigger is `relative`, so it and every later item's\n * trigger paint over a line that runs under the circles.\n */\nconst LAYOUTS = {\n horizontal: {\n root: \"flex w-full items-start\",\n item: \"relative flex flex-1 flex-col items-center gap-2 text-center\",\n rail: \"\",\n separator: \"absolute left-1/2 top-4 h-0.5 w-full -translate-y-1/2\",\n fill: \"origin-left scale-x-0\",\n filled: \"scale-x-100\",\n text: \"flex flex-col gap-0.5\",\n },\n vertical: {\n root: \"flex flex-col\",\n item: \"flex gap-3\",\n rail: \"flex flex-col items-center self-stretch\",\n // `order-1` rather than a second template branch: the connector has to be\n // declared before the trigger so the trigger paints over it, and has to sit\n // below the trigger once the rail is a column.\n separator: \"order-1 my-1 w-0.5 flex-1\",\n fill: \"origin-top scale-y-0\",\n filled: \"scale-y-100\",\n text: \"flex flex-col gap-0.5\",\n },\n} satisfies Record<StepperOrientation, Record<string, string>>;\n\nconst layout = computed(() => LAYOUTS[props.orientation]);\n\n/** The step that last held focus, and so owns the spine's single tab stop. */\nconst focused = ref<number | null>(null);\n\n/**\n * Reka's own focusability rule, mirrored: a linear stepper refuses focus to\n * anything past one step ahead of where the reader is. It has to be repeated\n * rather than read off the DOM, because the tab stop is decided while the\n * `tabindex` attributes are still being rendered — and a tab stop parked on a\n * step Reka will not focus leaves the whole spine unreachable by Tab.\n */\nfunction focusable(step: number, current: number): boolean {\n if (props.steps[step - 1]?.disabled) return false;\n return props.linear ? step <= current + 1 : true;\n}\n\n/**\n * The one step whose trigger is tabbable. Everything else is `tabindex=\"-1\"`.\n *\n * Reka types the value it hands its own slot as possibly absent even though\n * `StepperRoot` defaults it to 1, so the fallback here is that declaration\n * being honoured rather than a state this can reach.\n */\nfunction tabStop(current: number | undefined): number {\n const at = current ?? 1;\n if (focused.value !== null && focusable(focused.value, at)) return focused.value;\n if (focusable(at, at)) return at;\n return props.steps.findIndex((_, index) => focusable(index + 1, at)) + 1;\n}\n\n/**\n * Reka points every trigger's `aria-describedby` at a description id whether\n * or not that description was rendered, and a reference resolving to nothing\n * is worse than no reference: it claims a description exists. Binding the key\n * as `undefined` is what takes it back off, since an attribute set to\n * `undefined` is removed rather than merged.\n */\nfunction describedBy(step: StepperStep): Record<string, undefined> {\n return step.description === undefined ? { \"aria-describedby\": undefined } : {};\n}\n\n/** Reka reports its position as possibly absent; this component's model is a step number or nothing at all. */\nfunction onUpdate(value: number | undefined): void {\n if (value !== undefined) emit(\"update:modelValue\", value);\n}\n</script>\n\n<template>\n <StepperRoot\n v-slot=\"{ modelValue: current }\"\n v-bind=\"{ 'aria-label': text.group, ...rootAttrs, ...optional({ modelValue }) }\"\n data-loom-stepper\n :orientation=\"orientation\"\n :linear=\"linear\"\n :class=\"cn(layout.root, attrs.class as string)\"\n @update:model-value=\"onUpdate\"\n >\n <StepperItem\n v-for=\"(step, index) in steps\"\n :key=\"index\"\n v-slot=\"{ state }\"\n :step=\"index + 1\"\n :disabled=\"step.disabled ?? false\"\n :aria-current=\"undefined\"\n :class=\"layout.item\"\n >\n <div :class=\"layout.rail\">\n <!-- Declared before the trigger so the trigger paints over it: the\n connector runs from this indicator's centre to the next one's. -->\n <StepperSeparator\n v-if=\"index < steps.length - 1\"\n :class=\"cn('overflow-hidden rounded-full bg-border', layout.separator)\"\n >\n <span\n :class=\"\n cn(\n 'block h-full w-full bg-primary transition-transform ease-out',\n layout.fill,\n state === 'completed' && layout.filled,\n )\n \"\n />\n </StepperSeparator>\n\n <StepperTrigger\n v-bind=\"describedBy(step)\"\n :tabindex=\"tabStop(current) === index + 1 ? 0 : -1\"\n :aria-current=\"state === 'active' ? 'step' : undefined\"\n :class=\"\n cn(\n 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-xs font-medium',\n 'transition-colors ease-out',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Unavailable is a *colour*, and the whole distinction lives on\n // the indicator. It used to be `opacity-50` on the item, which\n // took the title and the description down with it:\n // `--color-muted-foreground` is 5.76:1 on white and 2.11:1 once\n // composited at half alpha, so the one thing that had to stay\n // readable was the one thing the fade broke. A grey-filled disc\n // against the inactive step's white one says the same thing and\n // costs nothing, and every string below is a measured pair.\n //\n // Keyed off the prop and NOT off `data-[disabled]:`, because in\n // a linear flow Reka marks every step past the next one disabled\n // — painting all of them unavailable reads as broken rather than\n // as later, and a step that is merely ahead already says so by\n // being inactive. The branch also keeps the two apart in the\n // cascade: the inactive rules carry an attribute selector, so a\n // plain `bg-muted` alongside them would lose.\n step.disabled\n ? 'border-border-strong bg-muted text-muted-foreground'\n : [\n 'data-[state=inactive]:border-input data-[state=inactive]:bg-background data-[state=inactive]:text-muted-foreground',\n 'data-[state=inactive]:enabled:hover:bg-subtle',\n ],\n 'data-[state=active]:border-primary data-[state=active]:bg-primary-muted data-[state=active]:text-primary-text',\n 'data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground',\n 'disabled:cursor-not-allowed',\n )\n \"\n @focus=\"focused = index + 1\"\n >\n <StepperIndicator class=\"flex items-center justify-center\">\n <!-- Three states, none of them colour alone: the check is a shape\n change, the number is text, and the current step carries\n `aria-current` above. -->\n <Check v-if=\"state === 'completed'\" class=\"h-4 w-4 animate-scale-in\" />\n <template v-else>{{ index + 1 }}</template>\n </StepperIndicator>\n </StepperTrigger>\n </div>\n\n <div\n :class=\"cn(layout.text, orientation === 'vertical' && index < steps.length - 1 && 'pb-6')\"\n >\n <!-- `as=\"span\"`, not Reka's default `h4`: a step label is not a\n section heading, and a component cannot know what heading level\n the page it lands in has room for. -->\n <!-- The title mutes rather than fades: `--color-muted-foreground` is a\n measured 5.76:1 on the card it sits on, two steps back from\n `--color-foreground` and still comfortably readable. The\n description below is already that colour and stays there — an\n unreachable step's description is the one line a reader most needs\n in order to find out why it is unreachable. -->\n <StepperTitle\n as=\"span\"\n :class=\"\n cn('text-sm font-medium', step.disabled ? 'text-muted-foreground' : 'text-foreground')\n \"\n >\n <!-- ARIA has a value for \"you are here\" and none for \"done\", and the\n check glyph is invisible to a screen reader. This element is what\n `aria-labelledby` names the trigger with, so the word has to join\n the name from inside it.\n Two nodes rather than the title plus a trailing \"(completed)\"\n span, because a suffix is Loom deciding where a qualifier sits\n relative to the thing it qualifies, and that is a property of the\n language. The visible half is `aria-hidden` and excluded from the\n name computation; the hidden half carries the whole finished\n sentence the vocabulary returned. -->\n <template v-if=\"state === 'completed'\">\n <span aria-hidden=\"true\">{{ step.title }}</span>\n <span class=\"sr-only\">{{ text.completed({ title: step.title }) }}</span>\n </template>\n <template v-else>{{ step.title }}</template>\n </StepperTitle>\n <StepperDescription v-if=\"step.description\" class=\"text-xs text-muted-foreground\">\n {{ step.description }}\n </StepperDescription>\n </div>\n </StepperItem>\n\n <!-- Loom's own position announcement, standing in for the one `StepperRoot`\n renders as a text node its render function hard-codes in English.\n `data-loom-live` is what the rule in `global.css` selects *around* when\n it removes Reka's from the accessibility tree, so the two are told\n apart by what Loom marked rather than by DOM order.\n The count comes from `steps` rather than from Reka's registry, which\n fills in over a tick as each trigger mounts — the same number, one\n render sooner, which matters for a region a screen reader reads on\n arrival. -->\n <span data-loom-live role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"sr-only\">{{\n text.position({ step: current ?? 1, stepCount: steps.length })\n }}</span>\n </StepperRoot>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"Stepper.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/stepper/src/Stepper.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { StepperLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n\n/**\n * Loom's English, co-located with the component so it tree-shakes with it, and\n * exported so a host can build a partial vocabulary against the real thing.\n *\n * `group` is capitalised where Reka's is not, which is what lets\n * `Stepper.test.ts` assert whose string reached the DOM rather than merely\n * what it said.\n */\nexport const STEPPER_LABELS: StepperLabels = {\n group: \"Progress\",\n position: ({ step, stepCount }) => `Step ${String(step)} of ${String(stepCount)}`,\n step: ({ step }) => `Step ${String(step)}:`,\n completed: ({ title }) => `${title} (completed)`,\n};\n\n/** One step in the spine. Its `title` is also the accessible name of that step's trigger. */\nexport interface StepperStep {\n /** The step's label, shown next to its indicator and read out as the trigger's name. */\n title: string;\n /** A line under the title saying what the step asks for. Omit it for a bare spine. */\n description?: string;\n /** Present but unreachable: the indicator greys and the title mutes, and neither pointer nor keyboard can select it. */\n disabled?: boolean;\n}\n\n/** The axis the spine runs along, which also decides which arrow keys move between steps. */\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport {\n StepperDescription,\n StepperIndicator,\n StepperItem,\n StepperRoot,\n StepperSeparator,\n StepperTitle,\n StepperTrigger,\n} from \"reka-ui\";\nimport { Check } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * Stepper — the progress spine of a multi-step flow: a checkout, an\n * onboarding sequence, a wizard. It says how many steps there are, which one\n * a reader is on, and which are behind them; the step's own content is the\n * host's, rendered next to or under the spine.\n *\n * It is **not** a number stepper. A numeric value with increment and\n * decrement buttons is NumberField. The word is genuinely ambiguous and both\n * controls answer to it, so the two are worth naming together: this one is\n * about position in a flow, that one is about a quantity.\n *\n * Progress shows one bar for one task whose extent is a percentage — reach\n * for it when the stages have no names worth showing. Tabs switches between\n * panels that have no order at all; a stepper's steps are ordered by\n * definition, which is why it carries a connector and Tabs carries none.\n *\n * Built on Reka UI's Stepper, which supplies the item state machine\n * (completed / active / inactive), the `linear` reachability rule and\n * arrow-key navigation along the orientation's axis.\n *\n * Three things are added on top of it.\n *\n * The first is the roving tab stop. Reka gives *every* focusable trigger\n * `tabindex=\"0\"`, so a five-step spine would be five Tab stops — the defect\n * the accessibility bar names outright. The `tabindex` bound below overrides\n * Reka's, leaving exactly one trigger tabbable at a time (the one last\n * focused, else the current step); Reka's own arrow-key handler still moves\n * real focus between them, which `tabindex=\"-1\"` does not prevent.\n *\n * The second is where `aria-current` sits. Reka puts `aria-current=\"true\"` on\n * the item wrapper, a `div` with no role that a reader never lands on, so the\n * marking is not conveyed at the moment it matters. It is moved here onto the\n * trigger — the element that takes focus — and given the value the WAI-ARIA\n * pattern asks for on a step in a process.\n *\n * The third is the live region. Reka renders one of its own on the root and\n * writes \"Step N of M\" into it as a **text node inside its render function** —\n * not a prop, not a slot, not an attribute — which makes it the one string in\n * this library that no binding on Loom's side can replace. So Loom renders its\n * own alongside it and takes Reka's out of the accessibility tree from\n * `src/styles/global.css`, which carries that reasoning and the coupling it\n * creates. `e2e/stepper.e2e.ts` is what keeps the coupling honest: it asserts\n * that a Loom stepper has exactly one live region and that its words are\n * Loom's.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current step, 1-based. Omit it and Stepper owns its own position. */\n modelValue?: number;\n /** The ordered steps. Their index decides their number; there is no id to keep. */\n steps: StepperStep[];\n /** The axis the spine runs along; vertical moves the titles beside the indicators. */\n orientation?: StepperOrientation;\n /** Locks the flow to its furthest point: a reader may go back, or one step on, never jump ahead. */\n linear?: boolean;\n /**\n * Names for everything the spine says out loud, as any subset of\n * `StepperLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * A whole application's language belongs in that vocabulary rather than\n * here. This is the per-instance correction, and the key it exists for is\n * `group`: a page with two flows on it needs each named for what it steps\n * through.\n */\n labels?: LabelOverrides<StepperLabels>;\n }>(),\n { orientation: \"horizontal\", linear: false },\n);\n\nconst emit = defineEmits<{\n /** The step chosen, 1-based. Fires for a trigger click and for Enter/Space on one. */\n \"update:modelValue\": [value: number];\n}>();\n\n// `text`, not `labels`: the prop of that name is one of the three sources this\n// resolves, and a template reading the raw prop would be reading the overrides\n// rather than the answer.\nconst text = useLabels(\"stepper\", STEPPER_LABELS, () => props.labels);\n\n// The root renders a real element and it is the one every fallthrough\n// attribute belongs on: it is the `role=\"group\"` the whole spine lives in, so\n// an `aria-label` naming the flow lands there. It is spread *after*\n// `text.group` in the template for that reason — the vocabulary supplies the\n// name a spine has when nobody names it, and a caller who does name one still\n// wins. `class` is pulled out of the spread and merged through `cn()` instead,\n// so a caller's layout utility resolves against the root's own rather than\n// merely being concatenated after it.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: rootAttrs } = useSplitAttrs();\n\n/**\n * The two layouts, side by side rather than as template branches that drift\n * apart. `satisfies` is what stops an orientation being added to the type and\n * forgotten here.\n *\n * Horizontal hangs the connector off the item box (`relative`) rather than\n * off the rail: `left-1/2 w-full` then spans exactly the distance between two\n * indicator centres, since the items are equal-width flex children. It is\n * drawn behind them — the trigger is `relative`, so it and every later item's\n * trigger paint over a line that runs under the circles.\n */\nconst LAYOUTS = {\n horizontal: {\n root: \"flex w-full items-start\",\n item: \"relative flex flex-1 flex-col items-center gap-2 text-center\",\n rail: \"\",\n separator: \"absolute left-1/2 top-4 h-0.5 w-full -translate-y-1/2\",\n fill: \"origin-left scale-x-0\",\n filled: \"scale-x-100\",\n text: \"flex flex-col gap-0.5\",\n },\n vertical: {\n root: \"flex flex-col\",\n item: \"flex gap-3\",\n rail: \"flex flex-col items-center self-stretch\",\n // `order-1` rather than a second template branch: the connector has to be\n // declared before the trigger so the trigger paints over it, and has to sit\n // below the trigger once the rail is a column.\n separator: \"order-1 my-1 w-0.5 flex-1\",\n fill: \"origin-top scale-y-0\",\n filled: \"scale-y-100\",\n text: \"flex flex-col gap-0.5\",\n },\n} satisfies Record<StepperOrientation, Record<string, string>>;\n\nconst layout = computed(() => LAYOUTS[props.orientation]);\n\n/** The step that last held focus, and so owns the spine's single tab stop. */\nconst focused = ref<number | null>(null);\n\n/**\n * Reka's own focusability rule, mirrored: a linear stepper refuses focus to\n * anything past one step ahead of where the reader is. It has to be repeated\n * rather than read off the DOM, because the tab stop is decided while the\n * `tabindex` attributes are still being rendered — and a tab stop parked on a\n * step Reka will not focus leaves the whole spine unreachable by Tab.\n */\nfunction focusable(step: number, current: number): boolean {\n if (props.steps[step - 1]?.disabled) return false;\n return props.linear ? step <= current + 1 : true;\n}\n\n/**\n * The one step whose trigger is tabbable. Everything else is `tabindex=\"-1\"`.\n *\n * Reka types the value it hands its own slot as possibly absent even though\n * `StepperRoot` defaults it to 1, so the fallback here is that declaration\n * being honoured rather than a state this can reach.\n */\nfunction tabStop(current: number | undefined): number {\n const at = current ?? 1;\n if (focused.value !== null && focusable(focused.value, at)) return focused.value;\n if (focusable(at, at)) return at;\n return props.steps.findIndex((_, index) => focusable(index + 1, at)) + 1;\n}\n\n/**\n * Reka points every trigger's `aria-describedby` at a description id whether\n * or not that description was rendered, and a reference resolving to nothing\n * is worse than no reference: it claims a description exists. Binding the key\n * as `undefined` is what takes it back off, since an attribute set to\n * `undefined` is removed rather than merged.\n */\nfunction describedBy(step: StepperStep): Record<string, undefined> {\n return step.description === undefined ? { \"aria-describedby\": undefined } : {};\n}\n\n/** Reka reports its position as possibly absent; this component's model is a step number or nothing at all. */\nfunction onUpdate(value: number | undefined): void {\n if (value !== undefined) emit(\"update:modelValue\", value);\n}\n</script>\n\n<template>\n <StepperRoot\n v-slot=\"{ modelValue: current }\"\n v-bind=\"{ 'aria-label': text.group, ...rootAttrs, ...optional({ modelValue }) }\"\n data-loom-stepper\n :orientation=\"orientation\"\n :linear=\"linear\"\n :class=\"cn(layout.root, attrs.class as string)\"\n @update:model-value=\"onUpdate\"\n >\n <StepperItem\n v-for=\"(step, index) in steps\"\n :key=\"index\"\n v-slot=\"{ state }\"\n :step=\"index + 1\"\n :disabled=\"step.disabled ?? false\"\n :aria-current=\"undefined\"\n :class=\"layout.item\"\n >\n <div :class=\"layout.rail\">\n <!-- Declared before the trigger so the trigger paints over it: the\n connector runs from this indicator's centre to the next one's. -->\n <StepperSeparator\n v-if=\"index < steps.length - 1\"\n :class=\"cn('overflow-hidden rounded-full bg-border', layout.separator)\"\n >\n <span\n :class=\"\n cn(\n 'block h-full w-full bg-primary transition-transform ease-out',\n layout.fill,\n state === 'completed' && layout.filled,\n )\n \"\n />\n </StepperSeparator>\n\n <StepperTrigger\n v-bind=\"describedBy(step)\"\n :tabindex=\"tabStop(current) === index + 1 ? 0 : -1\"\n :aria-current=\"state === 'active' ? 'step' : undefined\"\n :class=\"\n cn(\n 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-xs font-medium',\n 'transition-colors ease-out',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Unavailable is a *colour*, and the whole distinction lives on\n // the indicator. It used to be `opacity-50` on the item, which\n // took the title and the description down with it:\n // `--color-muted-foreground` is 5.76:1 on white and 2.11:1 once\n // composited at half alpha, so the one thing that had to stay\n // readable was the one thing the fade broke. A grey-filled disc\n // against the inactive step's white one says the same thing and\n // costs nothing, and every string below is a measured pair.\n //\n // Keyed off the prop and NOT off `data-[disabled]:`, because in\n // a linear flow Reka marks every step past the next one disabled\n // — painting all of them unavailable reads as broken rather than\n // as later, and a step that is merely ahead already says so by\n // being inactive. The branch also keeps the two apart in the\n // cascade: the inactive rules carry an attribute selector, so a\n // plain `bg-muted` alongside them would lose.\n step.disabled\n ? 'border-border-strong bg-muted text-muted-foreground'\n : [\n 'data-[state=inactive]:border-input data-[state=inactive]:bg-background data-[state=inactive]:text-muted-foreground',\n 'data-[state=inactive]:enabled:hover:bg-subtle',\n ],\n 'data-[state=active]:border-primary data-[state=active]:bg-primary-muted data-[state=active]:text-primary-text',\n 'data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground',\n 'disabled:cursor-not-allowed',\n )\n \"\n @focus=\"focused = index + 1\"\n >\n <StepperIndicator class=\"flex items-center justify-center\">\n <!-- Three states, none of them colour alone: the check is a shape\n change, the number is text, and the current step carries\n `aria-current` above. -->\n <Check v-if=\"state === 'completed'\" class=\"h-4 w-4 animate-scale-in\" />\n <template v-else>{{ index + 1 }}</template>\n </StepperIndicator>\n </StepperTrigger>\n </div>\n\n <div\n :class=\"cn(layout.text, orientation === 'vertical' && index < steps.length - 1 && 'pb-6')\"\n >\n <!-- `as=\"span\"`, not Reka's default `h4`: a step label is not a\n section heading, and a component cannot know what heading level\n the page it lands in has room for. -->\n <!-- The title mutes rather than fades: `--color-muted-foreground` is a\n measured 5.76:1 on the card it sits on, two steps back from\n `--color-foreground` and still comfortably readable. The\n description below is already that colour and stays there — an\n unreachable step's description is the one line a reader most needs\n in order to find out why it is unreachable. -->\n <StepperTitle\n as=\"span\"\n :class=\"\n cn('text-sm font-medium', step.disabled ? 'text-muted-foreground' : 'text-foreground')\n \"\n >\n <!-- ARIA has a value for \"you are here\" and none for \"done\", and the\n check glyph is invisible to a screen reader. This element is what\n `aria-labelledby` names the trigger with, so the word has to join\n the name from inside it.\n Two nodes rather than the title plus a trailing \"(completed)\"\n span, because a suffix is Loom deciding where a qualifier sits\n relative to the thing it qualifies, and that is a property of the\n language. The visible half is `aria-hidden` and excluded from the\n name computation; the hidden half carries the whole finished\n sentence the vocabulary returned. -->\n <!-- The position leads the name for the same reason: the trigger's\n visible text is the step's number, and WCAG 2.5.3 (Label in\n Name) makes a control's visible text part of its accessible\n name. Measured 2026-08-26, `label-content-name-mismatch` fired\n on every non-completed trigger until the number joined the\n name — a voice user saying \"Step 2\" has to land on the same\n trigger one saying \"Shipping\" does. -->\n <span class=\"sr-only\">{{ text.step({ step: index + 1 }) }}</span>\n <template v-if=\"state === 'completed'\">\n <span aria-hidden=\"true\">{{ step.title }}</span>\n <span class=\"sr-only\">{{ text.completed({ title: step.title }) }}</span>\n </template>\n <template v-else>{{ step.title }}</template>\n </StepperTitle>\n <StepperDescription v-if=\"step.description\" class=\"text-xs text-muted-foreground\">\n {{ step.description }}\n </StepperDescription>\n </div>\n </StepperItem>\n\n <!-- Loom's own position announcement, standing in for the one `StepperRoot`\n renders as a text node its render function hard-codes in English.\n `data-loom-live` is what the rule in `global.css` selects *around* when\n it removes Reka's from the accessibility tree, so the two are told\n apart by what Loom marked rather than by DOM order.\n The count comes from `steps` rather than from Reka's registry, which\n fills in over a tick as each trigger mounts — the same number, one\n render sooner, which matters for a region a screen reader reads on\n arrival. -->\n <span data-loom-live role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" class=\"sr-only\">{{\n text.position({ step: current ?? 1, stepCount: steps.length })\n }}</span>\n </StepperRoot>\n</template>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Switch.js","names":[],"sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { SwitchRoot, SwitchThumb } from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * Switch — toggles a boolean setting that takes effect immediately, with no\n * \"Save\" step. Built on Reka UI's Switch for role=\"switch\" + aria-checked +\n * Space/Enter. Label and description live on the surrounding setting row —\n * wire them in with `aria-labelledby`, since Switch renders a button, not a\n * labelable input.\n *\n * Inside a [Field](../Field/Field.vue) that row is the wrapper itself, and the\n * wiring stops being the caller's job: `useFieldControl()` puts the row's id on\n * the button — labelable, so the row's `<label for>` names the control that has\n * no name of its own — and brings the row's description, `name`, `required` and\n * `invalid` with it.\n *\n * There is no `readonly` here, and a row's is ignored rather than approximated.\n * A switch that cannot be flipped is disabled; one that looks live and refuses\n * the flip is the only state worse than either.\n */\nconst props = withDefaults(\n defineProps<{\n /** Whether the setting is currently on. Omit to let the switch own its own state. */\n modelValue?: boolean | undefined;\n /** Blocks both click and keyboard input, and drains the control to the neutral well. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n }>(),\n {\n // Not `false` — see Checkbox: a concrete default claims host ownership and\n // leaves an unbound switch emitting updates while never moving.\n modelValue: undefined,\n // Not `false` either: absent has to stay tellable from `false` for a Field\n // above to disable the row and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: boolean] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: switchAttrs } = useSplitAttrs();\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `switchAttrs`, and that position is the contract (see\n// TextField.vue). `name` in that bag reaches SwitchRoot's own `name` prop,\n// which is what mints the hidden input a `<form>` submits.\n//\n// `name`, `required` and `invalid` are omitted below because Switch has no prop\n// for any of them: the row's answer stands unopposed. `readonly` is omitted for\n// the reason the docblock gives.\n//\n// The resolved `required` then reaches the element through Reka's own prop\n// rather than the `aria-required` in `field.attrs` — see Checkbox.vue, which\n// carries the reasoning. SwitchRoot happens to spread the fallthrough last, so\n// the attribute would win here; going through the prop keeps the four\n// Reka-rooted controls one shape instead of four merge orders to re-derive.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n disabled: props.disabled,\n}));\n</script>\n\n<template>\n <SwitchRoot\n v-bind=\"{ modelValue, ...switchAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n :class=\"\n cn(\n 'relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full border border-transparent',\n 'transition-[background-color,filter,box-shadow] duration-instant ease-out',\n 'hover:brightness-95',\n // Checked = a decision (Loom law): the track fills with primary.\n 'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',\n // Focus draws the weave tight: the brand ring blooms.\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Drained, not dimmed. `opacity-50` faded track and thumb together,\n // compositing both below their measured contrast — the same failure\n // Button records for its own dim. The unchecked track falls to\n // `bg-muted`, the well every unavailable control drains to, with the\n // rim Chip slackens to; a *checked* one keeps its primary fill,\n // because switched-on is information (Chip and Pagination keep the\n // wash a selected control already wears) and the native `disabled`\n // attribute carries unavailability to assistive tech regardless.\n // The compound `disabled:data-[state=unchecked]:` form is deliberate:\n // Tailwind sorts data variants after plain `disabled:`, so a bare\n // `disabled:bg-muted` would lose to `data-[state=unchecked]:bg-*` and\n // never fire.\n 'disabled:cursor-not-allowed disabled:data-[state=unchecked]:bg-muted disabled:data-[state=unchecked]:border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', $event)\"\n >\n <SwitchThumb\n style=\"transition: transform var(--duration-fast) var(--ease-spring)\"\n :class=\"\n cn(\n 'absolute top-1/2 left-0.5 block h-5 w-5 -translate-y-1/2 rounded-full bg-background shadow-sm',\n 'active:scale-x-[1.15] active:scale-y-[0.85]',\n // The travel is a translate, not a `left` change: animating an inset\n // property relayouts the track on every frame of the slide, while a\n // transform stays on the compositor. The distance is the checked\n // inset (1.125rem) minus the resting one (left-0.5), and the\n // -translate-y-1/2 above composes with it Tailwind's translate\n // utilities stack rather than overwrite.\n 'data-[state=checked]:translate-x-[1rem]',\n )\n \"\n />\n </SwitchRoot>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"Switch.js","names":[],"sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { SwitchRoot, SwitchThumb } from \"reka-ui\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\n\n/**\n * Switch — toggles a boolean setting that takes effect immediately, with no\n * \"Save\" step. Built on Reka UI's Switch for role=\"switch\" + aria-checked +\n * Space/Enter. Label and description live on the surrounding setting row —\n * wire them in with `aria-labelledby`, since Switch renders a button, not a\n * labelable input.\n *\n * Inside a [Field](../Field/Field.vue) that row is the wrapper itself, and the\n * wiring stops being the caller's job: `useFieldControl()` puts the row's id on\n * the button — labelable, so the row's `<label for>` names the control that has\n * no name of its own — and brings the row's description, `name`, `required` and\n * `invalid` with it.\n *\n * There is no `readonly` here, and a row's is ignored rather than approximated.\n * A switch that cannot be flipped is disabled; one that looks live and refuses\n * the flip is the only state worse than either.\n */\nconst props = withDefaults(\n defineProps<{\n /** Whether the setting is currently on. Omit to let the switch own its own state. */\n modelValue?: boolean | undefined;\n /** Blocks both click and keyboard input, and drains the control to the neutral well. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n }>(),\n {\n // Not `false` — see Checkbox: a concrete default claims host ownership and\n // leaves an unbound switch emitting updates while never moving.\n modelValue: undefined,\n // Not `false` either: absent has to stay tellable from `false` for a Field\n // above to disable the row and for `:disabled=\"false\"` to overrule one that\n // does. Vue casts an absent Boolean prop with no declared default to\n // `false`, which would leave the context wired and inert.\n disabled: undefined,\n },\n);\n\ndefineEmits<{ \"update:modelValue\": [value: boolean] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: switchAttrs } = useSplitAttrs();\n\n// One rendered node, so the split is only about `class` — a caller's class\n// needs a Tailwind-aware merge rather than Vue's concatenating fallthrough.\n//\n// `id` and `aria-describedby` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values; `field.attrs` then spreads\n// **after** `switchAttrs`, and that position is the contract (see\n// TextField.vue). `name` in that bag reaches SwitchRoot's own `name` prop,\n// which is what mints the hidden input a `<form>` submits.\n//\n// `name`, `required` and `invalid` are omitted below because Switch has no prop\n// for any of them: the row's answer stands unopposed. `readonly` is omitted for\n// the reason the docblock gives.\n//\n// The resolved `required` then reaches the element through Reka's own prop\n// rather than the `aria-required` in `field.attrs` — see Checkbox.vue, which\n// carries the reasoning. SwitchRoot happens to spread the fallthrough last, so\n// the attribute would win here; going through the prop keeps the four\n// Reka-rooted controls one shape instead of four merge orders to re-derive.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n disabled: props.disabled,\n}));\n</script>\n\n<template>\n <SwitchRoot\n v-bind=\"{ modelValue, ...switchAttrs, ...field.attrs }\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n :class=\"\n cn(\n 'relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full border border-transparent',\n 'transition-[background-color,filter,box-shadow] duration-instant ease-out',\n 'hover:brightness-95',\n // Checked = a decision (Loom law): the track fills with primary.\n 'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',\n // Focus draws the weave tight: the brand ring blooms.\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo',\n // Drained, not dimmed. `opacity-50` faded track and thumb together,\n // compositing both below their measured contrast — the same failure\n // Button records for its own dim. The unchecked track falls to\n // `bg-muted`, the well every unavailable control drains to, with the\n // rim Chip slackens to; a *checked* one keeps its primary fill,\n // because switched-on is information (Chip and Pagination keep the\n // wash a selected control already wears) and the native `disabled`\n // attribute carries unavailability to assistive tech regardless.\n // The compound `disabled:data-[state=unchecked]:` form is deliberate:\n // Tailwind sorts data variants after plain `disabled:`, so a bare\n // `disabled:bg-muted` would lose to `data-[state=unchecked]:bg-*` and\n // never fire.\n 'disabled:cursor-not-allowed disabled:data-[state=unchecked]:bg-muted disabled:data-[state=unchecked]:border-border',\n attrs.class as string,\n )\n \"\n @update:model-value=\"$emit('update:modelValue', $event)\"\n >\n <SwitchThumb\n style=\"transition: transform var(--duration-fast) var(--ease-spring)\"\n :class=\"\n cn(\n 'absolute top-1/2 start-0.5 block h-5 w-5 -translate-y-1/2 rounded-full bg-background shadow-sm',\n 'active:scale-x-[1.15] active:scale-y-[0.85]',\n // The anchor is the *inline* start edge, so the off state rests on\n // the reading-order side in every direction — physically left under\n // LTR (pixel-identical to the old `left-0.5`) and right under RTL,\n // where a physical `left` would pin both states to one edge and\n // make the motion read backwards.\n //\n // The travel is still a translate, not an inset change: animating an\n // inset relayouts the track on every frame, while a transform stays\n // on the compositor. Its sign is the one thing direction has left to\n // say checked moves toward the inline-end edge, +x under LTR, -x\n // under RTL and it is written through this custom property rather\n // than a second competing utility because Tailwind's `rtl:` wraps in\n // a zero-specificity `:where()`, which would leave the flip's winner\n // to stylesheet order. One rule writes the variable; everything else\n // reads the fallback, so LTR needs no rule at all (and survives even\n // a browser without `:dir()`).\n 'data-[state=checked]:translate-x-[var(--switch-travel-x,1rem)]',\n 'rtl:[--switch-travel-x:-1rem]',\n )\n \"\n />\n </SwitchRoot>\n</template>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"Switch.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"names":[],"mappings":"AAkIA;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,WAAW,GAAG;IACf,qFAAqF;IACrF,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,0HAA0H;IAC1H,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC,CAAC;AAwMJ,QAAA,MAAM,YAAY;;;;;cAzMH,OAAO,GAAG,SAAS;gBAFjB,OAAO,GAAG,SAAS;6EA+MlC,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
1
+ {"version":3,"file":"Switch.vue.d.ts","sourceRoot":"","sources":["../../../../../packages/primitives/switch/src/Switch.vue"],"names":[],"mappings":"AA6IA;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,WAAW,GAAG;IACf,qFAAqF;IACrF,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,0HAA0H;IAC1H,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC,CAAC;AA8NJ,QAAA,MAAM,YAAY;;;;;cA/NH,OAAO,GAAG,SAAS;gBAFjB,OAAO,GAAG,SAAS;6EAqOlC,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -58,7 +58,7 @@ var Switch_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCom
58
58
  }), {
59
59
  default: withCtx(() => [createVNode(unref(SwitchThumb), {
60
60
  style: { "transition": "transform var(--duration-fast) var(--ease-spring)" },
61
- class: normalizeClass(unref(cn)("absolute top-1/2 left-0.5 block h-5 w-5 -translate-y-1/2 rounded-full bg-background shadow-sm", "active:scale-x-[1.15] active:scale-y-[0.85]", "data-[state=checked]:translate-x-[1rem]"))
61
+ class: normalizeClass(unref(cn)("absolute top-1/2 start-0.5 block h-5 w-5 -translate-y-1/2 rounded-full bg-background shadow-sm", "active:scale-x-[1.15] active:scale-y-[0.85]", "data-[state=checked]:translate-x-[var(--switch-travel-x,1rem)]", "rtl:[--switch-travel-x:-1rem]"))
62
62
  }, null, 8, ["class"])]),
63
63
  _: 1
64
64
  }, 16, [