@ecoma-io/loom 0.3.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 (587) hide show
  1. package/README.md +11 -9
  2. package/dist/a11y.js +2 -2
  3. package/dist/index.js +73 -6
  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/app-header/src/AppHeader.js.map +1 -1
  19. package/dist/packages/blocks/app-header/src/AppHeader.vue.d.ts.map +1 -1
  20. package/dist/packages/blocks/app-header/src/AppHeader.vue_vue_type_script_setup_true_lang.js +1 -0
  21. package/dist/packages/blocks/app-header/src/AppHeader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/packages/blocks/desktop-app-shell/src/DesktopAppShell.vue.d.ts +3 -3
  23. package/dist/packages/blocks/empty-state/src/EmptyState.js.map +1 -1
  24. package/dist/packages/blocks/empty-state/src/EmptyState.vue.d.ts.map +1 -1
  25. package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js +5 -6
  26. package/dist/packages/blocks/empty-state/src/EmptyState.vue_vue_type_script_setup_true_lang.js.map +1 -1
  27. package/dist/packages/blocks/error-summary/src/ErrorSummary.js +7 -0
  28. package/dist/packages/blocks/error-summary/src/ErrorSummary.js.map +1 -0
  29. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue.d.ts +31 -0
  30. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js +90 -0
  31. package/dist/packages/blocks/error-summary/src/ErrorSummary.vue_vue_type_script_setup_true_lang.js.map +1 -0
  32. package/dist/packages/blocks/error-summary/src/index.d.ts +4 -0
  33. package/dist/packages/blocks/error-summary/src/index.js +3 -0
  34. package/dist/packages/blocks/metric-card/src/MetricCard.js.map +1 -1
  35. package/dist/packages/blocks/metric-card/src/MetricCard.vue.d.ts +22 -4
  36. package/dist/packages/blocks/metric-card/src/MetricCard.vue.d.ts.map +1 -1
  37. package/dist/packages/blocks/metric-card/src/MetricCard.vue_vue_type_script_setup_true_lang.js +38 -15
  38. package/dist/packages/blocks/metric-card/src/MetricCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  39. package/dist/packages/blocks/title-bar/src/TitleBar.vue.d.ts +3 -3
  40. package/dist/packages/blocks/toast-stack/src/ToastStack.vue.d.ts +3 -3
  41. package/dist/packages/composition/scroll-reel/src/ScrollReel.js +2 -2
  42. package/dist/packages/composition/scroll-reel/src/ScrollReel.js.map +1 -1
  43. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue.d.ts +26 -0
  44. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js +28 -7
  45. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  46. package/dist/packages/composition/scroll-reel/src/index.d.ts +2 -1
  47. package/dist/packages/composition/scroll-reel/src/index.js +2 -1
  48. package/dist/packages/composition/sidebar/src/Sidebar.js.map +1 -1
  49. package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts +4 -0
  50. package/dist/packages/composition/sidebar/src/Sidebar.vue.d.ts.map +1 -1
  51. package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js +13 -4
  52. package/dist/packages/composition/sidebar/src/Sidebar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  53. package/dist/packages/composition/split/src/Split.js.map +1 -1
  54. package/dist/packages/composition/split/src/Split.vue.d.ts +11 -12
  55. package/dist/packages/composition/split/src/Split.vue.d.ts.map +1 -1
  56. package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js +15 -5
  57. package/dist/packages/composition/split/src/Split.vue_vue_type_script_setup_true_lang.js.map +1 -1
  58. package/dist/packages/core/src/a11y-scope.d.ts +71 -5
  59. package/dist/packages/core/src/a11y-scope.d.ts.map +1 -1
  60. package/dist/packages/core/src/a11y-scope.js +126 -6
  61. package/dist/packages/core/src/a11y-scope.js.map +1 -1
  62. package/dist/packages/core/src/index.d.ts +2 -2
  63. package/dist/packages/core/src/index.d.ts.map +1 -1
  64. package/dist/packages/core/src/index.js +3 -3
  65. package/dist/packages/core/src/motion.d.ts +13 -0
  66. package/dist/packages/core/src/motion.d.ts.map +1 -1
  67. package/dist/packages/core/src/motion.js +20 -13
  68. package/dist/packages/core/src/motion.js.map +1 -1
  69. package/dist/packages/core/src/theme.d.ts +9 -0
  70. package/dist/packages/core/src/theme.d.ts.map +1 -1
  71. package/dist/packages/core/src/theme.js +36 -10
  72. package/dist/packages/core/src/theme.js.map +1 -1
  73. package/dist/packages/labels/src/ancestor-disabled.d.ts +5 -1
  74. package/dist/packages/labels/src/ancestor-disabled.d.ts.map +1 -1
  75. package/dist/packages/labels/src/ancestor-disabled.js +5 -1
  76. package/dist/packages/labels/src/ancestor-disabled.js.map +1 -1
  77. package/dist/packages/labels/src/component-shapes.d.ts +236 -1
  78. package/dist/packages/labels/src/component-shapes.d.ts.map +1 -1
  79. package/dist/packages/labels/src/date-labels.d.ts +86 -7
  80. package/dist/packages/labels/src/date-labels.d.ts.map +1 -1
  81. package/dist/packages/labels/src/date-labels.js +58 -9
  82. package/dist/packages/labels/src/date-labels.js.map +1 -1
  83. package/dist/packages/labels/src/field-context.d.ts +2 -2
  84. package/dist/packages/labels/src/field-context.js.map +1 -1
  85. package/dist/packages/labels/src/index.d.ts +3 -3
  86. package/dist/packages/labels/src/index.d.ts.map +1 -1
  87. package/dist/packages/labels/src/index.js +2 -2
  88. package/dist/packages/labels/src/label-registry.d.ts +15 -1
  89. package/dist/packages/labels/src/label-registry.d.ts.map +1 -1
  90. package/dist/packages/layouts/dashboard/src/Dashboard.js.map +1 -1
  91. package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts +4 -4
  92. package/dist/packages/layouts/dashboard/src/Dashboard.vue.d.ts.map +1 -1
  93. package/dist/packages/layouts/dashboard/src/Dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  94. package/dist/packages/layouts/reading/src/Reading.js.map +1 -1
  95. package/dist/packages/layouts/reading/src/Reading.vue.d.ts +16 -15
  96. package/dist/packages/layouts/reading/src/Reading.vue.d.ts.map +1 -1
  97. package/dist/packages/layouts/reading/src/Reading.vue_vue_type_script_setup_true_lang.js.map +1 -1
  98. package/dist/packages/layouts/reading/src/index.d.ts +1 -0
  99. package/dist/packages/layouts/reading/src/index.d.ts.map +1 -1
  100. package/dist/packages/layouts/settings/src/Settings.js.map +1 -1
  101. package/dist/packages/layouts/settings/src/Settings.vue.d.ts +3 -3
  102. package/dist/packages/layouts/settings/src/Settings.vue.d.ts.map +1 -1
  103. package/dist/packages/layouts/settings/src/Settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  104. package/dist/packages/layouts/split-layout/src/SplitLayout.js.map +1 -1
  105. package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts +4 -6
  106. package/dist/packages/layouts/split-layout/src/SplitLayout.vue.d.ts.map +1 -1
  107. package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js +20 -5
  108. package/dist/packages/layouts/split-layout/src/SplitLayout.vue_vue_type_script_setup_true_lang.js.map +1 -1
  109. package/dist/packages/layouts/split-layout/src/index.d.ts +1 -1
  110. package/dist/packages/layouts/split-layout/src/index.d.ts.map +1 -1
  111. package/dist/packages/loom/src/a11y.d.ts +1 -1
  112. package/dist/packages/loom/src/index.d.ts +217 -174
  113. package/dist/packages/loom/src/theme.d.ts +3 -0
  114. package/dist/packages/primitives/alert/src/Alert.js +7 -0
  115. package/dist/packages/primitives/alert/src/Alert.js.map +1 -0
  116. package/dist/packages/primitives/alert/src/Alert.vue.d.ts +91 -0
  117. package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js +130 -0
  118. package/dist/packages/primitives/alert/src/Alert.vue_vue_type_script_setup_true_lang.js.map +1 -0
  119. package/dist/packages/primitives/alert/src/index.d.ts +4 -0
  120. package/dist/packages/primitives/alert/src/index.js +3 -0
  121. package/dist/packages/primitives/alert-dialog/src/AlertDialog.js.map +1 -1
  122. package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue.d.ts +2 -2
  123. package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue_vue_type_script_setup_true_lang.js +2 -2
  124. package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  125. package/dist/packages/primitives/alert-dialog/src/index.d.ts +1 -1
  126. package/dist/packages/primitives/avatar/src/Avatar.js.map +1 -1
  127. package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts +2 -2
  128. package/dist/packages/primitives/avatar/src/Avatar.vue.d.ts.map +1 -1
  129. package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js +1 -1
  130. package/dist/packages/primitives/avatar/src/Avatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  131. package/dist/packages/primitives/avatar-group/src/AvatarGroup.vue.d.ts +3 -3
  132. package/dist/packages/primitives/avatar-group/src/index.d.ts +1 -1
  133. package/dist/packages/primitives/badge/src/Badge.js.map +1 -1
  134. package/dist/packages/primitives/badge/src/Badge.vue.d.ts.map +1 -1
  135. package/dist/packages/primitives/badge/src/Badge.vue_vue_type_script_setup_true_lang.js +1 -1
  136. package/dist/packages/primitives/badge/src/Badge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  137. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.js +2 -2
  138. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.js.map +1 -1
  139. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue.d.ts +28 -0
  140. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue_vue_type_script_setup_true_lang.js +16 -5
  141. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  142. package/dist/packages/primitives/breadcrumb/src/index.d.ts +2 -1
  143. package/dist/packages/primitives/breadcrumb/src/index.js +2 -1
  144. package/dist/packages/primitives/button/src/Button.js.map +1 -1
  145. package/dist/packages/primitives/button/src/Button.vue.d.ts.map +1 -1
  146. package/dist/packages/primitives/button/src/Button.vue_vue_type_script_setup_true_lang.js.map +1 -1
  147. package/dist/packages/primitives/button/src/index.js +2 -2
  148. package/dist/packages/primitives/calendar/src/Calendar.js +7 -0
  149. package/dist/packages/primitives/calendar/src/Calendar.js.map +1 -0
  150. package/dist/packages/primitives/calendar/src/Calendar.vue.d.ts +95 -0
  151. package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js +204 -0
  152. package/dist/packages/primitives/calendar/src/Calendar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  153. package/dist/packages/primitives/calendar/src/index.d.ts +3 -0
  154. package/dist/packages/primitives/calendar/src/index.js +3 -0
  155. package/dist/packages/primitives/card/src/Card.js +7 -0
  156. package/dist/packages/primitives/card/src/Card.js.map +1 -0
  157. package/dist/packages/primitives/card/src/Card.vue.d.ts +72 -0
  158. package/dist/packages/primitives/card/src/Card.vue_vue_type_script_setup_true_lang.js +65 -0
  159. package/dist/packages/primitives/card/src/Card.vue_vue_type_script_setup_true_lang.js.map +1 -0
  160. package/dist/packages/primitives/card/src/index.d.ts +3 -0
  161. package/dist/packages/primitives/card/src/index.d.ts.map +1 -0
  162. package/dist/packages/primitives/card/src/index.js +2 -0
  163. package/dist/packages/primitives/carousel/src/Carousel.js +7 -0
  164. package/dist/packages/primitives/carousel/src/Carousel.js.map +1 -0
  165. package/dist/packages/primitives/carousel/src/Carousel.vue.d.ts +34 -0
  166. package/dist/packages/primitives/carousel/src/Carousel.vue_vue_type_script_setup_true_lang.js +175 -0
  167. package/dist/packages/primitives/carousel/src/Carousel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  168. package/dist/packages/primitives/carousel/src/index.d.ts +3 -0
  169. package/dist/packages/primitives/carousel/src/index.js +3 -0
  170. package/dist/packages/primitives/checkbox/src/Checkbox.js.map +1 -1
  171. package/dist/packages/primitives/checkbox/src/Checkbox.vue.d.ts +1 -1
  172. package/dist/packages/primitives/checkbox/src/Checkbox.vue.d.ts.map +1 -1
  173. package/dist/packages/primitives/checkbox/src/Checkbox.vue_vue_type_script_setup_true_lang.js +3 -3
  174. package/dist/packages/primitives/checkbox/src/Checkbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  175. package/dist/packages/primitives/chip/src/Chip.js.map +1 -1
  176. package/dist/packages/primitives/chip/src/Chip.vue.d.ts.map +1 -1
  177. package/dist/packages/primitives/chip/src/Chip.vue_vue_type_script_setup_true_lang.js +1 -1
  178. package/dist/packages/primitives/chip/src/Chip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/packages/primitives/collapse/src/Collapse.js +7 -0
  180. package/dist/packages/primitives/collapse/src/Collapse.js.map +1 -0
  181. package/dist/packages/primitives/collapse/src/Collapse.vue.d.ts +56 -0
  182. package/dist/packages/primitives/collapse/src/Collapse.vue.d.ts.map +1 -0
  183. package/dist/packages/primitives/collapse/src/Collapse.vue_vue_type_script_setup_true_lang.js +81 -0
  184. package/dist/packages/primitives/collapse/src/Collapse.vue_vue_type_script_setup_true_lang.js.map +1 -0
  185. package/dist/packages/primitives/collapse/src/index.d.ts +2 -0
  186. package/dist/packages/primitives/collapse/src/index.d.ts.map +1 -0
  187. package/dist/packages/primitives/collapse/src/index.js +2 -0
  188. package/dist/packages/primitives/color-picker/src/ColorPicker.js.map +1 -1
  189. package/dist/packages/primitives/color-picker/src/ColorPicker.vue.d.ts +4 -4
  190. package/dist/packages/primitives/color-picker/src/ColorPicker.vue_vue_type_script_setup_true_lang.js +2 -2
  191. package/dist/packages/primitives/color-picker/src/ColorPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  192. package/dist/packages/primitives/color-picker/src/index.d.ts +1 -1
  193. package/dist/packages/primitives/combobox/src/Combobox.js.map +1 -1
  194. package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts +3 -3
  195. package/dist/packages/primitives/combobox/src/Combobox.vue_vue_type_script_setup_true_lang.js +2 -2
  196. package/dist/packages/primitives/combobox/src/Combobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  197. package/dist/packages/primitives/combobox/src/index.d.ts +1 -1
  198. package/dist/packages/primitives/command/src/Command.js +7 -0
  199. package/dist/packages/primitives/command/src/Command.js.map +1 -0
  200. package/dist/packages/primitives/command/src/Command.vue.d.ts +77 -0
  201. package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js +333 -0
  202. package/dist/packages/primitives/command/src/Command.vue_vue_type_script_setup_true_lang.js.map +1 -0
  203. package/dist/packages/primitives/command/src/index.d.ts +4 -0
  204. package/dist/packages/primitives/command/src/index.js +3 -0
  205. package/dist/packages/primitives/context-menu/src/ContextMenu.js.map +1 -1
  206. package/dist/packages/primitives/context-menu/src/ContextMenu.vue.d.ts +12 -0
  207. package/dist/packages/primitives/context-menu/src/ContextMenu.vue.d.ts.map +1 -1
  208. package/dist/packages/primitives/context-menu/src/ContextMenu.vue_vue_type_script_setup_true_lang.js +7 -3
  209. package/dist/packages/primitives/context-menu/src/ContextMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  210. package/dist/packages/primitives/copy-button/src/CopyButton.js +7 -0
  211. package/dist/packages/primitives/copy-button/src/CopyButton.js.map +1 -0
  212. package/dist/packages/primitives/copy-button/src/CopyButton.vue.d.ts +64 -0
  213. package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js +113 -0
  214. package/dist/packages/primitives/copy-button/src/CopyButton.vue_vue_type_script_setup_true_lang.js.map +1 -0
  215. package/dist/packages/primitives/copy-button/src/index.d.ts +4 -0
  216. package/dist/packages/primitives/copy-button/src/index.js +3 -0
  217. package/dist/packages/primitives/data-grid/src/DataGrid.js +7 -0
  218. package/dist/packages/primitives/data-grid/src/DataGrid.js.map +1 -0
  219. package/dist/packages/primitives/data-grid/src/DataGrid.vue.d.ts +128 -0
  220. package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js +347 -0
  221. package/dist/packages/primitives/data-grid/src/DataGrid.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/packages/primitives/data-grid/src/index.d.ts +4 -0
  223. package/dist/packages/primitives/data-grid/src/index.js +3 -0
  224. package/dist/packages/primitives/date-picker/src/DatePicker.js.map +1 -1
  225. package/dist/packages/primitives/date-picker/src/DatePicker.vue.d.ts +2 -2
  226. package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js +14 -14
  227. package/dist/packages/primitives/date-picker/src/DatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  228. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.js.map +1 -1
  229. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue.d.ts +3 -3
  230. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js +22 -19
  231. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  232. package/dist/packages/primitives/date-range-picker/src/index.d.ts +1 -1
  233. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.js.map +1 -1
  234. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue.d.ts +2 -2
  235. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js +18 -17
  236. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  237. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.js.map +1 -1
  238. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue.d.ts +3 -3
  239. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js +18 -17
  240. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  241. package/dist/packages/primitives/date-time-range-picker/src/index.d.ts +1 -1
  242. package/dist/packages/primitives/dialog/src/Dialog.js.map +1 -1
  243. package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts +27 -11
  244. package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js +6 -4
  245. package/dist/packages/primitives/dialog/src/Dialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  246. package/dist/packages/primitives/dialog/src/index.d.ts +1 -1
  247. package/dist/packages/primitives/drawer/src/Drawer.js.map +1 -1
  248. package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts +12 -4
  249. package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js +13 -5
  250. package/dist/packages/primitives/drawer/src/Drawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  251. package/dist/packages/primitives/drawer/src/index.d.ts +1 -1
  252. package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.js.map +1 -1
  253. package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue.d.ts +26 -0
  254. package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue.d.ts.map +1 -1
  255. package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue_vue_type_script_setup_true_lang.js +10 -4
  256. package/dist/packages/primitives/dropdown-menu/src/DropdownMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  257. package/dist/packages/primitives/editable/src/Editable.js.map +1 -1
  258. package/dist/packages/primitives/editable/src/Editable.vue.d.ts +2 -2
  259. package/dist/packages/primitives/editable/src/Editable.vue_vue_type_script_setup_true_lang.js +1 -1
  260. package/dist/packages/primitives/editable/src/Editable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  261. package/dist/packages/primitives/editable/src/index.d.ts +1 -1
  262. package/dist/packages/primitives/file-upload/src/FileUpload.js.map +1 -1
  263. package/dist/packages/primitives/file-upload/src/FileUpload.vue.d.ts +2 -2
  264. package/dist/packages/primitives/file-upload/src/FileUpload.vue_vue_type_script_setup_true_lang.js +1 -1
  265. package/dist/packages/primitives/file-upload/src/FileUpload.vue_vue_type_script_setup_true_lang.js.map +1 -1
  266. package/dist/packages/primitives/file-upload/src/index.d.ts +1 -1
  267. package/dist/packages/primitives/hover-card/src/HoverCard.js.map +1 -1
  268. package/dist/packages/primitives/hover-card/src/HoverCard.vue.d.ts.map +1 -1
  269. package/dist/packages/primitives/hover-card/src/HoverCard.vue_vue_type_script_setup_true_lang.js +1 -1
  270. package/dist/packages/primitives/hover-card/src/HoverCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  271. package/dist/packages/primitives/hover-card/src/index.d.ts.map +1 -1
  272. package/dist/packages/primitives/icon-button/src/IconButton.js.map +1 -1
  273. package/dist/packages/primitives/icon-button/src/IconButton.vue.d.ts +10 -4
  274. package/dist/packages/primitives/icon-button/src/IconButton.vue.d.ts.map +1 -1
  275. package/dist/packages/primitives/icon-button/src/IconButton.vue_vue_type_script_setup_true_lang.js +18 -6
  276. package/dist/packages/primitives/icon-button/src/IconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  277. package/dist/packages/primitives/indicator/src/Indicator.vue.d.ts +2 -2
  278. package/dist/packages/primitives/indicator/src/index.d.ts +1 -1
  279. package/dist/packages/primitives/kbd/src/Kbd.js +7 -0
  280. package/dist/packages/primitives/kbd/src/Kbd.js.map +1 -0
  281. package/dist/packages/primitives/kbd/src/Kbd.vue.d.ts +30 -0
  282. package/dist/packages/primitives/kbd/src/Kbd.vue.d.ts.map +1 -0
  283. package/dist/packages/primitives/kbd/src/Kbd.vue_vue_type_script_setup_true_lang.js +29 -0
  284. package/dist/packages/primitives/kbd/src/Kbd.vue_vue_type_script_setup_true_lang.js.map +1 -0
  285. package/dist/packages/primitives/kbd/src/index.d.ts +3 -0
  286. package/dist/packages/primitives/kbd/src/index.d.ts.map +1 -0
  287. package/dist/packages/primitives/kbd/src/index.js +3 -0
  288. package/dist/packages/primitives/link/src/Link.js.map +1 -1
  289. package/dist/packages/primitives/link/src/Link.vue.d.ts.map +1 -1
  290. package/dist/packages/primitives/link/src/Link.vue_vue_type_script_setup_true_lang.js +3 -3
  291. package/dist/packages/primitives/link/src/Link.vue_vue_type_script_setup_true_lang.js.map +1 -1
  292. package/dist/packages/primitives/list/src/List.js +7 -0
  293. package/dist/packages/primitives/list/src/List.js.map +1 -0
  294. package/dist/packages/primitives/list/src/List.vue.d.ts +22 -0
  295. package/dist/packages/primitives/list/src/List.vue.d.ts.map +1 -0
  296. package/dist/packages/primitives/list/src/List.vue_vue_type_script_setup_true_lang.js +27 -0
  297. package/dist/packages/primitives/list/src/List.vue_vue_type_script_setup_true_lang.js.map +1 -0
  298. package/dist/packages/primitives/list/src/ListItem.js +7 -0
  299. package/dist/packages/primitives/list/src/ListItem.js.map +1 -0
  300. package/dist/packages/primitives/list/src/ListItem.vue.d.ts +75 -0
  301. package/dist/packages/primitives/list/src/ListItem.vue.d.ts.map +1 -0
  302. package/dist/packages/primitives/list/src/ListItem.vue_vue_type_script_setup_true_lang.js +101 -0
  303. package/dist/packages/primitives/list/src/ListItem.vue_vue_type_script_setup_true_lang.js.map +1 -0
  304. package/dist/packages/primitives/list/src/index.d.ts +4 -0
  305. package/dist/packages/primitives/list/src/index.d.ts.map +1 -0
  306. package/dist/packages/primitives/list/src/index.js +3 -0
  307. package/dist/packages/primitives/live-region/src/LiveRegion.js +7 -0
  308. package/dist/packages/primitives/live-region/src/LiveRegion.js.map +1 -0
  309. package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts +36 -0
  310. package/dist/packages/primitives/live-region/src/LiveRegion.vue.d.ts.map +1 -0
  311. package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js +59 -0
  312. package/dist/packages/primitives/live-region/src/LiveRegion.vue_vue_type_script_setup_true_lang.js.map +1 -0
  313. package/dist/packages/primitives/live-region/src/context.d.ts +40 -0
  314. package/dist/packages/primitives/live-region/src/context.d.ts.map +1 -0
  315. package/dist/packages/primitives/live-region/src/context.js +85 -0
  316. package/dist/packages/primitives/live-region/src/context.js.map +1 -0
  317. package/dist/packages/primitives/live-region/src/index.d.ts +4 -0
  318. package/dist/packages/primitives/live-region/src/index.d.ts.map +1 -0
  319. package/dist/packages/primitives/live-region/src/index.js +3 -0
  320. package/dist/packages/primitives/live-region/src/useAnnounce.d.ts +17 -0
  321. package/dist/packages/primitives/live-region/src/useAnnounce.d.ts.map +1 -0
  322. package/dist/packages/primitives/live-region/src/useAnnounce.js +25 -0
  323. package/dist/packages/primitives/live-region/src/useAnnounce.js.map +1 -0
  324. package/dist/packages/primitives/menubar/src/Menubar.js.map +1 -1
  325. package/dist/packages/primitives/menubar/src/Menubar.vue.d.ts +20 -6
  326. package/dist/packages/primitives/menubar/src/Menubar.vue.d.ts.map +1 -1
  327. package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js +211 -64
  328. package/dist/packages/primitives/menubar/src/Menubar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  329. package/dist/packages/primitives/meter/src/Meter.js +7 -0
  330. package/dist/packages/primitives/meter/src/Meter.js.map +1 -0
  331. package/dist/packages/primitives/meter/src/Meter.vue.d.ts +87 -0
  332. package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js +236 -0
  333. package/dist/packages/primitives/meter/src/Meter.vue_vue_type_script_setup_true_lang.js.map +1 -0
  334. package/dist/packages/primitives/meter/src/index.d.ts +4 -0
  335. package/dist/packages/primitives/meter/src/index.js +3 -0
  336. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js +7 -0
  337. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.js.map +1 -0
  338. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts +69 -0
  339. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue.d.ts.map +1 -0
  340. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js +118 -0
  341. package/dist/packages/primitives/navigation-menu/src/NavigationMenu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  342. package/dist/packages/primitives/navigation-menu/src/index.d.ts +3 -0
  343. package/dist/packages/primitives/navigation-menu/src/index.d.ts.map +1 -0
  344. package/dist/packages/primitives/navigation-menu/src/index.js +2 -0
  345. package/dist/packages/primitives/number-field/src/NumberField.js.map +1 -1
  346. package/dist/packages/primitives/number-field/src/NumberField.vue.d.ts +2 -2
  347. package/dist/packages/primitives/number-field/src/NumberField.vue_vue_type_script_setup_true_lang.js +1 -0
  348. package/dist/packages/primitives/number-field/src/NumberField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  349. package/dist/packages/primitives/number-field/src/index.d.ts +1 -1
  350. package/dist/packages/primitives/otp-input/src/OtpInput.vue.d.ts +2 -2
  351. package/dist/packages/primitives/otp-input/src/index.d.ts +1 -1
  352. package/dist/packages/primitives/pagination/src/Pagination.js.map +1 -1
  353. package/dist/packages/primitives/pagination/src/Pagination.vue.d.ts +3 -3
  354. package/dist/packages/primitives/pagination/src/Pagination.vue_vue_type_script_setup_true_lang.js +1 -1
  355. package/dist/packages/primitives/pagination/src/Pagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  356. package/dist/packages/primitives/pagination/src/index.d.ts +1 -1
  357. package/dist/packages/primitives/popover/src/Popover.js.map +1 -1
  358. package/dist/packages/primitives/popover/src/Popover.vue.d.ts.map +1 -1
  359. package/dist/packages/primitives/popover/src/Popover.vue_vue_type_script_setup_true_lang.js +1 -1
  360. package/dist/packages/primitives/popover/src/Popover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  361. package/dist/packages/primitives/progress/src/Progress.vue.d.ts +2 -2
  362. package/dist/packages/primitives/progress/src/index.d.ts +1 -1
  363. package/dist/packages/primitives/radial-progress/src/RadialProgress.vue.d.ts +2 -2
  364. package/dist/packages/primitives/radial-progress/src/index.d.ts +1 -1
  365. package/dist/packages/primitives/radio-group/src/RadioGroup.js.map +1 -1
  366. package/dist/packages/primitives/radio-group/src/RadioGroup.vue.d.ts.map +1 -1
  367. package/dist/packages/primitives/radio-group/src/RadioGroup.vue_vue_type_script_setup_true_lang.js +1 -1
  368. package/dist/packages/primitives/radio-group/src/RadioGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  369. package/dist/packages/primitives/rating/src/Rating.vue.d.ts +2 -2
  370. package/dist/packages/primitives/rating/src/index.d.ts +1 -1
  371. package/dist/packages/primitives/segmented-control/src/SegmentedControl.js.map +1 -1
  372. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue.d.ts.map +1 -1
  373. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js +2 -3
  374. package/dist/packages/primitives/segmented-control/src/SegmentedControl.vue_vue_type_script_setup_true_lang.js.map +1 -1
  375. package/dist/packages/primitives/select/src/Select.js.map +1 -1
  376. package/dist/packages/primitives/select/src/Select.vue.d.ts.map +1 -1
  377. package/dist/packages/primitives/select/src/Select.vue_vue_type_script_setup_true_lang.js +1 -1
  378. package/dist/packages/primitives/select/src/Select.vue_vue_type_script_setup_true_lang.js.map +1 -1
  379. package/dist/packages/primitives/skip-link/src/SkipLink.js +7 -0
  380. package/dist/packages/primitives/skip-link/src/SkipLink.js.map +1 -0
  381. package/dist/packages/primitives/skip-link/src/SkipLink.vue.d.ts +62 -0
  382. package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js +64 -0
  383. package/dist/packages/primitives/skip-link/src/SkipLink.vue_vue_type_script_setup_true_lang.js.map +1 -0
  384. package/dist/packages/primitives/skip-link/src/index.d.ts +3 -0
  385. package/dist/packages/primitives/skip-link/src/index.js +3 -0
  386. package/dist/packages/primitives/slider/src/Slider.js.map +1 -1
  387. package/dist/packages/primitives/slider/src/Slider.vue.d.ts +6 -3
  388. package/dist/packages/primitives/slider/src/Slider.vue.d.ts.map +1 -1
  389. package/dist/packages/primitives/slider/src/Slider.vue_vue_type_script_setup_true_lang.js +15 -10
  390. package/dist/packages/primitives/slider/src/Slider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  391. package/dist/packages/primitives/stepper/src/Stepper.js.map +1 -1
  392. package/dist/packages/primitives/stepper/src/Stepper.vue.d.ts +2 -2
  393. package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js +7 -5
  394. package/dist/packages/primitives/stepper/src/Stepper.vue_vue_type_script_setup_true_lang.js.map +1 -1
  395. package/dist/packages/primitives/stepper/src/index.d.ts +1 -1
  396. package/dist/packages/primitives/switch/src/Switch.js.map +1 -1
  397. package/dist/packages/primitives/switch/src/Switch.vue.d.ts +1 -1
  398. package/dist/packages/primitives/switch/src/Switch.vue.d.ts.map +1 -1
  399. package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js +3 -3
  400. package/dist/packages/primitives/switch/src/Switch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  401. package/dist/packages/primitives/table/src/Table.js +7 -0
  402. package/dist/packages/primitives/table/src/Table.js.map +1 -0
  403. package/dist/packages/primitives/table/src/Table.vue.d.ts +45 -0
  404. package/dist/packages/primitives/table/src/Table.vue_vue_type_script_setup_true_lang.js +72 -0
  405. package/dist/packages/primitives/table/src/Table.vue_vue_type_script_setup_true_lang.js.map +1 -0
  406. package/dist/packages/primitives/table/src/TableCell.js +7 -0
  407. package/dist/packages/primitives/table/src/TableCell.js.map +1 -0
  408. package/dist/packages/primitives/table/src/TableCell.vue.d.ts +19 -0
  409. package/dist/packages/primitives/table/src/TableCell.vue.d.ts.map +1 -0
  410. package/dist/packages/primitives/table/src/TableCell.vue_vue_type_script_setup_true_lang.js +17 -0
  411. package/dist/packages/primitives/table/src/TableCell.vue_vue_type_script_setup_true_lang.js.map +1 -0
  412. package/dist/packages/primitives/table/src/TableHead.js +7 -0
  413. package/dist/packages/primitives/table/src/TableHead.js.map +1 -0
  414. package/dist/packages/primitives/table/src/TableHead.vue.d.ts +56 -0
  415. package/dist/packages/primitives/table/src/TableHead.vue_vue_type_script_setup_true_lang.js +79 -0
  416. package/dist/packages/primitives/table/src/TableHead.vue_vue_type_script_setup_true_lang.js.map +1 -0
  417. package/dist/packages/primitives/table/src/TableRow.js +7 -0
  418. package/dist/packages/primitives/table/src/TableRow.js.map +1 -0
  419. package/dist/packages/primitives/table/src/TableRow.vue.d.ts +39 -0
  420. package/dist/packages/primitives/table/src/TableRow.vue.d.ts.map +1 -0
  421. package/dist/packages/primitives/table/src/TableRow.vue_vue_type_script_setup_true_lang.js +61 -0
  422. package/dist/packages/primitives/table/src/TableRow.vue_vue_type_script_setup_true_lang.js.map +1 -0
  423. package/dist/packages/primitives/table/src/index.d.ts +10 -0
  424. package/dist/packages/primitives/table/src/index.js +8 -0
  425. package/dist/packages/primitives/tabs/src/Tabs.js.map +1 -1
  426. package/dist/packages/primitives/tabs/src/Tabs.vue.d.ts.map +1 -1
  427. package/dist/packages/primitives/tabs/src/Tabs.vue_vue_type_script_setup_true_lang.js +1 -1
  428. package/dist/packages/primitives/tabs/src/Tabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  429. package/dist/packages/primitives/tags-input/src/TagsInput.js.map +1 -1
  430. package/dist/packages/primitives/tags-input/src/TagsInput.vue.d.ts +2 -2
  431. package/dist/packages/primitives/tags-input/src/TagsInput.vue_vue_type_script_setup_true_lang.js +1 -1
  432. package/dist/packages/primitives/tags-input/src/TagsInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  433. package/dist/packages/primitives/tags-input/src/index.d.ts +1 -1
  434. package/dist/packages/primitives/text-field/src/TextField.js.map +1 -1
  435. package/dist/packages/primitives/text-field/src/TextField.vue.d.ts +2 -2
  436. package/dist/packages/primitives/text-field/src/TextField.vue_vue_type_script_setup_true_lang.js +1 -1
  437. package/dist/packages/primitives/text-field/src/TextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  438. package/dist/packages/primitives/text-field/src/index.d.ts +1 -1
  439. package/dist/packages/primitives/text-field/src/index.js +2 -2
  440. package/dist/packages/primitives/textarea/src/Textarea.js.map +1 -1
  441. package/dist/packages/primitives/textarea/src/Textarea.vue.d.ts +2 -2
  442. package/dist/packages/primitives/textarea/src/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
  443. package/dist/packages/primitives/textarea/src/Textarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  444. package/dist/packages/primitives/textarea/src/index.d.ts +1 -1
  445. package/dist/packages/primitives/textarea/src/index.js +2 -2
  446. package/dist/packages/primitives/time-picker/src/TimePicker.js.map +1 -1
  447. package/dist/packages/primitives/time-picker/src/TimePicker.vue.d.ts +2 -2
  448. package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js +10 -8
  449. package/dist/packages/primitives/time-picker/src/TimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  450. package/dist/packages/primitives/timeline/src/Timeline.js +7 -0
  451. package/dist/packages/primitives/timeline/src/Timeline.js.map +1 -0
  452. package/dist/packages/primitives/timeline/src/Timeline.vue.d.ts +37 -0
  453. package/dist/packages/primitives/timeline/src/Timeline.vue_vue_type_script_setup_true_lang.js +44 -0
  454. package/dist/packages/primitives/timeline/src/Timeline.vue_vue_type_script_setup_true_lang.js.map +1 -0
  455. package/dist/packages/primitives/timeline/src/TimelineItem.js +7 -0
  456. package/dist/packages/primitives/timeline/src/TimelineItem.js.map +1 -0
  457. package/dist/packages/primitives/timeline/src/TimelineItem.vue.d.ts +50 -0
  458. package/dist/packages/primitives/timeline/src/TimelineItem.vue_vue_type_script_setup_true_lang.js +96 -0
  459. package/dist/packages/primitives/timeline/src/TimelineItem.vue_vue_type_script_setup_true_lang.js.map +1 -0
  460. package/dist/packages/primitives/timeline/src/index.d.ts +5 -0
  461. package/dist/packages/primitives/timeline/src/index.js +5 -0
  462. package/dist/packages/primitives/toast/src/Toast.vue.d.ts +2 -2
  463. package/dist/packages/primitives/toast/src/ToastItem.js.map +1 -1
  464. package/dist/packages/primitives/toast/src/ToastItem.vue.d.ts +2 -2
  465. package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js +5 -3
  466. package/dist/packages/primitives/toast/src/ToastItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  467. package/dist/packages/primitives/toast/src/index.d.ts +1 -1
  468. package/dist/packages/primitives/toggle-group/src/ToggleGroup.js +7 -0
  469. package/dist/packages/primitives/toggle-group/src/ToggleGroup.js.map +1 -0
  470. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts +53 -0
  471. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue.d.ts.map +1 -0
  472. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js +111 -0
  473. package/dist/packages/primitives/toggle-group/src/ToggleGroup.vue_vue_type_script_setup_true_lang.js.map +1 -0
  474. package/dist/packages/primitives/toggle-group/src/index.d.ts +3 -0
  475. package/dist/packages/primitives/toggle-group/src/index.d.ts.map +1 -0
  476. package/dist/packages/primitives/toggle-group/src/index.js +2 -0
  477. package/dist/packages/primitives/toolbar/src/Toolbar.js +7 -0
  478. package/dist/packages/primitives/toolbar/src/Toolbar.js.map +1 -0
  479. package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts +31 -0
  480. package/dist/packages/primitives/toolbar/src/Toolbar.vue.d.ts.map +1 -0
  481. package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js +149 -0
  482. package/dist/packages/primitives/toolbar/src/Toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  483. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js +7 -0
  484. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.js.map +1 -0
  485. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts +24 -0
  486. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue.d.ts.map +1 -0
  487. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js +40 -0
  488. package/dist/packages/primitives/toolbar/src/ToolbarSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -0
  489. package/dist/packages/primitives/toolbar/src/index.d.ts +4 -0
  490. package/dist/packages/primitives/toolbar/src/index.d.ts.map +1 -0
  491. package/dist/packages/primitives/toolbar/src/index.js +3 -0
  492. package/dist/packages/primitives/tooltip/src/Tooltip.js.map +1 -1
  493. package/dist/packages/primitives/tooltip/src/Tooltip.vue.d.ts.map +1 -1
  494. package/dist/packages/primitives/tooltip/src/Tooltip.vue_vue_type_script_setup_true_lang.js +1 -1
  495. package/dist/packages/primitives/tooltip/src/Tooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  496. package/dist/packages/primitives/tree-view/src/TreeView.js +7 -0
  497. package/dist/packages/primitives/tree-view/src/TreeView.js.map +1 -0
  498. package/dist/packages/primitives/tree-view/src/TreeView.vue.d.ts +99 -0
  499. package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js +310 -0
  500. package/dist/packages/primitives/tree-view/src/TreeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  501. package/dist/packages/primitives/tree-view/src/TreeViewNode.js +7 -0
  502. package/dist/packages/primitives/tree-view/src/TreeViewNode.js.map +1 -0
  503. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts +15 -0
  504. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue.d.ts.map +1 -0
  505. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js +103 -0
  506. package/dist/packages/primitives/tree-view/src/TreeViewNode.vue_vue_type_script_setup_true_lang.js.map +1 -0
  507. package/dist/packages/primitives/tree-view/src/context.d.ts +39 -0
  508. package/dist/packages/primitives/tree-view/src/context.d.ts.map +1 -0
  509. package/dist/packages/primitives/tree-view/src/context.js +6 -0
  510. package/dist/packages/primitives/tree-view/src/context.js.map +1 -0
  511. package/dist/packages/primitives/tree-view/src/index.d.ts +4 -0
  512. package/dist/packages/primitives/tree-view/src/index.js +3 -0
  513. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js +7 -0
  514. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.js.map +1 -0
  515. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts +21 -0
  516. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue.d.ts.map +1 -0
  517. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js +27 -0
  518. package/dist/packages/primitives/visually-hidden/src/VisuallyHidden.vue_vue_type_script_setup_true_lang.js.map +1 -0
  519. package/dist/packages/primitives/visually-hidden/src/index.d.ts +3 -0
  520. package/dist/packages/primitives/visually-hidden/src/index.d.ts.map +1 -0
  521. package/dist/packages/primitives/visually-hidden/src/index.js +2 -0
  522. package/dist/packages/primitives/window-controls/src/WindowControls.vue.d.ts +2 -2
  523. package/dist/packages/primitives/window-controls/src/index.d.ts +1 -1
  524. package/dist/styles/global.css +59 -0
  525. package/dist/styles/theme.css +13 -0
  526. package/dist/theme.js +3 -0
  527. package/package.json +37 -30
  528. package/dist/packages/blocks/desktop-app-shell/src/DesktopAppShell.vue.d.ts.map +0 -1
  529. package/dist/packages/blocks/title-bar/src/TitleBar.vue.d.ts.map +0 -1
  530. package/dist/packages/blocks/toast-stack/src/ToastStack.vue.d.ts.map +0 -1
  531. package/dist/packages/composition/scroll-reel/src/ScrollReel.vue.d.ts.map +0 -1
  532. package/dist/packages/composition/scroll-reel/src/index.d.ts.map +0 -1
  533. package/dist/packages/loom/src/a11y.d.ts.map +0 -1
  534. package/dist/packages/loom/src/index.d.ts.map +0 -1
  535. package/dist/packages/primitives/alert-dialog/src/AlertDialog.vue.d.ts.map +0 -1
  536. package/dist/packages/primitives/alert-dialog/src/index.d.ts.map +0 -1
  537. package/dist/packages/primitives/avatar-group/src/AvatarGroup.vue.d.ts.map +0 -1
  538. package/dist/packages/primitives/avatar-group/src/index.d.ts.map +0 -1
  539. package/dist/packages/primitives/breadcrumb/src/Breadcrumb.vue.d.ts.map +0 -1
  540. package/dist/packages/primitives/breadcrumb/src/index.d.ts.map +0 -1
  541. package/dist/packages/primitives/color-picker/src/ColorPicker.vue.d.ts.map +0 -1
  542. package/dist/packages/primitives/color-picker/src/index.d.ts.map +0 -1
  543. package/dist/packages/primitives/combobox/src/Combobox.vue.d.ts.map +0 -1
  544. package/dist/packages/primitives/combobox/src/index.d.ts.map +0 -1
  545. package/dist/packages/primitives/date-picker/src/DatePicker.vue.d.ts.map +0 -1
  546. package/dist/packages/primitives/date-range-picker/src/DateRangePicker.vue.d.ts.map +0 -1
  547. package/dist/packages/primitives/date-range-picker/src/index.d.ts.map +0 -1
  548. package/dist/packages/primitives/date-time-picker/src/DateTimePicker.vue.d.ts.map +0 -1
  549. package/dist/packages/primitives/date-time-range-picker/src/DateTimeRangePicker.vue.d.ts.map +0 -1
  550. package/dist/packages/primitives/date-time-range-picker/src/index.d.ts.map +0 -1
  551. package/dist/packages/primitives/dialog/src/Dialog.vue.d.ts.map +0 -1
  552. package/dist/packages/primitives/dialog/src/index.d.ts.map +0 -1
  553. package/dist/packages/primitives/drawer/src/Drawer.vue.d.ts.map +0 -1
  554. package/dist/packages/primitives/drawer/src/index.d.ts.map +0 -1
  555. package/dist/packages/primitives/editable/src/Editable.vue.d.ts.map +0 -1
  556. package/dist/packages/primitives/editable/src/index.d.ts.map +0 -1
  557. package/dist/packages/primitives/file-upload/src/FileUpload.vue.d.ts.map +0 -1
  558. package/dist/packages/primitives/file-upload/src/index.d.ts.map +0 -1
  559. package/dist/packages/primitives/indicator/src/Indicator.vue.d.ts.map +0 -1
  560. package/dist/packages/primitives/indicator/src/index.d.ts.map +0 -1
  561. package/dist/packages/primitives/number-field/src/NumberField.vue.d.ts.map +0 -1
  562. package/dist/packages/primitives/number-field/src/index.d.ts.map +0 -1
  563. package/dist/packages/primitives/otp-input/src/OtpInput.vue.d.ts.map +0 -1
  564. package/dist/packages/primitives/otp-input/src/index.d.ts.map +0 -1
  565. package/dist/packages/primitives/pagination/src/Pagination.vue.d.ts.map +0 -1
  566. package/dist/packages/primitives/pagination/src/index.d.ts.map +0 -1
  567. package/dist/packages/primitives/progress/src/Progress.vue.d.ts.map +0 -1
  568. package/dist/packages/primitives/progress/src/index.d.ts.map +0 -1
  569. package/dist/packages/primitives/radial-progress/src/RadialProgress.vue.d.ts.map +0 -1
  570. package/dist/packages/primitives/radial-progress/src/index.d.ts.map +0 -1
  571. package/dist/packages/primitives/rating/src/Rating.vue.d.ts.map +0 -1
  572. package/dist/packages/primitives/rating/src/index.d.ts.map +0 -1
  573. package/dist/packages/primitives/stepper/src/Stepper.vue.d.ts.map +0 -1
  574. package/dist/packages/primitives/stepper/src/index.d.ts.map +0 -1
  575. package/dist/packages/primitives/tags-input/src/TagsInput.vue.d.ts.map +0 -1
  576. package/dist/packages/primitives/tags-input/src/index.d.ts.map +0 -1
  577. package/dist/packages/primitives/text-field/src/TextField.vue.d.ts.map +0 -1
  578. package/dist/packages/primitives/text-field/src/index.d.ts.map +0 -1
  579. package/dist/packages/primitives/textarea/src/Textarea.vue.d.ts.map +0 -1
  580. package/dist/packages/primitives/textarea/src/index.d.ts.map +0 -1
  581. package/dist/packages/primitives/time-picker/src/TimePicker.vue.d.ts.map +0 -1
  582. package/dist/packages/primitives/toast/src/Toast.vue.d.ts.map +0 -1
  583. package/dist/packages/primitives/toast/src/ToastItem.vue.d.ts.map +0 -1
  584. package/dist/packages/primitives/toast/src/index.d.ts.map +0 -1
  585. package/dist/packages/primitives/window-controls/src/WindowControls.vue.d.ts.map +0 -1
  586. package/dist/packages/primitives/window-controls/src/index.d.ts.map +0 -1
  587. package/dist/styles/theme.contrast.test.ts +0 -460
@@ -1 +1 @@
1
- {"version":3,"file":"TagsInput.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n { deep: true },\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"TagsInput.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n // Shallow, deliberately: the contract is whole-list replacement — `commit`\n // emits the complete token list on every change and a host echoes it\n // straight back down — so in-place mutation is not a case this mirror owes\n // anyone. Same reasoning as Combobox's mirror of its selection.\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
- import { type LabelOverrides } from "@ecoma-io/loom-labels";
2
- import type { TagsInputLabels, TagsInputRejection } from "@ecoma-io/loom-labels";
1
+ import { type LabelOverrides } from "../../../labels/src/index";
2
+ import type { TagsInputLabels, TagsInputRejection } from "../../../labels/src/index";
3
3
  /**
4
4
  * Loom's English, co-located with the component so it tree-shakes with it, and
5
5
  * exported so a host can build a partial vocabulary against the real thing
@@ -119,7 +119,7 @@ var TagsInput_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
119
119
  const tokens = ref([...props.modelValue ?? []]);
120
120
  watch(() => props.modelValue, (next) => {
121
121
  if (next !== void 0) tokens.value = [...next];
122
- }, { deep: true });
122
+ });
123
123
  const rejections = ref([]);
124
124
  const message = computed(() => rejections.value.length > 0 ? text.value.rejected({
125
125
  rejections: rejections.value,
@@ -1 +1 @@
1
- {"version":3,"file":"TagsInput.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n { deep: true },\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"TagsInput.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../../../packages/primitives/tags-input/src/TagsInput.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type {\n TagsInputLabels,\n TagsInputRejectReason,\n TagsInputRejection,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * Every refusal names the value, because a reader looking at eight tokens\n * cannot tell which \"that one\" is.\n *\n * Loom's English does not name the limit, though `rejected` is handed it: a\n * `max` refusal only ever happens when a limit is set, so a sentence that also\n * had to word the limitless case would be a branch nothing can reach. The\n * number is there for a host that wants \"only 3 can be added\" — and the count\n * description already reads \"3 of 3 tags\" on focus either way.\n */\nfunction describeRejection(rejection: TagsInputRejection): string {\n switch (rejection.reason) {\n case \"duplicate\":\n return `${rejection.value} has already been added.`;\n case \"max\":\n return `${rejection.value} was not added — the field is full.`;\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 * rather than a transcription of it.\n */\nexport const TAGS_INPUT_LABELS: TagsInputLabels = {\n remove: ({ value }) => `Remove ${value}`,\n count: ({ count, max }) =>\n max === undefined\n ? `${String(count)} ${count === 1 ? \"tag\" : \"tags\"}`\n : `${String(count)} of ${String(max)} tags`,\n rejected: ({ rejections }) => rejections.map(describeRejection).join(\" \"),\n clear: \"Clear all tags\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport {\n TagsInputRoot,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemText,\n TagsInputClear,\n} from \"reka-ui\";\nimport { TriangleAlert, X } from \"@lucide/vue\";\nimport Chip from \"@ecoma-io/loom-chip\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TagsInput — several values typed into one field, each committed to a\n * removable token: keywords on an article, recipients on a message, labels on\n * an issue.\n *\n * The line against its neighbours is what the values *are*. A\n * [Select](../Select/Select.vue) or a [Combobox](../Combobox/Combobox.vue)\n * picks from a list somebody else wrote, and cannot produce a value that is not\n * already in it; this field's whole point is that the reader invents the\n * values. A [TextField](../TextField/TextField.vue) holds one value, and a\n * comma-separated string in one is a list the application has to parse and the\n * reader has to punctuate correctly. Reach for\n * [Chip](@ecoma-io/loom-chip) alone when the tokens are a display of something\n * already decided — a chip row nobody types into is not a field.\n *\n * Each token *is* a Chip, in the removable shape Chip already ships, so a token\n * here and a chip elsewhere in the same form are the same object. The remove\n * control is Chip's own rather than Reka's `TagsInputItemDelete`, which is\n * `tabindex=\"-1\"` by design: Reka pairs it with an arrow-key selection that\n * moves no focus and sets no `aria-activedescendant`, so the token it\n * highlights is announced to nobody. Loom keeps the buttons on the tab order\n * and replaces the selection outright — see `onKeydownCapture`.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id of\n * the hint or error line, and `required` / `invalid` / `disabled` / `readonly` /\n * `name` all arrive through `useFieldControl()`. Every prop below still wins\n * over the row when it is set, in both directions — which is why the four\n * booleans default to `undefined` rather than `false`.\n *\n * `readonly` and `disabled` are different states here as everywhere else, and\n * the difference is larger than usual because there are two things to freeze. A\n * read-only field shows its tokens, refuses to grow one and drops every remove\n * control, while staying a tab stop and staying in the submitted data. A\n * disabled field is unavailable: not a tab stop, not submitted, and its tokens\n * drain to the neutral fill.\n *\n * That makes **three** resting appearances rather than two, and each is three\n * changes rather than one — fill, text colour and border weight together. See\n * the box's class list for why one channel is not enough.\n */\nconst props = withDefaults(\n defineProps<{\n /** The committed tokens. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string[] | undefined;\n /** Text shown in the empty text box. It is hidden once a token exists, where it would sit beside the tokens claiming the field is empty. */\n placeholder?: string;\n /** The most tokens the field will hold. Anything past it is refused and reported rather than dropped. */\n max?: number;\n /** Whether the same value may be committed twice. Off, so a repeated value is refused and says so. */\n duplicates?: boolean;\n /** What commits a token besides Enter. Typing it, or pasting text containing it, splits at it. */\n delimiter?: string;\n /** Renders a control that empties the field in one press. */\n clearable?: boolean;\n /** Disables the field: no tab stop, no editing, no remove controls, nothing submitted. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the tokens without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post. Each token is submitted as `name[0]`, `name[1]`, … Unset defers to a wrapping Field. */\n name?: string;\n /** The accessible name of the text box, when no visible element already labels it. */\n ariaLabel?: string;\n /** The id of the visible element that labels the text box (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TagsInputLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field whose tokens are people,\n * where \"Remove Ana Duarte\" reads and \"Remove tag\" does not.\n */\n labels?: LabelOverrides<TagsInputLabels>;\n }>(),\n {\n // Not `[]`. An empty array is a concrete value, so it would say \"the host\n // owns this list\", and a host that binds nothing would watch every token it\n // typed vanish on the next render. `undefined` is what leaves the control\n // managing its own — the same distinction `src/lib/props.ts` exists to\n // preserve for the Reka-backed primitives.\n modelValue: undefined,\n duplicates: false,\n delimiter: \",\",\n clearable: false,\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that past\n // Vue's absent-Boolean-casts-to-false rule. TextField.vue carries the full\n // reasoning.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The full token list after the change, trimmed and de-duplicated. Never a partial list. */\n \"update:modelValue\": [values: string[]];\n /** Every value refused by this one interaction, each with the rule that refused it. Fires once per interaction, not once per value. */\n reject: [rejections: TagsInputRejection[]];\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(\"tagsInput\", TAGS_INPUT_LABELS, () => props.labels);\n\n// `class` sizes the whole control and lands on the outer element; everything\n// else a caller passes — `id`, `data-testid`, `aria-describedby`,\n// `autocomplete` — describes the real `<input>` and lands there. The outer\n// element is not a form control and the input is not the box a caller lays out,\n// so there is no one node both belong on.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\n\nconst generatedId = useId();\nconst countId = useId();\nconst messageId = useId();\n\nconst inputRef = useTemplateRef<{ $el: HTMLInputElement }>(\"input\");\n\n/**\n * The committed tokens, and the reason this control keeps its own copy rather\n * than forwarding `modelValue` into Reka and letting it manage the state.\n *\n * Loom applies four rules Reka has no notion of — trim, drop the blanks a paste\n * leaves behind, refuse a duplicate, refuse anything past `max` — so Reka is\n * configured to accept everything (`duplicate`, no `max`) and *propose*, and\n * `onProposed` decides. That needs a value flowing back down, which means a\n * `:model-value` that is always defined and always ours.\n *\n * It is `useVModel(…, { passive: true })` written out, and behaves the same way\n * Reka's own would: the change is taken locally and emitted, and a controlled\n * host that declines it is honoured on the next value it does send. Seeded from\n * the prop rather than sanitised, because a list the host already holds is the\n * host's to hold — the rules above are about what this field *accepts*.\n */\nconst tokens = ref<string[]>([...(props.modelValue ?? [])]);\nwatch(\n () => props.modelValue,\n (next) => {\n if (next !== undefined) tokens.value = [...next];\n },\n // Shallow, deliberately: the contract is whole-list replacement — `commit`\n // emits the complete token list on every change and a host echoes it\n // straight back down — so in-place mutation is not a case this mirror owes\n // anyone. Same reasoning as Combobox's mirror of its selection.\n);\n\nconst rejections = ref<TagsInputRejection[]>([]);\nconst message = computed(() =>\n rejections.value.length > 0\n ? text.value.rejected({ rejections: rejections.value, max: props.max })\n : \"\",\n);\n\n// The refusal message reads first and the count second: both describe the\n// field, and the one that just changed is the more specific thing to say. A\n// wrapping Field's own hint or error is merged in after both by\n// `useFieldControl`, never in place of them.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [\n message.value.length > 0 ? messageId : undefined,\n countId,\n attrs[\"aria-describedby\"],\n ]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// The `<label for>` a Field renders has to resolve to the text box, and a\n// caller passing their own id must not end up with a label pointing at nothing.\nconst inputId = computed(() => field.id ?? generatedId);\n\n/**\n * A stable key per token, since the token itself cannot be one: `duplicates`\n * makes two identical strings legal and Vue warns on the collision.\n *\n * The n-th occurrence of a value keys as `n:value`, so removing one of two\n * identical tokens unmounts exactly one node and the survivor keeps the other —\n * indistinguishable, because the two say the same thing. Keying by index\n * instead would re-key every row after the one removed, which patches text into\n * nodes that belong to other tokens.\n */\nconst rows = computed(() => {\n const seen = new Map<string, number>();\n return tokens.value.map((value) => {\n const ordinal = seen.get(value) ?? 0;\n seen.set(value, ordinal + 1);\n return { value, key: `${String(ordinal)}:${value}` };\n });\n});\n\n/**\n * Every rule this field applies to a value, in the order a reader meets them,\n * over the *whole* proposal rather than only its tail — the list on screen can\n * never hold a token the box would have refused, whatever a host seeded it with.\n *\n * `settled` is how many entries were already committed, and only what lies past\n * it is *reported*. Everything before it is normalised in silence: a host that\n * seeded two identical values while asking for none is corrected, but saying\n * \"design has already been added\" about a token that was there when the page\n * loaded, at the moment the reader adds something else entirely, describes\n * nothing they did.\n */\nfunction reduce(\n proposed: readonly string[],\n settled: number,\n): { kept: string[]; refused: TagsInputRejection[] } {\n const kept: string[] = [];\n const refused: TagsInputRejection[] = [];\n\n proposed.forEach((raw, index) => {\n const value = raw.trim();\n // Skipped rather than refused. A blank is what a trailing delimiter leaves\n // behind — \"a, b, c,\" pastes as four segments — and a reader who pasted\n // three values did not ask about a fourth.\n if (value === \"\") return;\n\n const reason: TagsInputRejectReason | undefined =\n props.max !== undefined && kept.length >= props.max\n ? \"max\"\n : !props.duplicates && kept.includes(value)\n ? \"duplicate\"\n : undefined;\n\n if (reason === undefined) kept.push(value);\n else if (index >= settled) refused.push({ value, reason });\n });\n\n return { kept, refused };\n}\n\nfunction same(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\nfunction focusInput(): void {\n void nextTick(() => inputRef.value?.$el.focus());\n}\n\nfunction commit(next: string[]): void {\n // The message named values the reader has since edited past. An error sitting\n // under a field somebody is actively fixing reads as an error about the fix.\n rejections.value = [];\n tokens.value = next;\n emit(\"update:modelValue\", next);\n}\n\n/**\n * Reka has already written `proposed` into its own model by the time this runs.\n * `tokens.value` is assigned on every path, including the one where nothing\n * changed, and that is load-bearing: `reduce` returns a fresh array, and only a\n * new reference on the prop Reka watches puts its internal copy back in step\n * with what this field actually accepted. Skip the assignment and a refused\n * value stays in Reka's model — invisible on screen, and submitted by the\n * hidden inputs it renders for `name`.\n */\nfunction onProposed(proposed: string[]): void {\n const { kept, refused } = reduce(proposed, tokens.value.length);\n const changed = !same(kept, tokens.value);\n\n if (changed) commit(kept);\n else tokens.value = kept;\n\n if (refused.length === 0) return;\n rejections.value = refused;\n emit(\"reject\", refused);\n\n // One refusal from one typed commit is the reader's own draft, and Reka has\n // just cleared the box under them. Put it back: a field that eats what it\n // will not take leaves the reader retyping a value they can no longer see.\n // Only the single case — restoring one of three pasted values would be\n // choosing between them.\n const only = refused.length === 1 ? refused[0] : undefined;\n if (only === undefined) return;\n void nextTick(() => {\n const input = inputRef.value?.$el;\n if (!input || input.value !== \"\") return;\n input.value = only.value;\n input.setSelectionRange(only.value.length, only.value.length);\n });\n}\n\nfunction removeAt(index: number): void {\n if (field.disabled || field.readonly) return;\n commit(tokens.value.filter((_, at) => at !== index));\n // The button that was pressed has just unmounted, and a browser drops focus\n // to `<body>` when it does. The text box is where the reader was heading\n // anyway, and it is the one node in this control that never goes away.\n focusInput();\n}\n\n/**\n * The keys Reka's `onInputKeydown` claims once the caret sits at the very\n * start of the text box, and the reason this handler runs in the capture phase\n * on the way down rather than as a listener on the input itself: a fallthrough\n * handler is merged *after* Reka's and cannot stop it.\n *\n * What Reka does with them is a virtual selection — Backspace marks the last\n * token, arrows walk it, a second Backspace deletes it — that moves no focus\n * and publishes no `aria-activedescendant`. On screen it is a highlight; to a\n * screen reader it is nothing at all, so the second Backspace destroys a value\n * the reader was never told was chosen. That is the failure this control is\n * most likely to ship, so the whole mechanism is stopped here.\n *\n * In its place, one rule: **Backspace on an empty box lifts the last token back\n * into it**, as editable text with the caret at the end. Nothing is destroyed —\n * the value is on screen, in a box the reader is already focused on, and Enter\n * puts it straight back. The token that left is a `commit`, so a host watching\n * `update:modelValue` sees the edit begin.\n */\nconst CARET_KEYS = new Set([\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\nfunction onKeydownCapture(event: KeyboardEvent): void {\n // An IME's own Backspace edits the composition, and Reka's handlers bail on\n // the same flag for the same reason.\n if (event.isComposing) return;\n const input = event.target;\n if (!(input instanceof HTMLInputElement)) return;\n if (!CARET_KEYS.has(event.key)) return;\n if (input.selectionStart !== 0 || input.selectionEnd !== 0) return;\n\n // Stopped only for the keys and only at the caret position where Reka would\n // have acted. Everything else — Enter, the delimiter, every printable\n // character — reaches its handlers untouched.\n event.stopPropagation();\n\n if (event.key !== \"Backspace\" || input.value !== \"\") return;\n if (field.disabled || field.readonly) return;\n const last = tokens.value.at(-1);\n if (last === undefined) return;\n\n event.preventDefault();\n commit(tokens.value.slice(0, -1));\n input.value = last;\n input.setSelectionRange(last.length, last.length);\n}\n\n/**\n * A read-only field takes nothing, and the native `readonly` attribute is not\n * enough to say so. Reka's paste handler reads the clipboard and commits from\n * it directly, without ever consulting the input it fired on, so a paste into a\n * read-only field would grow tokens the reader cannot then remove.\n */\nfunction onPasteCapture(event: ClipboardEvent): void {\n if (!field.readonly && !field.disabled) return;\n event.preventDefault();\n event.stopPropagation();\n}\n</script>\n\n<template>\n <TagsInputRoot\n as=\"div\"\n v-bind=\"optional({ name: field.name })\"\n :model-value=\"tokens\"\n :delimiter=\"delimiter\"\n :disabled=\"field.disabled\"\n :required=\"field.required\"\n add-on-paste\n duplicate\n :class=\"cn('flex flex-col', attrs.class as string)\"\n @update:model-value=\"onProposed\"\n @keydown.capture=\"onKeydownCapture\"\n @paste.capture=\"onPasteCapture\"\n >\n <!-- `duplicate` and the absent `max` above are deliberate: Reka is told to\n accept every value it is offered, so that one place — `reduce` —\n decides what a token is. Two refusal ladders would be two answers to\n `duplicates`, and only one would be the one `reject` reports.\n\n The state markers live on the box rather than on the root, and not by\n preference: an attribute bound on `TagsInputRoot` is a fallthrough\n attr, and the root renders through Reka's `CollectionSlot`, which does\n not carry one to the element underneath. The box is also the honest\n node for them — it is what the state is painted on. -->\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-2 py-0.5 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus, exactly as TextField's\n // does — a field holding tokens is still a field, and a form row\n // should not have two focus languages in it.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Lifted rather than dimmed, and it keeps its focus ring: a read-only\n // field is a value **on show**, not an unavailable control. The fill\n // rises off `background` to `subtle` so the box reads as settled, and\n // the text stays full strength — 13.45:1 over that fill — because\n // being read is the entire purpose of the state. The border does not\n // move, because nothing about the field's reach has: it is still a\n // tab stop and still submitted.\n field.readonly && 'bg-subtle',\n // Unavailable is a step further down all three channels: the fill\n // drains to `muted`, the text follows to `muted-foreground` (4.67:1\n // over it) and the rim slackens from `input` to the lighter `border`.\n //\n // Read-only and disabled used to share `bg-muted` and part on the\n // text colour alone, which is a distinction made in hue and nothing\n // else — the one thing a reader with a colour deficiency does not\n // receive. Three channels is what makes available → on show →\n // unavailable legible without it.\n //\n // No `opacity-50` anywhere in that, and that is the point. Every\n // token inside is a Chip that has already drained itself to a\n // measured unavailable fill; halving the alpha over the top of that\n // takes its label below the contrast floor. `border-border` and not\n // the strong weight, for the reason `theme.css` gives the strong one:\n // it is for a block that must **assert** its edge, and an unavailable\n // field is the one thing that must not.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` and\n // every token's remove button natively and never touches\n // `field.disabled`, so the rule above cannot fire and the box\n // rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` reads the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree.\n // See TextField.vue for why it is preferred to a `has-[:disabled]:`\n // rule reading this box's own contents.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated for the reason the two rules below are ordered: Tailwind\n // emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would\n // win on emission order alone.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would\n // lose its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n )\n \"\n >\n <!-- A real list, so a reader browsing the field meets the tokens as\n items rather than as a run of buttons. It is not where the count\n comes from, though: Tailwind's preflight sets `list-style: none` on\n every `ul`, and Safari drops the list role from a list styled that\n way — which is why the count below is published in words instead of\n left to the browser. -->\n <ul v-if=\"tokens.length > 0\" class=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n <TagsInputItem\n v-for=\"(row, index) in rows\"\n :key=\"row.key\"\n as=\"li\"\n :value=\"row.value\"\n class=\"flex animate-fade-rise\"\n >\n <Chip\n size=\"sm\"\n :removable=\"!field.readonly\"\n :disabled=\"field.disabled\"\n :remove-label=\"text.remove({ value: row.value })\"\n @remove=\"removeAt(index)\"\n >\n <TagsInputItemText />\n </Chip>\n </TagsInputItem>\n </ul>\n\n <!-- `field.attrs` spreads **after** the fallthrough attrs, and that\n position is the contract: the bag has already resolved the caller's\n own `id` and `aria-describedby`, and it drops every key that resolved\n to nothing, so it can only add.\n\n `:id` restates what the bag already holds, or supplies the id a\n Field's `<label for>` needs when it holds none. `:name` undoes one\n key of it on purpose — the text box holds a draft, not the value, and\n a `name` on it would post whatever was half-typed instead of the\n tokens. The value is posted by the hidden inputs the root renders. -->\n <TagsInputInput\n :id=\"inputId\"\n ref=\"input\"\n v-bind=\"{\n ...inputAttrs,\n ...field.attrs,\n ...optional({ placeholder: tokens.length === 0 ? placeholder : undefined }),\n }\"\n :name=\"undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :readonly=\"field.readonly\"\n class=\"h-8 min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n\n <TagsInputClear\n v-if=\"clearable && tokens.length > 0 && !field.readonly\"\n :aria-label=\"text.clear\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo data-[disabled]:cursor-not-allowed\"\n @click=\"focusInput\"\n >\n <X class=\"h-4 w-4\" />\n </TagsInputClear>\n\n <!-- The count, and the only thing that tells a reader how many tokens are\n in the field without walking them. Reka publishes nothing of the\n sort: its root is an unlabelled `div`. It is wired through\n `aria-describedby` rather than announced, so it is heard on focus and\n on every return to the field, which is when the question is asked. -->\n <span :id=\"countId\" class=\"sr-only\">\n {{ text.count({ count: tokens.length, max }) }}\n </span>\n </div>\n\n <!-- The region is in the DOM from the first render, empty. A live region\n mounted at the same moment as its text is a region assistive tech was\n not yet watching, so the first refusal — the one that matters most — is\n the one it announces least reliably. -->\n <div aria-live=\"polite\" aria-atomic=\"true\">\n <p\n v-if=\"message.length > 0\"\n :id=\"messageId\"\n class=\"mt-1.5 flex animate-fade-rise items-start gap-2 text-small text-destructive-text\"\n >\n <TriangleAlert class=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span>{{ message }}</span>\n </p>\n </div>\n </TagsInputRoot>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  export { default, TAGS_INPUT_LABELS } from "./TagsInput.vue";
2
- export type { TagsInputLabels, TagsInputRejectReason, TagsInputRejection, } from "@ecoma-io/loom-labels";
2
+ export type { TagsInputLabels, TagsInputRejectReason, TagsInputRejection, } from "../../../labels/src/index";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextField.js","names":[],"sources":["../../../../../packages/primitives/text-field/src/TextField.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TextFieldLabels } from \"@ecoma-io/loom-labels\";\nimport { COUNT_LABELS, warningWindow, type CountBand } from \"@ecoma-io/loom-labels\";\n\nexport type TextFieldType = \"text\" | \"email\" | \"password\" | \"search\" | \"url\" | \"tel\";\n\nexport type TextFieldSize = \"sm\" | \"md\" | \"lg\";\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 * rather than a transcription of it.\n */\nexport const TEXT_FIELD_LABELS: TextFieldLabels = {\n ...COUNT_LABELS,\n reveal: \"Show password\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TextField — single-line text entry (text · email · password · search · url ·\n * tel). The border, focus ring and invalid state live on the wrapper so\n * optional `#leading`/`#trailing` adornments (a search icon, a unit suffix)\n * sit *inside* the field and share its focus bloom, instead of every caller\n * re-inventing an icon-inside-input layout. For numeric entry reach for a\n * number field; for a bounded continuous value, a slider.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on\n * the wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the\n * rest of the attrs (`id`, `name`, `data-testid`, `aria-describedby`,\n * `autocomplete`) belong on the real `<input>` — the split-attrs convention\n * shared with the other bare controls. The accessible name is a first-class\n * prop pair (`aria-label`/`aria-labelledby`, camelized by Vue) rather than a\n * loose fallthrough attr, so it is bound onto the input explicitly and shows\n * up in the props table — a bare input primitive can only be named by its\n * host.\n *\n * That same wrapper is why `revealable` and the character counter are props\n * here rather than a `PasswordField` and a `CountedField` beside it. `password`\n * is already a `TextFieldType` and the adornment row already exists, so a\n * second component would duplicate an entire input, its Field wiring and its\n * three sizes to add one button; and a counter rendered *below* the field\n * would have to move the fallthrough `class` off the box a caller sizes with\n * it. Both live inside the border, where the adornments already live.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id\n * of the hint or error line, and `required` / `invalid` / `disabled` /\n * `readonly` / `name` all arrive through `useFieldControl()`, so\n * `<Field label=\"Email\" error=\"…\"><TextField /></Field>` needs no attributes at\n * the call site. Every prop below still wins over the row when it is set, in\n * both directions — which is why the four booleans default to `undefined`\n * rather than `false`.\n *\n * `readonly` and `disabled` are different states, not two dials on one, so the\n * field has **three** resting appearances rather than two. A read-only field\n * is a value the reader may see and copy but not change: it stays a Tab stop,\n * stays in the form's submitted data, and takes a lifted fill with its text at\n * full strength. A disabled field is unavailable: no Tab stop, not submitted,\n * and drained a step further, in fill, in text colour and in border weight\n * together. Three channels rather than one is the point — see the class list.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\n /** The native input type — also the on-screen keyboard hint on mobile. */\n type?: TextFieldType;\n /** Text shown in the empty field. */\n placeholder?: string;\n /** Disables the field and blocks pointer and keyboard input. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the value without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`. Unset defers to a wrapping Field. */\n name?: string;\n /** Control height. */\n size?: TextFieldSize;\n /** The accessible name, when no visible element already labels the field. */\n ariaLabel?: string;\n /** The id of the visible element that labels the field (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Adds a toggle that shows the typed password in place, for\n * `type=\"password\"` **and nothing else** — on any other type it renders\n * nothing at all, because there is nothing to reveal.\n */\n revealable?: boolean;\n /**\n * The character limit the counter reports against, and the point past\n * which it reads as over-limit.\n *\n * Advisory, exactly as `required` is: it does **not** set the native\n * `maxlength`, which silently truncates a paste and tells the reader\n * nothing. Pass `maxlength` yourself as a plain attribute where you want\n * the browser to enforce it too.\n */\n maxLength?: number;\n /**\n * Forces the counter on or off. Left unset it follows `maxLength` — a\n * limit the reader is never shown is one they can only discover by\n * exceeding it — so set it `true` for a running total with no limit, and\n * `false` to hold a limit the field reports nowhere.\n */\n showCount?: boolean | undefined;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TextFieldLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field counting something other\n * than characters, where \"80 words left\" is the true sentence.\n */\n labels?: LabelOverrides<TextFieldLabels>;\n }>(),\n {\n type: \"text\",\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that. The\n // explicit default is also what beats Vue's absent-Boolean-casts-to-false\n // rule — the same reason Checkbox's `modelValue` carries one.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n size: \"md\",\n revealable: false,\n // `undefined` for the same Boolean-cast reason, and here it is the whole\n // feature: `showCount ?? (maxLength !== undefined)` would be a dead branch\n // if an absent prop arrived as `false`.\n showCount: undefined,\n },\n);\n\nconst emit = defineEmits<{ \"update:modelValue\": [value: string] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\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(\"textField\", TEXT_FIELD_LABELS, () => props.labels);\n\nconst inputRef = useTemplateRef<HTMLInputElement>(\"input\");\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue`.\n *\n * Used without `v-model` this component is uncontrolled — `modelValue` stays\n * `undefined` while the reader types — and the counter still has to count. So\n * the last typed value is kept here and `currentValue` prefers the prop: a\n * controlled host that *declines* an update keeps the count honest, and an\n * uncontrolled one gets the text in front of it.\n *\n * Binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing and not cosmetic. Vue's `value` patch coerces `null`/`undefined`\n * to `\"\"`, so any re-render of an uncontrolled field wipes the input — a defect\n * nothing had met only because nothing in this component re-rendered on a\n * keystroke until the counter did.\n */\nconst typed = ref(props.modelValue ?? \"\");\nconst currentValue = computed(() => props.modelValue ?? typed.value);\n\nfunction onInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\n}\n\n/**\n * The reveal, and the two guards that make `revealable` inert rather than\n * surprising on a non-password field: the toggle is not rendered, and the type\n * cannot flip. A `type` that stops being `password` also drops the reveal —\n * coming back as a password already revealed would put a secret on screen that\n * nobody asked for a second time.\n */\nconst canReveal = computed(() => props.revealable && props.type === \"password\");\nconst revealed = ref(false);\nwatch(canReveal, (able) => {\n if (!able) revealed.value = false;\n});\n\nconst inputType = computed(() => (canReveal.value && revealed.value ? \"text\" : props.type));\n\n/**\n * Flipping `type` is what loses the caret: the input is re-created enough,\n * engine by engine, that the selection lands back at 0 or at the end, and a\n * reader who was fixing the third character of a long passphrase is now\n * somewhere else with no way to tell where. So the range is read before the\n * flip and written back after the DOM has it.\n *\n * Both `password` and `text` support the selection API (`email`, `url` and\n * `number` do not, and throw), and this path is unreachable for any other type\n * by construction — see `canReveal`.\n *\n * Focus is deliberately *not* moved back to the input. A reader who pressed\n * this with the keyboard is standing on the toggle and will very often press\n * it again; dropping them into the field would make hiding the password a\n * Shift+Tab away. The restored range is what greets them when they do return.\n */\nfunction toggleReveal(): void {\n const input = inputRef.value;\n const start = input?.selectionStart ?? null;\n const end = input?.selectionEnd ?? null;\n const direction = input?.selectionDirection ?? \"none\";\n\n revealed.value = !revealed.value;\n\n if (input === null || start === null || end === null) return;\n void nextTick(() => {\n input.setSelectionRange(start, end, direction);\n });\n}\n\n/**\n * Length in UTF-16 code units, which is what the platform's own `maxlength`\n * counts. `[...value].length` would count code points and disagree with the\n * browser on any astral character, so a caller who pairs this with a native\n * `maxlength` would watch the counter and the enforcement drift apart.\n */\nconst count = computed(() => currentValue.value.length);\nconst showsCount = computed(() => props.showCount ?? props.maxLength !== undefined);\n\nconst band = computed<CountBand>(() => {\n const max = props.maxLength;\n if (max === undefined) return \"under\";\n const remaining = max - count.value;\n if (remaining < 0) return \"over\";\n return remaining <= warningWindow(max) ? \"near\" : \"under\";\n});\n\nconst countText = computed(() => {\n const max = props.maxLength;\n if (max === undefined) return text.value.count({ count: count.value });\n const over = count.value - max;\n return over > 0\n ? text.value.countOverMax({ count: count.value, max, over })\n : text.value.countOfMax({ count: count.value, max });\n});\n\n/**\n * What the live region says, and it changes **only when the band changes**.\n *\n * A region holding the counter itself would speak on every keystroke, which is\n * worse than silence: it buries the field's own echo of the character just\n * typed under a number nobody asked for. What a reader needs is the two facts\n * they cannot see coming — the limit is close, and the limit is passed — so\n * this fires once at each crossing and goes quiet again on the way back down.\n *\n * The numbers are therefore true at the moment they are spoken and not after.\n * That is the deliberate trade: the visible counter carries the live figure,\n * and it is wired into `aria-describedby`, so the exact count is one\n * re-read away whenever the reader wants it rather than pushed at them.\n */\nconst announcement = ref(\"\");\nwatch(band, (now) => {\n const max = props.maxLength;\n if (max === undefined || now === \"under\") {\n announcement.value = \"\";\n return;\n }\n announcement.value =\n now === \"over\"\n ? text.value.limitExceeded({ over: count.value - max, max })\n : text.value.approachingLimit({ remaining: max - count.value, max });\n});\n\n// `id` and `aria-describedby` reach a bare control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it. The counter joins\n// that list rather than replacing anything: it is one more thing this field\n// says about itself, and it reads after the caller's own description and before\n// the row's message, which is the same most-specific-first order\n// `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads onto the `<input>` **after** `inputAttrs`, and\n// that position is the contract: the bag has already resolved the caller's own\n// values, and it drops every key that resolved to nothing, so it can only add.\n// The `:aria-invalid` binding that used to sit on the input is gone with it —\n// an individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop and quietly re-break the link\n// to the row.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [attrs[\"aria-describedby\"] as string | undefined, showsCount.value ? countId : \"\"]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n</script>\n\n<template>\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'group flex items-center gap-2 rounded-md border border-input bg-background text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus (Signature law): the field\n // catches light instead of casting a shadow, and focus draws it tight.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n size === 'sm' && 'h-8 px-2.5 text-xs',\n size === 'md' && 'h-9 px-3 text-sm',\n size === 'lg' && 'h-11 px-4 text-base',\n // The three resting appearances, and each is three changes rather than\n // one. Read-only and disabled used to share `bg-muted` and part only on\n // the text colour, which is a distinction carried by hue alone — the\n // one thing a reader with a colour deficiency, or a low-quality\n // display, or forced-colors, does not get. The fill separates them and\n // the border weight separates them again, so the progression\n // available → on show → unavailable is legible on three channels.\n //\n // Read-only is a value **on show**: the fill lifts off `background` to\n // `subtle` so the row reads as settled rather than editable, and the\n // text stays full strength — 13.46:1 over that fill — because being\n // read is the entire purpose of the state. The border does not move,\n // because nothing about the control's reach has: it is still a Tab stop\n // and still submitted. The native `readonly` attribute below carries\n // the same fact to assistive tech, so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Disabled is **unavailable**: the fill drains one step further to\n // `muted`, the text follows it down to `muted-foreground` (4.68:1 over\n // it, still AA), and the rim slackens from `input` to the lighter\n // `border`.\n //\n // Drained rather than faded, and `opacity` is never the mechanism. The\n // element a dim would sit on is the box, and the box is wrapped around\n // the one thing a reader still needs: measured on the built site,\n // `opacity-50` put a disabled field's value at 3.08:1 and its\n // placeholder — already the muted colour before the fade — at 2.07:1.\n // The text colour is declared here rather than on the `<input>` because\n // the input sets no resting colour of its own and inherits this one,\n // which also carries it to the counter and the adornments in the same\n // box.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` inside\n // it natively and never touches `field.disabled`, so the rule above\n // cannot fire and the field rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` is a *read* of the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree — the\n // fieldset element is still the only place the fact is written. It is\n // preferred to `has-[:disabled]:` (a wrapper reading its own contents)\n // because that proxy is wrong wherever a control holds a part that\n // disables itself: measured on this branch, NumberField's increment\n // button goes `:disabled` at `max`, so a `has-` rule would drain a\n // perfectly editable field the moment its value reached the top.\n // Nothing about this field is a proxy — the fieldset either disabled it\n // or it did not.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated where the two rules below are ordered, and for the same reason.\n // Tailwind emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would win\n // on emission order alone — an unavailable field that is also in error\n // would quietly lose its destructive rim.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the three rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would lose\n // its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n >\n <span\n v-if=\"$slots.leading\"\n class=\"inline-flex shrink-0 text-muted-foreground transition-colors duration-fast ease-out group-focus-within:text-foreground\"\n >\n <slot name=\"leading\" />\n </span>\n <input\n ref=\"input\"\n v-bind=\"{ ...inputAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :type=\"inputType\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n class=\"min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n @input=\"onInput\"\n />\n <!-- The counter is an adornment like any other, and it renders inside the\n border for the reason the icons do: the box is what a caller's `class`\n sizes, so a readout hung underneath it would need an outer element and\n `w-64` would then size something other than the field. Absent, both\n nodes are gone and the box is exactly what it was.\n\n `tabular` because the number changes under itself on every keystroke,\n and proportional digits would shuffle the whole readout sideways as\n they do. -->\n <span\n v-if=\"showsCount\"\n :id=\"countId\"\n :data-over-limit=\"band === 'over' || undefined\"\n :class=\"\n cn(\n 'tabular shrink-0 select-none text-small',\n band === 'under' && 'text-muted-foreground',\n // Colour, and never only colour: `countOverMax` says the limit is\n // passed in words as well, and `approachingLimit` announces the near\n // band once. The digits themselves are the third reading — 21/20 is\n // over the limit whatever it is painted.\n band === 'near' && 'text-warning',\n band === 'over' && 'text-destructive-text',\n )\n \"\n >\n {{ countText }}\n </span>\n <!-- In the DOM from the first render, empty. A live region mounted at the\n same moment as its text is a region assistive tech was not yet\n watching, so the first warning — the one that matters most — is the one\n it announces least reliably. -->\n <span v-if=\"showsCount && maxLength !== undefined\" role=\"status\" class=\"sr-only\">\n {{ announcement }}\n </span>\n <span\n v-if=\"$slots.trailing || canReveal\"\n class=\"inline-flex shrink-0 items-center gap-1 text-muted-foreground transition-colors duration-fast ease-out group-focus-within:text-foreground\"\n >\n <slot name=\"trailing\" />\n <!-- Last in the row and last in the tab order, which is the requirement:\n a control that acts on the input must never stand between the reader\n and the input itself.\n\n `aria-pressed` rather than a name that changes with the state, and\n the two are not interchangeable. A name alone can only say what\n pressing will do, so a reader arriving on \"Hide password\" has to\n infer the state from the offer — and on activation the announcement\n is a *rename*, which some screen readers re-read and some do not, so\n the state change can pass in silence. A pressed state changes under a\n stable name, which every major screen reader announces as a state\n change, and it leaves the control findable by voice under the one\n name it always had. The glyph swap is the sighted half of the same\n fact.\n\n It stays available while the field is read-only: `readonly` is about\n changing the value, and revealing does not. -->\n <button\n v-if=\"canReveal\"\n type=\"button\"\n :aria-label=\"text.reveal\"\n :aria-pressed=\"revealed\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo disabled:cursor-not-allowed\"\n @click=\"toggleReveal\"\n >\n <EyeOff v-if=\"revealed\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <Eye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </span>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"TextField.js","names":[],"sources":["../../../../../packages/primitives/text-field/src/TextField.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TextFieldLabels } from \"@ecoma-io/loom-labels\";\nimport { COUNT_LABELS, warningWindow, type CountBand } from \"@ecoma-io/loom-labels\";\n\nexport type TextFieldType = \"text\" | \"email\" | \"password\" | \"search\" | \"url\" | \"tel\";\n\nexport type TextFieldSize = \"sm\" | \"md\" | \"lg\";\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 * rather than a transcription of it.\n */\nexport const TEXT_FIELD_LABELS: TextFieldLabels = {\n ...COUNT_LABELS,\n reveal: \"Show password\",\n};\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, useId, useTemplateRef, watch } from \"vue\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\n\n/**\n * TextField — single-line text entry (text · email · password · search · url ·\n * tel). The border, focus ring and invalid state live on the wrapper so\n * optional `#leading`/`#trailing` adornments (a search icon, a unit suffix)\n * sit *inside* the field and share its focus bloom, instead of every caller\n * re-inventing an icon-inside-input layout. For numeric entry reach for a\n * number field; for a bounded continuous value, a slider.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on\n * the wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the\n * rest of the attrs (`id`, `name`, `data-testid`, `aria-describedby`,\n * `autocomplete`) belong on the real `<input>` — the split-attrs convention\n * shared with the other bare controls. The accessible name is a first-class\n * prop pair (`aria-label`/`aria-labelledby`, camelized by Vue) rather than a\n * loose fallthrough attr, so it is bound onto the input explicitly and shows\n * up in the props table — a bare input primitive can only be named by its\n * host.\n *\n * That same wrapper is why `revealable` and the character counter are props\n * here rather than a `PasswordField` and a `CountedField` beside it. `password`\n * is already a `TextFieldType` and the adornment row already exists, so a\n * second component would duplicate an entire input, its Field wiring and its\n * three sizes to add one button; and a counter rendered *below* the field\n * would have to move the fallthrough `class` off the box a caller sizes with\n * it. Both live inside the border, where the adornments already live.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself: the row's id, the id\n * of the hint or error line, and `required` / `invalid` / `disabled` /\n * `readonly` / `name` all arrive through `useFieldControl()`, so\n * `<Field label=\"Email\" error=\"…\"><TextField /></Field>` needs no attributes at\n * the call site. Every prop below still wins over the row when it is set, in\n * both directions — which is why the four booleans default to `undefined`\n * rather than `false`.\n *\n * `readonly` and `disabled` are different states, not two dials on one, so the\n * field has **three** resting appearances rather than two. A read-only field\n * is a value the reader may see and copy but not change: it stays a Tab stop,\n * stays in the form's submitted data, and takes a lifted fill with its text at\n * full strength. A disabled field is unavailable: no Tab stop, not submitted,\n * and drained a step further, in fill, in text colour and in border weight\n * together. Three channels rather than one is the point — see the class list.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\n /** The native input type — also the on-screen keyboard hint on mobile. */\n type?: TextFieldType;\n /** Text shown in the empty field. */\n placeholder?: string;\n /** Disables the field and blocks pointer and keyboard input. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the value without allowing an edit, while staying focusable and still submitted. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border/ring and sets aria-invalid. Unset defers to a wrapping Field's `error`. */\n invalid?: boolean | undefined;\n /** Marks the field mandatory to assistive tech (`aria-required`). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`. Unset defers to a wrapping Field. */\n name?: string;\n /** Control height. */\n size?: TextFieldSize;\n /** The accessible name, when no visible element already labels the field. */\n ariaLabel?: string;\n /** The id of the visible element that labels the field (e.g. a Field wrapper's label). */\n ariaLabelledby?: string;\n /**\n * Adds a toggle that shows the typed password in place, for\n * `type=\"password\"` **and nothing else** — on any other type it renders\n * nothing at all, because there is nothing to reveal.\n */\n revealable?: boolean;\n /**\n * The character limit the counter reports against, and the point past\n * which it reads as over-limit.\n *\n * Advisory, exactly as `required` is: it does **not** set the native\n * `maxlength`, which silently truncates a paste and tells the reader\n * nothing. Pass `maxlength` yourself as a plain attribute where you want\n * the browser to enforce it too.\n */\n maxLength?: number;\n /**\n * Forces the counter on or off. Left unset it follows `maxLength` — a\n * limit the reader is never shown is one they can only discover by\n * exceeding it — so set it `true` for a running total with no limit, and\n * `false` to hold a limit the field reports nowhere.\n */\n showCount?: boolean | undefined;\n /**\n * Names for everything this control says out loud, as any subset of\n * `TextFieldLabels` — the rest stay as the host's `provideLoomLabels`\n * vocabulary left them, and then as Loom's English.\n *\n * This is the per-instance correction, not the place to localise an\n * application: the case it exists for is a field counting something other\n * than characters, where \"80 words left\" is the true sentence.\n */\n labels?: LabelOverrides<TextFieldLabels>;\n }>(),\n {\n type: \"text\",\n // Not `false`, for any of the four. `false` is a caller saying \"this field\n // is fine / enabled / editable even though its row is not\"; absent is a\n // caller saying nothing, and only `undefined` survives to mean that. The\n // explicit default is also what beats Vue's absent-Boolean-casts-to-false\n // rule — the same reason Checkbox's `modelValue` carries one.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n size: \"md\",\n revealable: false,\n // `undefined` for the same Boolean-cast reason, and here it is the whole\n // feature: `showCount ?? (maxLength !== undefined)` would be a dead branch\n // if an absent prop arrived as `false`.\n showCount: undefined,\n },\n);\n\nconst emit = defineEmits<{ \"update:modelValue\": [value: string] }>();\n\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: inputAttrs } = useSplitAttrs();\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(\"textField\", TEXT_FIELD_LABELS, () => props.labels);\n\nconst inputRef = useTemplateRef<HTMLInputElement>(\"input\");\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue`.\n *\n * Used without `v-model` this component is uncontrolled — `modelValue` stays\n * `undefined` while the reader types — and the counter still has to count. So\n * the last typed value is kept here and `currentValue` prefers the prop: a\n * controlled host that *declines* an update keeps the count honest, and an\n * uncontrolled one gets the text in front of it.\n *\n * Binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing and not cosmetic. Vue's `value` patch coerces `null`/`undefined`\n * to `\"\"`, so any re-render of an uncontrolled field wipes the input — a defect\n * nothing had met only because nothing in this component re-rendered on a\n * keystroke until the counter did.\n */\nconst typed = ref(props.modelValue ?? \"\");\nconst currentValue = computed(() => props.modelValue ?? typed.value);\n\nfunction onInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\n}\n\n/**\n * The reveal, and the two guards that make `revealable` inert rather than\n * surprising on a non-password field: the toggle is not rendered, and the type\n * cannot flip. A `type` that stops being `password` also drops the reveal —\n * coming back as a password already revealed would put a secret on screen that\n * nobody asked for a second time.\n */\nconst canReveal = computed(() => props.revealable && props.type === \"password\");\nconst revealed = ref(false);\nwatch(canReveal, (able) => {\n if (!able) revealed.value = false;\n});\n\nconst inputType = computed(() => (canReveal.value && revealed.value ? \"text\" : props.type));\n\n/**\n * Flipping `type` is what loses the caret: the input is re-created enough,\n * engine by engine, that the selection lands back at 0 or at the end, and a\n * reader who was fixing the third character of a long passphrase is now\n * somewhere else with no way to tell where. So the range is read before the\n * flip and written back after the DOM has it.\n *\n * Both `password` and `text` support the selection API (`email`, `url` and\n * `number` do not, and throw), and this path is unreachable for any other type\n * by construction — see `canReveal`.\n *\n * Focus is deliberately *not* moved back to the input. A reader who pressed\n * this with the keyboard is standing on the toggle and will very often press\n * it again; dropping them into the field would make hiding the password a\n * Shift+Tab away. The restored range is what greets them when they do return.\n */\nfunction toggleReveal(): void {\n const input = inputRef.value;\n const start = input?.selectionStart ?? null;\n const end = input?.selectionEnd ?? null;\n const direction = input?.selectionDirection ?? \"none\";\n\n revealed.value = !revealed.value;\n\n if (input === null || start === null || end === null) return;\n void nextTick(() => {\n input.setSelectionRange(start, end, direction);\n });\n}\n\n/**\n * Length in UTF-16 code units, which is what the platform's own `maxlength`\n * counts. `[...value].length` would count code points and disagree with the\n * browser on any astral character, so a caller who pairs this with a native\n * `maxlength` would watch the counter and the enforcement drift apart.\n */\nconst count = computed(() => currentValue.value.length);\nconst showsCount = computed(() => props.showCount ?? props.maxLength !== undefined);\n\nconst band = computed<CountBand>(() => {\n const max = props.maxLength;\n if (max === undefined) return \"under\";\n const remaining = max - count.value;\n if (remaining < 0) return \"over\";\n return remaining <= warningWindow(max) ? \"near\" : \"under\";\n});\n\nconst countText = computed(() => {\n const max = props.maxLength;\n if (max === undefined) return text.value.count({ count: count.value });\n const over = count.value - max;\n return over > 0\n ? text.value.countOverMax({ count: count.value, max, over })\n : text.value.countOfMax({ count: count.value, max });\n});\n\n/**\n * What the live region says, and it changes **only when the band changes**.\n *\n * A region holding the counter itself would speak on every keystroke, which is\n * worse than silence: it buries the field's own echo of the character just\n * typed under a number nobody asked for. What a reader needs is the two facts\n * they cannot see coming — the limit is close, and the limit is passed — so\n * this fires once at each crossing and goes quiet again on the way back down.\n *\n * The numbers are therefore true at the moment they are spoken and not after.\n * That is the deliberate trade: the visible counter carries the live figure,\n * and it is wired into `aria-describedby`, so the exact count is one\n * re-read away whenever the reader wants it rather than pushed at them.\n */\nconst announcement = ref(\"\");\nwatch(band, (now) => {\n const max = props.maxLength;\n if (max === undefined || now === \"under\") {\n announcement.value = \"\";\n return;\n }\n announcement.value =\n now === \"over\"\n ? text.value.limitExceeded({ over: count.value - max, max })\n : text.value.approachingLimit({ remaining: max - count.value, max });\n});\n\n// `id` and `aria-describedby` reach a bare control as fallthrough attrs rather\n// than props, so they are read off `attrs` and handed in as this caller's own\n// values — a caller who sets either still beats the row, and the row's\n// description is merged into theirs rather than replacing it. The counter joins\n// that list rather than replacing anything: it is one more thing this field\n// says about itself, and it reads after the caller's own description and before\n// the row's message, which is the same most-specific-first order\n// `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads onto the `<input>` **after** `inputAttrs`, and\n// that position is the contract: the bag has already resolved the caller's own\n// values, and it drops every key that resolved to nothing, so it can only add.\n// The `:aria-invalid` binding that used to sit on the input is gone with it —\n// an individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop and quietly re-break the link\n// to the row.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: [attrs[\"aria-describedby\"] as string | undefined, showsCount.value ? countId : \"\"]\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n .join(\" \"),\n name: props.name,\n disabled: props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n</script>\n\n<template>\n <div\n :data-disabled=\"field.disabled || undefined\"\n :data-readonly=\"field.readonly || undefined\"\n :data-invalid=\"field.invalid || undefined\"\n :class=\"\n cn(\n 'group flex items-center gap-2 rounded-md border border-input bg-background text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // Rim-lit at rest, the weave blooms on focus (Signature law): the field\n // catches light instead of casting a shadow, and focus draws it tight.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n size === 'sm' && 'h-8 px-2.5 text-xs',\n size === 'md' && 'h-9 px-3 text-sm',\n size === 'lg' && 'h-11 px-4 text-base',\n // The three resting appearances, and each is three changes rather than\n // one. Read-only and disabled used to share `bg-muted` and part only on\n // the text colour, which is a distinction carried by hue alone — the\n // one thing a reader with a colour deficiency, or a low-quality\n // display, or forced-colors, does not get. The fill separates them and\n // the border weight separates them again, so the progression\n // available → on show → unavailable is legible on three channels.\n //\n // Read-only is a value **on show**: the fill lifts off `background` to\n // `subtle` so the row reads as settled rather than editable, and the\n // text stays full strength — 13.46:1 over that fill — because being\n // read is the entire purpose of the state. The border does not move,\n // because nothing about the control's reach has: it is still a Tab stop\n // and still submitted. The native `readonly` attribute below carries\n // the same fact to assistive tech, so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Disabled is **unavailable**: the fill drains one step further to\n // `muted`, the text follows it down to `muted-foreground` (4.68:1 over\n // it, still AA), and the rim slackens from `input` to the lighter\n // `border`.\n //\n // Drained rather than faded, and `opacity` is never the mechanism. The\n // element a dim would sit on is the box, and the box is wrapped around\n // the one thing a reader still needs: measured on the built site,\n // `opacity-50` put a disabled field's value at 3.08:1 and its\n // placeholder — already the muted colour before the fade — at 2.07:1.\n // The text colour is declared here rather than on the `<input>` because\n // the input sets no resting colour of its own and inherits this one,\n // which also carries it to the counter and the adornments in the same\n // box.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // The same appearance, reached from the other direction: a\n // `<fieldset disabled>` above this field disables the `<input>` inside\n // it natively and never touches `field.disabled`, so the rule above\n // cannot fire and the field rendered byte-identical to an editable one.\n //\n // `in-[fieldset:disabled]:` is a *read* of the attribute that did the\n // disabling, so the fill and the native inertness cannot disagree — the\n // fieldset element is still the only place the fact is written. It is\n // preferred to `has-[:disabled]:` (a wrapper reading its own contents)\n // because that proxy is wrong wherever a control holds a part that\n // disables itself: measured on this branch, NumberField's increment\n // button goes `:disabled` at `max`, so a `has-` rule would drain a\n // perfectly editable field the moment its value reached the top.\n // Nothing about this field is a proxy — the fieldset either disabled it\n // or it did not.\n 'in-[fieldset:disabled]:cursor-not-allowed in-[fieldset:disabled]:bg-muted in-[fieldset:disabled]:text-muted-foreground',\n // Gated where the two rules below are ordered, and for the same reason.\n // Tailwind emits the ancestor half of `in-*` inside `:where()`, so this\n // carries no more specificity than `border-destructive` and would win\n // on emission order alone — an unavailable field that is also in error\n // would quietly lose its destructive rim.\n !field.invalid && 'in-[fieldset:disabled]:border-border',\n // Last of the three rules that name a border colour, and the order is\n // load-bearing: `cn()` resolves a border colour by whichever class it\n // saw last, so a field that is both in error and unavailable would lose\n // its destructive rim to the disabled one if this sat above it.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n >\n <span\n v-if=\"$slots.leading\"\n class=\"inline-flex shrink-0 text-muted-foreground transition-colors duration-fast ease-out group-focus-within:text-foreground\"\n >\n <slot name=\"leading\" />\n </span>\n <input\n ref=\"input\"\n v-bind=\"{ ...inputAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :type=\"inputType\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n class=\"min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n @input=\"onInput\"\n />\n <!-- The counter is an adornment like any other, and it renders inside the\n border for the reason the icons do: the box is what a caller's `class`\n sizes, so a readout hung underneath it would need an outer element and\n `w-64` would then size something other than the field. Absent, both\n nodes are gone and the box is exactly what it was.\n\n `tabular` because the number changes under itself on every keystroke,\n and proportional digits would shuffle the whole readout sideways as\n they do. -->\n <span\n v-if=\"showsCount\"\n :id=\"countId\"\n :data-over-limit=\"band === 'over' || undefined\"\n :class=\"\n cn(\n 'tabular shrink-0 select-none text-small',\n band === 'under' && 'text-muted-foreground',\n // Colour, and never only colour: `countOverMax` says the limit is\n // passed in words as well, and `approachingLimit` announces the near\n // band once. The digits themselves are the third reading — 21/20 is\n // over the limit whatever it is painted.\n band === 'near' && 'text-warning-text',\n band === 'over' && 'text-destructive-text',\n )\n \"\n >\n {{ countText }}\n </span>\n <!-- In the DOM from the first render, empty. A live region mounted at the\n same moment as its text is a region assistive tech was not yet\n watching, so the first warning — the one that matters most — is the one\n it announces least reliably. -->\n <span v-if=\"showsCount && maxLength !== undefined\" role=\"status\" class=\"sr-only\">\n {{ announcement }}\n </span>\n <span\n v-if=\"$slots.trailing || canReveal\"\n class=\"inline-flex shrink-0 items-center gap-1 text-muted-foreground transition-colors duration-fast ease-out group-focus-within:text-foreground\"\n >\n <slot name=\"trailing\" />\n <!-- Last in the row and last in the tab order, which is the requirement:\n a control that acts on the input must never stand between the reader\n and the input itself.\n\n `aria-pressed` rather than a name that changes with the state, and\n the two are not interchangeable. A name alone can only say what\n pressing will do, so a reader arriving on \"Hide password\" has to\n infer the state from the offer — and on activation the announcement\n is a *rename*, which some screen readers re-read and some do not, so\n the state change can pass in silence. A pressed state changes under a\n stable name, which every major screen reader announces as a state\n change, and it leaves the control findable by voice under the one\n name it always had. The glyph swap is the sighted half of the same\n fact.\n\n It stays available while the field is read-only: `readonly` is about\n changing the value, and revealing does not. -->\n <button\n v-if=\"canReveal\"\n type=\"button\"\n :aria-label=\"text.reveal\"\n :aria-pressed=\"revealed\"\n :disabled=\"field.disabled\"\n class=\"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-out hover:bg-subtle hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:shadow-halo disabled:cursor-not-allowed\"\n @click=\"toggleReveal\"\n >\n <EyeOff v-if=\"revealed\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <Eye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </span>\n </div>\n</template>\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
- import { type LabelOverrides } from "@ecoma-io/loom-labels";
2
- import type { TextFieldLabels } from "@ecoma-io/loom-labels";
1
+ import { type LabelOverrides } from "../../../labels/src/index";
2
+ import type { TextFieldLabels } from "../../../labels/src/index";
3
3
  export type TextFieldType = "text" | "email" | "password" | "search" | "url" | "tel";
4
4
  export type TextFieldSize = "sm" | "md" | "lg";
5
5
  /**
@@ -252,7 +252,7 @@ var TextField_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
252
252
  key: 1,
253
253
  id: unref(countId),
254
254
  "data-over-limit": band.value === "over" || void 0,
255
- class: normalizeClass(unref(cn)("tabular shrink-0 select-none text-small", band.value === "under" && "text-muted-foreground", band.value === "near" && "text-warning", band.value === "over" && "text-destructive-text"))
255
+ class: normalizeClass(unref(cn)("tabular shrink-0 select-none text-small", band.value === "under" && "text-muted-foreground", band.value === "near" && "text-warning-text", band.value === "over" && "text-destructive-text"))
256
256
  }, toDisplayString(countText.value), 11, _hoisted_4)) : createCommentVNode("", true),
257
257
  showsCount.value && __props.maxLength !== void 0 ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(announcement.value), 1)) : createCommentVNode("", true),
258
258
  _ctx.$slots.trailing || canReveal.value ? (openBlock(), createElementBlock("span", _hoisted_6, [renderSlot(_ctx.$slots, "trailing"), canReveal.value ? (openBlock(), createElementBlock("button", {