vectis-ui 0.10.1 → 0.10.3

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 (330) hide show
  1. package/dist/components/VAccordion/VAccordion.js.map +1 -1
  2. package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
  3. package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
  4. package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
  5. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/VAccordion/context.js.map +1 -1
  7. package/dist/components/VAvatar/VAvatar.d.ts +2 -12
  8. package/dist/components/VAvatar/VAvatar.js.map +1 -1
  9. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  10. package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
  11. package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
  12. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
  13. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  14. package/dist/components/VAvatar/context.d.ts +1 -5
  15. package/dist/components/VAvatar/context.js.map +1 -1
  16. package/dist/components/VBadge/VBadge.d.ts +13 -27
  17. package/dist/components/VBadge/VBadge.js.map +1 -1
  18. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  19. package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
  20. package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
  21. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/components/VButton/VButton.d.ts +10 -17
  23. package/dist/components/VButton/VButton.js.map +1 -1
  24. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  25. package/dist/components/VButton/VButtonGroup.d.ts +6 -15
  26. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  27. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VButton/context.d.ts +1 -9
  29. package/dist/components/VButton/context.js.map +1 -1
  30. package/dist/components/VCalendar/VCalendar.d.ts +9 -24
  31. package/dist/components/VCalendar/VCalendar.js.map +1 -1
  32. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/VCalendar/VCalendarEvent.d.ts +4 -13
  34. package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
  35. package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  36. package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
  37. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
  39. package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
  40. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
  41. package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
  42. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  43. package/dist/components/VCalendar/color.d.ts +1 -7
  44. package/dist/components/VCalendar/color.js.map +1 -1
  45. package/dist/components/VCalendar/edgeStep.d.ts +5 -15
  46. package/dist/components/VCalendar/edgeStep.js.map +1 -1
  47. package/dist/components/VCalendar/gesture.d.ts +12 -36
  48. package/dist/components/VCalendar/gesture.js.map +1 -1
  49. package/dist/components/VCalendar/keyboard.d.ts +5 -42
  50. package/dist/components/VCalendar/keyboard.js.map +1 -1
  51. package/dist/components/VCalendar/layout.d.ts +38 -199
  52. package/dist/components/VCalendar/layout.js.map +1 -1
  53. package/dist/components/VCalendar/timeGrid.d.ts +3 -25
  54. package/dist/components/VCalendar/timeGrid.js.map +1 -1
  55. package/dist/components/VCalendar/types.d.ts +6 -23
  56. package/dist/components/VCarousel/VCarousel.d.ts +25 -96
  57. package/dist/components/VCarousel/VCarousel.js.map +1 -1
  58. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
  59. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
  61. package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
  62. package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  63. package/dist/components/VCarousel/context.js.map +1 -1
  64. package/dist/components/VCheckbox/VCheckbox.d.ts +1 -5
  65. package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
  66. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  67. package/dist/components/VChip/VChip.d.ts +2 -8
  68. package/dist/components/VChip/VChip.js.map +1 -1
  69. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  70. package/dist/components/VCombobox/VCombobox.d.ts +21 -59
  71. package/dist/components/VCombobox/VCombobox.js.map +1 -1
  72. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
  73. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  74. package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
  75. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  76. package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
  77. package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
  78. package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
  79. package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  80. package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
  81. package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
  82. package/dist/components/VDataTable/VDataTable.d.ts +11 -36
  83. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  84. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
  85. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  86. package/dist/components/VDateInput/VDateInput.d.ts +12 -21
  87. package/dist/components/VDateInput/VDateInput.js.map +1 -1
  88. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  89. package/dist/components/VDateInput/mask.d.ts +14 -59
  90. package/dist/components/VDateInput/mask.js.map +1 -1
  91. package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
  92. package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
  93. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  94. package/dist/components/VDatePicker/keyboard.d.ts +10 -42
  95. package/dist/components/VDatePicker/keyboard.js.map +1 -1
  96. package/dist/components/VDatePicker/names.d.ts +8 -22
  97. package/dist/components/VDatePicker/names.js.map +1 -1
  98. package/dist/components/VDialog/VDialog.d.ts +5 -10
  99. package/dist/components/VDialog/VDialog.js.map +1 -1
  100. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
  101. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  102. package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
  103. package/dist/components/VDialog/VDialogAlert.js.map +1 -1
  104. package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
  105. package/dist/components/VFileInput/VFileInput.d.ts +11 -37
  106. package/dist/components/VFileInput/VFileInput.js.map +1 -1
  107. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  108. package/dist/components/VFileInput/truncate.d.ts +3 -23
  109. package/dist/components/VFileInput/truncate.js.map +1 -1
  110. package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
  111. package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
  112. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  113. package/dist/components/VFilePicker/fileKind.js.map +1 -1
  114. package/dist/components/VHotkeys/VHotkeys.d.ts +6 -15
  115. package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
  116. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
  117. package/dist/components/VHotkeys/platform.d.ts +10 -49
  118. package/dist/components/VHotkeys/platform.js.map +1 -1
  119. package/dist/components/VIcon/VIcon.d.ts +7 -19
  120. package/dist/components/VIcon/VIcon.js.map +1 -1
  121. package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
  122. package/dist/components/VIcon/iconProps.d.ts +5 -19
  123. package/dist/components/VIcon/iconProps.js.map +1 -1
  124. package/dist/components/VIcon/resolver.d.ts +15 -43
  125. package/dist/components/VIcon/resolver.js.map +1 -1
  126. package/dist/components/VIcon/types.d.ts +7 -34
  127. package/dist/components/VIconButton/VIconButton.d.ts +2 -3
  128. package/dist/components/VIconButton/VIconButton.js.map +1 -1
  129. package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  130. package/dist/components/VInput/VInput.d.ts +14 -29
  131. package/dist/components/VInput/VInput.js.map +1 -1
  132. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  133. package/dist/components/VInput/VInputGroup.js.map +1 -1
  134. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
  135. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  136. package/dist/components/VInput/context.js.map +1 -1
  137. package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
  138. package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
  139. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
  140. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
  141. package/dist/components/VMenu/VMenu.js.map +1 -1
  142. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  143. package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
  144. package/dist/components/VMenu/VMenuGroup.js.map +1 -1
  145. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  146. package/dist/components/VMenu/VMenuItem.d.ts +0 -5
  147. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  148. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  149. package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
  150. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  151. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  152. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  153. package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
  154. package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  155. package/dist/components/VMenu/context.d.ts +3 -11
  156. package/dist/components/VMenu/context.js.map +1 -1
  157. package/dist/components/VPagination/VPagination.d.ts +9 -20
  158. package/dist/components/VPagination/VPagination.js.map +1 -1
  159. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  160. package/dist/components/VPopover/VPopover.d.ts +7 -18
  161. package/dist/components/VPopover/VPopover.js.map +1 -1
  162. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
  163. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  164. package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
  165. package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
  166. package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
  167. package/dist/components/VProgressLinear/VProgressLinear.d.ts +4 -15
  168. package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
  169. package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  170. package/dist/components/VRadio/VRadio.d.ts +1 -5
  171. package/dist/components/VRadio/VRadio.js.map +1 -1
  172. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  173. package/dist/components/VSeparator/VSeparator.d.ts +2 -15
  174. package/dist/components/VSeparator/VSeparator.js.map +1 -1
  175. package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  176. package/dist/components/VSideNavigation/VSideNavigation.d.ts +1 -1
  177. package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
  178. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
  180. package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
  181. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  182. package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +1 -5
  183. package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
  184. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
  185. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  186. package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
  187. package/dist/components/VSideNavigation/context.js.map +1 -1
  188. package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +6 -19
  189. package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
  190. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  191. package/dist/components/VSlider/VSlider.d.ts +5 -13
  192. package/dist/components/VSlider/VSlider.js.map +1 -1
  193. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  194. package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
  195. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  196. package/dist/components/VSnackbar/state.d.ts +4 -17
  197. package/dist/components/VSnackbar/state.js.map +1 -1
  198. package/dist/components/VSpinner/VSpinner.d.ts +3 -5
  199. package/dist/components/VSpinner/VSpinner.js.map +1 -1
  200. package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
  201. package/dist/components/VSwitch/VSwitch.js.map +1 -1
  202. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  203. package/dist/components/VTabs/VTab.d.ts +1 -5
  204. package/dist/components/VTabs/VTab.js.map +1 -1
  205. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
  206. package/dist/components/VTabs/VTabPanel.js.map +1 -1
  207. package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  208. package/dist/components/VTabs/VTabs.d.ts +5 -17
  209. package/dist/components/VTabs/VTabs.js.map +1 -1
  210. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  211. package/dist/components/VTabs/context.js.map +1 -1
  212. package/dist/components/VTextarea/VTextarea.d.ts +13 -20
  213. package/dist/components/VTextarea/VTextarea.js.map +1 -1
  214. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  215. package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
  216. package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
  217. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  218. package/dist/components/VTimeInput/search.d.ts +3 -34
  219. package/dist/components/VTimeInput/search.js.map +1 -1
  220. package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
  221. package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
  222. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  223. package/dist/components/VTimePicker/limits.d.ts +18 -45
  224. package/dist/components/VTimePicker/limits.js.map +1 -1
  225. package/dist/components/VToast/VToast.js.map +1 -1
  226. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
  227. package/dist/components/VToast/VToaster.js.map +1 -1
  228. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  229. package/dist/components/VToast/state.d.ts +2 -7
  230. package/dist/components/VToast/state.js.map +1 -1
  231. package/dist/components/VToggle/VToggle.d.ts +5 -9
  232. package/dist/components/VToggle/VToggle.js.map +1 -1
  233. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  234. package/dist/components/VToggle/VToggleItem.d.ts +1 -9
  235. package/dist/components/VToggle/VToggleItem.js.map +1 -1
  236. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  237. package/dist/components/VToggle/context.js.map +1 -1
  238. package/dist/components/VTooltip/VTooltip.d.ts +3 -16
  239. package/dist/components/VTooltip/VTooltip.js.map +1 -1
  240. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
  241. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  242. package/dist/components/VTypography/VTypography.d.ts +4 -8
  243. package/dist/components/VTypography/VTypography.js.map +1 -1
  244. package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
  245. package/dist/composables/useAriaLabel.js.map +1 -1
  246. package/dist/composables/useClearable.d.ts +2 -8
  247. package/dist/composables/useClearable.js.map +1 -1
  248. package/dist/composables/useControlShape.js.map +1 -1
  249. package/dist/composables/useDetailsOpen.d.ts +2 -10
  250. package/dist/composables/useDetailsOpen.js.map +1 -1
  251. package/dist/composables/useFieldIds.js.map +1 -1
  252. package/dist/composables/useFieldPanel.d.ts +4 -17
  253. package/dist/composables/useFieldPanel.js.map +1 -1
  254. package/dist/composables/useFileDrop.js.map +1 -1
  255. package/dist/composables/useFileField.d.ts +1 -5
  256. package/dist/composables/useFileField.js.map +1 -1
  257. package/dist/composables/useFocusoutDismiss.js.map +1 -1
  258. package/dist/composables/useIconClickHandlers.d.ts +4 -19
  259. package/dist/composables/useIconClickHandlers.js.map +1 -1
  260. package/dist/composables/useInertLink.d.ts +1 -1
  261. package/dist/composables/useInertLink.js.map +1 -1
  262. package/dist/composables/useLiveAnnouncer.js.map +1 -1
  263. package/dist/composables/useMaskedField.d.ts +12 -27
  264. package/dist/composables/useMaskedField.js.map +1 -1
  265. package/dist/composables/usePopover.d.ts +3 -11
  266. package/dist/composables/usePopover.js.map +1 -1
  267. package/dist/composables/useProgressValue.js.map +1 -1
  268. package/dist/composables/useRootAttrs.d.ts +0 -9
  269. package/dist/composables/useRootAttrs.js +11 -4
  270. package/dist/composables/useRootAttrs.js.map +1 -1
  271. package/dist/composables/useSlotNodes.d.ts +3 -14
  272. package/dist/composables/useSlotNodes.js.map +1 -1
  273. package/dist/composables/useTextLimit.d.ts +4 -16
  274. package/dist/composables/useTextLimit.js.map +1 -1
  275. package/dist/composables/useTimer.d.ts +2 -15
  276. package/dist/composables/useTimer.js.map +1 -1
  277. package/dist/i18n/en.js.map +1 -1
  278. package/dist/i18n/fr.js.map +1 -1
  279. package/dist/i18n/state.d.ts +9 -38
  280. package/dist/i18n/state.js.map +1 -1
  281. package/dist/i18n/types.d.ts +19 -81
  282. package/dist/icons.d.ts +2 -12
  283. package/dist/tokens/css.d.ts +3 -16
  284. package/dist/tokens/css.js.map +1 -1
  285. package/dist/tokens/index.js.map +1 -1
  286. package/dist/tokens/primitives.js.map +1 -1
  287. package/dist/tokens/semantic.d.ts +1 -23
  288. package/dist/tokens/semantic.js.map +1 -1
  289. package/dist/tokens/themes/dark.js.map +1 -1
  290. package/dist/tokens/types.d.ts +2 -12
  291. package/dist/tokens/types.js.map +1 -1
  292. package/dist/types.d.ts +3 -21
  293. package/dist/utils/array.d.ts +2 -4
  294. package/dist/utils/array.js.map +1 -1
  295. package/dist/utils/arrowNav.d.ts +2 -26
  296. package/dist/utils/arrowNav.js.map +1 -1
  297. package/dist/utils/chip.d.ts +1 -8
  298. package/dist/utils/chip.js.map +1 -1
  299. package/dist/utils/clock.d.ts +16 -39
  300. package/dist/utils/clock.js.map +1 -1
  301. package/dist/utils/css.d.ts +5 -16
  302. package/dist/utils/css.js.map +1 -1
  303. package/dist/utils/date.d.ts +17 -60
  304. package/dist/utils/date.js.map +1 -1
  305. package/dist/utils/direction.d.ts +2 -9
  306. package/dist/utils/direction.js.map +1 -1
  307. package/dist/utils/env.d.ts +2 -15
  308. package/dist/utils/env.js.map +1 -1
  309. package/dist/utils/file.d.ts +8 -46
  310. package/dist/utils/file.js.map +1 -1
  311. package/dist/utils/focus.d.ts +1 -14
  312. package/dist/utils/focus.js +1 -6
  313. package/dist/utils/focus.js.map +1 -1
  314. package/dist/utils/hostWarns.d.ts +3 -8
  315. package/dist/utils/hostWarns.js.map +1 -1
  316. package/dist/utils/ids.d.ts +2 -7
  317. package/dist/utils/ids.js.map +1 -1
  318. package/dist/utils/matcher.d.ts +1 -8
  319. package/dist/utils/matcher.js.map +1 -1
  320. package/dist/utils/memo.d.ts +1 -11
  321. package/dist/utils/memo.js.map +1 -1
  322. package/dist/utils/number.d.ts +1 -7
  323. package/dist/utils/number.js.map +1 -1
  324. package/dist/utils/text.d.ts +4 -23
  325. package/dist/utils/text.js.map +1 -1
  326. package/dist/utils/time.d.ts +4 -12
  327. package/dist/utils/time.js.map +1 -1
  328. package/dist/utils/vnode.d.ts +0 -10
  329. package/dist/utils/vnode.js.map +1 -1
  330. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"VTabPanel.js","names":[],"sources":["../../../src/components/VTabs/VTabPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The panel a tab shows, matched to it by the same `value`.\n *\n * Every panel is rendered at all times, and the inactive ones are hidden with the\n * native `hidden` attribute rather than removed. That costs no JavaScript, and it\n * means the state of what they contain survives a change of tab: a half-typed field\n * keeps its text, a scrolled list its position, and a form's fields are still\n * submitted.\n */\n\nimport { computed, inject, ref, watch } from 'vue'\n\nimport { tabsKey } from './context'\nimport type { ItemValue } from '../../types'\n\ninterface TabPanelProps {\n /** Which tab shows this panel: it must be the `value` of one of them. */\n value: ItemValue\n /**\n * Holds the content back until the panel is first shown, and keeps it from then on.\n * It is for a panel expensive to build; the state it holds is still preserved\n * afterwards.\n */\n lazy?: boolean\n}\n\nconst props = withDefaults(defineProps<TabPanelProps>(), { lazy: false })\n\ndefineSlots<{\n /** What the panel contains. */\n default(): unknown\n}>()\n\nconst tabs = inject(tabsKey, null)\n\nconst selected = computed(() => tabs != null && tabs.value === props.value)\nconst tabId = computed(() => tabs?.tabId(props.value))\nconst panelId = computed(() => tabs?.panelId(props.value))\n\n/*\n * Once a deferred panel has been shown it stays built, so nothing it holds is lost.\n *\n * The `&&` is what keeps a panel that is not lazy out of the selection's dependencies: its\n * getter stops at `props.lazy`, so changing tab re-runs nothing for it. `immediate` latches\n * a lazy panel that starts selected during setup, on the server too, so its content is in\n * the first render rather than one tick later.\n */\nconst revealed = ref(false)\nwatch(\n () => props.lazy && selected.value,\n (shown) => {\n if (shown) revealed.value = true\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <div\n :id=\"panelId\"\n class=\"v-tabs-panel\"\n role=\"tabpanel\"\n :aria-labelledby=\"tabId\"\n tabindex=\"0\"\n :hidden=\"!selected\"\n >\n <slot v-if=\"!lazy || revealed\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A guard. The `hidden` attribute only hides an element through the browser's own\n * stylesheet, which ANY author declaration of a display overrides — a consumer's\n * `.v-tabs-panel { display: flex }` included, and it would then reveal every panel\n * at once.\n *\n * It is deliberately left overridable, with no `!important`: that would make this\n * the one component of the design system a consumer could not restyle. To give the\n * panel a display of your own, exclude the hidden ones —\n * `.v-tabs-panel:not([hidden])`.\n */\n .v-tabs-panel[hidden] {\n display: none;\n }\n\n .v-tabs-panel:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VTabPanel.js","names":[],"sources":["../../../src/components/VTabs/VTabPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Keep every panel root mounted for aria-controls. lazy defers its content until first\n * activation, then preserves that content.\n */\n\nimport { computed, inject, ref, watch } from 'vue'\n\nimport { tabsKey } from './context'\nimport type { ItemValue } from '../../types'\n\ninterface TabPanelProps {\n /** Which tab shows this panel: it must be the `value` of one of them. */\n value: ItemValue\n /**\n * Holds the content back until the panel is first shown, and keeps it from then on.\n * It is for a panel expensive to build; the state it holds is still preserved\n * afterwards.\n */\n lazy?: boolean\n}\n\nconst props = withDefaults(defineProps<TabPanelProps>(), { lazy: false })\n\ndefineSlots<{\n /** What the panel contains. */\n default(): unknown\n}>()\n\nconst tabs = inject(tabsKey, null)\n\nconst selected = computed(() => tabs != null && tabs.value === props.value)\nconst tabId = computed(() => tabs?.tabId(props.value))\nconst panelId = computed(() => tabs?.panelId(props.value))\n\n/*\n * Once a deferred panel has been shown it stays built, so nothing it holds is lost. The `&&` is\n * what keeps a panel that is not lazy out of the selection's dependencies: its getter stops at\n * `props.lazy`, so changing tab re-runs nothing for it.\n */\nconst revealed = ref(false)\nwatch(\n () => props.lazy && selected.value,\n (shown) => {\n if (shown) revealed.value = true\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <div\n :id=\"panelId\"\n class=\"v-tabs-panel\"\n role=\"tabpanel\"\n :aria-labelledby=\"tabId\"\n tabindex=\"0\"\n :hidden=\"!selected\"\n >\n <slot v-if=\"!lazy || revealed\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A guard. The `hidden` attribute only hides an element through the browser's own stylesheet,\n * which ANY author declaration of a display overrides; a consumer's\n * `.v-tabs-panel { display: flex }` included, and it would then reveal every panel at once.\n */\n .v-tabs-panel[hidden] {\n display: none;\n }\n\n .v-tabs-panel:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VTabPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTabs/VTabPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The panel a tab shows, matched to it by the same `value`.\n *\n * Every panel is rendered at all times, and the inactive ones are hidden with the\n * native `hidden` attribute rather than removed. That costs no JavaScript, and it\n * means the state of what they contain survives a change of tab: a half-typed field\n * keeps its text, a scrolled list its position, and a form's fields are still\n * submitted.\n */\n\nimport { computed, inject, ref, watch } from 'vue'\n\nimport { tabsKey } from './context'\nimport type { ItemValue } from '../../types'\n\ninterface TabPanelProps {\n /** Which tab shows this panel: it must be the `value` of one of them. */\n value: ItemValue\n /**\n * Holds the content back until the panel is first shown, and keeps it from then on.\n * It is for a panel expensive to build; the state it holds is still preserved\n * afterwards.\n */\n lazy?: boolean\n}\n\nconst props = withDefaults(defineProps<TabPanelProps>(), { lazy: false })\n\ndefineSlots<{\n /** What the panel contains. */\n default(): unknown\n}>()\n\nconst tabs = inject(tabsKey, null)\n\nconst selected = computed(() => tabs != null && tabs.value === props.value)\nconst tabId = computed(() => tabs?.tabId(props.value))\nconst panelId = computed(() => tabs?.panelId(props.value))\n\n/*\n * Once a deferred panel has been shown it stays built, so nothing it holds is lost.\n *\n * The `&&` is what keeps a panel that is not lazy out of the selection's dependencies: its\n * getter stops at `props.lazy`, so changing tab re-runs nothing for it. `immediate` latches\n * a lazy panel that starts selected during setup, on the server too, so its content is in\n * the first render rather than one tick later.\n */\nconst revealed = ref(false)\nwatch(\n () => props.lazy && selected.value,\n (shown) => {\n if (shown) revealed.value = true\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <div\n :id=\"panelId\"\n class=\"v-tabs-panel\"\n role=\"tabpanel\"\n :aria-labelledby=\"tabId\"\n tabindex=\"0\"\n :hidden=\"!selected\"\n >\n <slot v-if=\"!lazy || revealed\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A guard. The `hidden` attribute only hides an element through the browser's own\n * stylesheet, which ANY author declaration of a display overrides — a consumer's\n * `.v-tabs-panel { display: flex }` included, and it would then reveal every panel\n * at once.\n *\n * It is deliberately left overridable, with no `!important`: that would make this\n * the one component of the design system a consumer could not restyle. To give the\n * panel a display of your own, exclude the hidden ones —\n * `.v-tabs-panel:not([hidden])`.\n */\n .v-tabs-panel[hidden] {\n display: none;\n }\n\n .v-tabs-panel:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;EA2BA,IAAM,IAAQ,GAOR,IAAO,EAAO,GAAS,IAAI,GAE3B,IAAW,QAAe,KAAQ,QAAQ,EAAK,UAAU,EAAM,KAAK,GACpE,IAAQ,QAAe,GAAM,MAAM,EAAM,KAAK,CAAC,GAC/C,IAAU,QAAe,GAAM,QAAQ,EAAM,KAAK,CAAC,GAUnD,IAAW,EAAI,EAAK;SAC1B,QACQ,EAAM,QAAQ,EAAS,QAC5B,MAAU;GACT,AAAI,MAAO,EAAS,QAAQ;EAC9B,GACA,EAAE,WAAW,GAAK,CACpB,mBAIE,EASM,OAAA;GARH,IAAI,EAAA;GACL,OAAM;GACN,MAAK;GACJ,mBAAiB,EAAA;GAClB,UAAS;GACR,QAAM,CAAG,EAAA;OAEG,EAAA,QAAQ,EAAA,QAArB,EAAiC,EAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,KAAA,GAAA,CAAA,IAAA,EAAA,IAAA,EAAA,CAAA,GAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"VTabPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTabs/VTabPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Keep every panel root mounted for aria-controls. lazy defers its content until first\n * activation, then preserves that content.\n */\n\nimport { computed, inject, ref, watch } from 'vue'\n\nimport { tabsKey } from './context'\nimport type { ItemValue } from '../../types'\n\ninterface TabPanelProps {\n /** Which tab shows this panel: it must be the `value` of one of them. */\n value: ItemValue\n /**\n * Holds the content back until the panel is first shown, and keeps it from then on.\n * It is for a panel expensive to build; the state it holds is still preserved\n * afterwards.\n */\n lazy?: boolean\n}\n\nconst props = withDefaults(defineProps<TabPanelProps>(), { lazy: false })\n\ndefineSlots<{\n /** What the panel contains. */\n default(): unknown\n}>()\n\nconst tabs = inject(tabsKey, null)\n\nconst selected = computed(() => tabs != null && tabs.value === props.value)\nconst tabId = computed(() => tabs?.tabId(props.value))\nconst panelId = computed(() => tabs?.panelId(props.value))\n\n/*\n * Once a deferred panel has been shown it stays built, so nothing it holds is lost. The `&&` is\n * what keeps a panel that is not lazy out of the selection's dependencies: its getter stops at\n * `props.lazy`, so changing tab re-runs nothing for it.\n */\nconst revealed = ref(false)\nwatch(\n () => props.lazy && selected.value,\n (shown) => {\n if (shown) revealed.value = true\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <div\n :id=\"panelId\"\n class=\"v-tabs-panel\"\n role=\"tabpanel\"\n :aria-labelledby=\"tabId\"\n tabindex=\"0\"\n :hidden=\"!selected\"\n >\n <slot v-if=\"!lazy || revealed\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A guard. The `hidden` attribute only hides an element through the browser's own stylesheet,\n * which ANY author declaration of a display overrides; a consumer's\n * `.v-tabs-panel { display: flex }` included, and it would then reveal every panel at once.\n */\n .v-tabs-panel[hidden] {\n display: none;\n }\n\n .v-tabs-panel:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;EAsBA,IAAM,IAAQ,GAOR,IAAO,EAAO,GAAS,IAAI,GAE3B,IAAW,QAAe,KAAQ,QAAQ,EAAK,UAAU,EAAM,KAAK,GACpE,IAAQ,QAAe,GAAM,MAAM,EAAM,KAAK,CAAC,GAC/C,IAAU,QAAe,GAAM,QAAQ,EAAM,KAAK,CAAC,GAOnD,IAAW,EAAI,EAAK;SAC1B,QACQ,EAAM,QAAQ,EAAS,QAC5B,MAAU;GACT,AAAI,MAAO,EAAS,QAAQ;EAC9B,GACA,EAAE,WAAW,GAAK,CACpB,mBAIE,EASM,OAAA;GARH,IAAI,EAAA;GACL,OAAM;GACN,MAAK;GACJ,mBAAiB,EAAA;GAClB,UAAS;GACR,QAAM,CAAG,EAAA;OAEG,EAAA,QAAQ,EAAA,QAArB,EAAiC,EAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,KAAA,GAAA,CAAA,IAAA,EAAA,IAAA,EAAA,CAAA,GAAA,GAAA,CAAA"}
@@ -13,11 +13,7 @@ export type TabsAlign = 'start' | 'center' | 'end';
13
13
  /** Whether a tab is selected when it takes the focus, or only when it is activated. */
14
14
  export type TabsActivation = 'manual' | 'automatic';
15
15
  interface TabsProps {
16
- /**
17
- * How the bar is framed. `flat` draws nothing but a rule under the tabs, with the
18
- * selected one underlined; `outlined` puts that same bar and its panels inside a
19
- * card; `inset` turns the row into a segmented control sitting in a hollow track.
20
- */
16
+ /** How the bar is framed. */
21
17
  variant?: TabsVariant;
22
18
  /** The colour the selected tab takes. The others stay neutral whatever this says. */
23
19
  tone?: TabsTone;
@@ -47,16 +43,11 @@ interface TabsProps {
47
43
  prevLabel?: string;
48
44
  /** What the forward scroll button does, in words. It falls back to the dictionary. */
49
45
  nextLabel?: string;
50
- /**
51
- * Whether moving to a tab also selects it. Selecting on arrival is what the ARIA
52
- * authoring practices recommend when a panel appears instantly; leave it manual when
53
- * showing a panel costs a request, or every tab passed over would fire one.
54
- */
46
+ /** Whether moving to a tab also selects it. */
55
47
  activation?: TabsActivation;
56
48
  /**
57
49
  * Makes every tab unusable, and the scroll buttons with them: the tabs leave the tab order
58
- * and grey out through the colour tokens. A tab disabled on its own stays disabled either
59
- * way. The panel on show stays on show.
50
+ * and grey out through the colour tokens.
60
51
  */
61
52
  disabled?: boolean;
62
53
  /**
@@ -78,11 +69,8 @@ type __VLS_Slots = {
78
69
  declare function focusSelected(options?: FocusOptions): void;
79
70
  type __VLS_ModelProps = {
80
71
  /**
81
- * The `value` of the selected tab. There is deliberately no default: the component cannot
82
- * know which of the tabs a consumer wrote should open.
83
- *
84
- * CONTRACT: it must name a tab that exists and is not disabled. Pointing it anywhere else
85
- * leaves no tab with a tab stop, and the bar becomes unreachable from the keyboard.
72
+ * The `value` of the selected tab. There is deliberately no default: the component cannot know
73
+ * which of the tabs a consumer wrote should open.
86
74
  */
87
75
  modelValue?: ItemValue;
88
76
  };
@@ -1 +1 @@
1
- {"version":3,"file":"VTabs.js","names":[],"sources":["../../../src/components/VTabs/VTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * A row of tabs, and optionally the panels they show. Only one panel is visible at a\n * time, and the tabs follow the pattern assistive technology expects of them: the row\n * is a single stop in the tab order, and the arrow keys move between the tabs inside\n * it.\n *\n * No button is invented here. Every tab is a VButton and the scroll controls are\n * VIconButtons, so hovering, focusing, disabling and the respect of a reader's\n * reduced-motion setting all come from there, without a single state rule being\n * written again in this file.\n *\n * Nothing touches the DOM outside event handlers and effects that run after the page\n * has been updated, which is what lets the component render on a server.\n */\n\nimport { computed, onMounted, provide, ref, useId, useSlots, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { expand_less as expandLessIcon } from '../VIcon/icons/expand_less'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport { panelIdFor, tabIdFor, tabsKey } from './context'\n\nimport type { ItemValue } from '../../types'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the bar is drawn: a rule under the tabs, the same inside a card, or a sunken track. */\nexport type TabsVariant = 'flat' | 'outlined' | 'inset'\n/** The colour of the selected tab. */\nexport type TabsTone = 'accent' | 'neutral' | 'danger'\n/** The height of the tabs, from the scale every control shares. */\nexport type TabsSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the tabs run across the page or down its side. */\nexport type TabsOrientation = 'horizontal' | 'vertical'\n/** Where the tabs sit along the bar when they do not fill it. */\nexport type TabsAlign = 'start' | 'center' | 'end'\n/** Whether a tab is selected when it takes the focus, or only when it is activated. */\nexport type TabsActivation = 'manual' | 'automatic'\n\ninterface TabsProps {\n /**\n * How the bar is framed. `flat` draws nothing but a rule under the tabs, with the\n * selected one underlined; `outlined` puts that same bar and its panels inside a\n * card; `inset` turns the row into a segmented control sitting in a hollow track.\n */\n variant?: TabsVariant\n /** The colour the selected tab takes. The others stay neutral whatever this says. */\n tone?: TabsTone\n /** The height of the tabs, from the scale shared by every control. */\n size?: TabsSize\n /** Takes 4px off the height of every tab. */\n compact?: boolean\n /** Whether the tabs run across the page or down its side. */\n orientation?: TabsOrientation\n /** Where the tabs sit along the bar when they do not fill it. */\n align?: TabsAlign\n /**\n * Stretches the tabs across the whole bar, every tab taking an equal share of it, on the\n * terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * Adds a button at each end of the bar to scroll it, each disabled once that end is\n * reached. It only makes sense when the tabs can overflow, so it excludes `fullWidth`.\n */\n scrollButtons?: boolean\n /** The icon of the button scrolling backwards. It follows the orientation by default. */\n prevIcon?: IconSource\n /** The icon of the button scrolling forwards. It follows the orientation by default. */\n nextIcon?: IconSource\n /** What the backward scroll button does, in words. It falls back to the dictionary. */\n prevLabel?: string\n /** What the forward scroll button does, in words. It falls back to the dictionary. */\n nextLabel?: string\n /**\n * Whether moving to a tab also selects it. Selecting on arrival is what the ARIA\n * authoring practices recommend when a panel appears instantly; leave it manual when\n * showing a panel costs a request, or every tab passed over would fire one.\n */\n activation?: TabsActivation\n /**\n * Makes every tab unusable, and the scroll buttons with them: the tabs leave the tab order\n * and grey out through the colour tokens. A tab disabled on its own stays disabled either\n * way. The panel on show stays on show.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row of tabs. It falls back to the design\n * system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<TabsProps>(), {\n variant: 'flat',\n tone: 'accent',\n size: 'md',\n compact: false,\n orientation: 'horizontal',\n align: 'start',\n fullWidth: false,\n scrollButtons: false,\n prevIcon: undefined,\n nextIcon: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n activation: 'manual',\n disabled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The tabs themselves. */\n default(): unknown\n /**\n * The panels the tabs show. Leaving it out renders no panel area at all, which is\n * how the same component serves as a plain bar or a segmented control.\n */\n panels?(): unknown\n}>()\n\n/**\n * The `value` of the selected tab. There is deliberately no default: the component cannot\n * know which of the tabs a consumer wrote should open.\n *\n * CONTRACT: it must name a tab that exists and is not disabled. Pointing it anywhere else\n * leaves no tab with a tab stop, and the bar becomes unreachable from the keyboard.\n */\nconst model = defineModel<ItemValue>()\n\n// The root element is only a container; the one that matters is the row of tabs. So\n// `class` and `style` stay outside, where a consumer expects to place the component,\n// and everything else — the id, the aria-* naming it, the data-* — goes down onto the\n// row itself.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs: listAttrs } = useRootAttrs()\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins — that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.tabs.label)\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? m.value.tabs.previous)\nconst resolvedNextLabel = computed(() => props.nextLabel ?? m.value.tabs.next)\n\nconst slots = useSlots()\nconst baseId = useId()\n\n// Everything is exposed through getters rather than as values: that is what keeps the\n// root's props reactive on the other side of the injection, so a tab re-renders when\n// the size or the tone changes.\nprovide(tabsKey, {\n get value() {\n return model.value\n },\n select(value: ItemValue) {\n model.value = value\n },\n tabId: (value: ItemValue) => tabIdFor(baseId, value),\n panelId: (value: ItemValue) => panelIdFor(baseId, value),\n // @ssr\n /*\n * Whether a slot was given is decided by the parent as it renders, so the answer is\n * the same on the server and in the browser. A registry the panels filled in as they\n * mounted would instead read as empty on the server and full on the client, which is\n * a hydration mismatch.\n *\n * The trade-off is that this is not reactive: the panels slot must be there or not\n * there, and cannot appear halfway through the life of the component.\n */\n get hasPanels() {\n return slots.panels !== undefined\n },\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get activation() {\n return props.activation\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst resolvedPrevIcon = computed(\n () => props.prevIcon ?? (isVertical.value ? expandLessIcon : chevronLeftIcon),\n)\nconst resolvedNextIcon = computed(\n () => props.nextIcon ?? (isVertical.value ? expandMoreIcon : chevronRightIcon),\n)\n\nconst listEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/*\n * The arrow keys, through the shared implementation in `utils/arrowNav`. This handler\n * ONLY moves the focus. Selecting the tab that receives it — what `automatic`\n * activation means — is done by the tab itself, which knows its own typed value and\n * therefore never has to send it back through a DOM attribute.\n *\n * A tab the consumer has hidden is left out by the helper, which skips anything not\n * displayed.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const list = listEl.value\n if (!list) return\n arrowNavigate(event, list, () => navigableItems(list, '[role=\"tab\"]:not(:disabled)'), {\n vertical: isVertical.value,\n })\n}\n\n/*\n * Whether each end of the bar has been reached, which is what disables the scroll buttons.\n *\n * READ THIS BEFORE PROPOSING `@container scroll-state` AGAIN. CSS can ask the question —\n * VDialog uses that query for its scroll shadows — and still cannot answer this one, for two\n * independent reasons. It is Chrome/Edge 133+, with neither Safari nor Firefox shipping it,\n * and where VDialog's hairlines merely stay invisible without it these buttons are\n * functional; an `@supports` guard would mean keeping everything below anyway. And CSS\n * cannot set `disabled` on a button: hiding or greying an arrow is a different affordance\n * and a different announcement, not the same behaviour written another way.\n *\n * So two hairline sentinels sit at the ends of the row and are observed. ONE observer then\n * covers all three things that change the answer — scrolling, a resize, tabs added or\n * removed — where measuring the scroll would need a second mechanism for the resize alone.\n *\n * Both start as reached, so the buttons are disabled on the first render and on the server,\n * never wrongly enabled. That also gives the \"nothing overflows\" case for free: both\n * sentinels are visible at once, so both buttons stay disabled with no test of their own.\n */\nconst startSentinelEl = ref<HTMLElement | null>(null)\nconst endSentinelEl = ref<HTMLElement | null>(null)\nconst atStart = ref(true)\nconst atEnd = ref(true)\n\nwatch(\n [listEl, startSentinelEl, endSentinelEl],\n ([root, start, end], _previous, onCleanup) => {\n // @fallback\n // The observer exists neither on the server nor in the unit-test environment, so\n // its absence has to be tolerated rather than assumed away. The behaviour of the\n // ends is checked in a real browser instead, by the play functions.\n if (!root || !start || !end || typeof IntersectionObserver === 'undefined') return\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.target === start) atStart.value = entry.isIntersecting\n else atEnd.value = entry.isIntersecting\n }\n },\n { root },\n )\n observer.observe(start)\n observer.observe(end)\n onCleanup(() => observer.disconnect())\n },\n // `post` so the effect runs once the DOM is up to date: in the default timing the three\n // template refs are still null on the first pass, the guard above returns, and the\n // buttons stay disabled for good — the effect having tracked nothing to wake it again.\n { flush: 'post' },\n)\n\n// @a11y\n/*\n * A scroll button disables itself once its end is reached, and a focused button that becomes\n * disabled hands the focus to <body>: a keyboard reader pressing \"next\" until the end would be\n * sent back to the top of the page by the next Tab. The focus moves on first, to the opposite\n * button, which the row has just scrolled away from, or to the selected tab when both ends are\n * reached at once (the row no longer overflows). The two buttons are the list's own siblings.\n *\n * TRAP — the default `pre` timing is the point: the button still holds the focus when this\n * runs, the DOM not being patched with `disabled` yet. In `post` timing the focus would\n * already be on <body>, with nothing left to say which control lost it.\n */\nwatch([atStart, atEnd], ([start, end]) => {\n const list = listEl.value\n const prev = list?.previousElementSibling as HTMLElement | null\n const next = list?.nextElementSibling as HTMLElement | null\n const focused = document.activeElement\n const fromPrev = focused === prev\n if (!(fromPrev ? start : focused === next && end)) return\n if (fromPrev ? end : start) focusSelected()\n else (fromPrev ? next : prev)?.focus()\n})\n\n/**\n * Scrolls the bar by most of its own length. No scrolling behaviour is requested,\n * deliberately: leaving it out lets the stylesheet decide, which is what makes the\n * movement respect a reader who has asked for less motion.\n */\nfunction scrollStep(direction: -1 | 1) {\n const list = listEl.value\n if (!list) return\n // Called optionally because the unit-test environment implements no scrolling at\n // all; this behaviour is covered in a real browser.\n if (isVertical.value) {\n list.scrollBy?.({ top: direction * list.clientHeight * 0.8 })\n return\n }\n // The horizontal offset is physical, not logical: in a right-to-left page, moving\n // forward means moving left, so the direction has to be flipped by hand.\n const rtl = isRtl(list)\n list.scrollBy?.({ left: direction * list.clientWidth * 0.8 * (rtl ? -1 : 1) })\n}\n\n// @a11y @core\n/*\n * Brings the selected tab back into view when the selection was changed from code.\n * The keyboard needs nothing here: it moves the focus, and the browser scrolls a\n * focused element into view by itself.\n *\n * The scrolling is confined to the row rather than asking the element to reveal\n * itself, which would scroll every scrollable ancestor up to the page. The distances\n * are read from the two boxes, so they are already physical: the same code serves\n * left-to-right, right-to-left and vertical without a single test of direction.\n *\n * It also runs once on mount, INSTANTLY: a row opened on a tab past its visible end (a deep\n * link, a restored route) would otherwise show the first tabs with the selected one out of\n * sight, and a smooth scroll would animate the page as it loads.\n */\nfunction revealSelected(behavior?: ScrollBehavior) {\n const list = listEl.value\n const tab = list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]')\n if (!list || !tab) return\n const t = tab.getBoundingClientRect()\n const c = list.getBoundingClientRect()\n list.scrollBy?.({\n left: t.left < c.left ? t.left - c.left : t.right > c.right ? t.right - c.right : 0,\n top: t.top < c.top ? t.top - c.top : t.bottom > c.bottom ? t.bottom - c.bottom : 0,\n behavior,\n })\n}\n\n// `post`, so the tab that is now selected already carries `aria-selected` when it is looked\n// for: in the default timing the query would find the one that was selected before.\nwatch(model, () => revealSelected(), { flush: 'post' })\nonMounted(() => revealSelected('instant'))\n\n// The root is a container and the tabs are rendered by the consumer's slot, so a template\n// ref reaches neither the row nor a tab. `focus` goes where the Tab key would land: the\n// selected tab, which holds the row's single tab stop.\nfunction focusSelected(options?: FocusOptions) {\n const list = listEl.value\n const tab =\n list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]:not(:disabled)') ??\n list?.querySelector<HTMLElement>('[role=\"tab\"]:not(:disabled)')\n tab?.focus(options)\n}\n\ndefineExpose({\n /**\n * Moves the focus to the selected tab, or to the first tab that can take it when the\n * v-model names none.\n */\n focus: focusSelected,\n /** The `role=\"tablist\"` row, which is also where the consumer's attributes land. */\n el: listEl,\n})\n</script>\n\n<template>\n <div\n :class=\"['v-tabs', rootClass]\"\n :style=\"rootStyle\"\n :data-variant=\"variant\"\n :data-orientation=\"orientation\"\n :data-align=\"align\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n >\n <div class=\"v-tabs-bar\">\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedPrevLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atStart\"\n @click=\"scrollStep(-1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedPrevIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n\n <div\n ref=\"listEl\"\n :aria-orientation=\"isVertical ? 'vertical' : undefined\"\n v-bind=\"listAttrs\"\n class=\"v-tabs-list\"\n role=\"tablist\"\n :aria-label=\"ariaLabel\"\n @keydown=\"onKeydown\"\n >\n <!-- The two markers watched to know whether an end of the bar is reached.\n They are hidden from assistive technology: a row of tabs may contain\n nothing but tabs. -->\n <span\n v-if=\"scrollButtons\"\n ref=\"startSentinelEl\"\n class=\"v-tabs-sentinel\"\n aria-hidden=\"true\"\n />\n <slot />\n <span v-if=\"scrollButtons\" ref=\"endSentinelEl\" class=\"v-tabs-sentinel\" aria-hidden=\"true\" />\n </div>\n\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedNextLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atEnd\"\n @click=\"scrollStep(1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedNextIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n </div>\n\n <div v-if=\"$slots.panels\" class=\"v-tabs-panels\">\n <slot name=\"panels\" />\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-tabs {\n /*\n * The panels' padding, and the four pixels the compact density takes off it. The\n * delta is held apart from the value rather than the two being written out, so the\n * density stays one subtraction wherever the padding is read instead of a second\n * table to keep in step — the VAccordion idiom.\n *\n * It follows the density and deliberately not the SIZE: the padding is a property of\n * the layout, and in any case the size variables live on the descendant buttons, each\n * tab declaring its own, so the root could not read them.\n */\n --tabs-pad-delta: 0px;\n --tabs-panels-pad: calc(var(--vectis-space-4) - var(--tabs-pad-delta));\n\n display: flex;\n flex-direction: column;\n font-family: var(--vectis-text-family);\n }\n\n .v-tabs[data-compact] {\n --tabs-pad-delta: var(--vectis-space-1);\n }\n\n /* Turned vertical, the bar and the panels sit side by side rather than stacked. */\n .v-tabs[data-orientation='vertical'] {\n flex-direction: row;\n }\n\n /*\n * The card, taken from the decoration scale shared with VAccordion and VDataTable.\n * The flat default has nothing to undo, since it declares no decoration at all.\n *\n * The frame goes on the ROOT, so it encloses the bar AND the panels, and the rule\n * under the tabs then becomes the boundary between the two inside it.\n *\n * The bar spends NO gutter: the tabs sit against the card's border and the track runs\n * from edge to edge, so the whole of the padding is the panels'. That is also what\n * lands an end tab exactly on a corner of the card, where its outer radius has to be\n * the frame's INNER one — VTab.vue derives it, and the two cannot move apart without\n * the corner showing on hover.\n *\n * Unlike VDataTable, nothing is clipped here: clipping would cut the panel's focus\n * ring, the only one that leaves the component, and forbid a consumer any content\n * running edge to edge. The bar with no panels under it is the single exception, and\n * it is handled where that case is.\n */\n .v-tabs[data-variant='outlined'] {\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs-bar {\n display: flex;\n /* Stretched rather than centred: the row deliberately overlaps the track by one\n pixel through a negative margin, and centring would displace it by half of\n that. */\n align-items: stretch;\n /*\n * The alignment belongs to the BAR and not to the row of tabs. The row is allowed\n * to shrink below its content as soon as the tabs overflow, so there would be\n * nothing left to distribute; and setting an alignment on a scrolling container\n * makes whatever overflows past its start edge permanently unreachable, since a\n * scroll position cannot go negative.\n */\n justify-content: flex-start;\n gap: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar {\n flex-direction: column;\n }\n\n .v-tabs[data-align='center'] > .v-tabs-bar {\n justify-content: center;\n }\n\n .v-tabs[data-align='end'] > .v-tabs-bar {\n justify-content: flex-end;\n }\n\n /* The rule the tabs sit on, in the flat and outlined frames. It is drawn on the BAR\n rather than on the row, so that it also runs under the scroll buttons, which are\n siblings of the row and not part of it. Inside a card it is what separates the\n tabs from the panels — which is why that edge is left without a gutter, since\n otherwise the rule would separate nothing from nothing.\n\n Both frames are named explicitly rather than excluding the third: a fourth frame\n will have to opt in by hand instead of inheriting this silently. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar {\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar {\n border-block-end: none;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * Framed AND vertical, the rule MIGRATES to the other edge. Flat, it marks the edge\n * of the tab column, hence the start; inside a card that edge is already drawn by\n * the frame's own border — two parallel lines a pixel apart — and what is missing is\n * the boundary between the tabs and the panels.\n *\n * TRAP — this block has exactly the same specificity as the one above, so it is the\n * ORDER that decides. Moving it up would leave the vertical framed case with two\n * rules on one side and none between the two areas.\n */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar {\n border-inline-start: none;\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs-list {\n display: flex;\n align-items: center;\n /* Held in a variable because the sentinels cancel it beside themselves. */\n --tabs-list-gap: var(--vectis-space-1);\n gap: var(--tabs-list-gap);\n overflow: auto;\n /* Without these the row refuses to shrink below the width of its tabs, and it\n would widen the whole bar instead of scrolling. */\n min-inline-size: 0;\n min-block-size: 0;\n scrollbar-width: none;\n scroll-behavior: smooth;\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n flex-direction: column;\n align-items: stretch;\n }\n\n /* Pulling the row one pixel into the track is what lets the selected tab's indicator\n COVER that line rather than sit on top of it, which would read as a thicker rule.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: -1px;\n /* On a track the tabs are contiguous segments and not a queue of buttons: no gap\n here, and no rounded corners on the tabs themselves. */\n --tabs-list-gap: 0px;\n }\n\n /* The row is now a pixel taller than the tabs it holds, so they are pushed against\n its end edge; the indicator each tab draws there then falls exactly on the\n track. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='horizontal']\n > .v-tabs-bar\n > .v-tabs-list {\n align-items: flex-end;\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar\n > .v-tabs-list {\n margin-block-end: 0;\n margin-inline-start: -1px;\n }\n\n /* Framed AND vertical, there is no longer a track on that edge to cover, since the\n rule has migrated to the other side: the one-pixel overlap would simply slide the\n row under the frame's own border.\n\n TRAP — same specificity as the rule above, so again it is the order that decides. */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n margin-inline-start: 0;\n }\n\n /*\n * A framed bar with no panels: the card is reduced to the tabs alone. There is no\n * longer a boundary to mark, and the track would run right alongside the frame's own\n * border, doubling it. So the frame takes its place and the row stops overlapping.\n *\n * This is the one place the card may clip, and the one place it must. The tabs fill\n * it on all four sides here, so a hovered end tab reaches the two corners no radius\n * of its own covers — the ends round the edge the track used to occupy, and that one\n * only. Nothing is lost to the clip: there is no panel, hence no outer focus ring,\n * and a tab draws its own ring inwards.\n */\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) {\n overflow: clip;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar {\n border-block-end: none;\n border-inline-end: none;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: 0;\n }\n\n /* The hollow track of the segmented variant is drawn on the scrolling row itself,\n and not on the bar around it: its padding is what keeps the raised tab's shadow\n from being clipped as the row scrolls. */\n .v-tabs[data-variant='inset'] > .v-tabs-bar > .v-tabs-list {\n background: var(--vectis-color-surface-sunken);\n padding: var(--vectis-space-1);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs[data-full-width] > .v-tabs-bar > .v-tabs-list {\n flex: 1;\n }\n\n /*\n * A pixel square each, at the very ends of the row. They are items of the row, so its gap\n * opens beside them as it does between two tabs: each pulls its neighbour back over that\n * gap and over its own pixel, so the first and last tabs sit exactly where the row's\n * padding puts them. The pull is written on both axes, only the main one doing anything.\n *\n * Aligned to the start rather than stretched: a negative cross-axis margin on a stretched\n * item would GROW it past the row, which then scrolls on the other axis too.\n */\n .v-tabs-sentinel {\n flex: 0 0 1px;\n align-self: flex-start;\n inline-size: 1px;\n block-size: 1px;\n }\n\n .v-tabs-sentinel:first-child {\n margin-inline-end: calc(-1px - var(--tabs-list-gap));\n margin-block-end: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-sentinel:last-child {\n margin-inline-start: calc(-1px - var(--tabs-list-gap));\n margin-block-start: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-scroll {\n flex: none;\n /* The bar stretches its children for the track's sake, so the scroll buttons have\n to re-centre themselves on the row — which in the segmented variant is taller\n than they are. */\n align-self: center;\n }\n\n .v-tabs-panels {\n min-inline-size: 0;\n }\n\n /*\n * Flat and segmented, the panels sit in no card, so they take no gutter: their content\n * has to stay aligned with whatever the page puts above and below the component, which\n * an indent would break. What they do owe is the separation from the bar, the boundary\n * between two parts of the same component rather than the parent layout's business —\n * which the framed variant folds into the gutter it gives its panels on all four sides.\n *\n * The axis follows the ORIENTATION and not the writing mode, so it cannot be a single\n * logical declaration: the bar is above the panels when horizontal and beside them when\n * vertical.\n *\n * Both variants are named explicitly rather than excluding the framed one, as in the\n * track rules above: a fourth frame opts in by hand.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='horizontal']\n > .v-tabs-panels {\n padding-block-start: var(--tabs-panels-pad);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='vertical']\n > .v-tabs-panels {\n padding-inline-start: var(--tabs-panels-pad);\n }\n\n /* Inside a card the gutter is the panels' alone, on all four sides: the bar spends\n none, so the tabs and their track reach the frame and the content is the only thing\n set back from it. */\n .v-tabs[data-variant='outlined'] > .v-tabs-panels {\n padding: var(--tabs-panels-pad);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-panels {\n flex: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-tabs-list {\n scroll-behavior: auto;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VTabs.js","names":[],"sources":["../../../src/components/VTabs/VTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * Implement ARIA tab selection and roving focus over native buttons. Overflow measurements\n * control scrolling arrows because CSS cannot expose overflow state.\n */\n\nimport { computed, onMounted, provide, ref, useId, useSlots, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { expand_less as expandLessIcon } from '../VIcon/icons/expand_less'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport { panelIdFor, tabIdFor, tabsKey } from './context'\n\nimport type { ItemValue } from '../../types'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the bar is drawn: a rule under the tabs, the same inside a card, or a sunken track. */\nexport type TabsVariant = 'flat' | 'outlined' | 'inset'\n/** The colour of the selected tab. */\nexport type TabsTone = 'accent' | 'neutral' | 'danger'\n/** The height of the tabs, from the scale every control shares. */\nexport type TabsSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the tabs run across the page or down its side. */\nexport type TabsOrientation = 'horizontal' | 'vertical'\n/** Where the tabs sit along the bar when they do not fill it. */\nexport type TabsAlign = 'start' | 'center' | 'end'\n/** Whether a tab is selected when it takes the focus, or only when it is activated. */\nexport type TabsActivation = 'manual' | 'automatic'\n\ninterface TabsProps {\n /** How the bar is framed. */\n variant?: TabsVariant\n /** The colour the selected tab takes. The others stay neutral whatever this says. */\n tone?: TabsTone\n /** The height of the tabs, from the scale shared by every control. */\n size?: TabsSize\n /** Takes 4px off the height of every tab. */\n compact?: boolean\n /** Whether the tabs run across the page or down its side. */\n orientation?: TabsOrientation\n /** Where the tabs sit along the bar when they do not fill it. */\n align?: TabsAlign\n /**\n * Stretches the tabs across the whole bar, every tab taking an equal share of it, on the\n * terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * Adds a button at each end of the bar to scroll it, each disabled once that end is\n * reached. It only makes sense when the tabs can overflow, so it excludes `fullWidth`.\n */\n scrollButtons?: boolean\n /** The icon of the button scrolling backwards. It follows the orientation by default. */\n prevIcon?: IconSource\n /** The icon of the button scrolling forwards. It follows the orientation by default. */\n nextIcon?: IconSource\n /** What the backward scroll button does, in words. It falls back to the dictionary. */\n prevLabel?: string\n /** What the forward scroll button does, in words. It falls back to the dictionary. */\n nextLabel?: string\n /** Whether moving to a tab also selects it. */\n activation?: TabsActivation\n /**\n * Makes every tab unusable, and the scroll buttons with them: the tabs leave the tab order\n * and grey out through the colour tokens.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row of tabs. It falls back to the design\n * system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<TabsProps>(), {\n variant: 'flat',\n tone: 'accent',\n size: 'md',\n compact: false,\n orientation: 'horizontal',\n align: 'start',\n fullWidth: false,\n scrollButtons: false,\n prevIcon: undefined,\n nextIcon: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n activation: 'manual',\n disabled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The tabs themselves. */\n default(): unknown\n /**\n * The panels the tabs show. Leaving it out renders no panel area at all, which is\n * how the same component serves as a plain bar or a segmented control.\n */\n panels?(): unknown\n}>()\n\n/**\n * The `value` of the selected tab. There is deliberately no default: the component cannot know\n * which of the tabs a consumer wrote should open.\n */\nconst model = defineModel<ItemValue>()\n\n// The root element is only a container; the one that matters is the row of tabs. So `class` and\n// `style` stay outside, where a consumer expects to place the component, and everything else;\n// the id, the aria-* naming it, the data-*; goes down onto the row itself.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs: listAttrs } = useRootAttrs()\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.tabs.label)\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? m.value.tabs.previous)\nconst resolvedNextLabel = computed(() => props.nextLabel ?? m.value.tabs.next)\n\nconst slots = useSlots()\nconst baseId = useId()\n\n// Everything is exposed through getters rather than as values: that is what keeps the\n// root's props reactive on the other side of the injection, so a tab re-renders when\n// the size or the tone changes.\nprovide(tabsKey, {\n get value() {\n return model.value\n },\n select(value: ItemValue) {\n model.value = value\n },\n tabId: (value: ItemValue) => tabIdFor(baseId, value),\n panelId: (value: ItemValue) => panelIdFor(baseId, value),\n // @ssr\n /*\n * A registry the panels filled in as they mounted would instead read as empty on the server\n * and full on the client, which is a hydration mismatch. The trade-off is that this is not\n * reactive: the panels slot must be there or not there, and cannot appear halfway through the\n * life of the component.\n */\n get hasPanels() {\n return slots.panels !== undefined\n },\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get activation() {\n return props.activation\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst resolvedPrevIcon = computed(\n () => props.prevIcon ?? (isVertical.value ? expandLessIcon : chevronLeftIcon),\n)\nconst resolvedNextIcon = computed(\n () => props.nextIcon ?? (isVertical.value ? expandMoreIcon : chevronRightIcon),\n)\n\nconst listEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/*\n * This handler only moves the focus. Selecting the tab that receives it; what `automatic`\n * activation means; is done by the tab itself, which knows its own typed value and therefore\n * never has to send it back through a DOM attribute.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const list = listEl.value\n if (!list) return\n arrowNavigate(event, list, () => navigableItems(list, '[role=\"tab\"]:not(:disabled)'), {\n vertical: isVertical.value,\n })\n}\n\n/*\n * CSS can ask the question; VDialog uses that query for its scroll shadows; and still cannot\n * answer this one, for two independent reasons. It is Chrome/Edge 133+, with neither Safari nor\n * Firefox shipping it, and where VDialog's hairlines merely stay invisible without it these\n * buttons are functional; an `@supports` guard would mean keeping everything below anyway.\n */\nconst startSentinelEl = ref<HTMLElement | null>(null)\nconst endSentinelEl = ref<HTMLElement | null>(null)\nconst atStart = ref(true)\nconst atEnd = ref(true)\n\nwatch(\n [listEl, startSentinelEl, endSentinelEl],\n ([root, start, end], _previous, onCleanup) => {\n // @fallback\n // The observer exists neither on the server nor in the unit-test environment, so\n // its absence has to be tolerated rather than assumed away. The behaviour of the\n // ends is checked in a real browser instead, by the play functions.\n if (!root || !start || !end || typeof IntersectionObserver === 'undefined') return\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.target === start) atStart.value = entry.isIntersecting\n else atEnd.value = entry.isIntersecting\n }\n },\n { root },\n )\n observer.observe(start)\n observer.observe(end)\n onCleanup(() => observer.disconnect())\n },\n // `post` so the effect runs once the DOM is up to date: in the default timing the three\n // template refs are still null on the first pass, the guard above returns, and the buttons\n // stay disabled for good; the effect having tracked nothing to wake it again.\n { flush: 'post' },\n)\n\n// @a11y\n/*\n * The default `pre` timing is the point: the button still holds the focus when this runs, the\n * DOM not being patched with `disabled` yet. In `post` timing the focus would already be on\n * <body>, with nothing left to say which control lost it.\n */\nwatch([atStart, atEnd], ([start, end]) => {\n const list = listEl.value\n const prev = list?.previousElementSibling as HTMLElement | null\n const next = list?.nextElementSibling as HTMLElement | null\n const focused = document.activeElement\n const fromPrev = focused === prev\n if (!(fromPrev ? start : focused === next && end)) return\n if (fromPrev ? end : start) focusSelected()\n else (fromPrev ? next : prev)?.focus()\n})\n\n/** Scrolls the bar by most of its own length. */\nfunction scrollStep(direction: -1 | 1) {\n const list = listEl.value\n if (!list) return\n // Called optionally because the unit-test environment implements no scrolling at\n // all; this behaviour is covered in a real browser.\n if (isVertical.value) {\n list.scrollBy?.({ top: direction * list.clientHeight * 0.8 })\n return\n }\n // The horizontal offset is physical, not logical: in a right-to-left page, moving\n // forward means moving left, so the direction has to be flipped by hand.\n const rtl = isRtl(list)\n list.scrollBy?.({ left: direction * list.clientWidth * 0.8 * (rtl ? -1 : 1) })\n}\n\n// @a11y @core\n/*\n * The keyboard needs nothing here: it moves the focus, and the browser scrolls a focused\n * element into view by itself. The scrolling is confined to the row rather than asking the\n * element to reveal itself, which would scroll every scrollable ancestor up to the page.\n */\nfunction revealSelected(behavior?: ScrollBehavior) {\n const list = listEl.value\n const tab = list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]')\n if (!list || !tab) return\n const t = tab.getBoundingClientRect()\n const c = list.getBoundingClientRect()\n list.scrollBy?.({\n left: t.left < c.left ? t.left - c.left : t.right > c.right ? t.right - c.right : 0,\n top: t.top < c.top ? t.top - c.top : t.bottom > c.bottom ? t.bottom - c.bottom : 0,\n behavior,\n })\n}\n\n// `post`, so the tab that is now selected already carries `aria-selected` when it is looked\n// for: in the default timing the query would find the one that was selected before.\nwatch(model, () => revealSelected(), { flush: 'post' })\nonMounted(() => revealSelected('instant'))\n\n// `focus` goes where the Tab key would land: the selected tab, which holds the row's single tab\n// stop.\nfunction focusSelected(options?: FocusOptions) {\n const list = listEl.value\n const tab =\n list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]:not(:disabled)') ??\n list?.querySelector<HTMLElement>('[role=\"tab\"]:not(:disabled)')\n tab?.focus(options)\n}\n\ndefineExpose({\n /**\n * Moves the focus to the selected tab, or to the first tab that can take it when the\n * v-model names none.\n */\n focus: focusSelected,\n /** The `role=\"tablist\"` row, which is also where the consumer's attributes land. */\n el: listEl,\n})\n</script>\n\n<template>\n <div\n :class=\"['v-tabs', rootClass]\"\n :style=\"rootStyle\"\n :data-variant=\"variant\"\n :data-orientation=\"orientation\"\n :data-align=\"align\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n >\n <div class=\"v-tabs-bar\">\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedPrevLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atStart\"\n @click=\"scrollStep(-1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedPrevIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n\n <div\n ref=\"listEl\"\n :aria-orientation=\"isVertical ? 'vertical' : undefined\"\n v-bind=\"listAttrs\"\n class=\"v-tabs-list\"\n role=\"tablist\"\n :aria-label=\"ariaLabel\"\n @keydown=\"onKeydown\"\n >\n <!-- The two markers watched to know whether an end of the bar is reached.\n They are hidden from assistive technology: a row of tabs may contain\n nothing but tabs. -->\n <span\n v-if=\"scrollButtons\"\n ref=\"startSentinelEl\"\n class=\"v-tabs-sentinel\"\n aria-hidden=\"true\"\n />\n <slot />\n <span v-if=\"scrollButtons\" ref=\"endSentinelEl\" class=\"v-tabs-sentinel\" aria-hidden=\"true\" />\n </div>\n\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedNextLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atEnd\"\n @click=\"scrollStep(1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedNextIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n </div>\n\n <div v-if=\"$slots.panels\" class=\"v-tabs-panels\">\n <slot name=\"panels\" />\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-tabs {\n /*\n * The panels' padding, and the four pixels the compact density takes off it. The delta is\n * held apart from the value rather than the two being written out, so the density stays one\n * subtraction wherever the padding is read instead of a second table to keep in step; the\n * VAccordion idiom.\n */\n --tabs-pad-delta: 0px;\n --tabs-panels-pad: calc(var(--vectis-space-4) - var(--tabs-pad-delta));\n\n display: flex;\n flex-direction: column;\n font-family: var(--vectis-text-family);\n }\n\n .v-tabs[data-compact] {\n --tabs-pad-delta: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] {\n flex-direction: row;\n }\n\n /*\n * Keep card inner-radius calculations aligned with the tab corners; avoid clipping panel\n * focus rings and edge-to-edge consumer content.\n */\n .v-tabs[data-variant='outlined'] {\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs-bar {\n display: flex;\n /* Stretched rather than centred: the row deliberately overlaps the track by one\n pixel through a negative margin, and centring would displace it by half of\n that. */\n align-items: stretch;\n /*\n * The row is allowed to shrink below its content as soon as the tabs overflow, so there\n * would be nothing left to distribute; and setting an alignment on a scrolling container\n * makes whatever overflows past its start edge permanently unreachable, since a scroll\n * position cannot go negative.\n */\n justify-content: flex-start;\n gap: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar {\n flex-direction: column;\n }\n\n .v-tabs[data-align='center'] > .v-tabs-bar {\n justify-content: center;\n }\n\n .v-tabs[data-align='end'] > .v-tabs-bar {\n justify-content: flex-end;\n }\n\n /*\n * Inside a card it is what separates the tabs from the panels; which is why that edge is left\n * without a gutter, since otherwise the rule would separate nothing from nothing. Both frames\n * are named explicitly rather than excluding the third: a fourth frame will have to opt in by\n * hand instead of inheriting this silently.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar {\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar {\n border-block-end: none;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * This block has exactly the same specificity as the one above, so it is the ORDER that\n * decides. Moving it up would leave the vertical framed case with two rules on one side and\n * none between the two areas.\n */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar {\n border-inline-start: none;\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs-list {\n display: flex;\n align-items: center;\n /* Held in a variable because the sentinels cancel it beside themselves. */\n --tabs-list-gap: var(--vectis-space-1);\n gap: var(--tabs-list-gap);\n overflow: auto;\n /* Without these the row refuses to shrink below the width of its tabs, and it\n would widen the whole bar instead of scrolling. */\n min-inline-size: 0;\n min-block-size: 0;\n scrollbar-width: none;\n scroll-behavior: smooth;\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n flex-direction: column;\n align-items: stretch;\n }\n\n /* Pulling the row one pixel into the track is what lets the selected tab's indicator\n COVER that line rather than sit on top of it, which would read as a thicker rule.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: -1px;\n /* On a track the tabs are contiguous segments and not a queue of buttons: no gap\n here, and no rounded corners on the tabs themselves. */\n --tabs-list-gap: 0px;\n }\n\n /* The row is now a pixel taller than the tabs it holds, so they are pushed against\n its end edge; the indicator each tab draws there then falls exactly on the\n track. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='horizontal']\n > .v-tabs-bar\n > .v-tabs-list {\n align-items: flex-end;\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar\n > .v-tabs-list {\n margin-block-end: 0;\n margin-inline-start: -1px;\n }\n\n /* Same specificity as the rule above, so again it is the order that decides. */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n margin-inline-start: 0;\n }\n\n /*\n * This is the one place the card may clip, and the one place it must. Nothing is lost to the\n * clip: there is no panel, hence no outer focus ring, and a tab draws its own ring inwards.\n */\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) {\n overflow: clip;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar {\n border-block-end: none;\n border-inline-end: none;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: 0;\n }\n\n /* The hollow track of the segmented variant is drawn on the scrolling row itself,\n and not on the bar around it: its padding is what keeps the raised tab's shadow\n from being clipped as the row scrolls. */\n .v-tabs[data-variant='inset'] > .v-tabs-bar > .v-tabs-list {\n background: var(--vectis-color-surface-sunken);\n padding: var(--vectis-space-1);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs[data-full-width] > .v-tabs-bar > .v-tabs-list {\n flex: 1;\n }\n\n /*\n * A pixel square each, at the very ends of the row. They are items of the row, so its gap\n * opens beside them as it does between two tabs: each pulls its neighbour back over that gap\n * and over its own pixel, so the first and last tabs sit exactly where the row's padding puts\n * them.\n */\n .v-tabs-sentinel {\n flex: 0 0 1px;\n align-self: flex-start;\n inline-size: 1px;\n block-size: 1px;\n }\n\n .v-tabs-sentinel:first-child {\n margin-inline-end: calc(-1px - var(--tabs-list-gap));\n margin-block-end: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-sentinel:last-child {\n margin-inline-start: calc(-1px - var(--tabs-list-gap));\n margin-block-start: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-scroll {\n flex: none;\n /*\n * The bar stretches its children for the track's sake, so the scroll buttons have to\n * re-centre themselves on the row; which in the segmented variant is taller than they are.\n */\n align-self: center;\n }\n\n .v-tabs-panels {\n min-inline-size: 0;\n }\n\n /*\n * The axis follows the ORIENTATION and not the writing mode, so it cannot be a single logical\n * declaration: the bar is above the panels when horizontal and beside them when vertical.\n * Both variants are named explicitly rather than excluding the framed one, as in the track\n * rules above: a fourth frame opts in by hand.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='horizontal']\n > .v-tabs-panels {\n padding-block-start: var(--tabs-panels-pad);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='vertical']\n > .v-tabs-panels {\n padding-inline-start: var(--tabs-panels-pad);\n }\n\n /* Inside a card the gutter is the panels' alone, on all four sides: the bar spends\n none, so the tabs and their track reach the frame and the content is the only thing\n set back from it. */\n .v-tabs[data-variant='outlined'] > .v-tabs-panels {\n padding: var(--tabs-panels-pad);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-panels {\n flex: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-tabs-list {\n scroll-behavior: auto;\n }\n }\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VTabs.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VTabs/VTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * A row of tabs, and optionally the panels they show. Only one panel is visible at a\n * time, and the tabs follow the pattern assistive technology expects of them: the row\n * is a single stop in the tab order, and the arrow keys move between the tabs inside\n * it.\n *\n * No button is invented here. Every tab is a VButton and the scroll controls are\n * VIconButtons, so hovering, focusing, disabling and the respect of a reader's\n * reduced-motion setting all come from there, without a single state rule being\n * written again in this file.\n *\n * Nothing touches the DOM outside event handlers and effects that run after the page\n * has been updated, which is what lets the component render on a server.\n */\n\nimport { computed, onMounted, provide, ref, useId, useSlots, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { expand_less as expandLessIcon } from '../VIcon/icons/expand_less'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport { panelIdFor, tabIdFor, tabsKey } from './context'\n\nimport type { ItemValue } from '../../types'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the bar is drawn: a rule under the tabs, the same inside a card, or a sunken track. */\nexport type TabsVariant = 'flat' | 'outlined' | 'inset'\n/** The colour of the selected tab. */\nexport type TabsTone = 'accent' | 'neutral' | 'danger'\n/** The height of the tabs, from the scale every control shares. */\nexport type TabsSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the tabs run across the page or down its side. */\nexport type TabsOrientation = 'horizontal' | 'vertical'\n/** Where the tabs sit along the bar when they do not fill it. */\nexport type TabsAlign = 'start' | 'center' | 'end'\n/** Whether a tab is selected when it takes the focus, or only when it is activated. */\nexport type TabsActivation = 'manual' | 'automatic'\n\ninterface TabsProps {\n /**\n * How the bar is framed. `flat` draws nothing but a rule under the tabs, with the\n * selected one underlined; `outlined` puts that same bar and its panels inside a\n * card; `inset` turns the row into a segmented control sitting in a hollow track.\n */\n variant?: TabsVariant\n /** The colour the selected tab takes. The others stay neutral whatever this says. */\n tone?: TabsTone\n /** The height of the tabs, from the scale shared by every control. */\n size?: TabsSize\n /** Takes 4px off the height of every tab. */\n compact?: boolean\n /** Whether the tabs run across the page or down its side. */\n orientation?: TabsOrientation\n /** Where the tabs sit along the bar when they do not fill it. */\n align?: TabsAlign\n /**\n * Stretches the tabs across the whole bar, every tab taking an equal share of it, on the\n * terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * Adds a button at each end of the bar to scroll it, each disabled once that end is\n * reached. It only makes sense when the tabs can overflow, so it excludes `fullWidth`.\n */\n scrollButtons?: boolean\n /** The icon of the button scrolling backwards. It follows the orientation by default. */\n prevIcon?: IconSource\n /** The icon of the button scrolling forwards. It follows the orientation by default. */\n nextIcon?: IconSource\n /** What the backward scroll button does, in words. It falls back to the dictionary. */\n prevLabel?: string\n /** What the forward scroll button does, in words. It falls back to the dictionary. */\n nextLabel?: string\n /**\n * Whether moving to a tab also selects it. Selecting on arrival is what the ARIA\n * authoring practices recommend when a panel appears instantly; leave it manual when\n * showing a panel costs a request, or every tab passed over would fire one.\n */\n activation?: TabsActivation\n /**\n * Makes every tab unusable, and the scroll buttons with them: the tabs leave the tab order\n * and grey out through the colour tokens. A tab disabled on its own stays disabled either\n * way. The panel on show stays on show.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row of tabs. It falls back to the design\n * system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<TabsProps>(), {\n variant: 'flat',\n tone: 'accent',\n size: 'md',\n compact: false,\n orientation: 'horizontal',\n align: 'start',\n fullWidth: false,\n scrollButtons: false,\n prevIcon: undefined,\n nextIcon: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n activation: 'manual',\n disabled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The tabs themselves. */\n default(): unknown\n /**\n * The panels the tabs show. Leaving it out renders no panel area at all, which is\n * how the same component serves as a plain bar or a segmented control.\n */\n panels?(): unknown\n}>()\n\n/**\n * The `value` of the selected tab. There is deliberately no default: the component cannot\n * know which of the tabs a consumer wrote should open.\n *\n * CONTRACT: it must name a tab that exists and is not disabled. Pointing it anywhere else\n * leaves no tab with a tab stop, and the bar becomes unreachable from the keyboard.\n */\nconst model = defineModel<ItemValue>()\n\n// The root element is only a container; the one that matters is the row of tabs. So\n// `class` and `style` stay outside, where a consumer expects to place the component,\n// and everything else — the id, the aria-* naming it, the data-* — goes down onto the\n// row itself.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs: listAttrs } = useRootAttrs()\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins — that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.tabs.label)\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? m.value.tabs.previous)\nconst resolvedNextLabel = computed(() => props.nextLabel ?? m.value.tabs.next)\n\nconst slots = useSlots()\nconst baseId = useId()\n\n// Everything is exposed through getters rather than as values: that is what keeps the\n// root's props reactive on the other side of the injection, so a tab re-renders when\n// the size or the tone changes.\nprovide(tabsKey, {\n get value() {\n return model.value\n },\n select(value: ItemValue) {\n model.value = value\n },\n tabId: (value: ItemValue) => tabIdFor(baseId, value),\n panelId: (value: ItemValue) => panelIdFor(baseId, value),\n // @ssr\n /*\n * Whether a slot was given is decided by the parent as it renders, so the answer is\n * the same on the server and in the browser. A registry the panels filled in as they\n * mounted would instead read as empty on the server and full on the client, which is\n * a hydration mismatch.\n *\n * The trade-off is that this is not reactive: the panels slot must be there or not\n * there, and cannot appear halfway through the life of the component.\n */\n get hasPanels() {\n return slots.panels !== undefined\n },\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get activation() {\n return props.activation\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst resolvedPrevIcon = computed(\n () => props.prevIcon ?? (isVertical.value ? expandLessIcon : chevronLeftIcon),\n)\nconst resolvedNextIcon = computed(\n () => props.nextIcon ?? (isVertical.value ? expandMoreIcon : chevronRightIcon),\n)\n\nconst listEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/*\n * The arrow keys, through the shared implementation in `utils/arrowNav`. This handler\n * ONLY moves the focus. Selecting the tab that receives it — what `automatic`\n * activation means — is done by the tab itself, which knows its own typed value and\n * therefore never has to send it back through a DOM attribute.\n *\n * A tab the consumer has hidden is left out by the helper, which skips anything not\n * displayed.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const list = listEl.value\n if (!list) return\n arrowNavigate(event, list, () => navigableItems(list, '[role=\"tab\"]:not(:disabled)'), {\n vertical: isVertical.value,\n })\n}\n\n/*\n * Whether each end of the bar has been reached, which is what disables the scroll buttons.\n *\n * READ THIS BEFORE PROPOSING `@container scroll-state` AGAIN. CSS can ask the question —\n * VDialog uses that query for its scroll shadows — and still cannot answer this one, for two\n * independent reasons. It is Chrome/Edge 133+, with neither Safari nor Firefox shipping it,\n * and where VDialog's hairlines merely stay invisible without it these buttons are\n * functional; an `@supports` guard would mean keeping everything below anyway. And CSS\n * cannot set `disabled` on a button: hiding or greying an arrow is a different affordance\n * and a different announcement, not the same behaviour written another way.\n *\n * So two hairline sentinels sit at the ends of the row and are observed. ONE observer then\n * covers all three things that change the answer — scrolling, a resize, tabs added or\n * removed — where measuring the scroll would need a second mechanism for the resize alone.\n *\n * Both start as reached, so the buttons are disabled on the first render and on the server,\n * never wrongly enabled. That also gives the \"nothing overflows\" case for free: both\n * sentinels are visible at once, so both buttons stay disabled with no test of their own.\n */\nconst startSentinelEl = ref<HTMLElement | null>(null)\nconst endSentinelEl = ref<HTMLElement | null>(null)\nconst atStart = ref(true)\nconst atEnd = ref(true)\n\nwatch(\n [listEl, startSentinelEl, endSentinelEl],\n ([root, start, end], _previous, onCleanup) => {\n // @fallback\n // The observer exists neither on the server nor in the unit-test environment, so\n // its absence has to be tolerated rather than assumed away. The behaviour of the\n // ends is checked in a real browser instead, by the play functions.\n if (!root || !start || !end || typeof IntersectionObserver === 'undefined') return\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.target === start) atStart.value = entry.isIntersecting\n else atEnd.value = entry.isIntersecting\n }\n },\n { root },\n )\n observer.observe(start)\n observer.observe(end)\n onCleanup(() => observer.disconnect())\n },\n // `post` so the effect runs once the DOM is up to date: in the default timing the three\n // template refs are still null on the first pass, the guard above returns, and the\n // buttons stay disabled for good — the effect having tracked nothing to wake it again.\n { flush: 'post' },\n)\n\n// @a11y\n/*\n * A scroll button disables itself once its end is reached, and a focused button that becomes\n * disabled hands the focus to <body>: a keyboard reader pressing \"next\" until the end would be\n * sent back to the top of the page by the next Tab. The focus moves on first, to the opposite\n * button, which the row has just scrolled away from, or to the selected tab when both ends are\n * reached at once (the row no longer overflows). The two buttons are the list's own siblings.\n *\n * TRAP — the default `pre` timing is the point: the button still holds the focus when this\n * runs, the DOM not being patched with `disabled` yet. In `post` timing the focus would\n * already be on <body>, with nothing left to say which control lost it.\n */\nwatch([atStart, atEnd], ([start, end]) => {\n const list = listEl.value\n const prev = list?.previousElementSibling as HTMLElement | null\n const next = list?.nextElementSibling as HTMLElement | null\n const focused = document.activeElement\n const fromPrev = focused === prev\n if (!(fromPrev ? start : focused === next && end)) return\n if (fromPrev ? end : start) focusSelected()\n else (fromPrev ? next : prev)?.focus()\n})\n\n/**\n * Scrolls the bar by most of its own length. No scrolling behaviour is requested,\n * deliberately: leaving it out lets the stylesheet decide, which is what makes the\n * movement respect a reader who has asked for less motion.\n */\nfunction scrollStep(direction: -1 | 1) {\n const list = listEl.value\n if (!list) return\n // Called optionally because the unit-test environment implements no scrolling at\n // all; this behaviour is covered in a real browser.\n if (isVertical.value) {\n list.scrollBy?.({ top: direction * list.clientHeight * 0.8 })\n return\n }\n // The horizontal offset is physical, not logical: in a right-to-left page, moving\n // forward means moving left, so the direction has to be flipped by hand.\n const rtl = isRtl(list)\n list.scrollBy?.({ left: direction * list.clientWidth * 0.8 * (rtl ? -1 : 1) })\n}\n\n// @a11y @core\n/*\n * Brings the selected tab back into view when the selection was changed from code.\n * The keyboard needs nothing here: it moves the focus, and the browser scrolls a\n * focused element into view by itself.\n *\n * The scrolling is confined to the row rather than asking the element to reveal\n * itself, which would scroll every scrollable ancestor up to the page. The distances\n * are read from the two boxes, so they are already physical: the same code serves\n * left-to-right, right-to-left and vertical without a single test of direction.\n *\n * It also runs once on mount, INSTANTLY: a row opened on a tab past its visible end (a deep\n * link, a restored route) would otherwise show the first tabs with the selected one out of\n * sight, and a smooth scroll would animate the page as it loads.\n */\nfunction revealSelected(behavior?: ScrollBehavior) {\n const list = listEl.value\n const tab = list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]')\n if (!list || !tab) return\n const t = tab.getBoundingClientRect()\n const c = list.getBoundingClientRect()\n list.scrollBy?.({\n left: t.left < c.left ? t.left - c.left : t.right > c.right ? t.right - c.right : 0,\n top: t.top < c.top ? t.top - c.top : t.bottom > c.bottom ? t.bottom - c.bottom : 0,\n behavior,\n })\n}\n\n// `post`, so the tab that is now selected already carries `aria-selected` when it is looked\n// for: in the default timing the query would find the one that was selected before.\nwatch(model, () => revealSelected(), { flush: 'post' })\nonMounted(() => revealSelected('instant'))\n\n// The root is a container and the tabs are rendered by the consumer's slot, so a template\n// ref reaches neither the row nor a tab. `focus` goes where the Tab key would land: the\n// selected tab, which holds the row's single tab stop.\nfunction focusSelected(options?: FocusOptions) {\n const list = listEl.value\n const tab =\n list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]:not(:disabled)') ??\n list?.querySelector<HTMLElement>('[role=\"tab\"]:not(:disabled)')\n tab?.focus(options)\n}\n\ndefineExpose({\n /**\n * Moves the focus to the selected tab, or to the first tab that can take it when the\n * v-model names none.\n */\n focus: focusSelected,\n /** The `role=\"tablist\"` row, which is also where the consumer's attributes land. */\n el: listEl,\n})\n</script>\n\n<template>\n <div\n :class=\"['v-tabs', rootClass]\"\n :style=\"rootStyle\"\n :data-variant=\"variant\"\n :data-orientation=\"orientation\"\n :data-align=\"align\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n >\n <div class=\"v-tabs-bar\">\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedPrevLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atStart\"\n @click=\"scrollStep(-1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedPrevIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n\n <div\n ref=\"listEl\"\n :aria-orientation=\"isVertical ? 'vertical' : undefined\"\n v-bind=\"listAttrs\"\n class=\"v-tabs-list\"\n role=\"tablist\"\n :aria-label=\"ariaLabel\"\n @keydown=\"onKeydown\"\n >\n <!-- The two markers watched to know whether an end of the bar is reached.\n They are hidden from assistive technology: a row of tabs may contain\n nothing but tabs. -->\n <span\n v-if=\"scrollButtons\"\n ref=\"startSentinelEl\"\n class=\"v-tabs-sentinel\"\n aria-hidden=\"true\"\n />\n <slot />\n <span v-if=\"scrollButtons\" ref=\"endSentinelEl\" class=\"v-tabs-sentinel\" aria-hidden=\"true\" />\n </div>\n\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedNextLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atEnd\"\n @click=\"scrollStep(1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedNextIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n </div>\n\n <div v-if=\"$slots.panels\" class=\"v-tabs-panels\">\n <slot name=\"panels\" />\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-tabs {\n /*\n * The panels' padding, and the four pixels the compact density takes off it. The\n * delta is held apart from the value rather than the two being written out, so the\n * density stays one subtraction wherever the padding is read instead of a second\n * table to keep in step — the VAccordion idiom.\n *\n * It follows the density and deliberately not the SIZE: the padding is a property of\n * the layout, and in any case the size variables live on the descendant buttons, each\n * tab declaring its own, so the root could not read them.\n */\n --tabs-pad-delta: 0px;\n --tabs-panels-pad: calc(var(--vectis-space-4) - var(--tabs-pad-delta));\n\n display: flex;\n flex-direction: column;\n font-family: var(--vectis-text-family);\n }\n\n .v-tabs[data-compact] {\n --tabs-pad-delta: var(--vectis-space-1);\n }\n\n /* Turned vertical, the bar and the panels sit side by side rather than stacked. */\n .v-tabs[data-orientation='vertical'] {\n flex-direction: row;\n }\n\n /*\n * The card, taken from the decoration scale shared with VAccordion and VDataTable.\n * The flat default has nothing to undo, since it declares no decoration at all.\n *\n * The frame goes on the ROOT, so it encloses the bar AND the panels, and the rule\n * under the tabs then becomes the boundary between the two inside it.\n *\n * The bar spends NO gutter: the tabs sit against the card's border and the track runs\n * from edge to edge, so the whole of the padding is the panels'. That is also what\n * lands an end tab exactly on a corner of the card, where its outer radius has to be\n * the frame's INNER one — VTab.vue derives it, and the two cannot move apart without\n * the corner showing on hover.\n *\n * Unlike VDataTable, nothing is clipped here: clipping would cut the panel's focus\n * ring, the only one that leaves the component, and forbid a consumer any content\n * running edge to edge. The bar with no panels under it is the single exception, and\n * it is handled where that case is.\n */\n .v-tabs[data-variant='outlined'] {\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs-bar {\n display: flex;\n /* Stretched rather than centred: the row deliberately overlaps the track by one\n pixel through a negative margin, and centring would displace it by half of\n that. */\n align-items: stretch;\n /*\n * The alignment belongs to the BAR and not to the row of tabs. The row is allowed\n * to shrink below its content as soon as the tabs overflow, so there would be\n * nothing left to distribute; and setting an alignment on a scrolling container\n * makes whatever overflows past its start edge permanently unreachable, since a\n * scroll position cannot go negative.\n */\n justify-content: flex-start;\n gap: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar {\n flex-direction: column;\n }\n\n .v-tabs[data-align='center'] > .v-tabs-bar {\n justify-content: center;\n }\n\n .v-tabs[data-align='end'] > .v-tabs-bar {\n justify-content: flex-end;\n }\n\n /* The rule the tabs sit on, in the flat and outlined frames. It is drawn on the BAR\n rather than on the row, so that it also runs under the scroll buttons, which are\n siblings of the row and not part of it. Inside a card it is what separates the\n tabs from the panels — which is why that edge is left without a gutter, since\n otherwise the rule would separate nothing from nothing.\n\n Both frames are named explicitly rather than excluding the third: a fourth frame\n will have to opt in by hand instead of inheriting this silently. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar {\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar {\n border-block-end: none;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * Framed AND vertical, the rule MIGRATES to the other edge. Flat, it marks the edge\n * of the tab column, hence the start; inside a card that edge is already drawn by\n * the frame's own border — two parallel lines a pixel apart — and what is missing is\n * the boundary between the tabs and the panels.\n *\n * TRAP — this block has exactly the same specificity as the one above, so it is the\n * ORDER that decides. Moving it up would leave the vertical framed case with two\n * rules on one side and none between the two areas.\n */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar {\n border-inline-start: none;\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs-list {\n display: flex;\n align-items: center;\n /* Held in a variable because the sentinels cancel it beside themselves. */\n --tabs-list-gap: var(--vectis-space-1);\n gap: var(--tabs-list-gap);\n overflow: auto;\n /* Without these the row refuses to shrink below the width of its tabs, and it\n would widen the whole bar instead of scrolling. */\n min-inline-size: 0;\n min-block-size: 0;\n scrollbar-width: none;\n scroll-behavior: smooth;\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n flex-direction: column;\n align-items: stretch;\n }\n\n /* Pulling the row one pixel into the track is what lets the selected tab's indicator\n COVER that line rather than sit on top of it, which would read as a thicker rule.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: -1px;\n /* On a track the tabs are contiguous segments and not a queue of buttons: no gap\n here, and no rounded corners on the tabs themselves. */\n --tabs-list-gap: 0px;\n }\n\n /* The row is now a pixel taller than the tabs it holds, so they are pushed against\n its end edge; the indicator each tab draws there then falls exactly on the\n track. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='horizontal']\n > .v-tabs-bar\n > .v-tabs-list {\n align-items: flex-end;\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar\n > .v-tabs-list {\n margin-block-end: 0;\n margin-inline-start: -1px;\n }\n\n /* Framed AND vertical, there is no longer a track on that edge to cover, since the\n rule has migrated to the other side: the one-pixel overlap would simply slide the\n row under the frame's own border.\n\n TRAP — same specificity as the rule above, so again it is the order that decides. */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n margin-inline-start: 0;\n }\n\n /*\n * A framed bar with no panels: the card is reduced to the tabs alone. There is no\n * longer a boundary to mark, and the track would run right alongside the frame's own\n * border, doubling it. So the frame takes its place and the row stops overlapping.\n *\n * This is the one place the card may clip, and the one place it must. The tabs fill\n * it on all four sides here, so a hovered end tab reaches the two corners no radius\n * of its own covers — the ends round the edge the track used to occupy, and that one\n * only. Nothing is lost to the clip: there is no panel, hence no outer focus ring,\n * and a tab draws its own ring inwards.\n */\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) {\n overflow: clip;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar {\n border-block-end: none;\n border-inline-end: none;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: 0;\n }\n\n /* The hollow track of the segmented variant is drawn on the scrolling row itself,\n and not on the bar around it: its padding is what keeps the raised tab's shadow\n from being clipped as the row scrolls. */\n .v-tabs[data-variant='inset'] > .v-tabs-bar > .v-tabs-list {\n background: var(--vectis-color-surface-sunken);\n padding: var(--vectis-space-1);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs[data-full-width] > .v-tabs-bar > .v-tabs-list {\n flex: 1;\n }\n\n /*\n * A pixel square each, at the very ends of the row. They are items of the row, so its gap\n * opens beside them as it does between two tabs: each pulls its neighbour back over that\n * gap and over its own pixel, so the first and last tabs sit exactly where the row's\n * padding puts them. The pull is written on both axes, only the main one doing anything.\n *\n * Aligned to the start rather than stretched: a negative cross-axis margin on a stretched\n * item would GROW it past the row, which then scrolls on the other axis too.\n */\n .v-tabs-sentinel {\n flex: 0 0 1px;\n align-self: flex-start;\n inline-size: 1px;\n block-size: 1px;\n }\n\n .v-tabs-sentinel:first-child {\n margin-inline-end: calc(-1px - var(--tabs-list-gap));\n margin-block-end: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-sentinel:last-child {\n margin-inline-start: calc(-1px - var(--tabs-list-gap));\n margin-block-start: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-scroll {\n flex: none;\n /* The bar stretches its children for the track's sake, so the scroll buttons have\n to re-centre themselves on the row — which in the segmented variant is taller\n than they are. */\n align-self: center;\n }\n\n .v-tabs-panels {\n min-inline-size: 0;\n }\n\n /*\n * Flat and segmented, the panels sit in no card, so they take no gutter: their content\n * has to stay aligned with whatever the page puts above and below the component, which\n * an indent would break. What they do owe is the separation from the bar, the boundary\n * between two parts of the same component rather than the parent layout's business —\n * which the framed variant folds into the gutter it gives its panels on all four sides.\n *\n * The axis follows the ORIENTATION and not the writing mode, so it cannot be a single\n * logical declaration: the bar is above the panels when horizontal and beside them when\n * vertical.\n *\n * Both variants are named explicitly rather than excluding the framed one, as in the\n * track rules above: a fourth frame opts in by hand.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='horizontal']\n > .v-tabs-panels {\n padding-block-start: var(--tabs-panels-pad);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='vertical']\n > .v-tabs-panels {\n padding-inline-start: var(--tabs-panels-pad);\n }\n\n /* Inside a card the gutter is the panels' alone, on all four sides: the bar spends\n none, so the tabs and their track reach the frame and the content is the only thing\n set back from it. */\n .v-tabs[data-variant='outlined'] > .v-tabs-panels {\n padding: var(--tabs-panels-pad);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-panels {\n flex: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-tabs-list {\n scroll-behavior: auto;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwGA,IAAM,IAAQ,GAmCR,IAAQ,GAAsB,GAAA,YAAC,GAO/B,EAAE,cAAW,cAAW,gBAAgB,MAAc,EAAa,GAGnE,IAAI,EAAY,GAChB,IAAY,QAAmB,EAAM,SAAS,EAAE,MAAM,KAAK,KAAK,GAChE,IAAoB,QAAe,EAAM,aAAa,EAAE,MAAM,KAAK,QAAQ,GAC3E,IAAoB,QAAe,EAAM,aAAa,EAAE,MAAM,KAAK,IAAI,GAEvE,IAAQ,GAAS,GACjB,IAAS,GAAM;EAKrB,EAAQ,GAAS;GACf,IAAI,QAAQ;IACV,OAAO,EAAM;GACf;GACA,OAAO,GAAkB;IACvB,EAAM,QAAQ;GAChB;GACA,QAAQ,MAAqB,GAAS,GAAQ,CAAK;GACnD,UAAU,MAAqB,GAAW,GAAQ,CAAK;GAWvD,IAAI,YAAY;IACd,OAAO,EAAM,WAAW,KAAA;GAC1B;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,aAAa;IACf,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF,CAAC;EAED,IAAM,IAAa,QAAe,EAAM,gBAAgB,UAAU,GAC5D,KAAmB,QACjB,EAAM,aAAa,EAAW,QAAQ,KAAiB,EAC/D,GACM,KAAmB,QACjB,EAAM,aAAa,EAAW,QAAQ,IAAiB,EAC/D,GAEM,IAAS,EAAwB,IAAI;EAY3C,SAAS,GAAU,GAAsB;GACvC,IAAM,IAAO,EAAO;GACf,KACL,EAAc,GAAO,SAAY,EAAe,GAAM,+BAA6B,GAAG,EACpF,UAAU,EAAW,MACvB,CAAC;EACH;EAqBA,IAAM,IAAkB,EAAwB,IAAI,GAC9C,IAAgB,EAAwB,IAAI,GAC5C,IAAU,EAAI,EAAI,GAClB,IAAQ,EAAI,EAAI;EAyCtB,AAvCA,EACE;GAAC;GAAQ;GAAiB;EAAa,IACtC,CAAC,GAAM,GAAO,IAAM,GAAW,MAAc;GAK5C,IAAI,CAAC,KAAQ,CAAC,KAAS,CAAC,KAAO,OAAO,uBAAyB,KAAa;GAC5E,IAAM,IAAW,IAAI,sBAClB,MAAY;IACX,KAAK,IAAM,KAAS,GAClB,AAAI,EAAM,WAAW,IAAO,EAAQ,QAAQ,EAAM,iBAC7C,EAAM,QAAQ,EAAM;GAE7B,GACA,EAAE,QAAK,CACT;GAGA,AAFA,EAAS,QAAQ,CAAK,GACtB,EAAS,QAAQ,CAAG,GACpB,QAAgB,EAAS,WAAW,CAAC;EACvC,GAIA,EAAE,OAAO,OAAO,CAClB,GAcA,EAAM,CAAC,GAAS,CAAK,IAAI,CAAC,GAAO,OAAS;GACxC,IAAM,IAAO,EAAO,OACd,IAAO,GAAM,wBACb,IAAO,GAAM,oBACb,IAAU,SAAS,eACnB,IAAW,MAAY;GACvB,KAAW,IAAQ,MAAY,KAAQ,QACzC,IAAW,IAAM,KAAO,EAAc,KACpC,IAAW,IAAO,EAAA,EAAO,MAAM;EACvC,CAAC;EAOD,SAAS,EAAW,GAAmB;GACrC,IAAM,IAAO,EAAO;GACpB,IAAI,CAAC,GAAM;GAGX,IAAI,EAAW,OAAO;IACpB,EAAK,WAAW,EAAE,KAAK,IAAY,EAAK,eAAe,GAAI,CAAC;IAC5D;GACF;GAGA,IAAM,IAAM,GAAM,CAAI;GACtB,EAAK,WAAW,EAAE,MAAM,IAAY,EAAK,cAAc,MAAO,IAAM,KAAK,GAAG,CAAC;EAC/E;EAiBA,SAAS,EAAe,GAA2B;GACjD,IAAM,IAAO,EAAO,OACd,IAAM,GAAM,cAA2B,wCAAoC;GACjF,IAAI,CAAC,KAAQ,CAAC,GAAK;GACnB,IAAM,IAAI,EAAI,sBAAsB,GAC9B,IAAI,EAAK,sBAAsB;GACrC,EAAK,WAAW;IACd,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;IAClF,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS;IACjF;GACF,CAAC;EACH;EAKA,AADA,EAAM,SAAa,EAAe,GAAG,EAAE,OAAO,OAAO,CAAC,GACtD,QAAgB,EAAe,SAAS,CAAC;EAKzC,SAAS,EAAc,GAAwB;GAC7C,IAAM,IAAO,EAAO;GAIpB,CAFE,GAAM,cAA2B,uDAAmD,KACpF,GAAM,cAA2B,+BAA6B,EAAA,EAC3D,MAAM,CAAO;EACpB;SAEA,EAAa;GAKX,OAAO;GAEP,IAAI;EACN,CAAC,mBAIC,EA4DM,OAAA;GA3DH,OAAK,GAAA,CAAA,UAAa,EAAA,CAAA,CAAS,CAAA;GAC3B,OAAK,EAAE,EAAA,CAAA,CAAS;GAChB,gBAAc,EAAA;GACd,oBAAkB,EAAA;GAClB,cAAY,EAAA;GACZ,mBAAiB,EAAA,YAAS,KAAQ,KAAA;GAClC,gBAAc,EAAA,UAAO,KAAQ,KAAA;MAE9B,EA8CM,OA9CN,GA8CM;GA5CI,EAAA,iBAAA,EAAA,GADR,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACP,MAAM,EAAA;IACN,SAAS,EAAA;IACT,UAAU,EAAA,YAAY,EAAA;IACtB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAU,EAAA;;qBAEoD,CAAtE,EAAsE,GAAtE,EAAe,EAAuD,CAAA,CAA9C,CAAC,GAAA,KAAgB,GAAA,EAAI,UAAQ,CAAG,EAAA,MAAA,CAAA,GAAA,MAAA,IAAA,CAAA,UAAA,CAAA,CAAA,CAAA;;;;;;;;GAG1D,EAoBM,OApBN,EAoBM;aAnBA;IAAJ,KAAI;IACH,oBAAkB,EAAA,QAAU,aAAgB,KAAA;MACrC,EAAA,CAAA,GAAS;IACjB,OAAM;IACN,MAAK;IACJ,cAAY,EAAA,CAAA;IACH;;IAMF,EAAA,iBAAA,EAAA,GADR,EAKE,QAAA;;cAHI;KAAJ,KAAI;KACJ,OAAM;KACN,eAAY;;IAEd,EAAQ,EAAA,QAAA,SAAA;IACI,EAAA,iBAAA,EAAA,GAAZ,EAA4F,QAAA;;cAA7D;KAAJ,KAAI;KAAgB,OAAM;KAAkB,eAAY;;;GAI7E,EAAA,iBAAA,EAAA,GADR,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACP,MAAM,EAAA;IACN,SAAS,EAAA;IACT,UAAU,EAAA,YAAY,EAAA;IACtB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAU,CAAA;;qBAEoD,CAAtE,EAAsE,GAAtE,EAAe,EAAuD,CAAA,CAA9C,CAAC,GAAA,KAAgB,GAAA,EAAI,UAAQ,CAAG,EAAA,MAAA,CAAA,GAAA,MAAA,IAAA,CAAA,UAAA,CAAA,CAAA,CAAA;;;;;;;;MAIjDA,EAAAA,OAAO,UAAA,EAAA,GAAlB,EAEM,OAFN,GAEM,CADJ,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,GAAA,IAAA,CAAA"}
1
+ {"version":3,"file":"VTabs.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VTabs/VTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * Implement ARIA tab selection and roving focus over native buttons. Overflow measurements\n * control scrolling arrows because CSS cannot expose overflow state.\n */\n\nimport { computed, onMounted, provide, ref, useId, useSlots, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { expand_less as expandLessIcon } from '../VIcon/icons/expand_less'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport { panelIdFor, tabIdFor, tabsKey } from './context'\n\nimport type { ItemValue } from '../../types'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the bar is drawn: a rule under the tabs, the same inside a card, or a sunken track. */\nexport type TabsVariant = 'flat' | 'outlined' | 'inset'\n/** The colour of the selected tab. */\nexport type TabsTone = 'accent' | 'neutral' | 'danger'\n/** The height of the tabs, from the scale every control shares. */\nexport type TabsSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the tabs run across the page or down its side. */\nexport type TabsOrientation = 'horizontal' | 'vertical'\n/** Where the tabs sit along the bar when they do not fill it. */\nexport type TabsAlign = 'start' | 'center' | 'end'\n/** Whether a tab is selected when it takes the focus, or only when it is activated. */\nexport type TabsActivation = 'manual' | 'automatic'\n\ninterface TabsProps {\n /** How the bar is framed. */\n variant?: TabsVariant\n /** The colour the selected tab takes. The others stay neutral whatever this says. */\n tone?: TabsTone\n /** The height of the tabs, from the scale shared by every control. */\n size?: TabsSize\n /** Takes 4px off the height of every tab. */\n compact?: boolean\n /** Whether the tabs run across the page or down its side. */\n orientation?: TabsOrientation\n /** Where the tabs sit along the bar when they do not fill it. */\n align?: TabsAlign\n /**\n * Stretches the tabs across the whole bar, every tab taking an equal share of it, on the\n * terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * Adds a button at each end of the bar to scroll it, each disabled once that end is\n * reached. It only makes sense when the tabs can overflow, so it excludes `fullWidth`.\n */\n scrollButtons?: boolean\n /** The icon of the button scrolling backwards. It follows the orientation by default. */\n prevIcon?: IconSource\n /** The icon of the button scrolling forwards. It follows the orientation by default. */\n nextIcon?: IconSource\n /** What the backward scroll button does, in words. It falls back to the dictionary. */\n prevLabel?: string\n /** What the forward scroll button does, in words. It falls back to the dictionary. */\n nextLabel?: string\n /** Whether moving to a tab also selects it. */\n activation?: TabsActivation\n /**\n * Makes every tab unusable, and the scroll buttons with them: the tabs leave the tab order\n * and grey out through the colour tokens.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row of tabs. It falls back to the design\n * system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<TabsProps>(), {\n variant: 'flat',\n tone: 'accent',\n size: 'md',\n compact: false,\n orientation: 'horizontal',\n align: 'start',\n fullWidth: false,\n scrollButtons: false,\n prevIcon: undefined,\n nextIcon: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n activation: 'manual',\n disabled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The tabs themselves. */\n default(): unknown\n /**\n * The panels the tabs show. Leaving it out renders no panel area at all, which is\n * how the same component serves as a plain bar or a segmented control.\n */\n panels?(): unknown\n}>()\n\n/**\n * The `value` of the selected tab. There is deliberately no default: the component cannot know\n * which of the tabs a consumer wrote should open.\n */\nconst model = defineModel<ItemValue>()\n\n// The root element is only a container; the one that matters is the row of tabs. So `class` and\n// `style` stay outside, where a consumer expects to place the component, and everything else;\n// the id, the aria-* naming it, the data-*; goes down onto the row itself.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs: listAttrs } = useRootAttrs()\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.tabs.label)\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? m.value.tabs.previous)\nconst resolvedNextLabel = computed(() => props.nextLabel ?? m.value.tabs.next)\n\nconst slots = useSlots()\nconst baseId = useId()\n\n// Everything is exposed through getters rather than as values: that is what keeps the\n// root's props reactive on the other side of the injection, so a tab re-renders when\n// the size or the tone changes.\nprovide(tabsKey, {\n get value() {\n return model.value\n },\n select(value: ItemValue) {\n model.value = value\n },\n tabId: (value: ItemValue) => tabIdFor(baseId, value),\n panelId: (value: ItemValue) => panelIdFor(baseId, value),\n // @ssr\n /*\n * A registry the panels filled in as they mounted would instead read as empty on the server\n * and full on the client, which is a hydration mismatch. The trade-off is that this is not\n * reactive: the panels slot must be there or not there, and cannot appear halfway through the\n * life of the component.\n */\n get hasPanels() {\n return slots.panels !== undefined\n },\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get activation() {\n return props.activation\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst resolvedPrevIcon = computed(\n () => props.prevIcon ?? (isVertical.value ? expandLessIcon : chevronLeftIcon),\n)\nconst resolvedNextIcon = computed(\n () => props.nextIcon ?? (isVertical.value ? expandMoreIcon : chevronRightIcon),\n)\n\nconst listEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/*\n * This handler only moves the focus. Selecting the tab that receives it; what `automatic`\n * activation means; is done by the tab itself, which knows its own typed value and therefore\n * never has to send it back through a DOM attribute.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const list = listEl.value\n if (!list) return\n arrowNavigate(event, list, () => navigableItems(list, '[role=\"tab\"]:not(:disabled)'), {\n vertical: isVertical.value,\n })\n}\n\n/*\n * CSS can ask the question; VDialog uses that query for its scroll shadows; and still cannot\n * answer this one, for two independent reasons. It is Chrome/Edge 133+, with neither Safari nor\n * Firefox shipping it, and where VDialog's hairlines merely stay invisible without it these\n * buttons are functional; an `@supports` guard would mean keeping everything below anyway.\n */\nconst startSentinelEl = ref<HTMLElement | null>(null)\nconst endSentinelEl = ref<HTMLElement | null>(null)\nconst atStart = ref(true)\nconst atEnd = ref(true)\n\nwatch(\n [listEl, startSentinelEl, endSentinelEl],\n ([root, start, end], _previous, onCleanup) => {\n // @fallback\n // The observer exists neither on the server nor in the unit-test environment, so\n // its absence has to be tolerated rather than assumed away. The behaviour of the\n // ends is checked in a real browser instead, by the play functions.\n if (!root || !start || !end || typeof IntersectionObserver === 'undefined') return\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.target === start) atStart.value = entry.isIntersecting\n else atEnd.value = entry.isIntersecting\n }\n },\n { root },\n )\n observer.observe(start)\n observer.observe(end)\n onCleanup(() => observer.disconnect())\n },\n // `post` so the effect runs once the DOM is up to date: in the default timing the three\n // template refs are still null on the first pass, the guard above returns, and the buttons\n // stay disabled for good; the effect having tracked nothing to wake it again.\n { flush: 'post' },\n)\n\n// @a11y\n/*\n * The default `pre` timing is the point: the button still holds the focus when this runs, the\n * DOM not being patched with `disabled` yet. In `post` timing the focus would already be on\n * <body>, with nothing left to say which control lost it.\n */\nwatch([atStart, atEnd], ([start, end]) => {\n const list = listEl.value\n const prev = list?.previousElementSibling as HTMLElement | null\n const next = list?.nextElementSibling as HTMLElement | null\n const focused = document.activeElement\n const fromPrev = focused === prev\n if (!(fromPrev ? start : focused === next && end)) return\n if (fromPrev ? end : start) focusSelected()\n else (fromPrev ? next : prev)?.focus()\n})\n\n/** Scrolls the bar by most of its own length. */\nfunction scrollStep(direction: -1 | 1) {\n const list = listEl.value\n if (!list) return\n // Called optionally because the unit-test environment implements no scrolling at\n // all; this behaviour is covered in a real browser.\n if (isVertical.value) {\n list.scrollBy?.({ top: direction * list.clientHeight * 0.8 })\n return\n }\n // The horizontal offset is physical, not logical: in a right-to-left page, moving\n // forward means moving left, so the direction has to be flipped by hand.\n const rtl = isRtl(list)\n list.scrollBy?.({ left: direction * list.clientWidth * 0.8 * (rtl ? -1 : 1) })\n}\n\n// @a11y @core\n/*\n * The keyboard needs nothing here: it moves the focus, and the browser scrolls a focused\n * element into view by itself. The scrolling is confined to the row rather than asking the\n * element to reveal itself, which would scroll every scrollable ancestor up to the page.\n */\nfunction revealSelected(behavior?: ScrollBehavior) {\n const list = listEl.value\n const tab = list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]')\n if (!list || !tab) return\n const t = tab.getBoundingClientRect()\n const c = list.getBoundingClientRect()\n list.scrollBy?.({\n left: t.left < c.left ? t.left - c.left : t.right > c.right ? t.right - c.right : 0,\n top: t.top < c.top ? t.top - c.top : t.bottom > c.bottom ? t.bottom - c.bottom : 0,\n behavior,\n })\n}\n\n// `post`, so the tab that is now selected already carries `aria-selected` when it is looked\n// for: in the default timing the query would find the one that was selected before.\nwatch(model, () => revealSelected(), { flush: 'post' })\nonMounted(() => revealSelected('instant'))\n\n// `focus` goes where the Tab key would land: the selected tab, which holds the row's single tab\n// stop.\nfunction focusSelected(options?: FocusOptions) {\n const list = listEl.value\n const tab =\n list?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]:not(:disabled)') ??\n list?.querySelector<HTMLElement>('[role=\"tab\"]:not(:disabled)')\n tab?.focus(options)\n}\n\ndefineExpose({\n /**\n * Moves the focus to the selected tab, or to the first tab that can take it when the\n * v-model names none.\n */\n focus: focusSelected,\n /** The `role=\"tablist\"` row, which is also where the consumer's attributes land. */\n el: listEl,\n})\n</script>\n\n<template>\n <div\n :class=\"['v-tabs', rootClass]\"\n :style=\"rootStyle\"\n :data-variant=\"variant\"\n :data-orientation=\"orientation\"\n :data-align=\"align\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n >\n <div class=\"v-tabs-bar\">\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedPrevLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atStart\"\n @click=\"scrollStep(-1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedPrevIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n\n <div\n ref=\"listEl\"\n :aria-orientation=\"isVertical ? 'vertical' : undefined\"\n v-bind=\"listAttrs\"\n class=\"v-tabs-list\"\n role=\"tablist\"\n :aria-label=\"ariaLabel\"\n @keydown=\"onKeydown\"\n >\n <!-- The two markers watched to know whether an end of the bar is reached.\n They are hidden from assistive technology: a row of tabs may contain\n nothing but tabs. -->\n <span\n v-if=\"scrollButtons\"\n ref=\"startSentinelEl\"\n class=\"v-tabs-sentinel\"\n aria-hidden=\"true\"\n />\n <slot />\n <span v-if=\"scrollButtons\" ref=\"endSentinelEl\" class=\"v-tabs-sentinel\" aria-hidden=\"true\" />\n </div>\n\n <VIconButton\n v-if=\"scrollButtons\"\n class=\"v-tabs-scroll\"\n :label=\"resolvedNextLabel\"\n :size=\"size\"\n :compact=\"compact\"\n :disabled=\"disabled || atEnd\"\n @click=\"scrollStep(1)\"\n >\n <VIcon v-bind=\"iconProps(resolvedNextIcon)\" :mirrored=\"!isVertical\" />\n </VIconButton>\n </div>\n\n <div v-if=\"$slots.panels\" class=\"v-tabs-panels\">\n <slot name=\"panels\" />\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-tabs {\n /*\n * The panels' padding, and the four pixels the compact density takes off it. The delta is\n * held apart from the value rather than the two being written out, so the density stays one\n * subtraction wherever the padding is read instead of a second table to keep in step; the\n * VAccordion idiom.\n */\n --tabs-pad-delta: 0px;\n --tabs-panels-pad: calc(var(--vectis-space-4) - var(--tabs-pad-delta));\n\n display: flex;\n flex-direction: column;\n font-family: var(--vectis-text-family);\n }\n\n .v-tabs[data-compact] {\n --tabs-pad-delta: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] {\n flex-direction: row;\n }\n\n /*\n * Keep card inner-radius calculations aligned with the tab corners; avoid clipping panel\n * focus rings and edge-to-edge consumer content.\n */\n .v-tabs[data-variant='outlined'] {\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs-bar {\n display: flex;\n /* Stretched rather than centred: the row deliberately overlaps the track by one\n pixel through a negative margin, and centring would displace it by half of\n that. */\n align-items: stretch;\n /*\n * The row is allowed to shrink below its content as soon as the tabs overflow, so there\n * would be nothing left to distribute; and setting an alignment on a scrolling container\n * makes whatever overflows past its start edge permanently unreachable, since a scroll\n * position cannot go negative.\n */\n justify-content: flex-start;\n gap: var(--vectis-space-1);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar {\n flex-direction: column;\n }\n\n .v-tabs[data-align='center'] > .v-tabs-bar {\n justify-content: center;\n }\n\n .v-tabs[data-align='end'] > .v-tabs-bar {\n justify-content: flex-end;\n }\n\n /*\n * Inside a card it is what separates the tabs from the panels; which is why that edge is left\n * without a gutter, since otherwise the rule would separate nothing from nothing. Both frames\n * are named explicitly rather than excluding the third: a fourth frame will have to opt in by\n * hand instead of inheriting this silently.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar {\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar {\n border-block-end: none;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * This block has exactly the same specificity as the one above, so it is the ORDER that\n * decides. Moving it up would leave the vertical framed case with two rules on one side and\n * none between the two areas.\n */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar {\n border-inline-start: none;\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-tabs-list {\n display: flex;\n align-items: center;\n /* Held in a variable because the sentinels cancel it beside themselves. */\n --tabs-list-gap: var(--vectis-space-1);\n gap: var(--tabs-list-gap);\n overflow: auto;\n /* Without these the row refuses to shrink below the width of its tabs, and it\n would widen the whole bar instead of scrolling. */\n min-inline-size: 0;\n min-block-size: 0;\n scrollbar-width: none;\n scroll-behavior: smooth;\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n flex-direction: column;\n align-items: stretch;\n }\n\n /* Pulling the row one pixel into the track is what lets the selected tab's indicator\n COVER that line rather than sit on top of it, which would read as a thicker rule.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined']) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: -1px;\n /* On a track the tabs are contiguous segments and not a queue of buttons: no gap\n here, and no rounded corners on the tabs themselves. */\n --tabs-list-gap: 0px;\n }\n\n /* The row is now a pixel taller than the tabs it holds, so they are pushed against\n its end edge; the indicator each tab draws there then falls exactly on the\n track. */\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='horizontal']\n > .v-tabs-bar\n > .v-tabs-list {\n align-items: flex-end;\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='outlined'])[data-orientation='vertical']\n > .v-tabs-bar\n > .v-tabs-list {\n margin-block-end: 0;\n margin-inline-start: -1px;\n }\n\n /* Same specificity as the rule above, so again it is the order that decides. */\n .v-tabs[data-variant='outlined'][data-orientation='vertical'] > .v-tabs-bar > .v-tabs-list {\n margin-inline-start: 0;\n }\n\n /*\n * This is the one place the card may clip, and the one place it must. Nothing is lost to the\n * clip: there is no panel, hence no outer focus ring, and a tab draws its own ring inwards.\n */\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) {\n overflow: clip;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar {\n border-block-end: none;\n border-inline-end: none;\n }\n\n .v-tabs[data-variant='outlined']:not(:has(> .v-tabs-panels)) > .v-tabs-bar > .v-tabs-list {\n margin-block-end: 0;\n }\n\n /* The hollow track of the segmented variant is drawn on the scrolling row itself,\n and not on the bar around it: its padding is what keeps the raised tab's shadow\n from being clipped as the row scrolls. */\n .v-tabs[data-variant='inset'] > .v-tabs-bar > .v-tabs-list {\n background: var(--vectis-color-surface-sunken);\n padding: var(--vectis-space-1);\n border-radius: var(--vectis-radius-surface);\n }\n\n .v-tabs[data-full-width] > .v-tabs-bar > .v-tabs-list {\n flex: 1;\n }\n\n /*\n * A pixel square each, at the very ends of the row. They are items of the row, so its gap\n * opens beside them as it does between two tabs: each pulls its neighbour back over that gap\n * and over its own pixel, so the first and last tabs sit exactly where the row's padding puts\n * them.\n */\n .v-tabs-sentinel {\n flex: 0 0 1px;\n align-self: flex-start;\n inline-size: 1px;\n block-size: 1px;\n }\n\n .v-tabs-sentinel:first-child {\n margin-inline-end: calc(-1px - var(--tabs-list-gap));\n margin-block-end: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-sentinel:last-child {\n margin-inline-start: calc(-1px - var(--tabs-list-gap));\n margin-block-start: calc(-1px - var(--tabs-list-gap));\n }\n\n .v-tabs-scroll {\n flex: none;\n /*\n * The bar stretches its children for the track's sake, so the scroll buttons have to\n * re-centre themselves on the row; which in the segmented variant is taller than they are.\n */\n align-self: center;\n }\n\n .v-tabs-panels {\n min-inline-size: 0;\n }\n\n /*\n * The axis follows the ORIENTATION and not the writing mode, so it cannot be a single logical\n * declaration: the bar is above the panels when horizontal and beside them when vertical.\n * Both variants are named explicitly rather than excluding the framed one, as in the track\n * rules above: a fourth frame opts in by hand.\n */\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='horizontal']\n > .v-tabs-panels {\n padding-block-start: var(--tabs-panels-pad);\n }\n\n .v-tabs:is([data-variant='flat'], [data-variant='inset'])[data-orientation='vertical']\n > .v-tabs-panels {\n padding-inline-start: var(--tabs-panels-pad);\n }\n\n /* Inside a card the gutter is the panels' alone, on all four sides: the bar spends\n none, so the tabs and their track reach the frame and the content is the only thing\n set back from it. */\n .v-tabs[data-variant='outlined'] > .v-tabs-panels {\n padding: var(--tabs-panels-pad);\n }\n\n .v-tabs[data-orientation='vertical'] > .v-tabs-panels {\n flex: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-tabs-list {\n scroll-behavior: auto;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqFA,IAAM,IAAQ,GAgCR,IAAQ,GAAsB,GAAA,YAAC,GAM/B,EAAE,cAAW,cAAW,gBAAgB,MAAc,EAAa,GAGnE,IAAI,EAAY,GAChB,IAAY,QAAmB,EAAM,SAAS,EAAE,MAAM,KAAK,KAAK,GAChE,IAAoB,QAAe,EAAM,aAAa,EAAE,MAAM,KAAK,QAAQ,GAC3E,IAAoB,QAAe,EAAM,aAAa,EAAE,MAAM,KAAK,IAAI,GAEvE,IAAQ,GAAS,GACjB,IAAS,GAAM;EAKrB,EAAQ,GAAS;GACf,IAAI,QAAQ;IACV,OAAO,EAAM;GACf;GACA,OAAO,GAAkB;IACvB,EAAM,QAAQ;GAChB;GACA,QAAQ,MAAqB,GAAS,GAAQ,CAAK;GACnD,UAAU,MAAqB,GAAW,GAAQ,CAAK;GAQvD,IAAI,YAAY;IACd,OAAO,EAAM,WAAW,KAAA;GAC1B;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,aAAa;IACf,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF,CAAC;EAED,IAAM,IAAa,QAAe,EAAM,gBAAgB,UAAU,GAC5D,KAAmB,QACjB,EAAM,aAAa,EAAW,QAAQ,KAAiB,EAC/D,GACM,KAAmB,QACjB,EAAM,aAAa,EAAW,QAAQ,IAAiB,EAC/D,GAEM,IAAS,EAAwB,IAAI;EAQ3C,SAAS,GAAU,GAAsB;GACvC,IAAM,IAAO,EAAO;GACf,KACL,EAAc,GAAO,SAAY,EAAe,GAAM,+BAA6B,GAAG,EACpF,UAAU,EAAW,MACvB,CAAC;EACH;EAQA,IAAM,IAAkB,EAAwB,IAAI,GAC9C,IAAgB,EAAwB,IAAI,GAC5C,IAAU,EAAI,EAAI,GAClB,IAAQ,EAAI,EAAI;EAmCtB,AAjCA,EACE;GAAC;GAAQ;GAAiB;EAAa,IACtC,CAAC,GAAM,GAAO,IAAM,GAAW,MAAc;GAK5C,IAAI,CAAC,KAAQ,CAAC,KAAS,CAAC,KAAO,OAAO,uBAAyB,KAAa;GAC5E,IAAM,IAAW,IAAI,sBAClB,MAAY;IACX,KAAK,IAAM,KAAS,GAClB,AAAI,EAAM,WAAW,IAAO,EAAQ,QAAQ,EAAM,iBAC7C,EAAM,QAAQ,EAAM;GAE7B,GACA,EAAE,QAAK,CACT;GAGA,AAFA,EAAS,QAAQ,CAAK,GACtB,EAAS,QAAQ,CAAG,GACpB,QAAgB,EAAS,WAAW,CAAC;EACvC,GAIA,EAAE,OAAO,OAAO,CAClB,GAQA,EAAM,CAAC,GAAS,CAAK,IAAI,CAAC,GAAO,OAAS;GACxC,IAAM,IAAO,EAAO,OACd,IAAO,GAAM,wBACb,IAAO,GAAM,oBACb,IAAU,SAAS,eACnB,IAAW,MAAY;GACvB,KAAW,IAAQ,MAAY,KAAQ,QACzC,IAAW,IAAM,KAAO,EAAc,KACpC,IAAW,IAAO,EAAA,EAAO,MAAM;EACvC,CAAC;EAGD,SAAS,EAAW,GAAmB;GACrC,IAAM,IAAO,EAAO;GACpB,IAAI,CAAC,GAAM;GAGX,IAAI,EAAW,OAAO;IACpB,EAAK,WAAW,EAAE,KAAK,IAAY,EAAK,eAAe,GAAI,CAAC;IAC5D;GACF;GAGA,IAAM,IAAM,GAAM,CAAI;GACtB,EAAK,WAAW,EAAE,MAAM,IAAY,EAAK,cAAc,MAAO,IAAM,KAAK,GAAG,CAAC;EAC/E;EAQA,SAAS,EAAe,GAA2B;GACjD,IAAM,IAAO,EAAO,OACd,IAAM,GAAM,cAA2B,wCAAoC;GACjF,IAAI,CAAC,KAAQ,CAAC,GAAK;GACnB,IAAM,IAAI,EAAI,sBAAsB,GAC9B,IAAI,EAAK,sBAAsB;GACrC,EAAK,WAAW;IACd,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;IAClF,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS;IACjF;GACF,CAAC;EACH;EAKA,AADA,EAAM,SAAa,EAAe,GAAG,EAAE,OAAO,OAAO,CAAC,GACtD,QAAgB,EAAe,SAAS,CAAC;EAIzC,SAAS,EAAc,GAAwB;GAC7C,IAAM,IAAO,EAAO;GAIpB,CAFE,GAAM,cAA2B,uDAAmD,KACpF,GAAM,cAA2B,+BAA6B,EAAA,EAC3D,MAAM,CAAO;EACpB;SAEA,EAAa;GAKX,OAAO;GAEP,IAAI;EACN,CAAC,mBAIC,EA4DM,OAAA;GA3DH,OAAK,GAAA,CAAA,UAAa,EAAA,CAAA,CAAS,CAAA;GAC3B,OAAK,EAAE,EAAA,CAAA,CAAS;GAChB,gBAAc,EAAA;GACd,oBAAkB,EAAA;GAClB,cAAY,EAAA;GACZ,mBAAiB,EAAA,YAAS,KAAQ,KAAA;GAClC,gBAAc,EAAA,UAAO,KAAQ,KAAA;MAE9B,EA8CM,OA9CN,GA8CM;GA5CI,EAAA,iBAAA,EAAA,GADR,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACP,MAAM,EAAA;IACN,SAAS,EAAA;IACT,UAAU,EAAA,YAAY,EAAA;IACtB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAU,EAAA;;qBAEoD,CAAtE,EAAsE,GAAtE,EAAe,EAAuD,CAAA,CAA9C,CAAC,GAAA,KAAgB,GAAA,EAAI,UAAQ,CAAG,EAAA,MAAA,CAAA,GAAA,MAAA,IAAA,CAAA,UAAA,CAAA,CAAA,CAAA;;;;;;;;GAG1D,EAoBM,OApBN,EAoBM;aAnBA;IAAJ,KAAI;IACH,oBAAkB,EAAA,QAAU,aAAgB,KAAA;MACrC,EAAA,CAAA,GAAS;IACjB,OAAM;IACN,MAAK;IACJ,cAAY,EAAA,CAAA;IACH;;IAMF,EAAA,iBAAA,EAAA,GADR,EAKE,QAAA;;cAHI;KAAJ,KAAI;KACJ,OAAM;KACN,eAAY;;IAEd,EAAQ,EAAA,QAAA,SAAA;IACI,EAAA,iBAAA,EAAA,GAAZ,EAA4F,QAAA;;cAA7D;KAAJ,KAAI;KAAgB,OAAM;KAAkB,eAAY;;;GAI7E,EAAA,iBAAA,EAAA,GADR,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACP,MAAM,EAAA;IACN,SAAS,EAAA;IACT,UAAU,EAAA,YAAY,EAAA;IACtB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAU,CAAA;;qBAEoD,CAAtE,EAAsE,GAAtE,EAAe,EAAuD,CAAA,CAA9C,CAAC,GAAA,KAAgB,GAAA,EAAI,UAAQ,CAAG,EAAA,MAAA,CAAA,GAAA,MAAA,IAAA,CAAA,UAAA,CAAA,CAAA,CAAA;;;;;;;;MAIjDA,EAAAA,OAAO,UAAA,EAAA,GAAlB,EAEM,OAFN,GAEM,CADJ,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,GAAA,IAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VTabs/context.ts"],"sourcesContent":["/**\n * What a VTabs passes down to the tabs and panels inside it.\n *\n * A tab and its panel have to know each other's identifier, since each names the\n * other for assistive technology. Rather than a registry they fill in as they mount,\n * both DERIVE that identifier from one generated base and their own `value` — so they\n * agree without ever exchanging anything, and the server and the browser produce the\n * same markup.\n *\n * The size travels through here rather than through CSS inheritance, as it does in\n * VAccordion, for a concrete reason: each tab renders a button that declares its own\n * size on itself, so an inherited value would never reach it.\n */\n\nimport type { InjectionKey } from 'vue'\n\nimport type { TabsActivation, TabsSize, TabsTone, TabsVariant } from './VTabs.vue'\nimport type { ItemValue } from '../../types'\n\nexport interface TabsContext {\n /** The selected tab, or nothing when the v-model names no tab that exists. */\n readonly value: ItemValue | undefined\n /** Selects a tab. */\n select: (value: ItemValue) => void\n /** The identifier of a tab, so its panel can point at it. */\n tabId: (value: ItemValue) => string\n /** The identifier of a panel, so its tab can point at it. */\n panelId: (value: ItemValue) => string\n /**\n * Whether panels were given at all. Without them the tabs are a bar of buttons and\n * must not claim to control anything.\n */\n readonly hasPanels: boolean\n /** The frame the bar is drawn in. */\n readonly variant: TabsVariant\n /** The colour the selected tab takes. */\n readonly tone: TabsTone\n /** The height of the tabs. */\n readonly size: TabsSize\n /** The reduced density. */\n readonly compact: boolean\n /** Whether every tab is unusable, which no tab can refuse. */\n readonly disabled: boolean\n /** Whether moving to a tab selects it, or merely focuses it. */\n readonly activation: TabsActivation\n}\n\nexport const tabsKey: InjectionKey<TabsContext> = Symbol('v-tabs')\n\n/**\n * Makes a tab's value safe to use inside an identifier. The attribute tying a tab to\n * its panel holds a LIST of identifiers separated by spaces, so a value containing a\n * space would be read there as two references, both to elements that do not exist.\n *\n * TRAP — the encoding has to be INJECTIVE, or two tabs get one identifier and a tab\n * points its panel reference at its neighbour's panel, with no error anywhere. So every\n * character outside `[A-Za-z0-9-]` is spelled out as its code point between two\n * underscores (the underscore itself included, which is what keeps `a b` and `a_b`\n * apart), and the TYPE leads the result, the number 1 and the string \"1\" being two\n * different tabs.\n */\nconst slug = (value: ItemValue) =>\n (typeof value === 'number' ? 'n-' : 's-') +\n String(value).replace(/[^A-Za-z0-9-]/gu, (char) => `_${char.codePointAt(0)!.toString(16)}_`)\n\nexport const tabIdFor = (base: string, value: ItemValue) => `${base}-tab-${slug(value)}`\nexport const panelIdFor = (base: string, value: ItemValue) => `${base}-panel-${slug(value)}`\n"],"mappings":";AA+CA,IAAa,IAAqC,OAAO,QAAQ,GAc3D,KAAQ,OACX,OAAO,KAAU,WAAW,OAAO,QACpC,OAAO,CAAK,CAAC,CAAC,QAAQ,oBAAoB,MAAS,IAAI,EAAK,YAAY,CAAC,CAAC,CAAE,SAAS,EAAE,EAAE,EAAE,GAEhF,KAAY,GAAc,MAAqB,GAAG,EAAK,OAAO,EAAK,CAAK,KACxE,KAAc,GAAc,MAAqB,GAAG,EAAK,SAAS,EAAK,CAAK"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VTabs/context.ts"],"sourcesContent":["/**\n * Derive tab/panel IDs from a shared base and value for SSR consistency. Pass size through\n * context because child buttons declare their own size.\n */\n\nimport type { InjectionKey } from 'vue'\n\nimport type { TabsActivation, TabsSize, TabsTone, TabsVariant } from './VTabs.vue'\nimport type { ItemValue } from '../../types'\n\nexport interface TabsContext {\n /** The selected tab, or nothing when the v-model names no tab that exists. */\n readonly value: ItemValue | undefined\n /** Selects a tab. */\n select: (value: ItemValue) => void\n /** The identifier of a tab, so its panel can point at it. */\n tabId: (value: ItemValue) => string\n /** The identifier of a panel, so its tab can point at it. */\n panelId: (value: ItemValue) => string\n /**\n * Whether panels were given at all. Without them the tabs are a bar of buttons and\n * must not claim to control anything.\n */\n readonly hasPanels: boolean\n /** The frame the bar is drawn in. */\n readonly variant: TabsVariant\n /** The colour the selected tab takes. */\n readonly tone: TabsTone\n /** The height of the tabs. */\n readonly size: TabsSize\n /** The reduced density. */\n readonly compact: boolean\n /** Whether every tab is unusable, which no tab can refuse. */\n readonly disabled: boolean\n /** Whether moving to a tab selects it, or merely focuses it. */\n readonly activation: TabsActivation\n}\n\nexport const tabsKey: InjectionKey<TabsContext> = Symbol('v-tabs')\n\n/**\n * Encode type and every non-safe character, including underscore, so different values cannot\n * collide in tab/panel IDs.\n */\nconst slug = (value: ItemValue) =>\n (typeof value === 'number' ? 'n-' : 's-') +\n String(value).replace(/[^A-Za-z0-9-]/gu, (char) => `_${char.codePointAt(0)!.toString(16)}_`)\n\nexport const tabIdFor = (base: string, value: ItemValue) => `${base}-tab-${slug(value)}`\nexport const panelIdFor = (base: string, value: ItemValue) => `${base}-panel-${slug(value)}`\n"],"mappings":";AAsCA,IAAa,IAAqC,OAAO,QAAQ,GAM3D,KAAQ,OACX,OAAO,KAAU,WAAW,OAAO,QACpC,OAAO,CAAK,CAAC,CAAC,QAAQ,oBAAoB,MAAS,IAAI,EAAK,YAAY,CAAC,CAAC,CAAE,SAAS,EAAE,EAAE,EAAE,GAEhF,KAAY,GAAc,MAAqB,GAAG,EAAK,OAAO,EAAK,CAAK,KACxE,KAAc,GAAc,MAAqB,GAAG,EAAK,SAAS,EAAK,CAAK"}
@@ -10,15 +10,13 @@ interface TextareaProps {
10
10
  */
11
11
  compact?: boolean;
12
12
  /**
13
- * How many lines of text the field shows: the native `rows` attribute, which is
14
- * what gives the field its height. Anything under 1 is raised to 1, and at 1 the
15
- * field is exactly as tall as a VInput of the same size.
13
+ * How many lines of text the field shows: the native `rows` attribute, which gives the field
14
+ * its height.
16
15
  */
17
16
  rows?: number;
18
17
  /**
19
- * Lets the field grow as the text is typed, instead of scrolling inside the height
20
- * `rows` gives it, which stays its starting height. It is pure CSS; where the
21
- * browser does not support it, the field simply behaves like an ordinary textarea.
18
+ * Lets the field grow as the text is typed, instead of scrolling inside the height `rows`
19
+ * gives it, which stays its starting height.
22
20
  */
23
21
  autoGrow?: boolean;
24
22
  /**
@@ -41,10 +39,9 @@ interface TextareaProps {
41
39
  */
42
40
  hint?: string;
43
41
  /**
44
- * An icon inside the field, at the start. It is decorative by default and becomes a
45
- * real button as soon as a `@click:icon-start` listener is attached, in which case
46
- * it needs `iconStartLabel`. The `#start` slot is rendered after it, so the two can
47
- * be given together.
42
+ * An icon inside the field, at the start. It is decorative by default and becomes a real
43
+ * button as soon as a `@click:icon-start` listener is attached, in which case it needs
44
+ * `iconStartLabel`.
48
45
  */
49
46
  iconStart?: IconSource;
50
47
  /**
@@ -69,10 +66,8 @@ interface TextareaProps {
69
66
  */
70
67
  clearable?: boolean;
71
68
  /**
72
- * Decides whether the cross is shown, instead of letting the field work it out from its
73
- * own content (a read-only field included). It is the same escape hatch VInput offers,
74
- * for the same reason: a component built on top of this one may hold what there is to
75
- * clear somewhere other than the text.
69
+ * Decides whether the cross is shown, instead of letting the field work it out from its own
70
+ * content (a read-only field included).
76
71
  */
77
72
  clearVisible?: boolean;
78
73
  /** What the clear button does, in words. It falls back to the design system dictionary. */
@@ -83,9 +78,8 @@ interface TextareaProps {
83
78
  */
84
79
  maxlength?: number;
85
80
  /**
86
- * Turns that limit into a soft one: the reader may type past it, and the field goes
87
- * into error instead of silently refusing the keystrokes. It is reported through
88
- * the native validity, so a form cannot be submitted over the limit.
81
+ * Turns that limit into a soft one: the reader may type past it, and the field goes into
82
+ * error instead of silently refusing the keystrokes.
89
83
  */
90
84
  softLimit?: boolean;
91
85
  /**
@@ -107,9 +101,8 @@ type __VLS_Slots = {
107
101
  */
108
102
  end?(): unknown;
109
103
  /**
110
- * Controls of your own inside the field, placed before the field's own: the clear cross
111
- * and the end icon. It is the slot for something that acts on the VALUE rather than on
112
- * the field, and it is VInput's slot of the same name.
104
+ * Controls of your own inside the field, placed before the field's own: the clear cross and
105
+ * the end icon.
113
106
  */
114
107
  'value-end'?(): unknown;
115
108
  };
@@ -1 +1 @@
1
- {"version":3,"file":"VTextarea.js","names":[],"sources":["../../../src/components/VTextarea/VTextarea.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A multi-line field, with the same label, hint, icons, counter, loading state and clear\n * cross as VInput, arranged around a real `<textarea>`.\n *\n * Two things differ. It grows with what is typed, which is pure CSS (`field-sizing`) — a\n * browser without it keeps a fixed height and its usual scrollbar. And the counter goes\n * UNDER the field rather than inside it, where several lines of text would run into it.\n *\n * As in VInput: the attrs split, validation stays the browser's, and the only behavioural JS\n * is the v-model and the clear cross, which hands focus back to the field it just emptied.\n */\nimport { computed, ref } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconName, iconProps } from '../VIcon/iconProps'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport type { IconSource } from '../VIcon/types'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useClearable } from '../../composables/useClearable'\nimport { useFieldIds } from '../../composables/useFieldIds'\nimport { useIconClickHandlers } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useTextLimit } from '../../composables/useTextLimit'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of one line: 32, 40 or 48 pixels. */\nexport type TextareaSize = 'sm' | 'md' | 'lg'\n\ninterface TextareaProps {\n /** The size of the field, which sets its padding, its type scale and its icons. */\n size?: TextareaSize\n /**\n * Takes 4px off the field by tightening its padding, leaving the number of lines,\n * the type and the icons alone.\n */\n compact?: boolean\n /**\n * How many lines of text the field shows: the native `rows` attribute, which is\n * what gives the field its height. Anything under 1 is raised to 1, and at 1 the\n * field is exactly as tall as a VInput of the same size.\n */\n rows?: number\n /**\n * Lets the field grow as the text is typed, instead of scrolling inside the height\n * `rows` gives it, which stays its starting height. It is pure CSS; where the\n * browser does not support it, the field simply behaves like an ordinary textarea.\n */\n autoGrow?: boolean\n /**\n * Marks the field as invalid whatever the browser thinks: the route for a rule\n * only the server can check.\n */\n invalid?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the text without allowing it to be changed. The field can still be focused\n * and copied from, and the clear button is hidden.\n */\n readonly?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, tied to the textarea for assistive technology so\n * that it is read out along with the label.\n */\n hint?: string\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case\n * it needs `iconStartLabel`. The `#start` slot is rendered after it, so the two can\n * be given together.\n */\n iconStart?: IconSource\n /**\n * The same at the end of the field. The `#end` slot replaces it, and the loading\n * spinner takes its place while it turns.\n */\n iconEnd?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /** What the end icon does, in words, once it is clickable. */\n iconEndLabel?: string\n /** Shows a spinner at the end of the field, in place of the end icon or slot. */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the field. It appears when there is something to\n * clear and the field can be edited.\n */\n clearable?: boolean\n /**\n * Decides whether the cross is shown, instead of letting the field work it out from its\n * own content (a read-only field included). It is the same escape hatch VInput offers,\n * for the same reason: a component built on top of this one may hold what there is to\n * clear somewhere other than the text.\n */\n clearVisible?: boolean\n /** What the clear button does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The maximum number of characters. By default this is the browser's own limit,\n * which simply refuses anything beyond it.\n */\n maxlength?: number\n /**\n * Turns that limit into a soft one: the reader may type past it, and the field goes\n * into error instead of silently refusing the keystrokes. It is reported through\n * the native validity, so a form cannot be submitted over the limit.\n */\n softLimit?: boolean\n /**\n * Shows how much has been typed, under the field: \"12/80\" against a limit, or just\n * \"12\" without one.\n */\n counter?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TextareaProps>(), {\n size: 'md',\n compact: false,\n rows: 5,\n autoGrow: false,\n invalid: false,\n disabled: false,\n readonly: false,\n label: undefined,\n hint: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconStartLabel: undefined,\n iconEndLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearVisible: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n softLimit: false,\n counter: false,\n})\n\nconst emit = defineEmits<{\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n /** The end icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-end': [event: MouseEvent]\n /** The clear button was pressed. The field has already been emptied. */\n clear: []\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its\n * place, as in VInput.\n */\n start?(): unknown\n /**\n * Content at the end of the field, which replaces `iconEnd`. It is hidden while the\n * field is loading, the spinner taking that place.\n */\n end?(): unknown\n /**\n * Controls of your own inside the field, placed before the field's own: the clear cross\n * and the end icon. It is the slot for something that acts on the VALUE rather than on\n * the field, and it is VInput's slot of the same name.\n */\n 'value-end'?(): unknown\n}>()\n\n/** The text in the field, empty to begin with. */\nconst model = defineModel<string>({ default: '' })\n\n// HTML wants `rows` to be a positive integer, and it drops anything else on the floor:\n// a 0, a negative or a fractional value would leave the browser falling back to its own\n// default of two rows, which is neither what was asked for nor this component's default.\n// Rounding and flooring here is what keeps the prop's contract out of the parser's hands.\nconst resolvedRows = computed(() => Math.max(1, Math.round(props.rows)))\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the\n// field; every other attribute goes to the textarea, where it does something.\nconst { attrs, rootClass, rootStyle, forwardedAttrs: restAttrs } = useRootAttrs()\n\n// The prop keeps priority; what it falls back to is the dictionary, so the default\n// wording follows the language the design system is set to.\nconst m = useMessages()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.common.clear)\n\nconst { fieldId, hintId, counterId, describedBy } = useFieldIds(\n attrs,\n () => !!props.hint,\n () => props.counter,\n)\n\n// Every length measurement goes through this projection: a value straight from an API or\n// a database is often `null`, and reading `.length` on it would throw at mount.\nconst modelText = computed(() => model.value ?? '')\n\nconst { hasIconStartHandler, hasIconEndHandler } = useIconClickHandlers({\n name: 'VTextarea',\n iconStartLabel: props.iconStartLabel,\n iconEndLabel: props.iconEndLabel,\n})\n\nconst controlEl = ref<HTMLTextAreaElement | null>(null)\n\nconst { showClear, onClear } = useClearable({\n clearable: () => props.clearable,\n clearVisible: () => props.clearVisible,\n disabled: () => props.disabled,\n readonly: () => props.readonly,\n text: () => modelText.value,\n controlEl,\n onCleared: () => {\n model.value = ''\n emit('clear')\n },\n})\n\nconst { counterText, over } = useTextLimit({\n el: controlEl,\n text: () => modelText.value,\n maxlength: () => props.maxlength,\n softLimit: () => props.softLimit,\n})\n\ndefineExpose({\n /** Moves the focus to the real textarea. */\n focus: (options?: FocusOptions) => controlEl.value?.focus(options),\n /** Selects everything in the field. */\n select: () => controlEl.value?.select(),\n /** The real `<textarea>`, for what neither of the two above covers. */\n el: controlEl,\n})\n</script>\n\n<template>\n <div\n class=\"v-textarea v-control\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <VTypography v-if=\"label\" as=\"label\" variant=\"label\" class=\"v-textarea-label\" :for=\"fieldId\">\n {{ label }}\n </VTypography>\n\n <div\n class=\"v-textarea-field\"\n :style=\"{ '--textarea-rows': resolvedRows }\"\n :data-auto-grow=\"autoGrow ? '' : undefined\"\n >\n <!-- The start icon is rendered BEFORE the slot, where the end icon is the slot's own\n fallback. The asymmetry is VInput's, mirrored here so the two fields answer the\n same way: what a composed field puts in that zone is OTHER content beside the\n icon, not another way of drawing it. -->\n <button\n v-if=\"iconStart && hasIconStartHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-start\"\n :aria-label=\"iconStartLabel ?? iconName(iconStart)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-start', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconStart)\" />\n </button>\n <VIcon v-else-if=\"iconStart\" v-bind=\"iconProps(iconStart)\" class=\"v-textarea-icon-start\" />\n <slot name=\"start\" />\n\n <textarea\n :id=\"fieldId\"\n ref=\"controlEl\"\n v-model=\"model\"\n :aria-invalid=\"invalid || undefined\"\n v-bind=\"restAttrs\"\n class=\"v-textarea-control\"\n :rows=\"resolvedRows\"\n :maxlength=\"softLimit ? undefined : maxlength\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-describedby=\"describedBy\"\n />\n\n <slot name=\"value-end\" />\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-clear\"\n :aria-label=\"resolvedClearLabel\"\n @click=\"onClear\"\n >\n <VIcon :name=\"closeIcon\" />\n </button>\n\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <slot v-else name=\"end\">\n <button\n v-if=\"iconEnd && hasIconEndHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-end\"\n :aria-label=\"iconEndLabel ?? iconName(iconEnd)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-end', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconEnd)\" />\n </button>\n <VIcon v-else-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" class=\"v-textarea-icon-end\" />\n </slot>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-textarea-meta v-field-meta\">\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-textarea-hint\">\n {{ hint }}\n </VTypography>\n <span\n v-if=\"counter\"\n :id=\"counterId\"\n class=\"v-textarea-counter v-field-counter\"\n :data-over=\"over ? '' : undefined\"\n >\n {{ counterText }}\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-textarea {\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 .v-textarea-label and .v-textarea-hint classes remain as hooks: a consumer\n overrides through them, and the disabled state below reaches them that way. */\n\n /* This is the box that carries the border, the background, the focus ring and the\n resize handle — which is why it also hides its overflow, `resize` having no\n effect on a box whose overflow is visible. `--field-border-color` is the single\n source of truth for its colour, and the hover, error and disabled states do\n nothing but redefine it. */\n .v-textarea-field {\n --field-border-color: var(--vectis-color-border-strong);\n\n /*\n * Nothing here restates the size scale: the `--control-*` variables are inherited\n * from the v-control root (styles/control-size.css), the icon context included.\n *\n * The field carries NO height of its own: it is the `rows` attribute on the\n * textarea that sets one, and the field is simply as tall as the control it wraps.\n * That is what makes dragging the resize handle behave like a bare textarea — the\n * field shrinks and the control scrolls — instead of stopping at a CSS floor.\n */\n\n /* The leading is named ONCE and feeds two things that have to agree by\n construction: the field's own `line-height`, and the first line box below,\n against which everything sitting beside the text is offset. */\n --textarea-leading: var(--vectis-text-body-md-leading);\n\n /* The height of the FIRST line box, the row the icons and the buttons align on.\n\n TRAP — it is DERIVED and must never be written as `1lh`. The `lh` unit resolves\n against the element the property is USED on, and the children do not share the\n field's font: an icon carries `--vectis-icon-size` (20px against the field's\n 14px), so `1lh` measured a 30px line there and dropped every icon 4.5px below\n the text and the buttons it is supposed to line up with. The buttons happened to\n be right only because the reset gives them `font: inherit`. */\n --textarea-line: calc(var(--control-font-size) * var(--textarea-leading));\n\n display: flex;\n align-items: flex-start;\n gap: var(--control-gap);\n padding-inline: var(--control-padding-inline-field);\n /* Half of what is left of the control height once its two 1px borders and one line\n box are taken out — so at `rows=\"1\"` the field is exactly a VInput of the same\n size, and every further row adds precisely one line box.\n\n It reads --control-height and not --control-height-base, which is what carries\n the density here: with no height of its own left to shrink, the padding is the\n only lever `compact` has, and 2px off each edge is the -4px the whole scale\n promises. */\n padding-block: calc((var(--control-height) - 2px - var(--textarea-line)) / 2);\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--field-border-color);\n /* TRAP — the cap is what carries the VInput identity above into the OTHER row counts,\n and it has to stay derived from --control-height. A browser scales down any radius it\n cannot fit, so on a one-row field a pill override is already painted as half the\n control height; min() applies that same reduction to a field that is taller. Written\n bare, the identical override paints half of a FIVE-row box instead: 60px corners on a\n 120px field, an ellipse sitting beside buttons that kept 20px, and the same again on\n every drag of the resize handle. At the shipped 6px it resolves to 6px on every size,\n compact included, so nothing at the default value can see this line go — only the\n PillRadius play function can. */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-size: var(--control-font-size);\n /* Text running over several lines takes the body line height. The `control` type\n role, whose lines are set tight against one another, only makes sense for a\n single-line label. */\n line-height: var(--textarea-leading);\n resize: vertical;\n overflow: hidden;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-textarea-control {\n flex: 1;\n min-width: 0;\n align-self: stretch;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n outline: none; /* the focus ring is drawn by the field around it, not here */\n resize: none; /* the resize handle belongs to the field, so there is only one */\n }\n\n .v-textarea-control::placeholder {\n color: var(--vectis-color-text-subtle);\n }\n\n /* The icons and buttons align with the FIRST line of text rather than with the\n middle of a field whose height varies: half the difference between the line box and\n the element centres it there. On a single line that comes out identical to the\n centring VInput gets from its fixed height, which is the point.\n\n The measure is `--textarea-line` and never `1lh` — see the trap where it is\n declared: `lh` would be resolved against each child's own font, which differs from\n the field's.\n\n TRAP — the margin is SYMMETRIC, the VToast rule. It looks redundant under\n `align-items: flex-start`, where only the start side moves anything, but the end\n side is what keeps the item's OUTER box down to one line box. Left off, a clear\n cross (24px against a 21px line) would be the tallest thing in the flex line and\n the field would be 1.5px taller than the rows asked for — 2px at `lg` — which is\n exactly the gap that stops `rows=\"1\"` from being a VInput. */\n .v-textarea-field > .v-icon,\n .v-textarea-field > .v-spinner {\n margin-block: calc((var(--textarea-line) - var(--vectis-icon-size)) / 2);\n }\n\n .v-textarea-field > .v-textarea-action {\n margin-block: calc((var(--textarea-line) - var(--control-action-size)) / 2);\n }\n\n /* A decorative icon is drawn in a muted grey, so it stays quieter than the text\n being typed beside it. */\n .v-textarea-field > .v-icon {\n color: var(--vectis-color-text-muted);\n }\n\n .v-textarea-field > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n /* A read-only field sinks slightly into the page but keeps its text at full\n strength — the value is there to be read — and still shows the accent focus ring.\n The state is read from [data-readonly] and never from `:read-only`, which the\n browser also matches on a disabled field.\n\n TRAP — this block must stay FIRST in the sequence of states: read-only, then\n hover, focus, invalid and disabled. The read-only, focus, invalid and disabled\n selectors all weigh (0,3,0), `:has()` taking the specificity of what it contains, so\n nothing but the source order arbitrates between them. Hover alone weighs more, (0,6,0):\n its `:not()` keeps it off a focused, invalid or disabled field, and on a read-only\n one it darkens the border as on any other. Moved further down, this rule would repaint the\n error border and the accent focus ring grey, with no error anywhere. What belongs\n to a read-only field is its BASE colour alone. */\n .v-textarea[data-readonly] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-sunken);\n }\n\n .v-textarea-field:hover:not(:has(.v-textarea-control:focus)):not(\n :has(\n .v-textarea-control:disabled,\n .v-textarea-control:user-invalid,\n .v-textarea-control[aria-invalid='true']\n )\n ) {\n --field-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 field appears to have a two-pixel border: it is really its own 1px\n border plus a 1px shadow of the same colour just outside it, which costs no\n layout and therefore makes nothing jump.\n\n The selector watches the CONTROL's focus and not `:focus-within`, so that when one\n of the field's own buttons takes keyboard focus, only that button's outline\n lights up: two indicators at once would be unreadable. And it is `:focus` rather\n than `:focus-visible`, because a text field shows its focus even when it was\n reached with the mouse.\n\n The transparent outline is the safety net for Windows forced colours, which drop\n box-shadows entirely; an outline survives and keeps the field marked. */\n .v-textarea-field:has(.v-textarea-control:focus) {\n --field-border-color: var(--vectis-color-accent);\n\n box-shadow: 0 0 0 1px var(--field-border-color);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n /* The invalid state, from the browser's own verdict first and from the `invalid`\n prop second. Only the colour variable is changed, which is why the border AND the\n focus ring both turn red without either being restated. */\n .v-textarea-field:has(.v-textarea-control:user-invalid),\n .v-textarea-field:has(.v-textarea-control[aria-invalid='true']) {\n --field-border-color: var(--vectis-color-danger);\n }\n\n /* A disabled field greys out through the colour tokens, the same ones VCheckbox and\n VRadio use, and never through opacity. It comes LAST in the sequence of states,\n which at equal specificity is what makes it win over all of them, the error\n included: a disabled field is not submitted, so it has nothing to report.\n\n The text here is `text-muted` and not the `text-subtle` used by the label and the\n hint below, because those sit on the page surface where this sits on\n `surface-muted` — against which subtle falls to a 4.4:1 contrast. The control\n itself is exempt from that rule, being natively disabled, but the icons the field\n CONTAINS are not, and they inherit this colour. */\n .v-textarea[data-disabled] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n cursor: not-allowed;\n resize: none;\n }\n\n .v-textarea[data-disabled] .v-textarea-label,\n .v-textarea[data-disabled] .v-textarea-hint,\n .v-textarea[data-disabled] .v-textarea-counter {\n color: var(--vectis-color-text-subtle);\n }\n\n .v-textarea[data-disabled] .v-textarea-action,\n .v-textarea[data-disabled] .v-textarea-field > .v-icon {\n color: inherit;\n cursor: not-allowed;\n }\n\n .v-textarea-control:disabled {\n cursor: not-allowed;\n }\n\n /* Growing with the content is pure CSS: `field-sizing` makes the textarea's height\n follow what is typed, and the field around it follows in turn. The resize handle\n goes, there being nothing left to resize by hand. A browser without support keeps\n the fixed height and its scrollbar, which is the intended fallback. */\n .v-textarea-field[data-auto-grow] {\n resize: none;\n }\n\n /* `field-sizing: content` sizes the control to what has been typed and ignores `rows`\n outright, so an auto-growing field would open one line tall whatever the prop said.\n The floor hands the STARTING height back to `rows` and leaves the growing to the\n browser. It is scoped to this branch and must not be lifted onto the control at\n large: a floor there would stop the control shrinking with the resize handle, and\n the field, which hides its overflow, would clip it instead of scrolling. */\n .v-textarea-field[data-auto-grow] .v-textarea-control {\n field-sizing: content;\n min-block-size: calc(var(--textarea-rows) * var(--textarea-line));\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-textarea-field {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VTextarea.js","names":[],"sources":["../../../src/components/VTextarea/VTextarea.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * Native textarea owns editing and validity. JavaScript bridges the model, text limits and\n * clear-button focus; CSS controls optional content-based height.\n */\nimport { computed, ref } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconName, iconProps } from '../VIcon/iconProps'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport type { IconSource } from '../VIcon/types'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useClearable } from '../../composables/useClearable'\nimport { useFieldIds } from '../../composables/useFieldIds'\nimport { useIconClickHandlers } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useTextLimit } from '../../composables/useTextLimit'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of one line: 32, 40 or 48 pixels. */\nexport type TextareaSize = 'sm' | 'md' | 'lg'\n\ninterface TextareaProps {\n /** The size of the field, which sets its padding, its type scale and its icons. */\n size?: TextareaSize\n /**\n * Takes 4px off the field by tightening its padding, leaving the number of lines,\n * the type and the icons alone.\n */\n compact?: boolean\n /**\n * How many lines of text the field shows: the native `rows` attribute, which gives the field\n * its height.\n */\n rows?: number\n /**\n * Lets the field grow as the text is typed, instead of scrolling inside the height `rows`\n * gives it, which stays its starting height.\n */\n autoGrow?: boolean\n /**\n * Marks the field as invalid whatever the browser thinks: the route for a rule\n * only the server can check.\n */\n invalid?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the text without allowing it to be changed. The field can still be focused\n * and copied from, and the clear button is hidden.\n */\n readonly?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, tied to the textarea for assistive technology so\n * that it is read out along with the label.\n */\n hint?: string\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a real\n * button as soon as a `@click:icon-start` listener is attached, in which case it needs\n * `iconStartLabel`.\n */\n iconStart?: IconSource\n /**\n * The same at the end of the field. The `#end` slot replaces it, and the loading\n * spinner takes its place while it turns.\n */\n iconEnd?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /** What the end icon does, in words, once it is clickable. */\n iconEndLabel?: string\n /** Shows a spinner at the end of the field, in place of the end icon or slot. */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the field. It appears when there is something to\n * clear and the field can be edited.\n */\n clearable?: boolean\n /**\n * Decides whether the cross is shown, instead of letting the field work it out from its own\n * content (a read-only field included).\n */\n clearVisible?: boolean\n /** What the clear button does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The maximum number of characters. By default this is the browser's own limit,\n * which simply refuses anything beyond it.\n */\n maxlength?: number\n /**\n * Turns that limit into a soft one: the reader may type past it, and the field goes into\n * error instead of silently refusing the keystrokes.\n */\n softLimit?: boolean\n /**\n * Shows how much has been typed, under the field: \"12/80\" against a limit, or just\n * \"12\" without one.\n */\n counter?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TextareaProps>(), {\n size: 'md',\n compact: false,\n rows: 5,\n autoGrow: false,\n invalid: false,\n disabled: false,\n readonly: false,\n label: undefined,\n hint: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconStartLabel: undefined,\n iconEndLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearVisible: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n softLimit: false,\n counter: false,\n})\n\nconst emit = defineEmits<{\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n /** The end icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-end': [event: MouseEvent]\n /** The clear button was pressed. The field has already been emptied. */\n clear: []\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its\n * place, as in VInput.\n */\n start?(): unknown\n /**\n * Content at the end of the field, which replaces `iconEnd`. It is hidden while the\n * field is loading, the spinner taking that place.\n */\n end?(): unknown\n /**\n * Controls of your own inside the field, placed before the field's own: the clear cross and\n * the end icon.\n */\n 'value-end'?(): unknown\n}>()\n\n/** The text in the field, empty to begin with. */\nconst model = defineModel<string>({ default: '' })\n\n// HTML wants `rows` to be a positive integer, and it drops anything else on the floor:\n// a 0, a negative or a fractional value would leave the browser falling back to its own\n// default of two rows, which is neither what was asked for nor this component's default.\n// Rounding and flooring here is what keeps the prop's contract out of the parser's hands.\nconst resolvedRows = computed(() => Math.max(1, Math.round(props.rows)))\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the\n// field; every other attribute goes to the textarea, where it does something.\nconst { attrs, rootClass, rootStyle, forwardedAttrs: restAttrs } = useRootAttrs()\n\n// The prop keeps priority; what it falls back to is the dictionary, so the default\n// wording follows the language the design system is set to.\nconst m = useMessages()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.common.clear)\n\nconst { fieldId, hintId, counterId, describedBy } = useFieldIds(\n attrs,\n () => !!props.hint,\n () => props.counter,\n)\n\n// Every length measurement goes through this projection: a value straight from an API or\n// a database is often `null`, and reading `.length` on it would throw at mount.\nconst modelText = computed(() => model.value ?? '')\n\nconst { hasIconStartHandler, hasIconEndHandler } = useIconClickHandlers({\n name: 'VTextarea',\n iconStartLabel: props.iconStartLabel,\n iconEndLabel: props.iconEndLabel,\n})\n\nconst controlEl = ref<HTMLTextAreaElement | null>(null)\n\nconst { showClear, onClear } = useClearable({\n clearable: () => props.clearable,\n clearVisible: () => props.clearVisible,\n disabled: () => props.disabled,\n readonly: () => props.readonly,\n text: () => modelText.value,\n controlEl,\n onCleared: () => {\n model.value = ''\n emit('clear')\n },\n})\n\nconst { counterText, over } = useTextLimit({\n el: controlEl,\n text: () => modelText.value,\n maxlength: () => props.maxlength,\n softLimit: () => props.softLimit,\n})\n\ndefineExpose({\n /** Moves the focus to the real textarea. */\n focus: (options?: FocusOptions) => controlEl.value?.focus(options),\n /** Selects everything in the field. */\n select: () => controlEl.value?.select(),\n /** The real `<textarea>`, for what neither of the two above covers. */\n el: controlEl,\n})\n</script>\n\n<template>\n <div\n class=\"v-textarea v-control\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <VTypography v-if=\"label\" as=\"label\" variant=\"label\" class=\"v-textarea-label\" :for=\"fieldId\">\n {{ label }}\n </VTypography>\n\n <div\n class=\"v-textarea-field\"\n :style=\"{ '--textarea-rows': resolvedRows }\"\n :data-auto-grow=\"autoGrow ? '' : undefined\"\n >\n <!--\n The start icon is rendered before the slot, where the end icon is the slot's own\n fallback. The asymmetry is VInput's, mirrored here so the two fields answer the same\n way: what a composed field puts in that zone is OTHER content beside the icon, not\n another way of drawing it.\n -->\n <button\n v-if=\"iconStart && hasIconStartHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-start\"\n :aria-label=\"iconStartLabel ?? iconName(iconStart)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-start', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconStart)\" />\n </button>\n <VIcon v-else-if=\"iconStart\" v-bind=\"iconProps(iconStart)\" class=\"v-textarea-icon-start\" />\n <slot name=\"start\" />\n\n <textarea\n :id=\"fieldId\"\n ref=\"controlEl\"\n v-model=\"model\"\n :aria-invalid=\"invalid || undefined\"\n v-bind=\"restAttrs\"\n class=\"v-textarea-control\"\n :rows=\"resolvedRows\"\n :maxlength=\"softLimit ? undefined : maxlength\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-describedby=\"describedBy\"\n />\n\n <slot name=\"value-end\" />\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-clear\"\n :aria-label=\"resolvedClearLabel\"\n @click=\"onClear\"\n >\n <VIcon :name=\"closeIcon\" />\n </button>\n\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <slot v-else name=\"end\">\n <button\n v-if=\"iconEnd && hasIconEndHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-end\"\n :aria-label=\"iconEndLabel ?? iconName(iconEnd)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-end', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconEnd)\" />\n </button>\n <VIcon v-else-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" class=\"v-textarea-icon-end\" />\n </slot>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-textarea-meta v-field-meta\">\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-textarea-hint\">\n {{ hint }}\n </VTypography>\n <span\n v-if=\"counter\"\n :id=\"counterId\"\n class=\"v-textarea-counter v-field-counter\"\n :data-over=\"over ? '' : undefined\"\n >\n {{ counterText }}\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-textarea {\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 /*\n * The .v-textarea-label and .v-textarea-hint classes remain as hooks: a consumer overrides\n * through them, and the disabled state below reaches them that way.\n */\n\n /*\n * This is the box that carries the border, the background, the focus ring and the resize\n * handle; which is why it also hides its overflow, `resize` having no effect on a box whose\n * overflow is visible. `--field-border-color` is the single source of truth for its colour,\n * and the hover, error and disabled states do nothing but redefine it.\n */\n .v-textarea-field {\n --field-border-color: var(--vectis-color-border-strong);\n\n /*\n * Nothing here restates the size scale: the `--control-*` variables are inherited from the\n * v-control root (styles/control-size.css), the icon context included. The field carries no\n * height of its own: it is the `rows` attribute on the textarea that sets one, and the\n * field is simply as tall as the control it wraps.\n */\n\n /*\n * The leading is named once and feeds two things that have to agree by construction: the\n * field's own `line-height`, and the first line box below, against which everything sitting\n * beside the text is offset.\n */\n --textarea-leading: var(--vectis-text-body-md-leading);\n\n /*\n * It is DERIVED and must never be written as `1lh`. The buttons happened to be right only\n * because the reset gives them `font: inherit`.\n */\n --textarea-line: calc(var(--control-font-size) * var(--textarea-leading));\n\n display: flex;\n align-items: flex-start;\n gap: var(--control-gap);\n padding-inline: var(--control-padding-inline-field);\n /*\n * Derive padding from control height minus borders and one line. Read the compact-adjusted\n * height so rows=1 matches VInput and each additional row adds one line.\n */\n padding-block: calc((var(--control-height) - 2px - var(--textarea-line)) / 2);\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--field-border-color);\n /*\n * The cap is what carries the VInput identity above into the OTHER row counts, and it has\n * to stay derived from --control-height. A browser scales down any radius it cannot fit, so\n * on a one-row field a pill override is already painted as half the control height; min()\n * applies that same reduction to a field that is taller.\n */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-size: var(--control-font-size);\n /* Text running over several lines takes the body line height. The `control` type\n role, whose lines are set tight against one another, only makes sense for a\n single-line label. */\n line-height: var(--textarea-leading);\n resize: vertical;\n overflow: hidden;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-textarea-control {\n flex: 1;\n min-width: 0;\n align-self: stretch;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n outline: none; /* The focus ring is drawn by the field around it, not here */\n resize: none;\n }\n\n .v-textarea-control::placeholder {\n color: var(--vectis-color-text-subtle);\n }\n\n /*\n * The margin is SYMMETRIC, the VToast rule. It looks redundant under\n * `align-items: flex-start`, where only the start side moves anything, but the end side is\n * what keeps the item's OUTER box down to one line box.\n */\n .v-textarea-field > .v-icon,\n .v-textarea-field > .v-spinner {\n margin-block: calc((var(--textarea-line) - var(--vectis-icon-size)) / 2);\n }\n\n .v-textarea-field > .v-textarea-action {\n margin-block: calc((var(--textarea-line) - var(--control-action-size)) / 2);\n }\n\n /* A decorative icon is drawn in a muted grey, so it stays quieter than the text\n being typed beside it. */\n .v-textarea-field > .v-icon {\n color: var(--vectis-color-text-muted);\n }\n\n .v-textarea-field > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n /*\n * This block must stay first in the sequence of states: read-only, then hover, focus, invalid\n * and disabled. The read-only, focus, invalid and disabled selectors all weigh (0,3,0),\n * `:has()` taking the specificity of what it contains, so nothing but the source order\n * arbitrates between them.\n */\n .v-textarea[data-readonly] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-sunken);\n }\n\n .v-textarea-field:hover:not(:has(.v-textarea-control:focus)):not(\n :has(\n .v-textarea-control:disabled,\n .v-textarea-control:user-invalid,\n .v-textarea-control[aria-invalid='true']\n )\n ) {\n --field-border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /*\n * The selector watches the control's focus and not `:focus-within`, so that when one of the\n * field's own buttons takes keyboard focus, only that button's outline lights up: two\n * indicators at once would be unreadable. And it is `:focus` rather than `:focus-visible`,\n * because a text field shows its focus even when it was reached with the mouse.\n */\n .v-textarea-field:has(.v-textarea-control:focus) {\n --field-border-color: var(--vectis-color-accent);\n\n box-shadow: 0 0 0 1px var(--field-border-color);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n /*\n * Only the colour variable is changed, which is why the border and the focus ring both turn\n * red without either being restated.\n */\n .v-textarea-field:has(.v-textarea-control:user-invalid),\n .v-textarea-field:has(.v-textarea-control[aria-invalid='true']) {\n --field-border-color: var(--vectis-color-danger);\n }\n\n /*\n * A disabled field greys out through the colour tokens, the same ones VCheckbox and VRadio\n * use, and never through opacity. It comes last in the sequence of states, which at equal\n * specificity is what makes it win over all of them, the error included: a disabled field is\n * not submitted, so it has nothing to report.\n */\n .v-textarea[data-disabled] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n cursor: not-allowed;\n resize: none;\n }\n\n .v-textarea[data-disabled] .v-textarea-label,\n .v-textarea[data-disabled] .v-textarea-hint,\n .v-textarea[data-disabled] .v-textarea-counter {\n color: var(--vectis-color-text-subtle);\n }\n\n .v-textarea[data-disabled] .v-textarea-action,\n .v-textarea[data-disabled] .v-textarea-field > .v-icon {\n color: inherit;\n cursor: not-allowed;\n }\n\n .v-textarea-control:disabled {\n cursor: not-allowed;\n }\n\n /*\n * A browser without support keeps the fixed height and its scrollbar, which is the intended\n * fallback.\n */\n .v-textarea-field[data-auto-grow] {\n resize: none;\n }\n\n /*\n * It is scoped to this branch and must not be lifted onto the control at large: a floor there\n * would stop the control shrinking with the resize handle, and the field, which hides its\n * overflow, would clip it instead of scrolling.\n */\n .v-textarea-field[data-auto-grow] .v-textarea-control {\n field-sizing: content;\n min-block-size: calc(var(--textarea-rows) * var(--textarea-line));\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-textarea-field {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}