@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":"TextField.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"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":"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"}
1
+ {"version":3,"file":"TextField.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"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":"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"}
@@ -1,5 +1,5 @@
1
1
  export { default } from "./TextField.vue";
2
2
  export { TEXT_FIELD_LABELS } from "./TextField.vue";
3
3
  export type { TextFieldType, TextFieldSize } from "./TextField.vue";
4
- export type { TextFieldLabels } from "@ecoma-io/loom-labels";
4
+ export type { TextFieldLabels } from "../../../labels/src/index";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,3 @@
1
- import "./TextField.vue_vue_type_script_setup_true_lang.js";
1
+ import { TEXT_FIELD_LABELS } from "./TextField.vue_vue_type_script_setup_true_lang.js";
2
2
  import TextField_default from "./TextField.js";
3
- export { TextField_default as default };
3
+ export { TEXT_FIELD_LABELS, TextField_default as default };
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.js","names":[],"sources":["../../../../../packages/primitives/textarea/src/Textarea.vue"],"sourcesContent":["<script lang=\"ts\">\nimport {\n COUNT_LABELS,\n warningWindow,\n type CountBand,\n type TextareaLabels,\n} from \"@ecoma-io/loom-labels\";\n\nexport type TextareaResize = \"none\" | \"vertical\";\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 TEXTAREA_LABELS: TextareaLabels = COUNT_LABELS;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"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 * Textarea — multi-line text entry, around a native `<textarea>`. The\n * accessible name is a first-class prop pair (`aria-label`/`aria-labelledby`,\n * camelized by Vue) bound onto the textarea explicitly, the same contract as\n * TextField.\n *\n * The border, focus ring and invalid state live on the wrapper so optional\n * `#leading`/`#trailing` adornments sit *inside* the field and share its focus\n * bloom, instead of every caller re-inventing an icon-inside-input layout.\n * The slot names and positions match TextField's, and the vertical alignment\n * differs — `items-start` for multi-line, where the text begins, versus\n * `items-center` for single-line — because a five-row box with a centered icon\n * mid-field reads as a mistake rather than a choice.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on the\n * wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the rest\n * of the attrs (`id`, `name`, `data-testid`, `aria-describedby`) belong on the\n * real `<textarea>` — the split-attrs convention shared with TextField.\n *\n * That same wrapper is why the character counter is a prop here rather than a\n * `CountedField` beside this one. A counter rendered *below* the field would\n * have to move the fallthrough `class` off the box a caller sizes with it, and\n * a textarea has no free column to hang one in — the value fills the box and\n * scrolls, and the bottom-right corner belongs to the native resize grabber.\n * The counter sits below the border, where `resize` moves the box's bottom\n * edge and the counter travels with it rather than floating over the text.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so\n * `<Field label=\"Bio\" hint=\"…\"><Textarea /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather\n * than `false`.\n *\n * `readonly` and `disabled` are different states, and they are told apart on\n * three channels rather than one. A read-only field is a value on show: the\n * subtle fill, the text at full strength, the input hairline, still a Tab stop\n * and still submitted. A disabled one is unavailable: the deeper neutral fill,\n * the muted text, and the hairline receding to the plain border — a difference\n * in shape as well as in colour, which is what survives a reader who cannot\n * resolve the two fills apart.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\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 /** Initial height in text rows. */\n rows?: number;\n /** Whether a person can drag-resize the field; `none` locks it to `rows`. The counter sits below the box either way, so a drag never moves it onto the value. */\n resize?: TextareaResize;\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 * 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 * `TextareaLabels` — 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<TextareaLabels>;\n }>(),\n {\n // Not `false`, for any of the four — see TextField.vue, which carries the\n // full reasoning. `false` is a caller's decision that must beat the row;\n // absent is a caller saying nothing, and only `undefined` survives to mean\n // that past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n rows: 3,\n resize: \"vertical\",\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: textareaAttrs } = 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(\"textarea\", TEXTAREA_LABELS, () => props.labels);\n\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue` — TextField's\n * reasoning, and the same defect waiting on this side of it.\n *\n * Used without `v-model` this component is uncontrolled, so `modelValue` stays\n * `undefined` while the reader types and the counter still has to count. And\n * binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing rather than cosmetic: Vue's `value` patch coerces\n * `null`/`undefined` to `\"\"`, so any re-render of an uncontrolled field wipes\n * it — a defect nothing had met only because nothing in this component\n * re-rendered on a 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 HTMLTextAreaElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\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 * TextField's decision, taken once for the pair rather than re-litigated here.\n *\n * A region holding the counter itself would speak on every keystroke, burying\n * the field's own echo of the character just typed under a number nobody asked\n * for. What a reader needs is the two facts they cannot see coming — the limit\n * is close, and the limit is passed — so this fires once at each crossing and\n * goes quiet again on the way back down. The numbers are therefore true at the\n * moment they are spoken and not after; the visible counter carries the live\n * figure, 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` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values: a caller who sets either\n// still beats the row, and the row's description is merged into theirs. The\n// counter joins that list rather than replacing anything — it reads after the\n// caller's own description and before the row's message, which is the same\n// most-specific-first order `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads **after** `textareaAttrs`, and that position is\n// the contract — see TextField.vue, which carries the full reasoning. The\n// `:aria-invalid` binding that used to sit below went with it: an individual\n// attribute written after a `v-bind` wins, so keeping one would overwrite the\n// resolved value with the raw prop.\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 <!-- Layout, and nothing else: no fallthrough attribute lands here and nothing\n is painted on it. `inline-flex` is what makes it shrink-wrap the field\n frame — a block wrapper would be as wide as the column, and the counter\n would right-align to that column rather than to the field's own edge. -->\n <div class=\"inline-flex max-w-full flex-col align-top\">\n <!-- The field frame: border, focus, state. Like TextField, the border lives\n here so optional #leading/#trailing adornments sit inside the field and\n share its focus bloom, instead of every caller re-inventing an\n icon-inside-input layout. -->\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-start gap-2 rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm',\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 // 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 `<textarea>` because\n // the textarea sets no resting colour of its own and inherits this one,\n // which also carries it to the adornments in the same 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 `<textarea>` 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.\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 <textarea\n v-bind=\"{ ...textareaAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :rows=\"rows\"\n :class=\"\n cn(\n 'min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',\n resize === 'none' ? 'resize-none' : 'resize-y',\n )\n \"\n @input=\"onInput\"\n />\n <span\n v-if=\"$slots.trailing\"\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 </span>\n </div>\n <!-- Below the box, never inside it. A textarea's value fills its box and\n scrolls, so there is no free column to hang a readout in, and the\n bottom-right corner belongs to the native resize grabber — an inset\n counter would sit on top of the one affordance that says the field can\n be dragged. Here it travels with the bottom edge instead, so `resize`\n moves it rather than moving the box out from under it.\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 mt-1 select-none self-end 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 </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"Textarea.js","names":[],"sources":["../../../../../packages/primitives/textarea/src/Textarea.vue"],"sourcesContent":["<script lang=\"ts\">\nimport {\n COUNT_LABELS,\n warningWindow,\n type CountBand,\n type TextareaLabels,\n} from \"@ecoma-io/loom-labels\";\n\nexport type TextareaResize = \"none\" | \"vertical\";\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 TEXTAREA_LABELS: TextareaLabels = COUNT_LABELS;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"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 * Textarea — multi-line text entry, around a native `<textarea>`. The\n * accessible name is a first-class prop pair (`aria-label`/`aria-labelledby`,\n * camelized by Vue) bound onto the textarea explicitly, the same contract as\n * TextField.\n *\n * The border, focus ring and invalid state live on the wrapper so optional\n * `#leading`/`#trailing` adornments sit *inside* the field and share its focus\n * bloom, instead of every caller re-inventing an icon-inside-input layout.\n * The slot names and positions match TextField's, and the vertical alignment\n * differs — `items-start` for multi-line, where the text begins, versus\n * `items-center` for single-line — because a five-row box with a centered icon\n * mid-field reads as a mistake rather than a choice.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on the\n * wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the rest\n * of the attrs (`id`, `name`, `data-testid`, `aria-describedby`) belong on the\n * real `<textarea>` — the split-attrs convention shared with TextField.\n *\n * That same wrapper is why the character counter is a prop here rather than a\n * `CountedField` beside this one. A counter rendered *below* the field would\n * have to move the fallthrough `class` off the box a caller sizes with it, and\n * a textarea has no free column to hang one in — the value fills the box and\n * scrolls, and the bottom-right corner belongs to the native resize grabber.\n * The counter sits below the border, where `resize` moves the box's bottom\n * edge and the counter travels with it rather than floating over the text.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so\n * `<Field label=\"Bio\" hint=\"…\"><Textarea /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather\n * than `false`.\n *\n * `readonly` and `disabled` are different states, and they are told apart on\n * three channels rather than one. A read-only field is a value on show: the\n * subtle fill, the text at full strength, the input hairline, still a Tab stop\n * and still submitted. A disabled one is unavailable: the deeper neutral fill,\n * the muted text, and the hairline receding to the plain border — a difference\n * in shape as well as in colour, which is what survives a reader who cannot\n * resolve the two fills apart.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\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 /** Initial height in text rows. */\n rows?: number;\n /** Whether a person can drag-resize the field; `none` locks it to `rows`. The counter sits below the box either way, so a drag never moves it onto the value. */\n resize?: TextareaResize;\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 * 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 * `TextareaLabels` — 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<TextareaLabels>;\n }>(),\n {\n // Not `false`, for any of the four — see TextField.vue, which carries the\n // full reasoning. `false` is a caller's decision that must beat the row;\n // absent is a caller saying nothing, and only `undefined` survives to mean\n // that past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n rows: 3,\n resize: \"vertical\",\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: textareaAttrs } = 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(\"textarea\", TEXTAREA_LABELS, () => props.labels);\n\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue` — TextField's\n * reasoning, and the same defect waiting on this side of it.\n *\n * Used without `v-model` this component is uncontrolled, so `modelValue` stays\n * `undefined` while the reader types and the counter still has to count. And\n * binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing rather than cosmetic: Vue's `value` patch coerces\n * `null`/`undefined` to `\"\"`, so any re-render of an uncontrolled field wipes\n * it — a defect nothing had met only because nothing in this component\n * re-rendered on a 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 HTMLTextAreaElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\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 * TextField's decision, taken once for the pair rather than re-litigated here.\n *\n * A region holding the counter itself would speak on every keystroke, burying\n * the field's own echo of the character just typed under a number nobody asked\n * for. What a reader needs is the two facts they cannot see coming — the limit\n * is close, and the limit is passed — so this fires once at each crossing and\n * goes quiet again on the way back down. The numbers are therefore true at the\n * moment they are spoken and not after; the visible counter carries the live\n * figure, 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` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values: a caller who sets either\n// still beats the row, and the row's description is merged into theirs. The\n// counter joins that list rather than replacing anything — it reads after the\n// caller's own description and before the row's message, which is the same\n// most-specific-first order `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads **after** `textareaAttrs`, and that position is\n// the contract — see TextField.vue, which carries the full reasoning. The\n// `:aria-invalid` binding that used to sit below went with it: an individual\n// attribute written after a `v-bind` wins, so keeping one would overwrite the\n// resolved value with the raw prop.\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 <!-- Layout, and nothing else: no fallthrough attribute lands here and nothing\n is painted on it. `inline-flex` is what makes it shrink-wrap the field\n frame — a block wrapper would be as wide as the column, and the counter\n would right-align to that column rather than to the field's own edge. -->\n <div class=\"inline-flex max-w-full flex-col align-top\">\n <!-- The field frame: border, focus, state. Like TextField, the border lives\n here so optional #leading/#trailing adornments sit inside the field and\n share its focus bloom, instead of every caller re-inventing an\n icon-inside-input layout. -->\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-start gap-2 rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm',\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 // 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 `<textarea>` because\n // the textarea sets no resting colour of its own and inherits this one,\n // which also carries it to the adornments in the same 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 `<textarea>` 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.\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 <textarea\n v-bind=\"{ ...textareaAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :rows=\"rows\"\n :class=\"\n cn(\n 'min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',\n resize === 'none' ? 'resize-none' : 'resize-y',\n )\n \"\n @input=\"onInput\"\n />\n <span\n v-if=\"$slots.trailing\"\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 </span>\n </div>\n <!-- Below the box, never inside it. A textarea's value fills its box and\n scrolls, so there is no free column to hang a readout in, and the\n bottom-right corner belongs to the native resize grabber — an inset\n counter would sit on top of the one affordance that says the field can\n be dragged. Here it travels with the bottom edge instead, so `resize`\n moves it rather than moving the box out from under it.\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 mt-1 select-none self-end 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 </div>\n</template>\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
- import { type LabelOverrides } from "@ecoma-io/loom-labels";
2
- import { type TextareaLabels } from "@ecoma-io/loom-labels";
1
+ import { type LabelOverrides } from "../../../labels/src/index";
2
+ import { type TextareaLabels } from "../../../labels/src/index";
3
3
  export type TextareaResize = "none" | "vertical";
4
4
  /**
5
5
  * Loom's English, co-located with the component so it tree-shakes with it, and
@@ -239,7 +239,7 @@ var Textarea_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineC
239
239
  key: 0,
240
240
  id: unref(countId),
241
241
  "data-over-limit": band.value === "over" || void 0,
242
- class: normalizeClass(unref(cn)("tabular mt-1 select-none self-end text-small", band.value === "under" && "text-muted-foreground", band.value === "near" && "text-warning", band.value === "over" && "text-destructive-text"))
242
+ class: normalizeClass(unref(cn)("tabular mt-1 select-none self-end text-small", band.value === "under" && "text-muted-foreground", band.value === "near" && "text-warning-text", band.value === "over" && "text-destructive-text"))
243
243
  }, toDisplayString(countText.value), 11, _hoisted_6)) : createCommentVNode("", true),
244
244
  showsCount.value && __props.maxLength !== void 0 ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(announcement.value), 1)) : createCommentVNode("", true)
245
245
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../../../packages/primitives/textarea/src/Textarea.vue"],"sourcesContent":["<script lang=\"ts\">\nimport {\n COUNT_LABELS,\n warningWindow,\n type CountBand,\n type TextareaLabels,\n} from \"@ecoma-io/loom-labels\";\n\nexport type TextareaResize = \"none\" | \"vertical\";\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 TEXTAREA_LABELS: TextareaLabels = COUNT_LABELS;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"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 * Textarea — multi-line text entry, around a native `<textarea>`. The\n * accessible name is a first-class prop pair (`aria-label`/`aria-labelledby`,\n * camelized by Vue) bound onto the textarea explicitly, the same contract as\n * TextField.\n *\n * The border, focus ring and invalid state live on the wrapper so optional\n * `#leading`/`#trailing` adornments sit *inside* the field and share its focus\n * bloom, instead of every caller re-inventing an icon-inside-input layout.\n * The slot names and positions match TextField's, and the vertical alignment\n * differs — `items-start` for multi-line, where the text begins, versus\n * `items-center` for single-line — because a five-row box with a centered icon\n * mid-field reads as a mistake rather than a choice.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on the\n * wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the rest\n * of the attrs (`id`, `name`, `data-testid`, `aria-describedby`) belong on the\n * real `<textarea>` — the split-attrs convention shared with TextField.\n *\n * That same wrapper is why the character counter is a prop here rather than a\n * `CountedField` beside this one. A counter rendered *below* the field would\n * have to move the fallthrough `class` off the box a caller sizes with it, and\n * a textarea has no free column to hang one in — the value fills the box and\n * scrolls, and the bottom-right corner belongs to the native resize grabber.\n * The counter sits below the border, where `resize` moves the box's bottom\n * edge and the counter travels with it rather than floating over the text.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so\n * `<Field label=\"Bio\" hint=\"…\"><Textarea /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather\n * than `false`.\n *\n * `readonly` and `disabled` are different states, and they are told apart on\n * three channels rather than one. A read-only field is a value on show: the\n * subtle fill, the text at full strength, the input hairline, still a Tab stop\n * and still submitted. A disabled one is unavailable: the deeper neutral fill,\n * the muted text, and the hairline receding to the plain border — a difference\n * in shape as well as in colour, which is what survives a reader who cannot\n * resolve the two fills apart.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\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 /** Initial height in text rows. */\n rows?: number;\n /** Whether a person can drag-resize the field; `none` locks it to `rows`. The counter sits below the box either way, so a drag never moves it onto the value. */\n resize?: TextareaResize;\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 * 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 * `TextareaLabels` — 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<TextareaLabels>;\n }>(),\n {\n // Not `false`, for any of the four — see TextField.vue, which carries the\n // full reasoning. `false` is a caller's decision that must beat the row;\n // absent is a caller saying nothing, and only `undefined` survives to mean\n // that past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n rows: 3,\n resize: \"vertical\",\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: textareaAttrs } = 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(\"textarea\", TEXTAREA_LABELS, () => props.labels);\n\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue` — TextField's\n * reasoning, and the same defect waiting on this side of it.\n *\n * Used without `v-model` this component is uncontrolled, so `modelValue` stays\n * `undefined` while the reader types and the counter still has to count. And\n * binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing rather than cosmetic: Vue's `value` patch coerces\n * `null`/`undefined` to `\"\"`, so any re-render of an uncontrolled field wipes\n * it — a defect nothing had met only because nothing in this component\n * re-rendered on a 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 HTMLTextAreaElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\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 * TextField's decision, taken once for the pair rather than re-litigated here.\n *\n * A region holding the counter itself would speak on every keystroke, burying\n * the field's own echo of the character just typed under a number nobody asked\n * for. What a reader needs is the two facts they cannot see coming — the limit\n * is close, and the limit is passed — so this fires once at each crossing and\n * goes quiet again on the way back down. The numbers are therefore true at the\n * moment they are spoken and not after; the visible counter carries the live\n * figure, 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` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values: a caller who sets either\n// still beats the row, and the row's description is merged into theirs. The\n// counter joins that list rather than replacing anything — it reads after the\n// caller's own description and before the row's message, which is the same\n// most-specific-first order `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads **after** `textareaAttrs`, and that position is\n// the contract — see TextField.vue, which carries the full reasoning. The\n// `:aria-invalid` binding that used to sit below went with it: an individual\n// attribute written after a `v-bind` wins, so keeping one would overwrite the\n// resolved value with the raw prop.\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 <!-- Layout, and nothing else: no fallthrough attribute lands here and nothing\n is painted on it. `inline-flex` is what makes it shrink-wrap the field\n frame — a block wrapper would be as wide as the column, and the counter\n would right-align to that column rather than to the field's own edge. -->\n <div class=\"inline-flex max-w-full flex-col align-top\">\n <!-- The field frame: border, focus, state. Like TextField, the border lives\n here so optional #leading/#trailing adornments sit inside the field and\n share its focus bloom, instead of every caller re-inventing an\n icon-inside-input layout. -->\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-start gap-2 rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm',\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 // 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 `<textarea>` because\n // the textarea sets no resting colour of its own and inherits this one,\n // which also carries it to the adornments in the same 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 `<textarea>` 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.\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 <textarea\n v-bind=\"{ ...textareaAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :rows=\"rows\"\n :class=\"\n cn(\n 'min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',\n resize === 'none' ? 'resize-none' : 'resize-y',\n )\n \"\n @input=\"onInput\"\n />\n <span\n v-if=\"$slots.trailing\"\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 </span>\n </div>\n <!-- Below the box, never inside it. A textarea's value fills its box and\n scrolls, so there is no free column to hang a readout in, and the\n bottom-right corner belongs to the native resize grabber — an inset\n counter would sit on top of the one affordance that says the field can\n be dragged. Here it travels with the bottom edge instead, so `resize`\n moves it rather than moving the box out from under it.\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 mt-1 select-none self-end 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 </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,IAAa,kBAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqD/C,MAAM,QAAQ;EAsEd,MAAM,OAAO;EAGb,MAAM,EAAE,OAAO,MAAM,kBAAkB,cAAc;EAKrD,MAAM,OAAO,UAAU,YAAY,uBAAuB,MAAM,MAAM;EAEtE,MAAM,UAAU,MAAM;;;;;;;;;;;;;EActB,MAAM,QAAQ,IAAI,MAAM,cAAc,EAAE;EACxC,MAAM,eAAe,eAAe,MAAM,cAAc,MAAM,KAAK;EAEnE,SAAS,QAAQ,OAAoB;GACnC,MAAM,QAAS,MAAM,OAA+B;GACpD,MAAM,QAAQ;GACd,KAAK,qBAAqB,KAAK;EACjC;;;;;;;EAQA,MAAM,QAAQ,eAAe,aAAa,MAAM,MAAM;EACtD,MAAM,aAAa,eAAe,MAAM,aAAa,MAAM,cAAc,KAAA,CAAS;EAElF,MAAM,OAAO,eAA0B;GACrC,MAAM,MAAM,MAAM;GAClB,IAAI,QAAQ,KAAA,GAAW,OAAO;GAC9B,MAAM,YAAY,MAAM,MAAM;GAC9B,IAAI,YAAY,GAAG,OAAO;GAC1B,OAAO,aAAa,cAAc,GAAG,IAAI,SAAS;EACpD,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,MAAM,MAAM,MAAM;GAClB,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAK,MAAM,MAAM,EAAE,OAAO,MAAM,MAAM,CAAC;GACrE,MAAM,OAAO,MAAM,QAAQ;GAC3B,OAAO,OAAO,IACV,KAAK,MAAM,aAAa;IAAE,OAAO,MAAM;IAAO;IAAK;GAAK,CAAC,IACzD,KAAK,MAAM,WAAW;IAAE,OAAO,MAAM;IAAO;GAAI,CAAC;EACvD,CAAC;;;;;;;;;;;;;;EAeD,MAAM,eAAe,IAAI,EAAE;EAC3B,MAAM,OAAO,QAAQ;GACnB,MAAM,MAAM,MAAM;GAClB,IAAI,QAAQ,KAAA,KAAa,QAAQ,SAAS;IACxC,aAAa,QAAQ;IACrB;GACF;GACA,aAAa,QACX,QAAQ,SACJ,KAAK,MAAM,cAAc;IAAE,MAAM,MAAM,QAAQ;IAAK;GAAI,CAAC,IACzD,KAAK,MAAM,iBAAiB;IAAE,WAAW,MAAM,MAAM;IAAO;GAAI,CAAC;EACzE,CAAC;EAcD,MAAM,QAAQ,uBAAuB;GACnC,IAAI,MAAM;GACV,aAAa,CAAC,MAAM,qBAA2C,WAAW,QAAQ,UAAU,EAAE,CAAA,CAC3F,QAAQ,OAAqB,OAAO,OAAO,YAAY,GAAG,SAAS,CAAC,CAAA,CACpE,KAAK,GAAG;GACX,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,SAAS,MAAM;GACf,UAAU,MAAM;EAClB,EAAE;;GAQA,OAAA,UAAA,GAAA,mBA4IM,OA5IN,YA4IM;IAvIJ,mBAkGM,OAAA;KAjGH,iBAAe,MAAA,KAAA,CAAK,CAAC,YAAY,KAAA;KACjC,iBAAe,MAAA,KAAA,CAAK,CAAC,YAAY,KAAA;KACjC,gBAAc,MAAA,KAAA,CAAK,CAAC,WAAW,KAAA;KAC/B,OAAK,eAAW,MAAA,EAAA,CAAE,CAAA,+GAAA,sFAAA,kFAAue,CAAA,MAAA,KAAA,CAAK,CAAC,WAAO,4BAAipC,MAAA,KAAA,CAAK,CAAC,YAAQ,aAAs6B,MAAA,KAAA,CAAK,CAAC,YAAQ,mEAAA,0HAAi1C,CAAA,MAAA,KAAA,CAAK,CAAC,WAAO,wCAAsX,MAAA,KAAA,CAAK,CAAC,WAAO,uDAAqE,MAAA,KAAA,CAAK,CAAC,KAAA,CAAA;;KAkE93IA,KAAAA,OAAO,WADf,UAAA,GAAA,mBAKO,QALP,YAKO,CADL,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;KAEzB,mBAgBE,YAhBF,WAgBE;MAAA,GAfa,MAAA,aAAA;MAAa,GAAK,MAAA,KAAA,CAAK,CAAC;KAAK,GAAA;MACzC,cAAY,QAAA;MACZ,mBAAiB,QAAA;MACjB,OAAO,aAAA;MACP,aAAa,QAAA;MACb,UAAU,MAAA,KAAA,CAAK,CAAC;MAChB,UAAU,MAAA,KAAA,CAAK,CAAC;MAChB,MAAM,QAAA;MACN,OAAkB,MAAA,EAAA,CAAE,CAAA,4GAAsI,QAAA,WAAM,SAAA,gBAAA,UAAA;MAMzJ;;KAGFA,KAAAA,OAAO,YADf,UAAA,GAAA,mBAKO,QALP,YAKO,CADL,WAAwB,KAAA,QAAA,UAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;IAcpB,WAAA,SADR,UAAA,GAAA,mBAkBO,QAAA;;KAhBJ,IAAI,MAAA,OAAA;KACJ,mBAAiB,KAAA,UAAI,UAAe,KAAA;KACpC,OAAK,eAAW,MAAA,EAAA,CAAE,CAAA,gDAAsE,KAAA,UAAI,WAAA,yBAAmV,KAAA,UAAI,UAAA,gBAAyC,KAAA,UAAI,UAAA,uBAAA,CAAA;IAa9d,GAAA,gBAAA,UAAA,KAAS,GAAA,IAAA,UAAA,KAAA,mBAAA,IAAA,IAAA;IAMF,WAAA,SAAc,QAAA,cAAc,KAAA,KAAxC,UAAA,GAAA,mBAEO,QAFP,YAEO,gBADF,aAAA,KAAY,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
1
+ {"version":3,"file":"Textarea.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../../../packages/primitives/textarea/src/Textarea.vue"],"sourcesContent":["<script lang=\"ts\">\nimport {\n COUNT_LABELS,\n warningWindow,\n type CountBand,\n type TextareaLabels,\n} from \"@ecoma-io/loom-labels\";\n\nexport type TextareaResize = \"none\" | \"vertical\";\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 TEXTAREA_LABELS: TextareaLabels = COUNT_LABELS;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from \"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 * Textarea — multi-line text entry, around a native `<textarea>`. The\n * accessible name is a first-class prop pair (`aria-label`/`aria-labelledby`,\n * camelized by Vue) bound onto the textarea explicitly, the same contract as\n * TextField.\n *\n * The border, focus ring and invalid state live on the wrapper so optional\n * `#leading`/`#trailing` adornments sit *inside* the field and share its focus\n * bloom, instead of every caller re-inventing an icon-inside-input layout.\n * The slot names and positions match TextField's, and the vertical alignment\n * differs — `items-start` for multi-line, where the text begins, versus\n * `items-center` for single-line — because a five-row box with a centered icon\n * mid-field reads as a mistake rather than a choice.\n *\n * Wrapping a rendered structure means the fallthrough `class` must land on the\n * wrapper (so a caller's `w-64`/`flex-1` sizes the whole field) while the rest\n * of the attrs (`id`, `name`, `data-testid`, `aria-describedby`) belong on the\n * real `<textarea>` — the split-attrs convention shared with TextField.\n *\n * That same wrapper is why the character counter is a prop here rather than a\n * `CountedField` beside this one. A counter rendered *below* the field would\n * have to move the fallthrough `class` off the box a caller sizes with it, and\n * a textarea has no free column to hang one in — the value fills the box and\n * scrolls, and the bottom-right corner belongs to the native resize grabber.\n * The counter sits below the border, where `resize` moves the box's bottom\n * edge and the counter travels with it rather than floating over the text.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so\n * `<Field label=\"Bio\" hint=\"…\"><Textarea /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather\n * than `false`.\n *\n * `readonly` and `disabled` are different states, and they are told apart on\n * three channels rather than one. A read-only field is a value on show: the\n * subtle fill, the text at full strength, the input hairline, still a Tab stop\n * and still submitted. A disabled one is unavailable: the deeper neutral fill,\n * the muted text, and the hairline receding to the plain border — a difference\n * in shape as well as in colour, which is what survives a reader who cannot\n * resolve the two fills apart.\n */\nconst props = withDefaults(\n defineProps<{\n /** The current value. Pair with `@update:modelValue` for `v-model`. */\n modelValue?: string;\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 /** Initial height in text rows. */\n rows?: number;\n /** Whether a person can drag-resize the field; `none` locks it to `rows`. The counter sits below the box either way, so a drag never moves it onto the value. */\n resize?: TextareaResize;\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 * 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 * `TextareaLabels` — 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<TextareaLabels>;\n }>(),\n {\n // Not `false`, for any of the four — see TextField.vue, which carries the\n // full reasoning. `false` is a caller's decision that must beat the row;\n // absent is a caller saying nothing, and only `undefined` survives to mean\n // that past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n rows: 3,\n resize: \"vertical\",\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: textareaAttrs } = 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(\"textarea\", TEXTAREA_LABELS, () => props.labels);\n\nconst countId = useId();\n\n/**\n * What is actually in the box, which is not always `modelValue` — TextField's\n * reasoning, and the same defect waiting on this side of it.\n *\n * Used without `v-model` this component is uncontrolled, so `modelValue` stays\n * `undefined` while the reader types and the counter still has to count. And\n * binding `:value=\"currentValue\"` rather than `:value=\"modelValue\"` is\n * load-bearing rather than cosmetic: Vue's `value` patch coerces\n * `null`/`undefined` to `\"\"`, so any re-render of an uncontrolled field wipes\n * it — a defect nothing had met only because nothing in this component\n * re-rendered on a 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 HTMLTextAreaElement).value;\n typed.value = value;\n emit(\"update:modelValue\", value);\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 * TextField's decision, taken once for the pair rather than re-litigated here.\n *\n * A region holding the counter itself would speak on every keystroke, burying\n * the field's own echo of the character just typed under a number nobody asked\n * for. What a reader needs is the two facts they cannot see coming — the limit\n * is close, and the limit is passed — so this fires once at each crossing and\n * goes quiet again on the way back down. The numbers are therefore true at the\n * moment they are spoken and not after; the visible counter carries the live\n * figure, 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` arrive as fallthrough attrs rather than props, so\n// they are handed in as this caller's own values: a caller who sets either\n// still beats the row, and the row's description is merged into theirs. The\n// counter joins that list rather than replacing anything — it reads after the\n// caller's own description and before the row's message, which is the same\n// most-specific-first order `useFieldControl` applies one layer out.\n//\n// `field.attrs` then spreads **after** `textareaAttrs`, and that position is\n// the contract — see TextField.vue, which carries the full reasoning. The\n// `:aria-invalid` binding that used to sit below went with it: an individual\n// attribute written after a `v-bind` wins, so keeping one would overwrite the\n// resolved value with the raw prop.\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 <!-- Layout, and nothing else: no fallthrough attribute lands here and nothing\n is painted on it. `inline-flex` is what makes it shrink-wrap the field\n frame — a block wrapper would be as wide as the column, and the counter\n would right-align to that column rather than to the field's own edge. -->\n <div class=\"inline-flex max-w-full flex-col align-top\">\n <!-- The field frame: border, focus, state. Like TextField, the border lives\n here so optional #leading/#trailing adornments sit inside the field and\n share its focus bloom, instead of every caller re-inventing an\n icon-inside-input layout. -->\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-start gap-2 rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm',\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 // 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 `<textarea>` because\n // the textarea sets no resting colour of its own and inherits this one,\n // which also carries it to the adornments in the same 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 `<textarea>` 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.\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 <textarea\n v-bind=\"{ ...textareaAttrs, ...field.attrs }\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :value=\"currentValue\"\n :placeholder=\"placeholder\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :rows=\"rows\"\n :class=\"\n cn(\n 'min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',\n resize === 'none' ? 'resize-none' : 'resize-y',\n )\n \"\n @input=\"onInput\"\n />\n <span\n v-if=\"$slots.trailing\"\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 </span>\n </div>\n <!-- Below the box, never inside it. A textarea's value fills its box and\n scrolls, so there is no free column to hang a readout in, and the\n bottom-right corner belongs to the native resize grabber — an inset\n counter would sit on top of the one affordance that says the field can\n be dragged. Here it travels with the bottom edge instead, so `resize`\n moves it rather than moving the box out from under it.\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 mt-1 select-none self-end 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 </div>\n</template>\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  export { default } from "./Textarea.vue";
2
2
  export { TEXTAREA_LABELS } from "./Textarea.vue";
3
3
  export type { TextareaResize } from "./Textarea.vue";
4
- export type { TextareaLabels } from "@ecoma-io/loom-labels";
4
+ export type { TextareaLabels } from "../../../labels/src/index";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,3 @@
1
- import "./Textarea.vue_vue_type_script_setup_true_lang.js";
1
+ import { TEXTAREA_LABELS } from "./Textarea.vue_vue_type_script_setup_true_lang.js";
2
2
  import Textarea_default from "./Textarea.js";
3
- export { Textarea_default as default };
3
+ export { TEXTAREA_LABELS, Textarea_default as default };
@@ -1 +1 @@
1
- {"version":3,"file":"TimePicker.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport {\n TIME_SEGMENT_LABELS,\n emptySegmentValueText,\n isTimeSegmentPart,\n} from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and 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` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. `undefined` for the `literal` separators, which\n// Reka renders `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return isTimeSegmentPart(part) ? text.value[part] : undefined;\n}\n\n// The companion to `segmentLabel`, and the reason it returns an object rather\n// than a string is in `emptySegmentValueText`: Reka's `aria-valuetext` is a\n// hard-coded \"Empty\" only while the segment holds nothing, so this replaces\n// that case and leaves the filled one — a number — exactly as Reka wrote it.\n//\n// The dayPeriod is the exception. Reka writes `\"AM\"` or `\"PM\"` from its own\n// hour-cycle logic, not from `Intl.DateTimeFormat`, so a localised field\n// still announces the period in English. `filledDayPeriod` lets a host replace\n// those with the locale's own period names (Arabic `\"ص\"`/`\"م\"`, Japanese\n// `\"午前\"`/`\"午後\"`).\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n const empty = emptySegmentValueText(part, value, text.value.empty);\n if (empty[\"aria-valuetext\"]) return empty;\n if (part === \"dayPeriod\") {\n return { \"aria-valuetext\": text.value.filledDayPeriod({ dayPeriod: value }) };\n }\n return {};\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, and the same three-way guard.\nconst groupLabelledBy = computed(() =>\n attrs[\"aria-label\"] || attrs[\"aria-labelledby\"] ? undefined : field.labelledBy,\n);\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"TimePicker.js","names":[],"sources":["../../../../../packages/primitives/time-picker/src/TimePicker.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { TimeSegmentLabels } from \"@ecoma-io/loom-labels\";\n\n/**\n * Everything this control publishes to assistive technology: the four names\n * Reka UI would otherwise give the segments in English of its own accord.\n * There is nothing else — the icon is decoration and hidden, and the field\n * group takes its name from the caller or from a wrapping Field.\n *\n * An alias for the shared slice rather than a fifth interface. The same four\n * names are said by `DateTimePicker` and `DateTimeRangePicker`, they come from\n * one Reka implementation, and `src/lib/date-labels.ts` carries why they are\n * declared once. The alias exists so this control's `labels` prop reads like\n * every other one's; annotate with `LabelOverrides<TimePickerLabels>` rather\n * than with this, which is total.\n */\nexport type TimePickerLabels = TimeSegmentLabels;\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { TimeFieldInput, TimeFieldRoot, type TimeValue } from \"reka-ui\";\nimport { Clock } from \"@lucide/vue\";\nimport { parseTime } from \"@internationalized/date\";\nimport { cn } from \"@ecoma-io/loom-core\";\nimport { useSplitAttrs } from \"@ecoma-io/loom-core\";\nimport { useAncestorDisabled } from \"@ecoma-io/loom-labels\";\nimport { useFieldControl } from \"@ecoma-io/loom-labels\";\nimport { optional } from \"@ecoma-io/loom-core\";\nimport { useLabels, type LabelOverrides } from \"@ecoma-io/loom-labels\";\nimport { TIME_SEGMENT_LABELS, segmentAriaLabel, segmentAriaValueText } from \"@ecoma-io/loom-labels\";\n\n/**\n * TimePicker — a time of day, typed into a segmented field. Reach for it\n * wherever a clock time is a value: a meeting start, a reminder, opening hours.\n *\n * **There is no clock face and no dropdown, deliberately.** A clock dial is\n * slower than typing for every reader who can type, and unusable for several\n * who cannot; a 96-row list of quarter hours is worse again. The segmented\n * field already is the control — arrow keys change the segment under the\n * cursor, digits fill it and advance — so a panel would only be a second, worse\n * path to the same value. That is the one place this diverges from DatePicker,\n * where the calendar earns its panel by answering \"which Tuesday?\", a question\n * a time never has.\n *\n * **The model value is a plain `\"HH:mm\"` string in 24-hour time**, or\n * `\"HH:mm:ss\"` when `granularity` asks for seconds — the same boundary\n * DatePicker draws for `\"YYYY-MM-DD\"`, and drawn here for the same reason: a\n * host has a string from its API and should not have to learn a date library to\n * bind a time. `hourCycle` moves the *display* to 12-hour and never the value,\n * because conflating the two is how a 12-hour locale ends up storing `\"01:30\"`\n * for half past one in the afternoon.\n *\n * **A time is not a date.** There is no timezone here, no offset, and no `Date`\n * anywhere near the public surface: `\"09:00\"` is nine in the morning wherever\n * it is read, and the moment that turns into is the host's question, not this\n * component's. Pair it with a DatePicker when an instant is what is wanted.\n *\n * Unlike DatePicker there are no `min`/`max` bounds. A time of day has no\n * ordering that survives midnight — a night shift is \"not before 22:00\" *and*\n * \"not after 02:00\", two bounds that cross rather than fence — so a min/max\n * pair would quietly call 01:00 out of range for the exact case it was reached\n * for. A window like that belongs to the host, which knows which day each end\n * is on.\n *\n * Inside a [Field](../Field/Field.vue) it wires itself through\n * `useFieldControl()` — the row's id, the id of its hint or error line, and\n * `required` / `invalid` / `disabled` / `readonly` / `name` — so `<Field\n * label=\"Starts at\" error=\"…\"><TimePicker /></Field>` needs no attributes at the\n * call site. Every prop below still wins over the row when it is set, in both\n * directions, which is why the four booleans default to `undefined` rather than\n * `false`. `readonly` and `disabled` are different states, so the field rests in\n * three appearances rather than two: read-only is a time on show — still a Tab\n * stop, still submitted, a lifted fill with the segments at full strength —\n * where disabled is unavailable and drained a step further, in fill, text\n * colour and border weight together.\n */\nconst props = withDefaults(\n defineProps<{\n /**\n * The chosen time as `\"HH:mm\"`, or `\"HH:mm:ss\"` at second granularity, in\n * 24-hour time whatever the field displays. Unset, empty or unparseable\n * means no time is chosen. The explicit `| undefined` is what lets\n * `v-model` bind a ref that clears — the emitted value drops to `undefined`\n * when the field is emptied, and a plain optional prop refuses to take it\n * back.\n */\n modelValue?: string | undefined;\n /**\n * Whether the field reads as 12-hour with an AM/PM segment or as 24-hour.\n * Display only: the value stays 24-hour either way. Unset follows `locale`,\n * which is what a reader of that locale expects to see.\n */\n hourCycle?: 12 | 24;\n /** How far down the field goes. `\"hour\"` and `\"minute\"` report `\"HH:mm\"`; `\"second\"` adds a seconds segment and reports `\"HH:mm:ss\"`. */\n granularity?: \"hour\" | \"minute\" | \"second\";\n /** BCP 47 tag driving the order of the segments, the separator between them, and the 12-or-24-hour default. */\n locale?: string;\n /** Unavailable: dims the control and takes the field out of the tab order. Unset defers to a wrapping Field. */\n disabled?: boolean | undefined;\n /** Shows the time without allowing an edit: the field stays a Tab stop, stays submitted, and is filled rather than dimmed. Unset defers to a wrapping Field. */\n readonly?: boolean | undefined;\n /** Error state: paints the destructive border and 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` on the segment group). Unset defers to a wrapping Field. */\n required?: boolean | undefined;\n /** The field name for a native form post or `FormData`, carried on the hidden input Reka submits the time through. Unset defers to a wrapping Field. */\n name?: string;\n /**\n * Names for the four segments, as any subset of `TimePickerLabels` — the\n * rest stay as the host's `provideLoomLabels` vocabulary left them, and then\n * as Loom's English.\n *\n * Every one of them replaces a literal Reka writes itself, so this is not\n * decoration: leave the vocabulary unset and the segments are named in\n * English rather than named not at all.\n */\n labels?: LabelOverrides<TimePickerLabels>;\n }>(),\n {\n granularity: \"minute\",\n locale: \"en\",\n // Not `false`, for any of the four — TextField.vue carries the full\n // reasoning: `false` is a caller's decision that must beat the row, absent\n // is a caller saying nothing, and only `undefined` survives to mean that\n // past Vue's absent-Boolean-casts-to-false rule.\n disabled: undefined,\n readonly: undefined,\n invalid: undefined,\n required: undefined,\n },\n);\n\nconst emit = defineEmits<{\n /** The chosen time as `\"HH:mm\"` (or `\"HH:mm:ss\"`), or `undefined` once the field has been cleared. */\n \"update:modelValue\": [value: string | undefined];\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(\"timeSegments\", TIME_SEGMENT_LABELS, () => props.labels);\n\n// Read through the ref on every call rather than resolved into a lookup table\n// once: a table built in `setup` freezes the first language and every later\n// switch silently does nothing. The resolution rules themselves are the shared\n// helpers' — five hand-copied pairs of these two functions drifted once\n// already, so the branches live in `@ecoma-io/loom-labels` now and only the\n// refs stay here. `undefined` for the `literal` separators, which Reka renders\n// `aria-hidden` and which must stay unnamed.\nfunction segmentLabel(part: string): string | undefined {\n return segmentAriaLabel({ time: text.value }, part);\n}\n\nfunction segmentValueText(part: string, value: string): { \"aria-valuetext\"?: string } {\n return segmentAriaValueText({ time: text.value, part, value, locale: props.locale });\n}\n\n// One rendered node, so both halves of the fallthrough land on it — the split\n// exists only because `class` needs the Tailwind-aware merge and Vue's own\n// fallthrough would concatenate a caller's `h-11` after our `h-9` instead of\n// replacing it. That node is Reka's `role=\"group\"`, which is also where\n// `aria-label` belongs: a screen reader announces a group's name once on\n// entering it, rather than once per segment.\n//\n// It is also why `invalid` is announced with `aria-invalid` and nothing else.\n// DatePicker moves its `data-invalid` marker onto the anchor because the field\n// group already spells that attribute with Reka's own meaning; here there is no\n// second element to move it to, and an attribute Reka binds itself wins outright\n// — its template merges `$attrs` first and its own bindings after, so a\n// `data-invalid` passed in from out here is overwritten by Reka's own\n// `undefined` before it reaches the DOM. Setting it would look like it worked\n// and silently do nothing.\ndefineOptions({ inheritAttrs: false });\nconst { attrs, rest: fieldAttrs } = useSplitAttrs();\n\n/**\n * The time segments are `<div role=\"spinbutton\" tabindex=\"0\">`, and\n * `<fieldset disabled>` reaches only `<input>`, `<button>`, `<select>` and\n * `<textarea>` — so Reka's hidden input went inert inside a disabled group\n * while the segments a reader actually uses still typed. Measured in jsdom: an\n * empty hour segment became `12` on an arrow key.\n *\n * Read off the DOM rather than taken from the Field context, which publishes no\n * `disabled` on purpose — see `../../lib/ancestor-disabled.ts`.\n */\nconst root = useTemplateRef<{ $el?: Element }>(\"root\");\nconst groupDisabled = useAncestorDisabled(() => root.value?.$el);\n\n// `id` and `aria-describedby` reach this control as fallthrough attrs rather\n// than props, so they are handed in as this caller's own values: a caller who\n// sets either still beats the row, and the row's description is merged into\n// theirs rather than replacing it. `field.attrs` then spreads onto\n// `TimeFieldRoot` **after** `fieldAttrs`, which is the node both were already\n// landing on.\n//\n// **Where each key ends up was checked rather than assumed**, given what the\n// note above says about attributes this node overwrites. The set Reka spells\n// itself is `role`, `aria-disabled`, `data-disabled`, `data-readonly`,\n// `data-invalid` and `dir`, and `field.attrs` spells none of them:\n// `aria-describedby`, `aria-required` and `aria-invalid` are undeclared and land\n// on the `role=\"group\"` as intended, while `id` and `name` are declared props\n// and land on the visually-hidden `<input>` the field submits through — which is\n// the element a `<label for>` can actually name, a `role=\"group\"` div not being\n// labelable.\n//\n// The `:aria-invalid` binding that used to sit on the root is gone with it: an\n// individual attribute written after a `v-bind` wins, so keeping one would\n// overwrite the resolved value with the raw prop.\nconst field = useFieldControl(() => ({\n id: attrs.id as string | undefined,\n describedBy: attrs[\"aria-describedby\"] as string | undefined,\n ariaLabelledby: attrs[\"aria-labelledby\"] as string | undefined,\n name: props.name,\n // The fieldset beats the prop in both directions, exactly as it does for the\n // native input Reka renders inside this very control.\n disabled: groupDisabled.value ? true : props.disabled,\n readonly: props.readonly,\n invalid: props.invalid,\n required: props.required,\n}));\n\n// `aria-labelledby` for the group, resolved against the row — DatePicker's\n// reasoning, minus the old third arm of the guard: a caller's own labelledby\n// has to survive here, because the binding below beats the spread and an\n// `undefined` would strip it instead of deferring to it.\nconst groupLabelledBy = computed(() => (attrs[\"aria-label\"] ? undefined : field.labelledBy));\n\nfunction fromClock(value: string | undefined): TimeValue | undefined {\n if (!value) return undefined;\n try {\n return parseTime(value);\n } catch {\n // Same contract as DatePicker: a half-typed or wrongly-shaped value is read\n // as no time chosen. A component library cannot decide that a host's bad\n // data is fatal.\n return undefined;\n }\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n// Written out rather than taken from `Time.toString()`, which always prints\n// seconds and appends a fractional part when the value carries one — so a\n// minute-granularity field would report `\"09:30:00\"`, and a value the host\n// parsed from `\"09:30:00.5\"` would round-trip a half second the field never\n// showed. The string that comes back is the string the segments are showing.\nfunction toClock(time: TimeValue): string {\n const hhmm = `${pad(time.hour)}:${pad(time.minute)}`;\n return props.granularity === \"second\" ? `${hhmm}:${pad(time.second)}` : hhmm;\n}\n\n// `optional()` rather than plain bindings: Reka reads an explicit `undefined`\n// `modelValue` as a value the host owns and is holding empty, which turns an\n// uncontrolled field into a permanently empty one, and an `hourCycle` of\n// `undefined` is how it is told to follow the locale. Dropping the keys says\n// what is true.\nconst rootValue = computed(() =>\n optional({ modelValue: fromClock(props.modelValue), hourCycle: props.hourCycle }),\n);\n\nfunction onTimeChange(time: TimeValue | undefined) {\n emit(\"update:modelValue\", time ? toClock(time) : undefined);\n}\n\n// The field is one Tab stop, not four. The reasoning, and the reason Reka's own\n// ArrowLeft/ArrowRight handling survives it untouched, is written out in\n// DatePicker — it moves focus with `focus()` directly, which ignores tabindex.\n// It matters more here: a 12-hour field with seconds is four segments, so\n// leaving Reka's default in place costs a keyboard reader four Tab presses to\n// cross one control.\nconst focusedPart = ref<string>();\n\nfunction segmentTabIndex(part: string, segments: readonly { part: string }[]): number | undefined {\n // Read-only keeps every stop it had, unlike disabled: the time is on show and\n // a reader still arrows across the segments to read it.\n if (field.disabled || part === \"literal\") return undefined;\n const editable = segments.filter((segment) => segment.part !== \"literal\");\n // The remembered segment is dropped when a granularity or locale change takes\n // it away — a field whose only tab stop is a seconds segment that no longer\n // exists is a field nothing can reach.\n const active =\n focusedPart.value !== undefined &&\n editable.some((segment) => segment.part === focusedPart.value)\n ? focusedPart.value\n : editable[0]?.part;\n return part === active ? 0 : -1;\n}\n\n// Reka reads `hourCycle` once, when it builds its formatter, and seeds its\n// segment values from the granularity in the same breath — neither is watched.\n// Observed: a field showing 13:30 that is moved to a 12-hour cycle relabels\n// itself \"1:30 AM\", half a day wrong, because the AM/PM segment appears with\n// its initial value and nothing recomputes it from the hour. Widening the\n// granularity on an empty field is the same fault with a different symptom: the\n// new seconds segment arrives with no entry to read, so it renders without a\n// role at all.\n//\n// Both are structural changes — they add and remove segments — so keying the\n// field on them rebuilds it rather than patching a half-migrated one. `locale`\n// is deliberately not in the key: Reka does watch that, and rebuilding on it\n// would throw away focus mid-edit for a change it handles correctly.\nconst shape = computed(() => `${props.granularity}-${props.hourCycle}`);\n\n/**\n * Reka's hour segment publishes `aria-valuenow` and `aria-valuetext` straight\n * off the internal 24-hour value while publishing `aria-valuemin`/`max` as\n * 1–12, so half past one in the afternoon announces itself as \"13 PM\", out of\n * its own declared range. The displayed hour is the one the reader is being\n * asked about, so the announcement is rebuilt from what the segment actually\n * shows. Only under an explicit 12-hour cycle: left to the locale, Reka defers\n * to the formatter and the pair is already consistent.\n */\nfunction hourAnnouncement(\n segments: readonly { part: string; value: string }[],\n): Record<string, string | number> | undefined {\n if (props.hourCycle !== 12) return undefined;\n const shown = Number(segments.find((segment) => segment.part === \"hour\")?.value);\n const period = segments.find((segment) => segment.part === \"dayPeriod\")?.value;\n // An unfilled hour reads as its placeholder rather than a number, and there is\n // no number to announce for it. The empty case is not left to Reka's English\n // \"Empty\" either — `segmentValueText` binds the vocabulary's word for it on\n // the same element, and the two never collide: this returns a value only for\n // a filled hour and that one only for an empty segment.\n if (!Number.isInteger(shown) || period === undefined) return undefined;\n return { \"aria-valuenow\": shown, \"aria-valuetext\": `${shown} ${period}` };\n}\n</script>\n\n<template>\n <TimeFieldRoot\n ref=\"root\"\n :key=\"shape\"\n v-slot=\"{ segments }\"\n v-bind=\"{ ...rootValue, ...fieldAttrs, ...field.attrs }\"\n :aria-labelledby=\"groupLabelledBy\"\n :locale=\"locale\"\n :granularity=\"granularity\"\n :disabled=\"field.disabled\"\n :readonly=\"field.readonly\"\n :class=\"\n cn(\n // The text input's own height scale, so a time sitting in a form row\n // beside a text field lines up rather than nearly lining up. No gap:\n // the colon between two segments is itself a segment, so spacing them\n // apart writes the time as `09 : 30`.\n 'flex h-9 w-full items-center rounded-md border border-input bg-background px-3 text-sm text-foreground',\n 'transition-[color,background-color,border-color,box-shadow] duration-fast ease-out',\n // The box carries the ring and the focused segment below carries a fill\n // instead of a second outline, exactly as DatePicker's field does — a\n // ring drawn inside a ring reads as a rendering fault.\n 'focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring',\n !field.invalid && 'focus-within:shadow-halo',\n // Three resting appearances, and each is painted on this group rather\n // than on the segments inside it: a per-segment fill leaves the colon\n // between them on `background`, and the time comes out striped.\n //\n // Read-only lifts to `subtle` and keeps its focus ring and its\n // full-strength segments — 13.46:1 over that fill — because a read-only\n // time is a value on show, not an unavailable control. The rim stays\n // `input`: the field is still a Tab stop and still submitted. Reka's own\n // `data-readonly` on this node carries the same state to assistive tech,\n // so nothing here rests on colour.\n field.readonly && 'bg-subtle',\n // Drained rather than faded, for the reason DatePicker and\n // DateTimePicker both write out — this control was the one member of\n // the family the sweep still caught. `opacity-50` took the segments\n // down with the box: measured on the built site, the hour and the\n // period read 3.08:1 and the colon between them 2.07:1, and those\n // segments *are* the value. The fill drains one step past read-only,\n // the muted text over it is 4.68:1, and the rim slackens from `input`\n // to `border` — three channels, so the two states do not part on hue\n // alone. Set on the group because no segment declares a resting colour\n // of its own, so each inherits this one.\n field.disabled && 'cursor-not-allowed border-border bg-muted text-muted-foreground',\n // Last of the rules that name a border colour: `cn()` resolves one by\n // whichever class it saw last, so a field both in error and unavailable\n // would otherwise lose its destructive rim to the disabled one.\n field.invalid && 'border-destructive focus-within:outline-destructive',\n attrs.class as string,\n )\n \"\n @update:model-value=\"onTimeChange\"\n >\n <!-- Leading, where DatePicker's calendar button is trailing: that trailing\n slot is a button in every other member of this family, and a glyph\n sitting in it that opens nothing would promise a panel this control\n does not have. Decoration, so it is hidden from the accessibility\n tree — the group's own name says what the field is. -->\n <Clock aria-hidden=\"true\" class=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\n\n <!-- The `:aria-label` replaces the one Reka writes in English inside its\n own render function. It reaches the DOM node as a fallthrough\n attribute, which Vue merges last, so it is a replacement rather than a\n second name. Removing it does not fall back to nothing; it falls back\n to Reka's English. -->\n <TimeFieldInput\n v-for=\"(item, index) in segments\"\n :key=\"index\"\n :part=\"item.part\"\n :tabindex=\"segmentTabIndex(item.part, segments)\"\n :aria-label=\"segmentLabel(item.part)\"\n v-bind=\"{\n ...segmentValueText(item.part, item.value),\n ...(item.part === 'hour' ? hourAnnouncement(segments) : undefined),\n }\"\n :class=\"\n cn(\n 'tabular rounded-sm px-0.5 outline-none',\n 'transition-colors duration-instant ease-out',\n 'focus:bg-primary-muted focus:text-primary-text',\n 'data-[placeholder]:text-muted-foreground',\n item.part === 'literal' && 'text-muted-foreground',\n )\n \"\n @focusin=\"focusedPart = item.part\"\n >\n {{ item.value }}\n </TimeFieldInput>\n </TimeFieldRoot>\n</template>\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
- import { type LabelOverrides } from "@ecoma-io/loom-labels";
2
- import type { TimeSegmentLabels } from "@ecoma-io/loom-labels";
1
+ import { type LabelOverrides } from "../../../labels/src/index";
2
+ import type { TimeSegmentLabels } from "../../../labels/src/index";
3
3
  /**
4
4
  * Everything this control publishes to assistive technology: the four names
5
5
  * Reka UI would otherwise give the segments in English of its own accord.