vectis-ui 0.8.1 → 0.9.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 (432) hide show
  1. package/README.md +3 -3
  2. package/dist/components/VAccordion/VAccordionItem.d.ts +4 -6
  3. package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
  4. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js +21 -19
  5. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/VAccordion/context.d.ts +6 -1
  7. package/dist/components/VAccordion/context.js.map +1 -1
  8. package/dist/components/VAvatar/VAvatar.css +1 -1
  9. package/dist/components/VAvatar/VAvatar.d.ts +5 -3
  10. package/dist/components/VAvatar/VAvatar.js.map +1 -1
  11. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js +53 -39
  12. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/VAvatar/VAvatarGroup.d.ts +12 -3
  14. package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
  15. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +27 -20
  16. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/VAvatar/context.d.ts +2 -2
  18. package/dist/components/VAvatar/context.js.map +1 -1
  19. package/dist/components/VBadge/VBadge.css +1 -1
  20. package/dist/components/VBadge/VBadge.d.ts +8 -4
  21. package/dist/components/VBadge/VBadge.js.map +1 -1
  22. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js +6 -3
  23. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  24. package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +5 -4
  25. package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
  26. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js +8 -3
  27. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VButton/VButton.css +1 -1
  29. package/dist/components/VButton/VButton.d.ts +4 -1
  30. package/dist/components/VButton/VButton.js.map +1 -1
  31. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js +8 -7
  32. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  34. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  35. package/dist/components/VCalendar/VCalendar.css +1 -1
  36. package/dist/components/VCalendar/VCalendar.d.ts +15 -6
  37. package/dist/components/VCalendar/VCalendar.js.map +1 -1
  38. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js +144 -126
  39. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  40. package/dist/components/VCalendar/VCalendarEvent.css +1 -1
  41. package/dist/components/VCalendar/VCalendarEvent.d.ts +1 -1
  42. package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
  43. package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  44. package/dist/components/VCalendar/VCalendarMonth.css +1 -1
  45. package/dist/components/VCalendar/VCalendarMonth.d.ts +1 -1
  46. package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
  47. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js +99 -90
  48. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
  49. package/dist/components/VCalendar/VCalendarTimeGrid.css +1 -1
  50. package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +3 -3
  51. package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
  52. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js +256 -246
  53. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
  54. package/dist/components/VCalendar/VCalendarYear.css +1 -1
  55. package/dist/components/VCalendar/VCalendarYear.d.ts +8 -1
  56. package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
  57. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js +39 -32
  58. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  59. package/dist/components/VCalendar/color.d.ts +1 -1
  60. package/dist/components/VCalendar/color.js.map +1 -1
  61. package/dist/components/VCalendar/edgeStep.d.ts +3 -3
  62. package/dist/components/VCalendar/edgeStep.js +5 -0
  63. package/dist/components/VCalendar/edgeStep.js.map +1 -1
  64. package/dist/components/VCalendar/gesture.d.ts +10 -9
  65. package/dist/components/VCalendar/gesture.js +60 -45
  66. package/dist/components/VCalendar/gesture.js.map +1 -1
  67. package/dist/components/VCalendar/keyboard.d.ts +22 -5
  68. package/dist/components/VCalendar/keyboard.js +17 -16
  69. package/dist/components/VCalendar/keyboard.js.map +1 -1
  70. package/dist/components/VCalendar/layout.d.ts +47 -37
  71. package/dist/components/VCalendar/layout.js +47 -37
  72. package/dist/components/VCalendar/layout.js.map +1 -1
  73. package/dist/components/VCalendar/timeGrid.d.ts +4 -4
  74. package/dist/components/VCalendar/timeGrid.js +6 -6
  75. package/dist/components/VCalendar/timeGrid.js.map +1 -1
  76. package/dist/components/VCalendar/types.d.ts +5 -5
  77. package/dist/components/VCarousel/VCarousel.css +1 -1
  78. package/dist/components/VCarousel/VCarousel.d.ts +59 -38
  79. package/dist/components/VCarousel/VCarousel.js.map +1 -1
  80. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +161 -132
  81. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  82. package/dist/components/VCarousel/VCarouselItem.d.ts +1 -1
  83. package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
  84. package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  85. package/dist/components/VCarousel/context.d.ts +1 -1
  86. package/dist/components/VCarousel/context.js.map +1 -1
  87. package/dist/components/VCheckbox/VCheckbox.css +1 -1
  88. package/dist/components/VCheckbox/VCheckbox.d.ts +2 -2
  89. package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
  90. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js +10 -8
  91. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  92. package/dist/components/VChip/VChip.css +1 -1
  93. package/dist/components/VChip/VChip.d.ts +7 -2
  94. package/dist/components/VChip/VChip.js.map +1 -1
  95. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js +69 -55
  96. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  97. package/dist/components/VCombobox/VCombobox.css +1 -1
  98. package/dist/components/VCombobox/VCombobox.d.ts +50 -36
  99. package/dist/components/VCombobox/VCombobox.js.map +1 -1
  100. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +167 -161
  101. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  102. package/dist/components/VCombobox/VComboboxGroup.css +1 -1
  103. package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
  104. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js +1 -1
  105. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  106. package/dist/components/VCombobox/infiniteScroll.d.ts +12 -3
  107. package/dist/components/VCombobox/infiniteScroll.js +18 -12
  108. package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
  109. package/dist/components/VDataTable/VDataTable.css +1 -1
  110. package/dist/components/VDataTable/VDataTable.d.ts +55 -24
  111. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  112. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +192 -155
  113. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  114. package/dist/components/VDateInput/VDateInput.css +1 -1
  115. package/dist/components/VDateInput/VDateInput.d.ts +15 -12
  116. package/dist/components/VDateInput/VDateInput.js.map +1 -1
  117. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js +180 -160
  118. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  119. package/dist/components/VDateInput/mask.d.ts +95 -0
  120. package/dist/components/VDateInput/mask.js +118 -0
  121. package/dist/components/VDateInput/mask.js.map +1 -0
  122. package/dist/components/VDatePicker/VDatePicker.css +1 -1
  123. package/dist/components/VDatePicker/VDatePicker.d.ts +6 -5
  124. package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
  125. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js +204 -173
  126. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  127. package/dist/components/VDatePicker/keyboard.d.ts +3 -3
  128. package/dist/components/VDatePicker/keyboard.js.map +1 -1
  129. package/dist/components/VDatePicker/names.d.ts +34 -0
  130. package/dist/components/VDatePicker/names.js +42 -0
  131. package/dist/components/VDatePicker/names.js.map +1 -0
  132. package/dist/components/VDialog/VDialog.css +1 -1
  133. package/dist/components/VDialog/VDialog.d.ts +2 -2
  134. package/dist/components/VDialog/VDialog.js.map +1 -1
  135. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +63 -41
  136. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  137. package/dist/components/VFileInput/VFileInput.d.ts +38 -25
  138. package/dist/components/VFileInput/VFileInput.js.map +1 -1
  139. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js +74 -71
  140. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  141. package/dist/components/VFileInput/truncate.d.ts +4 -4
  142. package/dist/components/VFileInput/truncate.js.map +1 -1
  143. package/dist/components/VFilePicker/VFilePicker.css +1 -1
  144. package/dist/components/VFilePicker/VFilePicker.d.ts +54 -37
  145. package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
  146. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js +152 -135
  147. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  148. package/dist/components/VFilePicker/fileKind.d.ts +2 -2
  149. package/dist/components/VFilePicker/fileKind.js.map +1 -1
  150. package/dist/components/VHotkeys/VHotkeys.css +1 -1
  151. package/dist/components/VHotkeys/VHotkeys.d.ts +10 -8
  152. package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
  153. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js +22 -20
  154. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
  155. package/dist/components/VHotkeys/platform.d.ts +9 -7
  156. package/dist/components/VHotkeys/platform.js +41 -30
  157. package/dist/components/VHotkeys/platform.js.map +1 -1
  158. package/dist/components/VIcon/VIcon.css +1 -1
  159. package/dist/components/VIcon/VIcon.d.ts +6 -6
  160. package/dist/components/VIcon/VIcon.js.map +1 -1
  161. package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
  162. package/dist/components/VIcon/iconProps.d.ts +4 -4
  163. package/dist/components/VIcon/iconProps.js.map +1 -1
  164. package/dist/components/VIcon/icons/index.d.ts +1 -1
  165. package/dist/components/VIcon/icons/index.js.map +1 -1
  166. package/dist/components/VIcon/icons/names.d.ts +1 -1
  167. package/dist/components/VIcon/icons/names.js.map +1 -1
  168. package/dist/components/VIcon/resolver.d.ts +3 -3
  169. package/dist/components/VIcon/resolver.js +23 -20
  170. package/dist/components/VIcon/resolver.js.map +1 -1
  171. package/dist/components/VIcon/types.d.ts +4 -4
  172. package/dist/components/VIconButton/VIconButton.d.ts +4 -4
  173. package/dist/components/VIconButton/VIconButton.js.map +1 -1
  174. package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  175. package/dist/components/VInput/VInput.d.ts +16 -8
  176. package/dist/components/VInput/VInput.js.map +1 -1
  177. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js +22 -16
  178. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/components/VInput/VInputGroup.css +1 -1
  180. package/dist/components/VInput/VInputGroup.js.map +1 -1
  181. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +43 -28
  182. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  183. package/dist/components/VInputOTP/VInputOTP.css +1 -1
  184. package/dist/components/VInputOTP/VInputOTP.d.ts +3 -3
  185. package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
  186. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +152 -85
  187. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
  188. package/dist/components/VMenu/VMenu.d.ts +4 -4
  189. package/dist/components/VMenu/VMenu.js.map +1 -1
  190. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  191. package/dist/components/VMenu/VMenuGroup.css +1 -1
  192. package/dist/components/VMenu/VMenuGroup.js.map +1 -1
  193. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js +18 -11
  194. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  195. package/dist/components/VMenu/VMenuItem.css +1 -1
  196. package/dist/components/VMenu/VMenuItem.d.ts +6 -6
  197. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  198. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +83 -69
  199. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  200. package/dist/components/VMenu/VMenuPanel.css +1 -1
  201. package/dist/components/VMenu/VMenuPanel.d.ts +2 -2
  202. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  203. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +56 -55
  204. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  205. package/dist/components/VMenu/context.d.ts +2 -2
  206. package/dist/components/VMenu/context.js.map +1 -1
  207. package/dist/components/VPagination/VPagination.css +1 -1
  208. package/dist/components/VPagination/VPagination.d.ts +22 -9
  209. package/dist/components/VPagination/VPagination.js.map +1 -1
  210. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +67 -57
  211. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  212. package/dist/components/VPopover/VPopover.css +1 -1
  213. package/dist/components/VPopover/VPopover.d.ts +1 -1
  214. package/dist/components/VPopover/VPopover.js.map +1 -1
  215. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  216. package/dist/components/VProgressCircular/VProgressCircular.css +1 -1
  217. package/dist/components/VProgressCircular/VProgressCircular.d.ts +12 -7
  218. package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
  219. package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
  220. package/dist/components/VProgressLinear/VProgressLinear.css +1 -1
  221. package/dist/components/VProgressLinear/VProgressLinear.d.ts +15 -10
  222. package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
  223. package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  224. package/dist/components/VRadio/VRadio.css +1 -1
  225. package/dist/components/VRadio/VRadio.js.map +1 -1
  226. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js +3 -3
  227. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  228. package/dist/components/VSeparator/VSeparator.d.ts +1 -1
  229. package/dist/components/VSeparator/VSeparator.js.map +1 -1
  230. package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  231. package/dist/components/VSideNavigation/VSideNavigation.d.ts +2 -2
  232. package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
  233. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js +1 -1
  234. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
  235. package/dist/components/VSideNavigation/VSideNavigationGroup.css +1 -1
  236. package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
  237. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js +1 -1
  238. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  239. package/dist/components/VSideNavigation/VSideNavigationItem.css +1 -1
  240. package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +12 -13
  241. package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
  242. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +87 -68
  243. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  244. package/dist/components/VSkeletonLoader/VSkeletonLoader.css +1 -1
  245. package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +10 -7
  246. package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
  247. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js +5 -5
  248. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  249. package/dist/components/VSlider/VSlider.css +1 -1
  250. package/dist/components/VSlider/VSlider.d.ts +21 -7
  251. package/dist/components/VSlider/VSlider.js.map +1 -1
  252. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js +159 -140
  253. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  254. package/dist/components/VSnackbar/VSnackbar.css +1 -1
  255. package/dist/components/VSnackbar/VSnackbar.d.ts +4 -4
  256. package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
  257. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js +85 -59
  258. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  259. package/dist/components/VSnackbar/state.d.ts +7 -7
  260. package/dist/components/VSnackbar/state.js +13 -11
  261. package/dist/components/VSnackbar/state.js.map +1 -1
  262. package/dist/components/VSpinner/VSpinner.css +1 -1
  263. package/dist/components/VSpinner/VSpinner.js.map +1 -1
  264. package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
  265. package/dist/components/VSwitch/VSwitch.css +1 -1
  266. package/dist/components/VSwitch/VSwitch.d.ts +1 -1
  267. package/dist/components/VSwitch/VSwitch.js.map +1 -1
  268. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js +4 -4
  269. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  270. package/dist/components/VTabs/VTab.css +1 -1
  271. package/dist/components/VTabs/VTab.d.ts +4 -0
  272. package/dist/components/VTabs/VTab.js.map +1 -1
  273. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js +9 -8
  274. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
  275. package/dist/components/VTabs/VTabs.css +1 -1
  276. package/dist/components/VTabs/VTabs.d.ts +17 -7
  277. package/dist/components/VTabs/VTabs.js.map +1 -1
  278. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js +84 -77
  279. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  280. package/dist/components/VTextarea/VTextarea.d.ts +8 -8
  281. package/dist/components/VTextarea/VTextarea.js.map +1 -1
  282. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js +71 -69
  283. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  284. package/dist/components/VTimeInput/VTimeInput.css +1 -1
  285. package/dist/components/VTimeInput/VTimeInput.d.ts +21 -12
  286. package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
  287. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js +230 -206
  288. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  289. package/dist/components/VTimeInput/search.d.ts +17 -2
  290. package/dist/components/VTimeInput/search.js +19 -9
  291. package/dist/components/VTimeInput/search.js.map +1 -1
  292. package/dist/components/VTimePicker/VTimePicker.css +1 -1
  293. package/dist/components/VTimePicker/VTimePicker.d.ts +8 -7
  294. package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
  295. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js +128 -106
  296. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  297. package/dist/components/VTimePicker/limits.d.ts +11 -4
  298. package/dist/components/VTimePicker/limits.js +3 -2
  299. package/dist/components/VTimePicker/limits.js.map +1 -1
  300. package/dist/components/VToast/VToast.css +1 -1
  301. package/dist/components/VToast/VToast.js.map +1 -1
  302. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js +33 -37
  303. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
  304. package/dist/components/VToast/VToaster.js.map +1 -1
  305. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js +91 -61
  306. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  307. package/dist/components/VToast/state.d.ts +8 -6
  308. package/dist/components/VToast/state.js +15 -13
  309. package/dist/components/VToast/state.js.map +1 -1
  310. package/dist/components/VToggle/VToggle.css +1 -1
  311. package/dist/components/VToggle/VToggle.d.ts +9 -4
  312. package/dist/components/VToggle/VToggle.js.map +1 -1
  313. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +2 -1
  314. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  315. package/dist/components/VToggle/VToggleItem.d.ts +4 -0
  316. package/dist/components/VToggle/VToggleItem.js.map +1 -1
  317. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js +5 -5
  318. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  319. package/dist/components/VTooltip/VTooltip.css +1 -1
  320. package/dist/components/VTooltip/VTooltip.d.ts +17 -10
  321. package/dist/components/VTooltip/VTooltip.js.map +1 -1
  322. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +46 -26
  323. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  324. package/dist/components/VTypography/VTypography.css +1 -1
  325. package/dist/components/VTypography/VTypography.d.ts +6 -4
  326. package/dist/components/VTypography/VTypography.js.map +1 -1
  327. package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
  328. package/dist/composables/useAriaLabel.js +1 -1
  329. package/dist/composables/useAriaLabel.js.map +1 -1
  330. package/dist/composables/useClearable.d.ts +2 -2
  331. package/dist/composables/useClearable.js.map +1 -1
  332. package/dist/composables/useDetailsOpen.d.ts +3 -3
  333. package/dist/composables/useDetailsOpen.js +14 -12
  334. package/dist/composables/useDetailsOpen.js.map +1 -1
  335. package/dist/composables/useFieldIds.d.ts +2 -1
  336. package/dist/composables/useFieldIds.js +6 -5
  337. package/dist/composables/useFieldIds.js.map +1 -1
  338. package/dist/composables/useFieldPanel.d.ts +2 -2
  339. package/dist/composables/useFieldPanel.js +47 -37
  340. package/dist/composables/useFieldPanel.js.map +1 -1
  341. package/dist/composables/useFileDrop.js +3 -3
  342. package/dist/composables/useFileDrop.js.map +1 -1
  343. package/dist/composables/useFileField.d.ts +1 -1
  344. package/dist/composables/useFileField.js +62 -41
  345. package/dist/composables/useFileField.js.map +1 -1
  346. package/dist/composables/useIconClickHandlers.d.ts +3 -3
  347. package/dist/composables/useIconClickHandlers.js +14 -13
  348. package/dist/composables/useIconClickHandlers.js.map +1 -1
  349. package/dist/composables/useInertLink.js +12 -10
  350. package/dist/composables/useInertLink.js.map +1 -1
  351. package/dist/composables/useLiveAnnouncer.d.ts +5 -0
  352. package/dist/composables/useLiveAnnouncer.js +23 -0
  353. package/dist/composables/useLiveAnnouncer.js.map +1 -0
  354. package/dist/composables/useMaskedField.d.ts +5 -5
  355. package/dist/composables/useMaskedField.js.map +1 -1
  356. package/dist/composables/usePopover.d.ts +3 -3
  357. package/dist/composables/usePopover.js +6 -4
  358. package/dist/composables/usePopover.js.map +1 -1
  359. package/dist/composables/useProgressValue.js +11 -2
  360. package/dist/composables/useProgressValue.js.map +1 -1
  361. package/dist/composables/useRootAttrs.d.ts +1 -1
  362. package/dist/composables/useRootAttrs.js.map +1 -1
  363. package/dist/composables/useSlotNodes.d.ts +18 -0
  364. package/dist/composables/useSlotNodes.js +13 -0
  365. package/dist/composables/useSlotNodes.js.map +1 -0
  366. package/dist/composables/useTextLimit.d.ts +2 -1
  367. package/dist/composables/useTextLimit.js +2 -2
  368. package/dist/composables/useTextLimit.js.map +1 -1
  369. package/dist/composables/useTimer.d.ts +5 -0
  370. package/dist/composables/useTimer.js +22 -11
  371. package/dist/composables/useTimer.js.map +1 -1
  372. package/dist/i18n/en.js +1 -1
  373. package/dist/i18n/en.js.map +1 -1
  374. package/dist/i18n/fr.js +14 -14
  375. package/dist/i18n/fr.js.map +1 -1
  376. package/dist/i18n/state.d.ts +7 -7
  377. package/dist/i18n/state.js +37 -28
  378. package/dist/i18n/state.js.map +1 -1
  379. package/dist/i18n/types.d.ts +12 -12
  380. package/dist/icons.d.ts +2 -2
  381. package/dist/index.d.ts +12 -11
  382. package/dist/styles.css +1 -1
  383. package/dist/tokens/css.d.ts +1 -1
  384. package/dist/tokens/css.js.map +1 -1
  385. package/dist/tokens/index.d.ts +6 -0
  386. package/dist/tokens/semantic.d.ts +6 -0
  387. package/dist/tokens/semantic.js +66 -60
  388. package/dist/tokens/semantic.js.map +1 -1
  389. package/dist/tokens/themes/dark.js +3 -3
  390. package/dist/tokens/themes/dark.js.map +1 -1
  391. package/dist/tokens.json +130 -66
  392. package/dist/types.d.ts +1 -1
  393. package/dist/utils/arrowNav.d.ts +4 -1
  394. package/dist/utils/arrowNav.js +7 -7
  395. package/dist/utils/arrowNav.js.map +1 -1
  396. package/dist/utils/chip.d.ts +1 -1
  397. package/dist/utils/chip.js.map +1 -1
  398. package/dist/utils/clock.d.ts +113 -0
  399. package/dist/utils/clock.js +84 -0
  400. package/dist/utils/clock.js.map +1 -0
  401. package/dist/utils/css.d.ts +6 -4
  402. package/dist/utils/css.js +2 -2
  403. package/dist/utils/css.js.map +1 -1
  404. package/dist/utils/date.d.ts +7 -102
  405. package/dist/utils/date.js +33 -160
  406. package/dist/utils/date.js.map +1 -1
  407. package/dist/utils/direction.d.ts +1 -1
  408. package/dist/utils/direction.js.map +1 -1
  409. package/dist/utils/env.d.ts +12 -3
  410. package/dist/utils/env.js +6 -0
  411. package/dist/utils/env.js.map +1 -0
  412. package/dist/utils/file.d.ts +3 -3
  413. package/dist/utils/file.js +1 -1
  414. package/dist/utils/file.js.map +1 -1
  415. package/dist/utils/focus.d.ts +1 -1
  416. package/dist/utils/focus.js.map +1 -1
  417. package/dist/utils/hostWarns.d.ts +1 -1
  418. package/dist/utils/hostWarns.js.map +1 -1
  419. package/dist/utils/ids.d.ts +1 -1
  420. package/dist/utils/ids.js.map +1 -1
  421. package/dist/utils/matcher.d.ts +2 -2
  422. package/dist/utils/matcher.js.map +1 -1
  423. package/dist/utils/memo.d.ts +1 -1
  424. package/dist/utils/memo.js.map +1 -1
  425. package/dist/utils/text.d.ts +3 -3
  426. package/dist/utils/text.js.map +1 -1
  427. package/dist/utils/time.d.ts +2 -101
  428. package/dist/utils/time.js +22 -98
  429. package/dist/utils/time.js.map +1 -1
  430. package/dist/utils/vnode.d.ts +5 -3
  431. package/dist/utils/vnode.js.map +1 -1
  432. package/package.json +2 -2
@@ -1,16 +1,17 @@
1
- import e from "../VTypography/VTypography.js";
2
- import { buttonGroupKey as t } from "../VButton/context.js";
3
- import { useRootAttrs as n } from "../../composables/useRootAttrs.js";
4
- import { joinIds as r } from "../../utils/ids.js";
5
- import { inputGroupKey as i } from "./context.js";
6
- import { flattenSlot as a } from "../../utils/vnode.js";
7
- import { computed as o, createBlock as s, createCommentVNode as c, createElementBlock as l, createElementVNode as u, createTextVNode as d, createVNode as f, defineComponent as p, mergeProps as m, openBlock as h, provide as g, toDisplayString as _, unref as v, useId as y, useSlots as b, withCtx as x } from "vue";
1
+ import { isDev as e } from "../../utils/env.js";
2
+ import t from "../VTypography/VTypography.js";
3
+ import { buttonGroupKey as n } from "../VButton/context.js";
4
+ import { useRootAttrs as r } from "../../composables/useRootAttrs.js";
5
+ import { joinIds as i } from "../../utils/ids.js";
6
+ import { inputGroupKey as a } from "./context.js";
7
+ import { useSlotNodes as o } from "../../composables/useSlotNodes.js";
8
+ import { computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createTextVNode as f, createVNode as p, defineComponent as m, mergeProps as h, openBlock as g, provide as _, toDisplayString as v, unref as y, useId as b, withCtx as x } from "vue";
8
9
  //#region src/components/VInput/VInputGroup.vue?vue&type=script&setup=true&lang.ts
9
10
  var S = [
10
11
  "aria-labelledby",
11
12
  "aria-describedby",
12
13
  "data-disabled"
13
- ], C = { class: "v-input-group-row" }, w = /*@__PURE__*/ p({
14
+ ], C = { class: "v-input-group-row" }, w = /*@__PURE__*/ m({
14
15
  inheritAttrs: !1,
15
16
  __name: "VInputGroup",
16
17
  props: {
@@ -26,11 +27,25 @@ var S = [
26
27
  default: void 0
27
28
  }
28
29
  },
29
- setup(p) {
30
- let w = p, { attrs: T, rootClass: E, rootStyle: D, forwardedAttrs: O } = n(), k = b(), A = y(), j = y(), M = o(() => {
30
+ setup(m) {
31
+ let w = m, { attrs: T, rootClass: E, rootStyle: D, forwardedAttrs: O } = r(), k = b(), A = b(), j = s(() => {
31
32
  let e = T["aria-labelledby"];
32
- return e === void 0 ? w.label && T["aria-label"] === void 0 ? A : void 0 : e;
33
- }), N = o(() => r(T["aria-describedby"], !!w.hint && j)), P = o(() => a(k.default?.())), F = () => P.value, I = {
33
+ return e === void 0 ? w.label && T["aria-label"] === void 0 ? k : void 0 : e;
34
+ }), M = s(() => i(T["aria-describedby"], !!w.hint && A)), N = /* @__PURE__ */ new Set([
35
+ "VInput",
36
+ "VTextarea",
37
+ "VCombobox",
38
+ "VDateInput",
39
+ "VTimeInput",
40
+ "VFileInput"
41
+ ]), P = /* @__PURE__ */ new Set(), F = o(), I = s(() => {
42
+ let t = F.value;
43
+ if (e) for (let e of t) {
44
+ let t = e.type, n = typeof t == "object" && t ? t.name ?? t.__name : void 0;
45
+ if (!(n === void 0 || !N.has(n))) for (let t of ["label", "hint"]) e.props?.[t] === void 0 || P.has(`${n}.${t}`) || (P.add(`${n}.${t}`), console.warn(`[VInputGroup] <${n}> carries its own \`${t}\`. The group renders one for the whole row, and a segment carrying one of its own is pushed a line out of the row. Move it to VInputGroup and name the segment with \`aria-label\`.`));
46
+ }
47
+ return t;
48
+ }), L = () => I.value, R = {
34
49
  get size() {
35
50
  return w.size;
36
51
  },
@@ -41,36 +56,36 @@ var S = [
41
56
  return w.disabled;
42
57
  }
43
58
  };
44
- return g(i, I), g(t, I), (t, n) => (h(), l("div", m({
45
- class: ["v-input-group", v(E)],
46
- style: v(D),
59
+ return _(a, R), _(n, R), (e, n) => (g(), u("div", h({
60
+ class: ["v-input-group", y(E)],
61
+ style: y(D),
47
62
  role: "group"
48
- }, v(O), {
49
- "aria-labelledby": M.value,
50
- "aria-describedby": N.value,
51
- "data-disabled": p.disabled ? "" : void 0
63
+ }, y(O), {
64
+ "aria-labelledby": j.value,
65
+ "aria-describedby": M.value,
66
+ "data-disabled": m.disabled ? "" : void 0
52
67
  }), [
53
- p.label ? (h(), s(e, {
68
+ m.label ? (g(), c(t, {
54
69
  key: 0,
55
- id: v(A),
70
+ id: y(k),
56
71
  as: "span",
57
72
  variant: "label",
58
73
  class: "v-input-group-label"
59
74
  }, {
60
- default: x(() => [d(_(p.label), 1)]),
75
+ default: x(() => [f(v(m.label), 1)]),
61
76
  _: 1
62
- }, 8, ["id"])) : c("", !0),
63
- u("div", C, [f(F)]),
64
- p.hint ? (h(), s(e, {
77
+ }, 8, ["id"])) : l("", !0),
78
+ d("div", C, [p(L)]),
79
+ m.hint ? (g(), c(t, {
65
80
  key: 1,
66
- id: v(j),
81
+ id: y(A),
67
82
  variant: "caption",
68
83
  tone: "muted",
69
84
  class: "v-input-group-hint"
70
85
  }, {
71
- default: x(() => [d(_(p.hint), 1)]),
86
+ default: x(() => [f(v(m.hint), 1)]),
72
87
  _: 1
73
- }, 8, ["id"])) : c("", !0)
88
+ }, 8, ["id"])) : l("", !0)
74
89
  ], 16, S));
75
90
  }
76
91
  });
@@ -1 +1 @@
1
- {"version":3,"file":"VInputGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VInput/VInputGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Joins two or more form controls into a single object: a country code against a phone\n * number, a search field against its magnifier. The shared borders melt into one line and\n * only the two ends of the row stay rounded.\n *\n * It does for fields what VButtonGroup does for buttons, with one structural difference that\n * shapes the whole sheet. For a button, the component's root IS the bordered box; for a\n * field it never is — `.v-input` is a column holding a label, the field and a hint, and the\n * bordered box is `.v-input-field`, a descendant. So the PULL that melts two borders lands\n * on the segment the row lays out, and the PAINT lands on the box that carries them.\n *\n * The row owns the label and the hint, and that is the reason this component exists rather\n * than a CSS recipe: a label above one segment alone pushes its box a line out of the row.\n * The group renders one for all of them and names itself with it, each segment taking an\n * `aria-label` instead — the ARIA pattern for a composed field.\n *\n * `role=\"group\"` on a div, not `<fieldset>`/`<legend>`, and not only for consistency with\n * VButtonGroup and VToggle. The accessibility tree is identical either way: browsers map\n * `<fieldset>` to `role=\"group\"` and `<legend>` to its accessible name. The layout is not. A\n * `<fieldset>` carries `min-inline-size: min-content` in the UA sheet, so it refuses to\n * shrink below its content, which is exactly the right segments in `flex: 1 1 0` are asking\n * for; and a rendered `<legend>` is pulled out of flow and painted into the border area.\n * The native primitive is the right answer when it buys something, and here it costs two\n * resets to impose the invisible UA behaviour that rule is meant to spare us.\n *\n * Horizontal only, and probably for good. A column of fields is a form, and the fields of a\n * form carry a label each: melting their borders would remove the very separation a form\n * needs. VButtonGroup has a vertical because a column of buttons is a toolbar.\n *\n * The behavioural JS is one development warning. The row cannot draw itself out of a segment\n * that brought its own label, and the misalignment is silent, so it says so.\n */\nimport { computed, provide, useId, useSlots } from 'vue'\n\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { buttonGroupKey } from '../VButton/context'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\nimport { flattenSlot } from '../../utils/vnode'\n\nimport { inputGroupKey } from './context'\n\n/** The heights a joined row offers, matching the field scale: 32, 40 or 48 pixels. */\nexport type InputGroupSize = 'sm' | 'md' | 'lg'\n\ninterface InputGroupProps {\n /**\n * The label above the row, rendered once for all of its segments and used as the\n * group's accessible name. A segment carrying one of its own is pushed out of line,\n * so name each of them with `aria-label` instead.\n */\n label?: string\n /**\n * A line of help under the row, tied to the group so assistive technology reads it\n * out along with the label.\n */\n hint?: string\n /**\n * The height every segment takes, whatever it names for itself: a row of controls of\n * two heights stops reading as one object. Left out, each segment keeps its own.\n */\n size?: InputGroupSize\n /** Takes 4px off the height of every segment, the way `compact` does on a lone field. */\n compact?: boolean\n /**\n * Makes the whole row unusable. It adds to what each segment says rather than\n * replacing it: a segment disabled on its own stays disabled under a row that says\n * nothing.\n */\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\n// TRAP — every one of the three below defaults to `undefined`, the booleans included. It is\n// `undefined`, and not `false`, that means \"the group has no opinion\", and that is what makes\n// a bare VInputGroup change nothing about what it contains. Writing `false` here would force\n// every segment out of compact and out of disabled, whatever it had asked for itself.\nconst props = withDefaults(defineProps<InputGroupProps>(), {\n label: undefined,\n hint: undefined,\n size: undefined,\n compact: undefined,\n disabled: undefined,\n})\n\ndefineSlots<{\n /** The fields and buttons to join. Each one is a segment of the row. */\n default(): unknown\n}>()\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\nconst slots = useSlots()\n\nconst labelId = useId()\nconst hintId = useId()\n\n// @a11y — the canonical cascade, resolved HERE rather than left to fallthrough: the binding\n// in the template comes after `v-bind=\"forwardedAttrs\"` and would otherwise overwrite what\n// the consumer wrote, so this computed has to hand their value back itself.\n//\n// An `aria-label` or an `aria-labelledby` written by the consumer names the group, and the\n// VISIBLE label must not double it: `aria-labelledby` wins over `aria-label` in the\n// accessible name computation, so emitting ours on top of a consumer's `aria-label` would\n// silently cancel it.\nconst labelledBy = computed(() => {\n const own = attrs['aria-labelledby'] as string | undefined\n if (own !== undefined) return own\n // TRUTHINESS, the test the template renders the label with: an empty `label=\"\"` renders\n // no element, and naming the group after an id nothing carries leaves it nameless.\n return props.label && attrs['aria-label'] === undefined ? labelId : undefined\n})\n\n// @a11y — `aria-describedby` is a LIST, so the hint is ADDED to whatever the consumer\n// pointed at rather than replacing it (the `useFieldIds` idiom). This aggregation is also\n// why the component takes `inheritAttrs: false` despite having a single root: left to\n// fallthrough, a consumer's `aria-describedby` would overwrite ours and the hint would stop\n// being announced, with nothing to show for it.\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The components whose `label` prop renders a real `<label>` above the field. VIconButton is\n * deliberately absent: its `label` is an accessible name for a control with no text, so\n * warning about it would be wrong.\n */\nconst LABELLED_FIELDS = new Set([\n 'VInput',\n 'VTextarea',\n 'VCombobox',\n 'VDateInput',\n 'VTimeInput',\n 'VFileInput',\n])\n\n// A dev warning fires from a computed that the render consumes, so a slot whose content\n// depends on a model would repeat it on every keystroke. One line per offender is enough to\n// be found and fixed.\nconst warned = new Set<string>()\n\n// @devwarn @ssr — the segments are read from the slot's VNODES, never from a registry the\n// children would fill at mount: a registry is empty on the server and full in the browser.\n//\n// TRAP — the read has to stay inside this computed, and the computed has to be consumed by\n// the render, which is what `<Segments />` does. Called from a `watchEffect`, which runs\n// BEFORE the render, Vue warns \"Slot invoked outside of the render function\" on any slot\n// passed as a raw function — which is exactly what a unit test passes.\nconst segments = computed(() => {\n const nodes = flattenSlot(slots.default?.())\n\n if (isDev) {\n for (const node of nodes) {\n const type = node.type as { name?: string; __name?: string } | string\n const name =\n typeof type === 'object' && type !== null ? (type.name ?? type.__name) : undefined\n if (name === undefined || !LABELLED_FIELDS.has(name)) continue\n\n for (const prop of ['label', 'hint'] as const) {\n if (node.props?.[prop] === undefined || warned.has(`${name}.${prop}`)) continue\n warned.add(`${name}.${prop}`)\n console.warn(\n `[VInputGroup] <${name}> carries its own \\`${prop}\\`. The group renders one for the whole row, and a segment carrying one of its own is pushed a line out of the row. Move it to VInputGroup and name the segment with \\`aria-label\\`.`,\n )\n }\n }\n }\n\n return nodes\n})\n\n// A functional component is the only way to render VNodes that have already been captured:\n// `<component :is>` expects a component definition rather than a vnode. Returning an array\n// produces a Fragment, so no DOM element is inserted and the sheet's `>` combinator holds.\nconst Segments = () => segments.value\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nconst rowContext = {\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get disabled() {\n return props.disabled\n },\n}\n\nprovide(inputGroupKey, rowContext)\n\n// The group provides VButtonGroup's context TOO, which is what makes a VButton or a\n// VIconButton segment follow the row's height without a new line in VButton: the arbitration\n// there is already `group?.x ?? props.x`, exactly this one. Variant, tone and elevation stay\n// `undefined` — in a row of fields those are the button's own decisions.\nprovide(buttonGroupKey, rowContext)\n</script>\n\n<template>\n <div\n class=\"v-input-group\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"group\"\n v-bind=\"forwardedAttrs\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :data-disabled=\"disabled ? '' : undefined\"\n >\n <!-- A span and not a `<label for>`: a label points at ONE control, and this names a row\n of them. The group takes it through aria-labelledby, the ARIA pattern for a\n composed field. -->\n <VTypography v-if=\"label\" :id=\"labelId\" as=\"span\" variant=\"label\" class=\"v-input-group-label\">\n {{ label }}\n </VTypography>\n\n <div class=\"v-input-group-row\">\n <Segments />\n </div>\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-group-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A SEGMENT is a direct child of the row, and it is almost never the box carrying the\n * border. For a button the two coincide; for a field they never do — VInput's bordered box\n * is `.v-input-field`, one level under its root, two or three under those of VCombobox,\n * VDateInput, VTimeInput and VFileInput. Companions add more: VTooltip renders `.v-tooltip`\n * around its trigger, VPopover `.v-popover`, VBadge `.v-badge-host`, and they stack.\n *\n * So every rule is written for BOTH forms:\n *\n * > .v-button<tests> the button IS the segment\n * > WRAPPER<tests> BOX the box is somewhere inside it\n *\n * WRAPPER = :not(:where(.v-overlay, .v-button-group))\n * BOX = :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *))\n *\n * The PULL — the negative margin that melts two borders into one line — belongs to the\n * SEGMENT, the box the row lays out. The PAINT — the corners, the stacking order —\n * belongs to the BOX, which carries them. Swapped, a field would be pulled by its field\n * box while its root stayed put, and the row would show a one-pixel gap.\n *\n * The tests weigh the same on both sides, so a pair is always ONE specificity.\n *\n * TRAP — `.v-overlay` is in both guards and neither is optional. A panel must not pass for\n * a segment: VMenu renders its own as a SIBLING of its trigger. And a panel's CONTENT must\n * never be painted: an open VDatePicker carries buttons, a combobox list carries options,\n * and they would all take a segment's flattened corners.\n *\n * TRAP — `.v-button-group` is in both guards, and the case easy to miss is VTimeInput: in\n * 12 hour form it renders an AM/PM VToggle beside its field, and a VToggle IS a\n * `.v-button-group`. Without `.v-button-group *` in BOX, those two buttons would be\n * painted as segments of THIS row, on top of the row VToggle draws for itself.\n *\n * No doubled class is needed anywhere here, which is worth stating so nobody adds one \"for\n * safety\". Our corners weigh (0,3,0) against the (0,1,0) that `.v-input-field` and\n * `.v-button` put on themselves; `position: relative` matches the (0,2,0) of\n * `.v-input-end-pinned > .v-input-field` at the same value; nothing else sets a `z-index` on these\n * elements. VButtonGroup's own (0,4,0) corners are unreachable — a button cannot be both a\n * direct child of `.v-input-group-row` and inside a `.v-button-group`, the WRAPPER guard\n * excludes it.\n */\n\n .v-input-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The label and the hint are rendered by VTypography, which carries their type. The\n classes stay as hooks: a consumer overrides through them, and the disabled state\n below reaches them that way. */\n\n /* The row carries NEITHER `v-control` NOR `data-size`: nothing in this sheet reads a\n `--control-*` variable, and `.v-control` set a second time above a subtree that already\n has it re-derives `--control-height` from the base height, dropping the compact its\n fields had applied (styles/control-size.css). */\n .v-input-group-row {\n display: flex;\n /* Same height whatever a segment measures. This changes nothing in an ordinary row,\n where everything is --control-height, and it is what stops a segment that GROWS from\n leaving its neighbours hanging at the top. */\n align-items: stretch;\n }\n\n /* A segment holding a field takes its share of the row and accepts shrinking below its own\n content: the intrinsic width of a field is that of its placeholder, which would push a\n row of two fields out of its parent. Everything else keeps its natural width, a button\n being measured by its label.\n\n The logic is written this way round because it is the one that enumerates nothing: a\n bare VButton, or one under two companions, has no `.v-input-field`, so it never stretches\n without our having to name it. The proportions belong to the consumer, an inline `flex`\n on the segment (style=\"flex: 0 0 9rem\").\n\n A panel is not a segment and takes no share. Closed it is `display: none`, open it is\n fixed, so it is not a flex item either way — the guard is there so the selector says\n what it does. */\n .v-input-group-row > :not(:where(.v-overlay)) {\n flex: none;\n min-inline-size: 0;\n }\n\n .v-input-group-row > :not(:where(.v-overlay)):has(.v-input-field) {\n flex: 1 1 0;\n }\n\n /* Every box is positioned, so they all paint in the SAME phase and in document order.\n\n TRAP — without this rule the paint order would depend on which components the row\n happens to hold. A combobox's field is already `position: relative` (VInput's\n `.v-input-end-pinned`, which lifts its chevron and its cross out of the flow) where a bare `.v-input-field` is not, and a\n positioned element paints AFTER every in-flow block whatever the document order. So a\n combobox would paint over a VInput placed BEFORE it, and the joint would show the wrong\n one of the two borders. The symptom is a hairline that changes colour when the template\n is reordered, with nothing in the sheet to explain it. */\n .v-input-group-row > .v-button,\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n position: relative;\n }\n\n /* The pull: each segment rides one pixel onto the one before it, so their two 1px borders\n collapse into a single line.\n\n That line is why this row needs no seam, where VButtonGroup does. VButton's base is\n `border: 1px solid transparent`, so a row of solid buttons has no line to inherit and the\n seam is the only separation possible. A field carries a real opaque border, and melting\n it with its neighbour's PRODUCES the line — a seam here would double it, and in\n VButtonGroup's `--vectis-color-border` it would come out LIGHTER than the row's own\n outline. Under a filled button it would draw a grey hair across the accent, where the\n button's background covering the edge is exactly what an attached search button should\n look like. */\n .v-input-group-row > .v-button:not(:first-child),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):has(\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *))\n ):not(:first-child) {\n margin-inline-start: -1px;\n }\n\n /* Only the two outer corners of the row stay rounded. Logical properties throughout, so a\n right-to-left row rounds its other end with no second rule, and the pull above is\n `margin-inline-start`. */\n .v-input-group-row > .v-button:not(:first-child),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /* TRAP — the last segment reads `:has(~ :not(.v-overlay))` and not `:not(:last-child)`,\n and that is VMenu's doing: it renders its panel as a SIBLING of its trigger, so a menu\n closing a row would leave its trigger without `:last-child` and the row would lose its\n outer corner with nothing in the DOM to say why. Asking whether a real SEGMENT follows\n weighs exactly what the test it replaces weighed. The first segment needs none of this:\n a panel is rendered AFTER its trigger. */\n .v-input-group-row > .v-button:has(~ :not(.v-overlay)),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n /* A field in error rises above its neighbours. Its red border shares a one-pixel column\n with the neighbour's grey one, and the neighbour, painted after it, would repaint it:\n the invalid field would lose that whole edge and read as open on one side.\n\n TRAP — this rule must stay ABOVE the focus rule. A field both invalid and focused\n matches the two, they weigh the same (0,4,0), and nothing but the source order arbitrates\n between them; it is the accent ring the reader has to see. And it is the two VALUES, 1\n and 2, that settle the other case: an invalid field NEXT TO a focused one. */\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-input-field:is(\n :has(.v-input-control:user-invalid),\n :has(.v-input-control[aria-invalid='true'])\n ) {\n z-index: 1;\n }\n\n /* A focused segment rises above everything. The field draws its focus as its own 1px\n border PLUS a 1px shadow just outside the border box, and that shadow falls into the\n neighbour the pull brought over it: without this rule the accent edge disappears from\n one side of every segment but the last.\n\n Two tests, because the two kinds of segment mark focus differently. A field lights up as\n soon as its control has focus, pointer included — VInput's own `:has(.v-input-control:focus)`,\n deliberately not `:focus-within`, so a clickable icon inside it takes its own ring —\n where a button only rises for `:focus-visible`, which is when it draws one.\n\n Hover and active are deliberately absent, as in VButtonGroup: the overlapping pixel IS\n the edge, and a segment rising under the pointer would rub it out for as long as it is\n there. */\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-input-field:has(.v-input-control:focus),\n .v-input-group-row > .v-button:focus-visible,\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *, .v-button-group *)) {\n z-index: 2;\n }\n\n /* VInput's rule, at the same values: through the colour tokens, never through opacity. */\n .v-input-group[data-disabled] :is(.v-input-group-label, .v-input-group-hint) {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmFA,IAAM,IAAQ,GAaR,EAAE,UAAO,cAAW,cAAW,sBAAmB,EAAa,GAC/D,IAAQ,EAAS,GAEjB,IAAU,EAAM,GAChB,IAAS,EAAM,GAUf,IAAa,QAAe;GAChC,IAAM,IAAM,EAAM;GAIlB,OAHI,MAAQ,KAAA,IAGL,EAAM,SAAS,EAAM,kBAAkB,KAAA,IAAY,IAAU,KAAA,IAHtC;EAIhC,CAAC,GAOK,IAAc,QAClB,EAAQ,EAAM,qBAA2C,CAAC,CAAC,EAAM,QAAQ,CAAM,CACjF,GA4BM,IAAW,QACD,EAAY,EAAM,UAAU,CAmBnC,CACR,GAKK,UAAiB,EAAS,OAG1B,IAAa;GACjB,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF;SAEA,EAAQ,GAAe,CAAU,GAMjC,EAAQ,GAAgB,CAAU,mBAIhC,EAwBM,OAxBN,EAwBM;GAvBJ,OAAK,CAAC,iBACE,EAAA,CAAA,CAAS;GAChB,OAAO,EAAA,CAAA;GACR,MAAK;KACG,EAAA,CAAA,GAAc;GACrB,mBAAiB,EAAA;GACjB,oBAAkB,EAAA;GAClB,iBAAe,EAAA,WAAQ,KAAQ,KAAA;;GAKb,EAAA,SAAA,EAAA,GAAnB,EAEc,GAAA;;IAFa,IAAI,EAAA,CAAA;IAAS,IAAG;IAAO,SAAQ;IAAQ,OAAM;;qBAC3D,CAAA,EAAA,EAAR,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;;GAGV,EAEM,OAFN,GAEM,CADJ,EAAY,CAAA,CAAA,CAAA;GAGK,EAAA,QAAA,EAAA,GAAnB,EAEc,GAAA;;IAFY,IAAI,EAAA,CAAA;IAAQ,SAAQ;IAAU,MAAK;IAAQ,OAAM;;qBAC/D,CAAA,EAAA,EAAP,EAAA,IAAI,GAAA,CAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"VInputGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VInput/VInputGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Joins two or more form controls into a single object: a country code against a phone\n * number, a search field against its magnifier. The shared borders melt into one line and\n * only the two ends of the row stay rounded.\n *\n * It does for fields what VButtonGroup does for buttons, with one structural difference that\n * shapes the whole sheet. For a button, the component's root IS the bordered box; for a\n * field it never is — `.v-input` is a column holding a label, the field and a hint, and the\n * bordered box is `.v-input-field`, a descendant. So the PULL that melts two borders lands\n * on the segment the row lays out, and the PAINT lands on the box that carries them.\n *\n * The row owns the label and the hint, and that is the reason this component exists rather\n * than a CSS recipe: a label above one segment alone pushes its box a line out of the row.\n * The group renders one for all of them and names itself with it, each segment taking an\n * `aria-label` instead — the ARIA pattern for a composed field.\n *\n * `role=\"group\"` on a div, not `<fieldset>`/`<legend>`, and not only for consistency with\n * VButtonGroup and VToggle. The accessibility tree is identical either way: browsers map\n * `<fieldset>` to `role=\"group\"` and `<legend>` to its accessible name. The layout is not. A\n * `<fieldset>` carries `min-inline-size: min-content` in the UA sheet, so it refuses to\n * shrink below its content, which is exactly the right segments in `flex: 1 1 0` are asking\n * for; and a rendered `<legend>` is pulled out of flow and painted into the border area.\n * The native primitive is the right answer when it buys something, and here it costs two\n * resets to impose the invisible UA behaviour that rule is meant to spare us.\n *\n * Horizontal only, and probably for good. A column of fields is a form, and the fields of a\n * form carry a label each: melting their borders would remove the very separation a form\n * needs. VButtonGroup has a vertical because a column of buttons is a toolbar.\n *\n * The behavioural JS is one development warning. The row cannot draw itself out of a segment\n * that brought its own label, and the misalignment is silent, so it says so.\n */\nimport { computed, provide, useId } from 'vue'\n\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { buttonGroupKey } from '../VButton/context'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useSlotNodes } from '../../composables/useSlotNodes'\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\n\nimport { inputGroupKey } from './context'\n\n/** The heights a joined row offers, matching the field scale: 32, 40 or 48 pixels. */\nexport type InputGroupSize = 'sm' | 'md' | 'lg'\n\ninterface InputGroupProps {\n /**\n * The label above the row, rendered once for all of its segments and used as the\n * group's accessible name. A segment carrying one of its own is pushed out of line,\n * so name each of them with `aria-label` instead.\n */\n label?: string\n /**\n * A line of help under the row, tied to the group so assistive technology reads it\n * out along with the label.\n */\n hint?: string\n /**\n * The height every segment takes, whatever it names for itself: a row of controls of\n * two heights stops reading as one object. Left out, each segment keeps its own.\n */\n size?: InputGroupSize\n /** Takes 4px off the height of every segment, the way `compact` does on a lone field. */\n compact?: boolean\n /**\n * Makes the whole row unusable. It adds to what each segment says rather than\n * replacing it: a segment disabled on its own stays disabled under a row that says\n * nothing.\n */\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\n// TRAP — every one of the three below defaults to `undefined`, the booleans included. It is\n// `undefined`, and not `false`, that means \"the group has no opinion\", and that is what makes\n// a bare VInputGroup change nothing about what it contains. Writing `false` here would force\n// every segment out of compact and out of disabled, whatever it had asked for itself.\nconst props = withDefaults(defineProps<InputGroupProps>(), {\n label: undefined,\n hint: undefined,\n size: undefined,\n compact: undefined,\n disabled: undefined,\n})\n\ndefineSlots<{\n /** The fields and buttons to join. Each one is a segment of the row. */\n default(): unknown\n}>()\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\nconst labelId = useId()\nconst hintId = useId()\n\n// @a11y — the canonical cascade, resolved HERE rather than left to fallthrough: the binding\n// in the template comes after `v-bind=\"forwardedAttrs\"` and would otherwise overwrite what\n// the consumer wrote, so this computed has to hand their value back itself.\n//\n// An `aria-label` or an `aria-labelledby` written by the consumer names the group, and the\n// VISIBLE label must not double it: `aria-labelledby` wins over `aria-label` in the\n// accessible name computation, so emitting ours on top of a consumer's `aria-label` would\n// silently cancel it.\nconst labelledBy = computed(() => {\n const own = attrs['aria-labelledby'] as string | undefined\n if (own !== undefined) return own\n // TRUTHINESS, the test the template renders the label with: an empty `label=\"\"` renders\n // no element, and naming the group after an id nothing carries leaves it nameless.\n return props.label && attrs['aria-label'] === undefined ? labelId : undefined\n})\n\n// @a11y — `aria-describedby` is a LIST, so the hint is ADDED to whatever the consumer\n// pointed at rather than replacing it (the `useFieldIds` idiom). This aggregation is also\n// why the component takes `inheritAttrs: false` despite having a single root: left to\n// fallthrough, a consumer's `aria-describedby` would overwrite ours and the hint would stop\n// being announced, with nothing to show for it.\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The components whose `label` prop renders a real `<label>` above the field. VIconButton is\n * deliberately absent: its `label` is an accessible name for a control with no text, so\n * warning about it would be wrong.\n */\nconst LABELLED_FIELDS = new Set([\n 'VInput',\n 'VTextarea',\n 'VCombobox',\n 'VDateInput',\n 'VTimeInput',\n 'VFileInput',\n])\n\n// A dev warning fires from a computed that the render consumes, so a slot whose content\n// depends on a model would repeat it on every keystroke. One line per offender is enough to\n// be found and fixed.\nconst warned = new Set<string>()\n\n// @devwarn @ssr — the segments are read from the slot's VNODES, never from a registry the\n// children would fill at mount: a registry is empty on the server and full in the browser.\n//\n// TRAP — the read has to stay inside a computed, and the computed has to be consumed by\n// the render, which is what `<Segments />` does. Called from a `watchEffect`, which runs\n// BEFORE the render, Vue warns \"Slot invoked outside of the render function\" on any slot\n// passed as a raw function — which is exactly what a unit test passes.\nconst slotNodes = useSlotNodes()\nconst segments = computed(() => {\n const nodes = slotNodes.value\n\n if (isDev) {\n for (const node of nodes) {\n const type = node.type as { name?: string; __name?: string } | string\n const name =\n typeof type === 'object' && type !== null ? (type.name ?? type.__name) : undefined\n if (name === undefined || !LABELLED_FIELDS.has(name)) continue\n\n for (const prop of ['label', 'hint'] as const) {\n if (node.props?.[prop] === undefined || warned.has(`${name}.${prop}`)) continue\n warned.add(`${name}.${prop}`)\n console.warn(\n `[VInputGroup] <${name}> carries its own \\`${prop}\\`. The group renders one for the whole row, and a segment carrying one of its own is pushed a line out of the row. Move it to VInputGroup and name the segment with \\`aria-label\\`.`,\n )\n }\n }\n }\n\n return nodes\n})\n\n// A functional component is the only way to render VNodes that have already been captured:\n// `<component :is>` expects a component definition rather than a vnode. Returning an array\n// produces a Fragment, so no DOM element is inserted and the sheet's `>` combinator holds.\nconst Segments = () => segments.value\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nconst rowContext = {\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get disabled() {\n return props.disabled\n },\n}\n\nprovide(inputGroupKey, rowContext)\n\n// The group provides VButtonGroup's context TOO, which is what makes a VButton or a\n// VIconButton segment follow the row's height without a new line in VButton: the arbitration\n// there is already `group?.x ?? props.x`, exactly this one. Variant, tone and elevation stay\n// `undefined` — in a row of fields those are the button's own decisions.\nprovide(buttonGroupKey, rowContext)\n</script>\n\n<template>\n <div\n class=\"v-input-group\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"group\"\n v-bind=\"forwardedAttrs\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :data-disabled=\"disabled ? '' : undefined\"\n >\n <!-- A span and not a `<label for>`: a label points at ONE control, and this names a row\n of them. The group takes it through aria-labelledby, the ARIA pattern for a\n composed field. -->\n <VTypography v-if=\"label\" :id=\"labelId\" as=\"span\" variant=\"label\" class=\"v-input-group-label\">\n {{ label }}\n </VTypography>\n\n <div class=\"v-input-group-row\">\n <Segments />\n </div>\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-group-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A SEGMENT is a direct child of the row, and it is almost never the box carrying the\n * border. For a button the two coincide; for a field they never do — VInput's bordered box\n * is `.v-input-field`, one level under its root, two or three under those of VCombobox,\n * VDateInput, VTimeInput and VFileInput. Companions add more: VTooltip renders `.v-tooltip`\n * around its trigger, VPopover `.v-popover`, VBadge `.v-badge-host`, and they stack.\n *\n * So every rule is written for BOTH forms:\n *\n * > .v-button<tests> the button IS the segment\n * > WRAPPER<tests> BOX the box is somewhere inside it\n *\n * WRAPPER = :not(:where(.v-overlay, .v-button-group))\n * BOX = :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *))\n *\n * The PULL — the negative margin that melts two borders into one line — belongs to the\n * SEGMENT, the box the row lays out. The PAINT — the corners, the stacking order —\n * belongs to the BOX, which carries them. Swapped, a field would be pulled by its field\n * box while its root stayed put, and the row would show a one-pixel gap.\n *\n * The tests weigh the same on both sides, so a pair is always ONE specificity.\n *\n * TRAP — `.v-overlay` is in both guards and neither is optional. A panel must not pass for\n * a segment: VMenu renders its own as a SIBLING of its trigger. And a panel's CONTENT must\n * never be painted: an open VDatePicker carries buttons, a combobox list carries options,\n * and they would all take a segment's flattened corners.\n *\n * TRAP — `.v-button-group` is in both guards, and the case easy to miss is VTimeInput: in\n * 12 hour form it renders an AM/PM VToggle beside its field, and a VToggle IS a\n * `.v-button-group`. Without `.v-button-group *` in BOX, those two buttons would be\n * painted as segments of THIS row, on top of the row VToggle draws for itself.\n *\n * No doubled class is needed anywhere here, which is worth stating so nobody adds one \"for\n * safety\". Our corners weigh (0,3,0) against the (0,1,0) that `.v-input-field` and\n * `.v-button` put on themselves; `position: relative` matches the (0,2,0) of\n * `.v-input-end-pinned > .v-input-field` at the same value; nothing else sets a `z-index` on these\n * elements. VButtonGroup's own (0,4,0) corners are unreachable — a button cannot be both a\n * direct child of `.v-input-group-row` and inside a `.v-button-group`, the WRAPPER guard\n * excludes it.\n */\n\n .v-input-group {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The label and the hint are rendered by VTypography, which carries their type. The\n classes stay as hooks: a consumer overrides through them, and the disabled state\n below reaches them that way. */\n\n /* The row carries NEITHER `v-control` NOR `data-size`: nothing in this sheet reads a\n `--control-*` variable, and `.v-control` set a second time above a subtree that already\n has it re-derives `--control-height` from the base height, dropping the compact its\n fields had applied (styles/control-size.css). */\n .v-input-group-row {\n display: flex;\n /* Same height whatever a segment measures. This changes nothing in an ordinary row,\n where everything is --control-height, and it is what stops a segment that GROWS from\n leaving its neighbours hanging at the top. */\n align-items: stretch;\n }\n\n /* A segment holding a field takes its share of the row and accepts shrinking below its own\n content: the intrinsic width of a field is that of its placeholder, which would push a\n row of two fields out of its parent. Everything else keeps its natural width, a button\n being measured by its label.\n\n The logic is written this way round because it is the one that enumerates nothing: a\n bare VButton, or one under two companions, has no `.v-input-field`, so it never stretches\n without our having to name it. The proportions belong to the consumer, an inline `flex`\n on the segment (style=\"flex: 0 0 9rem\").\n\n A panel is not a segment and takes no share. Closed it is `display: none`, open it is\n fixed, so it is not a flex item either way — the guard is there so the selector says\n what it does. */\n .v-input-group-row > :not(:where(.v-overlay)) {\n flex: none;\n min-inline-size: 0;\n }\n\n /* The field has to be the segment's OWN, never one inside a panel it opens: a VPopover\n segment holding a VInput in its panel is a button, and stays its natural width. */\n .v-input-group-row > :not(:where(.v-overlay)):has(.v-input-field:not(:where(.v-overlay *))) {\n flex: 1 1 0;\n }\n\n /* Every box is positioned, so they all paint in the SAME phase and in document order.\n\n TRAP — without this rule the paint order would depend on which components the row\n happens to hold. A combobox's field is already `position: relative` (VInput's\n `.v-input-end-pinned`, which lifts its chevron and its cross out of the flow) where a bare `.v-input-field` is not, and a\n positioned element paints AFTER every in-flow block whatever the document order. So a\n combobox would paint over a VInput placed BEFORE it, and the joint would show the wrong\n one of the two borders. The symptom is a hairline that changes colour when the template\n is reordered, with nothing in the sheet to explain it. */\n .v-input-group-row > .v-button,\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n position: relative;\n }\n\n /* The pull: each segment rides one pixel onto the one before it, so their two 1px borders\n collapse into a single line.\n\n That line is why this row needs no seam, where VButtonGroup does. VButton's base is\n `border: 1px solid transparent`, so a row of solid buttons has no line to inherit and the\n seam is the only separation possible. A field carries a real opaque border, and melting\n it with its neighbour's PRODUCES the line — a seam here would double it, and in\n VButtonGroup's `--vectis-color-border` it would come out LIGHTER than the row's own\n outline. Under a filled button it would draw a grey hair across the accent, where the\n button's background covering the edge is exactly what an attached search button should\n look like. */\n .v-input-group-row > .v-button:not(:first-child),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):has(\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *))\n ):not(:first-child) {\n margin-inline-start: -1px;\n }\n\n /* Only the two outer corners of the row stay rounded. Logical properties throughout, so a\n right-to-left row rounds its other end with no second rule, and the pull above is\n `margin-inline-start`. */\n .v-input-group-row > .v-button:not(:first-child),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /* TRAP — the last segment reads `:has(~ :not(.v-overlay))` and not `:not(:last-child)`,\n and that is VMenu's doing: it renders its panel as a SIBLING of its trigger, so a menu\n closing a row would leave its trigger without `:last-child` and the row would lose its\n outer corner with nothing in the DOM to say why. Asking whether a real SEGMENT follows\n weighs exactly what the test it replaces weighed. The first segment needs none of this:\n a panel is rendered AFTER its trigger. */\n .v-input-group-row > .v-button:has(~ :not(.v-overlay)),\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n :is(.v-input-field, .v-button):not(:where(.v-overlay *, .v-button-group *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n /* A field in error rises above its neighbours. Its red border shares a one-pixel column\n with the neighbour's grey one, and the neighbour, painted after it, would repaint it:\n the invalid field would lose that whole edge and read as open on one side.\n\n TRAP — this rule must stay ABOVE the focus rule. A field both invalid and focused\n matches the two, they weigh the same (0,4,0), and nothing but the source order arbitrates\n between them; it is the accent ring the reader has to see. And it is the two VALUES, 1\n and 2, that settle the other case: an invalid field NEXT TO a focused one. */\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-input-field:is(\n :has(.v-input-control:user-invalid),\n :has(.v-input-control[aria-invalid='true'])\n ) {\n z-index: 1;\n }\n\n /* A focused segment rises above everything. The field draws its focus as its own 1px\n border PLUS a 1px shadow just outside the border box, and that shadow falls into the\n neighbour the pull brought over it: without this rule the accent edge disappears from\n one side of every segment but the last.\n\n Two tests, because the two kinds of segment mark focus differently. A field lights up as\n soon as its control has focus, pointer included — VInput's own `:has(.v-input-control:focus)`,\n deliberately not `:focus-within`, so a clickable icon inside it takes its own ring —\n where a button only rises for `:focus-visible`, which is when it draws one.\n\n Hover and active are deliberately absent, as in VButtonGroup: the overlapping pixel IS\n the edge, and a segment rising under the pointer would rub it out for as long as it is\n there. */\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-input-field:has(.v-input-control:focus),\n .v-input-group-row > .v-button:focus-visible,\n .v-input-group-row\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *, .v-button-group *)) {\n z-index: 2;\n }\n\n /* VInput's rule, at the same values: through the colour tokens, never through opacity. */\n .v-input-group[data-disabled] :is(.v-input-group-label, .v-input-group-hint) {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmFA,IAAM,IAAQ,GAaR,EAAE,UAAO,cAAW,cAAW,sBAAmB,EAAa,GAE/D,IAAU,EAAM,GAChB,IAAS,EAAM,GAUf,IAAa,QAAe;GAChC,IAAM,IAAM,EAAM;GAIlB,OAHI,MAAQ,KAAA,IAGL,EAAM,SAAS,EAAM,kBAAkB,KAAA,IAAY,IAAU,KAAA,IAHtC;EAIhC,CAAC,GAOK,IAAc,QAClB,EAAQ,EAAM,qBAA2C,CAAC,CAAC,EAAM,QAAQ,CAAM,CACjF,GAOM,oBAAkB,IAAI,IAAI;GAC9B;GACA;GACA;GACA;GACA;GACA;EACF,CAAC,GAKK,oBAAS,IAAI,IAAY,GASzB,IAAY,EAAa,GACzB,IAAW,QAAe;GAC9B,IAAM,IAAQ,EAAU;GAExB,IAAI,GACF,KAAK,IAAM,KAAQ,GAAO;IACxB,IAAM,IAAO,EAAK,MACZ,IACJ,OAAO,KAAS,YAAY,IAAiB,EAAK,QAAQ,EAAK,SAAU,KAAA;IACvE,YAAS,KAAA,KAAa,CAAC,EAAgB,IAAI,CAAI,IAEnD,KAAK,IAAM,KAAQ,CAAC,SAAS,MAAM,GAC7B,EAAK,QAAQ,OAAU,KAAA,KAAa,EAAO,IAAI,GAAG,EAAK,GAAG,GAAM,MACpE,EAAO,IAAI,GAAG,EAAK,GAAG,GAAM,GAC5B,QAAQ,KACN,kBAAkB,EAAK,sBAAsB,EAAK,qLACpD;GAEJ;GAGF,OAAO;EACT,CAAC,GAKK,UAAiB,EAAS,OAG1B,IAAa;GACjB,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF;SAEA,EAAQ,GAAe,CAAU,GAMjC,EAAQ,GAAgB,CAAU,mBAIhC,EAwBM,OAxBN,EAwBM;GAvBJ,OAAK,CAAC,iBACE,EAAA,CAAA,CAAS;GAChB,OAAO,EAAA,CAAA;GACR,MAAK;KACG,EAAA,CAAA,GAAc;GACrB,mBAAiB,EAAA;GACjB,oBAAkB,EAAA;GAClB,iBAAe,EAAA,WAAQ,KAAQ,KAAA;;GAKb,EAAA,SAAA,EAAA,GAAnB,EAEc,GAAA;;IAFa,IAAI,EAAA,CAAA;IAAS,IAAG;IAAO,SAAQ;IAAQ,OAAM;;qBAC3D,CAAA,EAAA,EAAR,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;;GAGV,EAEM,OAFN,GAEM,CADJ,EAAY,CAAA,CAAA,CAAA;GAGK,EAAA,QAAA,EAAA,GAAnB,EAEc,GAAA;;IAFY,IAAI,EAAA,CAAA;IAAQ,SAAQ;IAAU,MAAK;IAAQ,OAAM;;qBAC/D,CAAA,EAAA,EAAP,EAAA,IAAI,GAAA,CAAA,CAAA,CAAA"}
@@ -1,2 +1,2 @@
1
1
  @layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
2
- @layer vectis.components{.v-input-otp{--input-otp-font-size:var(--vectis-font-size-lg);align-items:start;gap:var(--vectis-space-1);flex-direction:column;display:inline-flex}.v-input-otp-boxes{align-items:center;gap:var(--control-gap);display:flex}.v-input-otp-input{width:var(--control-height);height:var(--control-height);text-align:center;background:var(--vectis-color-surface);color:var(--vectis-color-text);border:1px solid var(--vectis-color-border-strong);border-radius:var(--vectis-radius-interactive);font-family:var(--vectis-text-family-code);font-size:var(--input-otp-font-size);transition:border-color var(--vectis-duration-fast) var(--vectis-ease-default)}.v-input-otp-input:focus-visible{border-color:var(--vectis-color-accent);box-shadow:0 0 0 1px var(--vectis-color-accent);outline:var(--vectis-focus-ring-width) solid transparent}.v-input-otp[data-readonly] .v-input-otp-input{background:var(--vectis-color-surface-sunken)}.v-input-otp[data-invalid] .v-input-otp-input{border-color:var(--vectis-color-danger)}.v-input-otp[data-invalid] .v-input-otp-input:focus-visible{box-shadow:0 0 0 1px var(--vectis-color-danger)}.v-input-otp-literal{color:var(--vectis-color-text-muted);font-family:var(--vectis-text-family-code);font-size:var(--input-otp-font-size);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;display:inline-flex}.v-input-otp[data-disabled] .v-input-otp-input{background:var(--vectis-color-surface-muted);color:var(--vectis-color-text-subtle);border-color:var(--vectis-color-border);cursor:not-allowed}.v-input-otp[data-disabled] .v-input-otp-literal,.v-input-otp[data-disabled] .v-input-otp-hint{color:var(--vectis-color-text-subtle)}.v-input-otp[data-size=sm]{--input-otp-font-size:var(--vectis-font-size-md)}.v-input-otp[data-size=lg]{--input-otp-font-size:var(--vectis-font-size-xl)}@media (prefers-reduced-motion:reduce){.v-input-otp-input{transition:none}}}
2
+ @layer vectis.components{.v-input-otp{--input-otp-font-size:var(--vectis-font-size-lg);align-items:start;gap:var(--vectis-space-1);flex-direction:column;display:inline-flex}.v-input-otp-boxes{align-items:center;gap:var(--control-gap);direction:ltr;display:flex}.v-input-otp-input{width:var(--control-height);height:var(--control-height);text-align:center;background:var(--vectis-color-surface);color:var(--vectis-color-text);border:1px solid var(--vectis-color-border-strong);border-radius:var(--vectis-radius-interactive);font-family:var(--vectis-text-family-code);font-size:var(--input-otp-font-size);transition:border-color var(--vectis-duration-fast) var(--vectis-ease-default), box-shadow var(--vectis-duration-fast) var(--vectis-ease-default), background-color var(--vectis-duration-fast) var(--vectis-ease-default)}.v-input-otp[data-readonly] .v-input-otp-input{background:var(--vectis-color-surface-sunken);border-color:var(--vectis-color-border)}.v-input-otp:not([data-invalid]) .v-input-otp-input:hover:not(:focus,:disabled){border-color:color-mix(in oklab, var(--vectis-color-border-strong), var(--vectis-color-text) 15%)}.v-input-otp-input:focus{border-color:var(--vectis-color-accent);box-shadow:0 0 0 1px var(--vectis-color-accent);outline:var(--vectis-focus-ring-width) solid transparent}.v-input-otp[data-invalid] .v-input-otp-input{border-color:var(--vectis-color-danger)}.v-input-otp[data-invalid] .v-input-otp-input:focus{box-shadow:0 0 0 1px var(--vectis-color-danger)}.v-input-otp-literal{color:var(--vectis-color-text-muted);font-family:var(--vectis-text-family-code);font-size:var(--input-otp-font-size);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;display:inline-flex}.v-input-otp[data-disabled] .v-input-otp-input{background:var(--vectis-color-surface-muted);color:var(--vectis-color-text-muted);border-color:var(--vectis-color-border);cursor:not-allowed}.v-input-otp[data-disabled] .v-input-otp-literal,.v-input-otp[data-disabled] .v-input-otp-hint{color:var(--vectis-color-text-subtle)}.v-input-otp[data-size=sm]{--input-otp-font-size:var(--vectis-font-size-md)}.v-input-otp[data-size=lg]{--input-otp-font-size:var(--vectis-font-size-xl)}@media (prefers-reduced-motion:reduce){.v-input-otp-input{transition:none}}}
@@ -13,8 +13,8 @@ interface InputOTPProps {
13
13
  format?: InputOTPFormat;
14
14
  /**
15
15
  * The shape of the code: each `#` is a box to fill, and every other character is a
16
- * separator shown between the boxes without ever being part of the value —
17
- * `'GT-###'`, `'###.###.###'`. It wins over `length`.
16
+ * separator shown between the boxes without ever being part of the value, as in
17
+ * `'GT-###'` or `'###.###.###'`. It wins over `length`.
18
18
  */
19
19
  pattern?: string;
20
20
  /**
@@ -46,7 +46,7 @@ interface InputOTPProps {
46
46
  */
47
47
  label?: string;
48
48
  /**
49
- * A line of help under the boxes — where the code was sent, how long it lasts. It is
49
+ * A line of help under the boxes: where the code was sent, how long it lasts. It is
50
50
  * tied to the row for assistive technology, so it is read out along with the label.
51
51
  */
52
52
  hint?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"VInputOTP.js","names":[],"sources":["../../../src/components/VInputOTP/VInputOTP.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * The row of boxes a one-time code is typed into, one character per box.\n *\n * The platform has no such control, so each box is a real `<input>` and the JS is what makes\n * the row behave as one field: a character moves to the next box, Backspace on an empty one\n * goes back, the arrows walk along, and a code pasted anywhere is spread across them. The\n * first box carries `autocomplete=\"one-time-code\"`, so a code arriving from an SMS or a\n * password manager is spread the same way.\n *\n * `pattern` cuts the row up: every `#` is a box, every other character a decorative literal\n * shown between them and never part of the value. Pasting understands those literals — a\n * code copied formatted, `GT-123`, is consumed with them in place — and `format` filters the\n * rest, forcing capitals outside a numeric code so the value has one canonical form.\n */\n\nimport { computed, ref, useAttrs, useId, watch } from 'vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of the boxes: 32, 40 or 48 pixels. */\nexport type InputOTPSize = 'sm' | 'md' | 'lg'\n\n/** Which characters the code is made of. */\nexport type InputOTPFormat = 'numeric' | 'alpha' | 'alphanumeric'\n\ninterface InputOTPProps {\n /** How many boxes the code has. It is ignored as soon as a `pattern` is given. */\n length?: number\n /**\n * Which characters the code is made of. It filters what can be typed or pasted, and\n * decides which keyboard a phone offers.\n */\n format?: InputOTPFormat\n /**\n * The shape of the code: each `#` is a box to fill, and every other character is a\n * separator shown between the boxes without ever being part of the value —\n * `'GT-###'`, `'###.###.###'`. It wins over `length`.\n */\n pattern?: string\n /**\n * An icon drawn in place of EVERY separator of the pattern. It suits a template\n * whose separators are purely decorative, `'###-###'`, and should not be used with\n * one carrying meaningful text such as `'GT-###'`, which the icon would erase.\n */\n separatorIcon?: IconSource\n /** The size of the boxes: 32, 40 or 48 pixels. */\n size?: InputOTPSize\n /** Takes 4px off the boxes, leaving the text and the icons as they are. */\n compact?: boolean\n /** Makes every box unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the code without letting it be changed. The boxes keep their focus and the code\n * can still be selected and copied, which is what separates it from `disabled`.\n */\n readonly?: boolean\n /** Marks the code as wrong, which colours the boxes and tells assistive technology so. */\n invalid?: boolean\n /**\n * What screen readers announce for the row as a whole. It falls back to the design\n * system dictionary.\n *\n * Unlike the `label` of VInput and the other text fields, NOTHING is rendered from it:\n * a row of boxes carries its own instructions above it, written by the page. Use `hint`\n * for a line the reader can see.\n */\n label?: string\n /**\n * A line of help under the boxes — where the code was sent, how long it lasts. It is\n * tied to the row for assistive technology, so it is read out along with the label.\n */\n hint?: string\n}\n\nconst props = withDefaults(defineProps<InputOTPProps>(), {\n length: 6,\n format: 'numeric',\n pattern: undefined,\n separatorIcon: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.inputOTP.label)\n\n// @a11y\n/*\n * The root IS the group, so the consumer's attributes belong on it — but they are bound\n * BEFORE the two the component decides itself. `aria-describedby` is a list, and the hint\n * is appended to whatever the consumer already pointed at instead of replacing it; left\n * to the fallthrough, their value would land last and take the hint out of the\n * announcement with nothing to show for it.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\n// No `useFieldIds` here: the row is named by `aria-label` and renders no `<label>`, so the\n// field id that composable generates would have nothing to point it at.\nconst hintId = useId()\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The code as one string, without the separators: a `GT-###` template still yields three\n * characters. It is empty to begin with, and shorter than the full length while it is being\n * typed.\n */\nconst model = defineModel<string>({ default: '' })\n\nconst emit = defineEmits<{\n /** Emitted the moment every box is filled, carrying the complete code. */\n complete: [code: string]\n}>()\n\ntype Cell = { type: 'slot'; slotIndex: number } | { type: 'literal'; char: string }\n\nconst cells = computed<Cell[]>(() => {\n if (props.pattern?.includes('#')) {\n let slotIndex = 0\n return [...props.pattern].map((char): Cell =>\n char === '#' ? { type: 'slot', slotIndex: slotIndex++ } : { type: 'literal', char },\n )\n }\n return Array.from({ length: props.length }, (_, i) => ({ type: 'slot', slotIndex: i }))\n})\nconst slotCount = computed(() => cells.value.filter((cell) => cell.type === 'slot').length)\n\n// @devwarn\nif (isDev) {\n if (props.pattern && !props.pattern.includes('#'))\n console.warn(\"[VInputOTP] pattern without '#' — falling back to `length`.\")\n}\n\nconst filters: Record<NonNullable<InputOTPProps['format']>, RegExp> = {\n numeric: /[^0-9]/g,\n alpha: /[^A-Z]/g,\n alphanumeric: /[^A-Z0-9]/g,\n}\n\n/**\n * Keeps only what the format allows. Outside a numeric code the text is put in\n * capitals first, so that the value has a single canonical form whichever case the\n * reader typed or pasted.\n */\nfunction sanitize(text: string) {\n const upper = props.format === 'numeric' ? text : text.toUpperCase()\n return upper.replace(filters[props.format], '')\n}\n\nconst inputs = ref<(HTMLInputElement | null)[]>([])\nconst rootEl = ref<HTMLElement | null>(null)\nconst digits = ref<string[]>([])\n\nfunction syncFromModel(value: string) {\n // A value longer than the row is simply shown cut short, and the model is NOT\n // rewritten to match: writing back here would feed the watcher below and the two\n // would keep correcting each other.\n digits.value = Array.from({ length: slotCount.value }, (_, i) => value[i] ?? '')\n}\nsyncFromModel(model.value)\nwatch([model, slotCount], ([value, count]) => {\n if (value !== digits.value.join('') || digits.value.length !== count) syncFromModel(value)\n})\n\n/*\n * TRAP — the list of boxes is trimmed AFTER the render, never before it. A box that\n * disappears calls its function ref with `null` while it is being unmounted, at its OLD\n * position: trimmed in a `pre` watcher, that late assignment would stretch the array back\n * out, and a pattern that shortened would keep its departed boxes as trailing entries.\n */\nwatch(\n slotCount,\n (count) => {\n inputs.value.length = count\n },\n { flush: 'post' },\n)\n\nfunction commit() {\n const code = digits.value.join('')\n model.value = code\n if (code.length === slotCount.value) emit('complete', code)\n}\n\n// @core\n/**\n * Spreads a run of characters across the boxes, starting from the one that received\n * them. It walks the row: a separator swallows the incoming character when the two\n * match, which is what lets a code pasted in its formatted shape line up, and a box\n * takes the next character the format accepts.\n *\n * It returns the last box it filled, so the caller knows where to put the focus, and\n * `null` when nothing in the text was usable.\n */\nfunction distribute(raw: string, startSlot: number): number | null {\n const allCells = cells.value\n let start = allCells.findIndex((cell) => cell.type === 'slot' && cell.slotIndex === startSlot)\n // Step back over the separators immediately before that box: pasting the whole\n // string onto the first box means pasting its prefix too, and \"GT-\" has to be\n // matched rather than treated as characters of the code.\n while (start > 0 && allCells[start - 1]?.type === 'literal') start--\n const chars = [...raw]\n let charIndex = 0\n let lastFilled: number | null = null\n for (const cell of allCells.slice(start)) {\n if (charIndex >= chars.length) break\n if (cell.type === 'literal') {\n if (chars[charIndex]?.toUpperCase() === cell.char.toUpperCase()) charIndex++\n continue\n }\n let char = ''\n while (charIndex < chars.length && !char) {\n char = sanitize(chars[charIndex] ?? '')\n charIndex++\n }\n if (!char) break\n digits.value[cell.slotIndex] = char\n lastFilled = cell.slotIndex\n }\n return lastFilled\n}\n\n// @keyboard @core — moving the focus forward on its own is what turns a row of\n// separate inputs into something that types like one field; spreading the characters\n// is the core behaviour underneath it.\nfunction onInput(slotIndex: number, event: Event) {\n const el = event.target as HTMLInputElement\n // The same path serves a single keystroke and a pasted code: both are spread from\n // this box onwards.\n const lastFilled = distribute(el.value, slotIndex)\n if (lastFilled === null) {\n // Either the box was emptied, or nothing typed was valid for this format.\n digits.value[slotIndex] = ''\n el.value = ''\n commit()\n return\n }\n el.value = digits.value[slotIndex] ?? ''\n inputs.value[Math.min(lastFilled + 1, slotCount.value - 1)]?.focus()\n commit()\n}\n\n// @keyboard\nfunction onKeydown(slotIndex: number, event: KeyboardEvent) {\n if (event.key === 'Backspace' && !digits.value[slotIndex] && slotIndex > 0) {\n event.preventDefault()\n digits.value[slotIndex - 1] = ''\n commit()\n inputs.value[slotIndex - 1]?.focus()\n } else if (event.key === 'ArrowLeft' && slotIndex > 0) {\n event.preventDefault()\n inputs.value[slotIndex - 1]?.focus()\n } else if (event.key === 'ArrowRight' && slotIndex < slotCount.value - 1) {\n event.preventDefault()\n inputs.value[slotIndex + 1]?.focus()\n }\n}\n/*\n * The same trio every other field of the size scale exposes. `focus` goes to the FIRST\n * EMPTY box rather than to the first box outright, which is where a reader resuming a\n * half-entered code expects to land.\n */\ndefineExpose({\n /** Moves the focus to the first empty box, or to the last one when the code is complete. */\n focus: (options?: FocusOptions) => {\n const empty = digits.value.findIndex((d) => !d)\n const target = empty === -1 ? slotCount.value - 1 : empty\n inputs.value[target]?.focus(options)\n },\n /** Selects the box the focus is on, as clicking into one already does. */\n select: () => {\n const active = inputs.value.find((el) => el === document.activeElement)\n ;(active ?? inputs.value[0])?.select()\n },\n /** The row itself, for what neither of the two above covers. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-bind=\"attrs\"\n class=\"v-input-otp v-control\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :data-invalid=\"invalid ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <div class=\"v-input-otp-boxes\">\n <template v-for=\"(cell, i) in cells\" :key=\"i\">\n <input\n v-if=\"cell.type === 'slot'\"\n :ref=\"\n (el) => {\n inputs[cell.slotIndex] = el as HTMLInputElement | null\n }\n \"\n type=\"text\"\n class=\"v-input-otp-input\"\n :inputmode=\"format === 'numeric' ? 'numeric' : 'text'\"\n :autocomplete=\"cell.slotIndex === 0 ? 'one-time-code' : 'off'\"\n :value=\"digits[cell.slotIndex]\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-label=\"m.inputOTP.slot(cell.slotIndex + 1, slotCount)\"\n :aria-invalid=\"invalid || undefined\"\n @input=\"onInput(cell.slotIndex, $event)\"\n @keydown=\"onKeydown(cell.slotIndex, $event)\"\n @focus=\"($event.target as HTMLInputElement).select()\"\n />\n <!-- A separator from the pattern: shown, never focusable, and never part of the\n value. It is hidden from screen readers, each box already announcing its\n own position in the code. -->\n <span v-else class=\"v-input-otp-literal\" aria-hidden=\"true\">\n <VIcon v-if=\"separatorIcon\" v-bind=\"iconProps(separatorIcon)\" />\n <template v-else>{{ cell.char }}</template>\n </span>\n </template>\n </div>\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-otp-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-input-otp {\n /*\n * The heights and the icon context come from the shared v-control class\n * (styles/control-size.css). The type is the one thing kept local, and set one or\n * two steps above the other fields: a single character has a whole square to\n * itself, and at the usual field size it would look lost in it.\n */\n --input-otp-font-size: var(--vectis-font-size-lg);\n\n /* A column, so the hint sits under the boxes rather than beside them; the row itself\n is the box below. `inline-flex` keeps the component's own width, and the start\n alignment stops the hint from stretching the row to its own length. */\n display: inline-flex;\n flex-direction: column;\n align-items: start;\n gap: var(--vectis-space-1);\n }\n\n .v-input-otp-boxes {\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-input-otp-input {\n /* The boxes are square: reading the same variable for both dimensions is what\n makes the size and the density scale them together. */\n width: var(--control-height);\n height: var(--control-height);\n text-align: center;\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--vectis-color-border-strong);\n border-radius: var(--vectis-radius-interactive);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n transition: border-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n /* The focused box appears to have a two-pixel border, exactly as in VInput and\n VTextarea: its own 1px border plus a 1px shadow of the same colour just outside\n it. The transparent outline is the safety net for Windows forced colours, which\n drop box-shadows entirely. */\n .v-input-otp-input:focus-visible {\n border-color: var(--vectis-color-accent);\n box-shadow: 0 0 0 1px var(--vectis-color-accent);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n /* A read-only row takes the sunken background of a read-only VInput, and the state is\n read from [data-readonly] rather than from `:read-only`, which the browser also matches\n on a disabled box. It is declared BEFORE the invalid and disabled blocks, all three\n weighing (0,2,0), so those still win over it. */\n .v-input-otp[data-readonly] .v-input-otp-input {\n background: var(--vectis-color-surface-sunken);\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input {\n border-color: var(--vectis-color-danger);\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input:focus-visible {\n box-shadow: 0 0 0 1px var(--vectis-color-danger);\n }\n\n .v-input-otp-literal {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--vectis-color-text-muted);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n user-select: none;\n }\n\n /* A disabled row greys out through the colour tokens and never through opacity, the\n same treatment as VInput. */\n .v-input-otp[data-disabled] .v-input-otp-input {\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-subtle);\n border-color: var(--vectis-color-border);\n cursor: not-allowed;\n }\n\n .v-input-otp[data-disabled] .v-input-otp-literal,\n .v-input-otp[data-disabled] .v-input-otp-hint {\n color: var(--vectis-color-text-subtle);\n }\n\n /* Of the whole size scale, only the raised type is restated here; the dimensions\n themselves come from v-control. */\n .v-input-otp[data-size='sm'] {\n --input-otp-font-size: var(--vectis-font-size-md);\n }\n\n .v-input-otp[data-size='lg'] {\n --input-otp-font-size: var(--vectis-font-size-xl);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-input-otp-input {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VInputOTP.js","names":[],"sources":["../../../src/components/VInputOTP/VInputOTP.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * The row of boxes a one-time code is typed into, one character per box.\n *\n * The platform has no such control, so each box is a real `<input>` and the JS is what makes\n * the row behave as one field: a character moves to the next box, Backspace on an empty one\n * goes back, the arrows walk along, and a code pasted anywhere is spread across them. The\n * first box carries `autocomplete=\"one-time-code\"`, so a code arriving from an SMS or a\n * password manager is spread the same way.\n *\n * `pattern` cuts the row up: every `#` is a box, every other character a decorative literal\n * shown between them and never part of the value. Pasting understands those literals — a\n * code copied formatted, `GT-123`, is consumed with them in place — and `format` filters the\n * rest, forcing capitals outside a numeric code so the value has one canonical form.\n *\n * The value is the filled boxes read in order, so the filled boxes always form a PREFIX:\n * typing or focusing past the first empty box lands in that box, and emptying a box closes\n * the gap. Without that invariant `1 _ 3 _` would be the value `13`, which the next sync\n * would redraw as `1 3 _ _`.\n *\n * A row of boxes is not a form control, so a visually hidden native input carries the code\n * for the form: `name`, `form` and `required` land on it, and a `pattern` of the full\n * length makes a half-typed code invalid.\n */\n\nimport { computed, ref, useAttrs, useId, watch } from 'vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { isDev } from '../../utils/env'\nimport { joinIds } from '../../utils/ids'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of the boxes: 32, 40 or 48 pixels. */\nexport type InputOTPSize = 'sm' | 'md' | 'lg'\n\n/** Which characters the code is made of. */\nexport type InputOTPFormat = 'numeric' | 'alpha' | 'alphanumeric'\n\ninterface InputOTPProps {\n /** How many boxes the code has. It is ignored as soon as a `pattern` is given. */\n length?: number\n /**\n * Which characters the code is made of. It filters what can be typed or pasted, and\n * decides which keyboard a phone offers.\n */\n format?: InputOTPFormat\n /**\n * The shape of the code: each `#` is a box to fill, and every other character is a\n * separator shown between the boxes without ever being part of the value, as in\n * `'GT-###'` or `'###.###.###'`. It wins over `length`.\n */\n pattern?: string\n /**\n * An icon drawn in place of EVERY separator of the pattern. It suits a template\n * whose separators are purely decorative, `'###-###'`, and should not be used with\n * one carrying meaningful text such as `'GT-###'`, which the icon would erase.\n */\n separatorIcon?: IconSource\n /** The size of the boxes: 32, 40 or 48 pixels. */\n size?: InputOTPSize\n /** Takes 4px off the boxes, leaving the text and the icons as they are. */\n compact?: boolean\n /** Makes every box unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the code without letting it be changed. The boxes keep their focus and the code\n * can still be selected and copied, which is what separates it from `disabled`.\n */\n readonly?: boolean\n /** Marks the code as wrong, which colours the boxes and tells assistive technology so. */\n invalid?: boolean\n /**\n * What screen readers announce for the row as a whole. It falls back to the design\n * system dictionary.\n *\n * Unlike the `label` of VInput and the other text fields, NOTHING is rendered from it:\n * a row of boxes carries its own instructions above it, written by the page. Use `hint`\n * for a line the reader can see.\n */\n label?: string\n /**\n * A line of help under the boxes: where the code was sent, how long it lasts. It is\n * tied to the row for assistive technology, so it is read out along with the label.\n */\n hint?: string\n}\n\nconst props = withDefaults(defineProps<InputOTPProps>(), {\n length: 6,\n format: 'numeric',\n pattern: undefined,\n separatorIcon: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.inputOTP.label)\n\n// @a11y\n/*\n * The root IS the group, so the consumer's attributes belong on it — but they are bound\n * BEFORE the two the component decides itself. `aria-describedby` is a list, and the hint\n * is appended to whatever the consumer already pointed at instead of replacing it; left\n * to the fallthrough, their value would land last and take the hint out of the\n * announcement with nothing to show for it.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\n\n// What the FORM reads goes on the hidden native input, and everything else on the group:\n// on the group a `name` submits nothing and `required` validates nothing, silently.\nconst NATIVE_ONLY = ['name', 'form', 'required']\nconst groupAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => !NATIVE_ONLY.includes(key))),\n)\nconst nativeAttrs = computed(() =>\n Object.fromEntries(Object.entries(attrs).filter(([key]) => NATIVE_ONLY.includes(key))),\n)\n// No `useFieldIds` here: the row is named by `aria-label` and renders no `<label>`, so the\n// field id that composable generates would have nothing to point it at.\nconst hintId = useId()\nconst describedBy = computed(() =>\n joinIds(attrs['aria-describedby'] as string | undefined, !!props.hint && hintId),\n)\n\n/**\n * The code as one string, without the separators: a `GT-###` template still yields three\n * characters. It is empty to begin with, and shorter than the full length while it is being\n * typed.\n */\nconst model = defineModel<string>({ default: '' })\n\nconst emit = defineEmits<{\n /**\n * Emitted the moment the code BECOMES complete, carrying it. Retyping a character of a\n * complete code with the same one does not emit it again.\n */\n complete: [code: string]\n}>()\n\ntype Cell = { type: 'slot'; slotIndex: number } | { type: 'literal'; char: string }\n\nconst cells = computed<Cell[]>(() => {\n if (props.pattern?.includes('#')) {\n let slotIndex = 0\n return [...props.pattern].map((char): Cell =>\n char === '#' ? { type: 'slot', slotIndex: slotIndex++ } : { type: 'literal', char },\n )\n }\n return Array.from({ length: props.length }, (_, i) => ({ type: 'slot', slotIndex: i }))\n})\nconst slotCount = computed(() => cells.value.filter((cell) => cell.type === 'slot').length)\n\n// @devwarn\nif (isDev) {\n if (props.pattern && !props.pattern.includes('#'))\n console.warn(\"[VInputOTP] pattern without '#' — falling back to `length`.\")\n}\n\nconst filters: Record<NonNullable<InputOTPProps['format']>, RegExp> = {\n numeric: /[^0-9]/g,\n alpha: /[^A-Z]/g,\n alphanumeric: /[^A-Z0-9]/g,\n}\n\n/**\n * Keeps only what the format allows. Outside a numeric code the text is put in\n * capitals first, so that the value has a single canonical form whichever case the\n * reader typed or pasted.\n */\nfunction sanitize(text: string) {\n const upper = props.format === 'numeric' ? text : text.toUpperCase()\n return upper.replace(filters[props.format], '')\n}\n\nconst inputs = ref<(HTMLInputElement | null)[]>([])\nconst rootEl = ref<HTMLElement | null>(null)\nconst digits = ref<string[]>([])\n\nfunction syncFromModel(value: string) {\n // A value longer than the row is simply shown cut short, and one holding characters the\n // format refuses is shown filtered — but the model is NOT rewritten to match: writing\n // back here would feed the watcher below and the two would keep correcting each other.\n const clean = sanitize(value)\n digits.value = Array.from({ length: slotCount.value }, (_, i) => clean[i] ?? '')\n}\nsyncFromModel(model.value)\nwatch([model, slotCount], ([value, count]) => {\n if (value !== digits.value.join('') || digits.value.length !== count) syncFromModel(value)\n})\n\n/*\n * TRAP — the list of boxes is trimmed AFTER the render, never before it. A box that\n * disappears calls its function ref with `null` while it is being unmounted, at its OLD\n * position: trimmed in a `pre` watcher, that late assignment would stretch the array back\n * out, and a pattern that shortened would keep its departed boxes as trailing entries.\n */\nwatch(\n slotCount,\n (count) => {\n inputs.value.length = count\n },\n { flush: 'post' },\n)\n\n/** The first empty box, or -1 once the code is complete. Every box before it is filled. */\nconst firstEmpty = () => digits.value.findIndex((d) => !d)\n\n/** Takes a character out and moves the following ones back, so no gap is left behind. */\nfunction removeAt(slot: number) {\n digits.value.splice(slot, 1)\n digits.value.push('')\n}\n\nfunction focusBox(slot: number) {\n const el = inputs.value[slot]\n el?.focus()\n // Selected, so the next keystroke REPLACES the character: a box that already has the\n // focus receives no focus event to select it.\n el?.select()\n}\n\nfunction commit() {\n const code = digits.value.join('')\n const previous = model.value\n model.value = code\n // Only when the code CHANGES into a full one: retyping a character of a complete code\n // must not submit it a second time for a consumer listening to `complete`.\n if (code.length === slotCount.value && code !== previous) emit('complete', code)\n}\n\n// @core\n/**\n * Spreads a run of characters across the boxes, starting from the one that received\n * them. It walks the row: a separator swallows the incoming character when the two\n * match, which is what lets a code pasted in its formatted shape line up, and a box\n * takes the next character the format accepts.\n *\n * It returns the last box it filled, so the caller knows where to put the focus, and\n * `null` when nothing in the text was usable.\n */\nfunction distribute(raw: string, startSlot: number): number | null {\n const allCells = cells.value\n let start = allCells.findIndex((cell) => cell.type === 'slot' && cell.slotIndex === startSlot)\n const chars = [...raw]\n // Step back over the separators immediately before that box: pasting the whole\n // string onto the first box means pasting its prefix too, and \"GT-\" has to be\n // matched rather than treated as characters of the code. Never for ONE character: a\n // `G` typed into the first box of `GT-###` is the code's first character, not the\n // separator, and swallowing it would make such a code impossible to type.\n if (chars.length > 1) while (start > 0 && allCells[start - 1]?.type === 'literal') start--\n let charIndex = 0\n let lastFilled: number | null = null\n for (const cell of allCells.slice(start)) {\n if (charIndex >= chars.length) break\n if (cell.type === 'literal') {\n if (chars[charIndex]?.toUpperCase() === cell.char.toUpperCase()) charIndex++\n continue\n }\n let char = ''\n while (charIndex < chars.length && !char) {\n char = sanitize(chars[charIndex] ?? '')\n charIndex++\n }\n if (!char) break\n digits.value[cell.slotIndex] = char\n lastFilled = cell.slotIndex\n }\n return lastFilled\n}\n\n// @keyboard @core — moving the focus forward on its own is what turns a row of\n// separate inputs into something that types like one field; spreading the characters\n// is the core behaviour underneath it.\nfunction onInput(slotIndex: number, event: Event) {\n const el = event.target as HTMLInputElement\n const empty = firstEmpty()\n const filled = empty === -1 || slotIndex < empty\n\n // A filled box whose character was not selected receives the new one NEXT to it. What\n // was inserted is what lies just before the caret, and it is what counts.\n const previous = digits.value[slotIndex] ?? ''\n let raw = el.value\n const inserted = raw.length - previous.length\n if (previous && inserted > 0 && el.selectionStart !== null) {\n raw = raw.slice(Math.max(0, el.selectionStart - inserted), el.selectionStart)\n }\n\n // The same path serves a single keystroke and a pasted code: both are spread from this\n // box onwards — or from the first empty box, when this one lies past it.\n const lastFilled = distribute(raw, filled ? slotIndex : empty)\n if (lastFilled === null) {\n // Either the box was emptied, or nothing typed was valid for this format. A filled box\n // loses its character and the ones after it close the gap.\n if (filled) removeAt(slotIndex)\n el.value = digits.value[slotIndex] ?? ''\n commit()\n return\n }\n // Written by hand: when the characters went to another box, this one's value is still\n // '' in the render, and the patch would leave the typed character on screen.\n el.value = digits.value[slotIndex] ?? ''\n focusBox(Math.min(lastFilled + 1, slotCount.value - 1))\n commit()\n}\n\n// @keyboard — the filled boxes form a prefix, so focusing a box past the first empty one\n// sends the focus there instead: a pointer landing on box 5 of a two-character code types\n// into box 3.\nfunction onFocus(slotIndex: number, event: FocusEvent) {\n const empty = firstEmpty()\n if (empty !== -1 && slotIndex > empty) inputs.value[empty]?.focus()\n else (event.target as HTMLInputElement).select()\n}\n\n// @keyboard\nfunction onKeydown(slotIndex: number, event: KeyboardEvent) {\n const empty = firstEmpty()\n // The furthest box the focus may reach: the first empty one, or the last of a full code.\n const reachable = empty === -1 ? slotCount.value - 1 : empty\n let target: number | null = null\n switch (event.key) {\n case 'Backspace':\n // A read-only box refuses its own edits natively, but this erases ANOTHER box, which\n // nothing native guards.\n if (props.readonly || props.disabled || digits.value[slotIndex] || slotIndex === 0) return\n removeAt(slotIndex - 1)\n commit()\n target = slotIndex - 1\n break\n case 'ArrowLeft':\n if (slotIndex > 0) target = slotIndex - 1\n break\n case 'ArrowRight':\n if (slotIndex < reachable) target = slotIndex + 1\n break\n case 'Home':\n target = 0\n break\n case 'End':\n target = reachable\n break\n }\n if (target === null) return\n event.preventDefault()\n focusBox(target)\n}\n\n// Function refs are handed to the template ONCE per box: an inline one is a new function\n// on every render, which Vue answers with a null-then-element call on every keystroke.\nconst refSetters = new Map<number, (el: unknown) => void>()\nfunction inputRef(slot: number) {\n let set = refSetters.get(slot)\n if (!set) {\n set = (el) => {\n inputs.value[slot] = el as HTMLInputElement | null\n }\n refSetters.set(slot, set)\n }\n return set\n}\n/*\n * The same trio every other field of the size scale exposes. `focus` goes to the FIRST\n * EMPTY box rather than to the first box outright, which is where a reader resuming a\n * half-entered code expects to land.\n */\ndefineExpose({\n /** Moves the focus to the first empty box, or to the last one when the code is complete. */\n focus: (options?: FocusOptions) => {\n const empty = digits.value.findIndex((d) => !d)\n const target = empty === -1 ? slotCount.value - 1 : empty\n inputs.value[target]?.focus(options)\n },\n /** Selects the box the focus is on, as clicking into one already does. */\n select: () => {\n const active = inputs.value.find((el) => el === document.activeElement)\n ;(active ?? inputs.value[0])?.select()\n },\n /** The row itself, for what neither of the two above covers. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-bind=\"groupAttrs\"\n class=\"v-input-otp v-control\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :data-invalid=\"invalid ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <div class=\"v-input-otp-boxes\">\n <template v-for=\"(cell, i) in cells\" :key=\"i\">\n <input\n v-if=\"cell.type === 'slot'\"\n :ref=\"inputRef(cell.slotIndex)\"\n type=\"text\"\n class=\"v-input-otp-input\"\n :inputmode=\"format === 'numeric' ? 'numeric' : 'text'\"\n :autocomplete=\"cell.slotIndex === 0 ? 'one-time-code' : 'off'\"\n :value=\"digits[cell.slotIndex]\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-label=\"m.inputOTP.slot(cell.slotIndex + 1, slotCount)\"\n :aria-invalid=\"invalid || undefined\"\n @input=\"onInput(cell.slotIndex, $event)\"\n @keydown=\"onKeydown(cell.slotIndex, $event)\"\n @focus=\"onFocus(cell.slotIndex, $event)\"\n />\n <!-- A separator from the pattern: shown, never focusable, and never part of the\n value. It is hidden from screen readers, each box already announcing its\n own position in the code. -->\n <span v-else class=\"v-input-otp-literal\" aria-hidden=\"true\">\n <VIcon v-if=\"separatorIcon\" v-bind=\"iconProps(separatorIcon)\" />\n <template v-else>{{ cell.char }}</template>\n </span>\n </template>\n </div>\n\n <!-- What a form reads: the code, under the consumer's name. Out of the tab order and\n hidden from assistive technology, the boxes being the control; a browser focusing\n it to report an invalid code is sent on to the box to fill. -->\n <input\n v-bind=\"nativeAttrs\"\n class=\"v-input-otp-native v-visually-hidden\"\n type=\"text\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n autocomplete=\"off\"\n :value=\"model\"\n :pattern=\"`.{${slotCount}}`\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n @focus=\"focusBox(firstEmpty() === -1 ? 0 : firstEmpty())\"\n />\n\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-input-otp-hint\">\n {{ hint }}\n </VTypography>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-input-otp {\n /*\n * The heights and the icon context come from the shared v-control class\n * (styles/control-size.css). The type is the one thing kept local, and set one or\n * two steps above the other fields: a single character has a whole square to\n * itself, and at the usual field size it would look lost in it.\n */\n --input-otp-font-size: var(--vectis-font-size-lg);\n\n /* A column, so the hint sits under the boxes rather than beside them; the row itself\n is the box below. `inline-flex` keeps the component's own width, and the start\n alignment stops the hint from stretching the row to its own length. */\n display: inline-flex;\n flex-direction: column;\n align-items: start;\n gap: var(--vectis-space-1);\n }\n\n /* A code reads left to right in every language, like the HH:MM of VTimeInput: mirrored\n under `dir=\"rtl\"`, the arrows would walk against the boxes and a `GT-###` pattern would\n be drawn backwards. */\n .v-input-otp-boxes {\n direction: ltr;\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-input-otp-input {\n /* The boxes are square: reading the same variable for both dimensions is what\n makes the size and the density scale them together. */\n width: var(--control-height);\n height: var(--control-height);\n text-align: center;\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--vectis-color-border-strong);\n border-radius: var(--vectis-radius-interactive);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n /* The states follow VInput's, in VInput's order: read-only, hover, focus, invalid,\n disabled. Read-only, focus, invalid and disabled all weigh (0,2,0), so the source order\n arbitrates between them and read-only has to come FIRST, or it would repaint the focus\n and error borders grey.\n\n A read-only row takes the sunken background and the lighter border of a read-only\n VInput, and the state is read from [data-readonly] rather than from `:read-only`, which\n the browser also matches on a disabled box. */\n .v-input-otp[data-readonly] .v-input-otp-input {\n background: var(--vectis-color-surface-sunken);\n border-color: var(--vectis-color-border);\n }\n\n /* The hover weighs (0,5,0) and keeps itself off a focused, invalid or disabled box\n through its own `:not()`, as VInput's does. */\n .v-input-otp:not([data-invalid]) .v-input-otp-input:hover:not(:focus, :disabled) {\n border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /* The focused box appears to have a two-pixel border, exactly as in VInput and\n VTextarea: its own 1px border plus a 1px shadow of the same colour just outside\n it. `:focus` rather than `:focus-visible`, like any text field, which shows its focus\n when clicked into too. The transparent outline is the safety net for Windows forced\n colours, which drop box-shadows entirely. */\n .v-input-otp-input:focus {\n border-color: var(--vectis-color-accent);\n box-shadow: 0 0 0 1px var(--vectis-color-accent);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input {\n border-color: var(--vectis-color-danger);\n }\n\n .v-input-otp[data-invalid] .v-input-otp-input:focus {\n box-shadow: 0 0 0 1px var(--vectis-color-danger);\n }\n\n .v-input-otp-literal {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--vectis-color-text-muted);\n font-family: var(--vectis-text-family-code);\n font-size: var(--input-otp-font-size);\n user-select: none;\n }\n\n /* A disabled row greys out through the colour tokens and never through opacity, the\n same treatment as VInput — `text-muted` inside the box, where `text-subtle` would fall\n under 4.5:1 against `surface-muted`, and `text-subtle` for what sits on the page. */\n .v-input-otp[data-disabled] .v-input-otp-input {\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n border-color: var(--vectis-color-border);\n cursor: not-allowed;\n }\n\n .v-input-otp[data-disabled] .v-input-otp-literal,\n .v-input-otp[data-disabled] .v-input-otp-hint {\n color: var(--vectis-color-text-subtle);\n }\n\n /* Of the whole size scale, only the raised type is restated here; the dimensions\n themselves come from v-control. */\n .v-input-otp[data-size='sm'] {\n --input-otp-font-size: var(--vectis-font-size-md);\n }\n\n .v-input-otp[data-size='lg'] {\n --input-otp-font-size: var(--vectis-font-size-xl);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-input-otp-input {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}