vectis-ui 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +68 -334
  2. package/dist/components/VAccordion/VAccordionItem.d.ts +4 -6
  3. package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
  4. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js +21 -19
  5. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/VAccordion/context.d.ts +6 -1
  7. package/dist/components/VAccordion/context.js.map +1 -1
  8. package/dist/components/VAvatar/VAvatar.css +1 -1
  9. package/dist/components/VAvatar/VAvatar.d.ts +5 -3
  10. package/dist/components/VAvatar/VAvatar.js.map +1 -1
  11. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js +53 -39
  12. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/VAvatar/VAvatarGroup.d.ts +12 -3
  14. package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
  15. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +27 -20
  16. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/VAvatar/context.d.ts +2 -2
  18. package/dist/components/VAvatar/context.js.map +1 -1
  19. package/dist/components/VBadge/VBadge.css +1 -1
  20. package/dist/components/VBadge/VBadge.d.ts +8 -4
  21. package/dist/components/VBadge/VBadge.js.map +1 -1
  22. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js +6 -3
  23. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  24. package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +5 -4
  25. package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
  26. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js +8 -3
  27. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VButton/VButton.css +1 -1
  29. package/dist/components/VButton/VButton.d.ts +4 -1
  30. package/dist/components/VButton/VButton.js.map +1 -1
  31. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js +8 -7
  32. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  34. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  35. package/dist/components/VCalendar/VCalendar.css +1 -1
  36. package/dist/components/VCalendar/VCalendar.d.ts +15 -6
  37. package/dist/components/VCalendar/VCalendar.js.map +1 -1
  38. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js +144 -126
  39. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  40. package/dist/components/VCalendar/VCalendarEvent.css +1 -1
  41. package/dist/components/VCalendar/VCalendarEvent.d.ts +1 -1
  42. package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
  43. package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  44. package/dist/components/VCalendar/VCalendarMonth.css +1 -1
  45. package/dist/components/VCalendar/VCalendarMonth.d.ts +1 -1
  46. package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
  47. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js +99 -90
  48. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
  49. package/dist/components/VCalendar/VCalendarTimeGrid.css +1 -1
  50. package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +3 -3
  51. package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
  52. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js +256 -246
  53. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
  54. package/dist/components/VCalendar/VCalendarYear.css +1 -1
  55. package/dist/components/VCalendar/VCalendarYear.d.ts +8 -1
  56. package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
  57. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js +39 -32
  58. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  59. package/dist/components/VCalendar/color.d.ts +1 -1
  60. package/dist/components/VCalendar/color.js.map +1 -1
  61. package/dist/components/VCalendar/edgeStep.d.ts +3 -3
  62. package/dist/components/VCalendar/edgeStep.js +5 -0
  63. package/dist/components/VCalendar/edgeStep.js.map +1 -1
  64. package/dist/components/VCalendar/gesture.d.ts +10 -9
  65. package/dist/components/VCalendar/gesture.js +60 -45
  66. package/dist/components/VCalendar/gesture.js.map +1 -1
  67. package/dist/components/VCalendar/keyboard.d.ts +22 -5
  68. package/dist/components/VCalendar/keyboard.js +17 -16
  69. package/dist/components/VCalendar/keyboard.js.map +1 -1
  70. package/dist/components/VCalendar/layout.d.ts +47 -37
  71. package/dist/components/VCalendar/layout.js +47 -37
  72. package/dist/components/VCalendar/layout.js.map +1 -1
  73. package/dist/components/VCalendar/timeGrid.d.ts +4 -4
  74. package/dist/components/VCalendar/timeGrid.js +6 -6
  75. package/dist/components/VCalendar/timeGrid.js.map +1 -1
  76. package/dist/components/VCalendar/types.d.ts +5 -5
  77. package/dist/components/VCarousel/VCarousel.css +1 -1
  78. package/dist/components/VCarousel/VCarousel.d.ts +59 -38
  79. package/dist/components/VCarousel/VCarousel.js.map +1 -1
  80. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +161 -132
  81. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  82. package/dist/components/VCarousel/VCarouselItem.d.ts +1 -1
  83. package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
  84. package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  85. package/dist/components/VCarousel/context.d.ts +1 -1
  86. package/dist/components/VCarousel/context.js.map +1 -1
  87. package/dist/components/VCheckbox/VCheckbox.css +1 -1
  88. package/dist/components/VCheckbox/VCheckbox.d.ts +2 -2
  89. package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
  90. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js +10 -8
  91. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  92. package/dist/components/VChip/VChip.css +1 -1
  93. package/dist/components/VChip/VChip.d.ts +7 -2
  94. package/dist/components/VChip/VChip.js.map +1 -1
  95. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js +69 -55
  96. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  97. package/dist/components/VCombobox/VCombobox.css +1 -1
  98. package/dist/components/VCombobox/VCombobox.d.ts +50 -36
  99. package/dist/components/VCombobox/VCombobox.js.map +1 -1
  100. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +167 -161
  101. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  102. package/dist/components/VCombobox/VComboboxGroup.css +1 -1
  103. package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
  104. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js +1 -1
  105. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  106. package/dist/components/VCombobox/infiniteScroll.d.ts +12 -3
  107. package/dist/components/VCombobox/infiniteScroll.js +18 -12
  108. package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
  109. package/dist/components/VDataTable/VDataTable.css +1 -1
  110. package/dist/components/VDataTable/VDataTable.d.ts +55 -24
  111. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  112. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +192 -155
  113. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  114. package/dist/components/VDateInput/VDateInput.css +1 -1
  115. package/dist/components/VDateInput/VDateInput.d.ts +15 -12
  116. package/dist/components/VDateInput/VDateInput.js.map +1 -1
  117. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js +180 -160
  118. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  119. package/dist/components/VDateInput/mask.d.ts +95 -0
  120. package/dist/components/VDateInput/mask.js +118 -0
  121. package/dist/components/VDateInput/mask.js.map +1 -0
  122. package/dist/components/VDatePicker/VDatePicker.css +1 -1
  123. package/dist/components/VDatePicker/VDatePicker.d.ts +6 -5
  124. package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
  125. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js +204 -173
  126. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  127. package/dist/components/VDatePicker/keyboard.d.ts +3 -3
  128. package/dist/components/VDatePicker/keyboard.js.map +1 -1
  129. package/dist/components/VDatePicker/names.d.ts +34 -0
  130. package/dist/components/VDatePicker/names.js +42 -0
  131. package/dist/components/VDatePicker/names.js.map +1 -0
  132. package/dist/components/VDialog/VDialog.css +1 -1
  133. package/dist/components/VDialog/VDialog.d.ts +2 -2
  134. package/dist/components/VDialog/VDialog.js.map +1 -1
  135. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +63 -41
  136. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  137. package/dist/components/VFileInput/VFileInput.d.ts +38 -25
  138. package/dist/components/VFileInput/VFileInput.js.map +1 -1
  139. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js +74 -71
  140. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  141. package/dist/components/VFileInput/truncate.d.ts +4 -4
  142. package/dist/components/VFileInput/truncate.js.map +1 -1
  143. package/dist/components/VFilePicker/VFilePicker.css +1 -1
  144. package/dist/components/VFilePicker/VFilePicker.d.ts +54 -37
  145. package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
  146. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js +152 -135
  147. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  148. package/dist/components/VFilePicker/fileKind.d.ts +2 -2
  149. package/dist/components/VFilePicker/fileKind.js.map +1 -1
  150. package/dist/components/VHotkeys/VHotkeys.css +1 -1
  151. package/dist/components/VHotkeys/VHotkeys.d.ts +10 -8
  152. package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
  153. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js +22 -20
  154. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
  155. package/dist/components/VHotkeys/platform.d.ts +9 -7
  156. package/dist/components/VHotkeys/platform.js +41 -30
  157. package/dist/components/VHotkeys/platform.js.map +1 -1
  158. package/dist/components/VIcon/VIcon.css +1 -1
  159. package/dist/components/VIcon/VIcon.d.ts +6 -6
  160. package/dist/components/VIcon/VIcon.js.map +1 -1
  161. package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
  162. package/dist/components/VIcon/iconProps.d.ts +4 -4
  163. package/dist/components/VIcon/iconProps.js.map +1 -1
  164. package/dist/components/VIcon/icons/index.d.ts +1 -1
  165. package/dist/components/VIcon/icons/index.js.map +1 -1
  166. package/dist/components/VIcon/icons/names.d.ts +1 -1
  167. package/dist/components/VIcon/icons/names.js.map +1 -1
  168. package/dist/components/VIcon/resolver.d.ts +3 -3
  169. package/dist/components/VIcon/resolver.js +23 -20
  170. package/dist/components/VIcon/resolver.js.map +1 -1
  171. package/dist/components/VIcon/types.d.ts +4 -4
  172. package/dist/components/VIconButton/VIconButton.d.ts +4 -4
  173. package/dist/components/VIconButton/VIconButton.js.map +1 -1
  174. package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  175. package/dist/components/VInput/VInput.d.ts +16 -8
  176. package/dist/components/VInput/VInput.js.map +1 -1
  177. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js +22 -16
  178. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/components/VInput/VInputGroup.css +1 -1
  180. package/dist/components/VInput/VInputGroup.js.map +1 -1
  181. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +43 -28
  182. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  183. package/dist/components/VInputOTP/VInputOTP.css +1 -1
  184. package/dist/components/VInputOTP/VInputOTP.d.ts +3 -3
  185. package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
  186. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +152 -85
  187. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
  188. package/dist/components/VMenu/VMenu.d.ts +4 -4
  189. package/dist/components/VMenu/VMenu.js.map +1 -1
  190. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  191. package/dist/components/VMenu/VMenuGroup.css +1 -1
  192. package/dist/components/VMenu/VMenuGroup.js.map +1 -1
  193. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js +18 -11
  194. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  195. package/dist/components/VMenu/VMenuItem.css +1 -1
  196. package/dist/components/VMenu/VMenuItem.d.ts +6 -6
  197. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  198. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +83 -69
  199. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  200. package/dist/components/VMenu/VMenuPanel.css +1 -1
  201. package/dist/components/VMenu/VMenuPanel.d.ts +2 -2
  202. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  203. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +56 -55
  204. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  205. package/dist/components/VMenu/context.d.ts +2 -2
  206. package/dist/components/VMenu/context.js.map +1 -1
  207. package/dist/components/VPagination/VPagination.css +1 -1
  208. package/dist/components/VPagination/VPagination.d.ts +22 -9
  209. package/dist/components/VPagination/VPagination.js.map +1 -1
  210. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +67 -57
  211. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  212. package/dist/components/VPopover/VPopover.css +1 -1
  213. package/dist/components/VPopover/VPopover.d.ts +1 -1
  214. package/dist/components/VPopover/VPopover.js.map +1 -1
  215. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  216. package/dist/components/VProgressCircular/VProgressCircular.css +1 -1
  217. package/dist/components/VProgressCircular/VProgressCircular.d.ts +12 -7
  218. package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
  219. package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
  220. package/dist/components/VProgressLinear/VProgressLinear.css +1 -1
  221. package/dist/components/VProgressLinear/VProgressLinear.d.ts +15 -10
  222. package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
  223. package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  224. package/dist/components/VRadio/VRadio.css +1 -1
  225. package/dist/components/VRadio/VRadio.js.map +1 -1
  226. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js +3 -3
  227. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  228. package/dist/components/VSeparator/VSeparator.d.ts +1 -1
  229. package/dist/components/VSeparator/VSeparator.js.map +1 -1
  230. package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  231. package/dist/components/VSideNavigation/VSideNavigation.d.ts +2 -2
  232. package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
  233. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js +1 -1
  234. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
  235. package/dist/components/VSideNavigation/VSideNavigationGroup.css +1 -1
  236. package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
  237. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js +1 -1
  238. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  239. package/dist/components/VSideNavigation/VSideNavigationItem.css +1 -1
  240. package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +12 -13
  241. package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
  242. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +87 -68
  243. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  244. package/dist/components/VSkeletonLoader/VSkeletonLoader.css +1 -1
  245. package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +10 -7
  246. package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
  247. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js +5 -5
  248. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  249. package/dist/components/VSlider/VSlider.css +1 -1
  250. package/dist/components/VSlider/VSlider.d.ts +21 -7
  251. package/dist/components/VSlider/VSlider.js.map +1 -1
  252. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js +159 -140
  253. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  254. package/dist/components/VSnackbar/VSnackbar.css +1 -1
  255. package/dist/components/VSnackbar/VSnackbar.d.ts +4 -4
  256. package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
  257. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js +85 -59
  258. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  259. package/dist/components/VSnackbar/state.d.ts +7 -7
  260. package/dist/components/VSnackbar/state.js +13 -11
  261. package/dist/components/VSnackbar/state.js.map +1 -1
  262. package/dist/components/VSpinner/VSpinner.css +1 -1
  263. package/dist/components/VSpinner/VSpinner.js.map +1 -1
  264. package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
  265. package/dist/components/VSwitch/VSwitch.css +1 -1
  266. package/dist/components/VSwitch/VSwitch.d.ts +1 -1
  267. package/dist/components/VSwitch/VSwitch.js.map +1 -1
  268. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js +4 -4
  269. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  270. package/dist/components/VTabs/VTab.css +1 -1
  271. package/dist/components/VTabs/VTab.d.ts +4 -0
  272. package/dist/components/VTabs/VTab.js.map +1 -1
  273. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js +9 -8
  274. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
  275. package/dist/components/VTabs/VTabs.css +1 -1
  276. package/dist/components/VTabs/VTabs.d.ts +17 -7
  277. package/dist/components/VTabs/VTabs.js.map +1 -1
  278. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js +84 -77
  279. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  280. package/dist/components/VTextarea/VTextarea.d.ts +8 -8
  281. package/dist/components/VTextarea/VTextarea.js.map +1 -1
  282. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js +71 -69
  283. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  284. package/dist/components/VTimeInput/VTimeInput.css +1 -1
  285. package/dist/components/VTimeInput/VTimeInput.d.ts +21 -12
  286. package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
  287. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js +230 -206
  288. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  289. package/dist/components/VTimeInput/search.d.ts +17 -2
  290. package/dist/components/VTimeInput/search.js +19 -9
  291. package/dist/components/VTimeInput/search.js.map +1 -1
  292. package/dist/components/VTimePicker/VTimePicker.css +1 -1
  293. package/dist/components/VTimePicker/VTimePicker.d.ts +8 -7
  294. package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
  295. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js +128 -106
  296. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  297. package/dist/components/VTimePicker/limits.d.ts +11 -4
  298. package/dist/components/VTimePicker/limits.js +3 -2
  299. package/dist/components/VTimePicker/limits.js.map +1 -1
  300. package/dist/components/VToast/VToast.css +1 -1
  301. package/dist/components/VToast/VToast.js.map +1 -1
  302. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js +33 -37
  303. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
  304. package/dist/components/VToast/VToaster.js.map +1 -1
  305. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js +91 -61
  306. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  307. package/dist/components/VToast/state.d.ts +8 -6
  308. package/dist/components/VToast/state.js +15 -13
  309. package/dist/components/VToast/state.js.map +1 -1
  310. package/dist/components/VToggle/VToggle.css +1 -1
  311. package/dist/components/VToggle/VToggle.d.ts +9 -4
  312. package/dist/components/VToggle/VToggle.js.map +1 -1
  313. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +2 -1
  314. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  315. package/dist/components/VToggle/VToggleItem.d.ts +4 -0
  316. package/dist/components/VToggle/VToggleItem.js.map +1 -1
  317. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js +5 -5
  318. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  319. package/dist/components/VTooltip/VTooltip.css +1 -1
  320. package/dist/components/VTooltip/VTooltip.d.ts +17 -10
  321. package/dist/components/VTooltip/VTooltip.js.map +1 -1
  322. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +46 -26
  323. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  324. package/dist/components/VTypography/VTypography.css +1 -1
  325. package/dist/components/VTypography/VTypography.d.ts +6 -4
  326. package/dist/components/VTypography/VTypography.js.map +1 -1
  327. package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
  328. package/dist/composables/useAriaLabel.js +1 -1
  329. package/dist/composables/useAriaLabel.js.map +1 -1
  330. package/dist/composables/useClearable.d.ts +2 -2
  331. package/dist/composables/useClearable.js.map +1 -1
  332. package/dist/composables/useDetailsOpen.d.ts +3 -3
  333. package/dist/composables/useDetailsOpen.js +14 -12
  334. package/dist/composables/useDetailsOpen.js.map +1 -1
  335. package/dist/composables/useFieldIds.d.ts +2 -1
  336. package/dist/composables/useFieldIds.js +6 -5
  337. package/dist/composables/useFieldIds.js.map +1 -1
  338. package/dist/composables/useFieldPanel.d.ts +2 -2
  339. package/dist/composables/useFieldPanel.js +47 -37
  340. package/dist/composables/useFieldPanel.js.map +1 -1
  341. package/dist/composables/useFileDrop.js +3 -3
  342. package/dist/composables/useFileDrop.js.map +1 -1
  343. package/dist/composables/useFileField.d.ts +1 -1
  344. package/dist/composables/useFileField.js +62 -41
  345. package/dist/composables/useFileField.js.map +1 -1
  346. package/dist/composables/useIconClickHandlers.d.ts +3 -3
  347. package/dist/composables/useIconClickHandlers.js +14 -13
  348. package/dist/composables/useIconClickHandlers.js.map +1 -1
  349. package/dist/composables/useInertLink.js +12 -10
  350. package/dist/composables/useInertLink.js.map +1 -1
  351. package/dist/composables/useLiveAnnouncer.d.ts +5 -0
  352. package/dist/composables/useLiveAnnouncer.js +23 -0
  353. package/dist/composables/useLiveAnnouncer.js.map +1 -0
  354. package/dist/composables/useMaskedField.d.ts +5 -5
  355. package/dist/composables/useMaskedField.js.map +1 -1
  356. package/dist/composables/usePopover.d.ts +3 -3
  357. package/dist/composables/usePopover.js +6 -4
  358. package/dist/composables/usePopover.js.map +1 -1
  359. package/dist/composables/useProgressValue.js +11 -2
  360. package/dist/composables/useProgressValue.js.map +1 -1
  361. package/dist/composables/useRootAttrs.d.ts +1 -1
  362. package/dist/composables/useRootAttrs.js.map +1 -1
  363. package/dist/composables/useSlotNodes.d.ts +18 -0
  364. package/dist/composables/useSlotNodes.js +13 -0
  365. package/dist/composables/useSlotNodes.js.map +1 -0
  366. package/dist/composables/useTextLimit.d.ts +2 -1
  367. package/dist/composables/useTextLimit.js +2 -2
  368. package/dist/composables/useTextLimit.js.map +1 -1
  369. package/dist/composables/useTimer.d.ts +5 -0
  370. package/dist/composables/useTimer.js +22 -11
  371. package/dist/composables/useTimer.js.map +1 -1
  372. package/dist/i18n/en.js +1 -1
  373. package/dist/i18n/en.js.map +1 -1
  374. package/dist/i18n/fr.js +14 -14
  375. package/dist/i18n/fr.js.map +1 -1
  376. package/dist/i18n/state.d.ts +7 -7
  377. package/dist/i18n/state.js +37 -28
  378. package/dist/i18n/state.js.map +1 -1
  379. package/dist/i18n/types.d.ts +12 -12
  380. package/dist/icons.d.ts +2 -2
  381. package/dist/index.d.ts +12 -11
  382. package/dist/styles.css +1 -1
  383. package/dist/tokens/css.d.ts +1 -1
  384. package/dist/tokens/css.js.map +1 -1
  385. package/dist/tokens/index.d.ts +6 -0
  386. package/dist/tokens/semantic.d.ts +6 -0
  387. package/dist/tokens/semantic.js +66 -60
  388. package/dist/tokens/semantic.js.map +1 -1
  389. package/dist/tokens/themes/dark.js +3 -3
  390. package/dist/tokens/themes/dark.js.map +1 -1
  391. package/dist/tokens.json +130 -66
  392. package/dist/types.d.ts +1 -1
  393. package/dist/utils/arrowNav.d.ts +4 -1
  394. package/dist/utils/arrowNav.js +7 -7
  395. package/dist/utils/arrowNav.js.map +1 -1
  396. package/dist/utils/chip.d.ts +1 -1
  397. package/dist/utils/chip.js.map +1 -1
  398. package/dist/utils/clock.d.ts +113 -0
  399. package/dist/utils/clock.js +84 -0
  400. package/dist/utils/clock.js.map +1 -0
  401. package/dist/utils/css.d.ts +6 -4
  402. package/dist/utils/css.js +2 -2
  403. package/dist/utils/css.js.map +1 -1
  404. package/dist/utils/date.d.ts +7 -102
  405. package/dist/utils/date.js +33 -160
  406. package/dist/utils/date.js.map +1 -1
  407. package/dist/utils/direction.d.ts +1 -1
  408. package/dist/utils/direction.js.map +1 -1
  409. package/dist/utils/env.d.ts +12 -3
  410. package/dist/utils/env.js +6 -0
  411. package/dist/utils/env.js.map +1 -0
  412. package/dist/utils/file.d.ts +3 -3
  413. package/dist/utils/file.js +1 -1
  414. package/dist/utils/file.js.map +1 -1
  415. package/dist/utils/focus.d.ts +1 -1
  416. package/dist/utils/focus.js.map +1 -1
  417. package/dist/utils/hostWarns.d.ts +1 -1
  418. package/dist/utils/hostWarns.js.map +1 -1
  419. package/dist/utils/ids.d.ts +1 -1
  420. package/dist/utils/ids.js.map +1 -1
  421. package/dist/utils/matcher.d.ts +2 -2
  422. package/dist/utils/matcher.js.map +1 -1
  423. package/dist/utils/memo.d.ts +1 -1
  424. package/dist/utils/memo.js.map +1 -1
  425. package/dist/utils/text.d.ts +3 -3
  426. package/dist/utils/text.js.map +1 -1
  427. package/dist/utils/time.d.ts +2 -101
  428. package/dist/utils/time.js +22 -98
  429. package/dist/utils/time.js.map +1 -1
  430. package/dist/utils/vnode.d.ts +5 -3
  431. package/dist/utils/vnode.js.map +1 -1
  432. package/package.json +2 -2
@@ -4,19 +4,19 @@ import n from "../VIcon/VIcon.js";
4
4
  import r from "../VTypography/VTypography.js";
5
5
  import i from "../VIconButton/VIconButton.js";
6
6
  import { close as a } from "../VIcon/icons/close.js";
7
- import { Fragment as o, computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createTextVNode as f, createVNode as p, defineComponent as m, mergeModels as h, mergeProps as g, nextTick as _, onMounted as v, openBlock as y, ref as b, renderSlot as x, toDisplayString as S, unref as C, useAttrs as w, useId as T, useModel as E, watch as D, withCtx as O } from "vue";
7
+ import { Fragment as o, computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createTextVNode as f, createVNode as p, defineComponent as m, mergeModels as h, mergeProps as g, nextTick as _, onBeforeUnmount as v, onMounted as ee, openBlock as y, ref as b, renderSlot as x, toDisplayString as S, unref as C, useAttrs as w, useId as T, useModel as E, watch as D, withCtx as O } from "vue";
8
8
  //#region src/components/VDialog/VDialog.vue?vue&type=script&setup=true&lang.ts
9
9
  var k = [
10
- "role",
11
10
  "aria-labelledby",
12
- "aria-describedby"
13
- ], A = { class: "v-dialog-header" }, j = { class: "v-dialog-titles" }, M = {
11
+ "aria-describedby",
12
+ "role"
13
+ ], te = { class: "v-dialog-header" }, A = { class: "v-dialog-titles" }, j = {
14
14
  key: 0,
15
15
  class: "v-dialog-header-actions"
16
- }, N = { class: "v-dialog-scroll" }, P = { class: "v-dialog-body" }, F = {
16
+ }, M = { class: "v-dialog-scroll" }, N = { class: "v-dialog-body" }, P = {
17
17
  key: 0,
18
18
  class: "v-dialog-footer"
19
- }, I = /*@__PURE__*/ m({
19
+ }, F = /*@__PURE__*/ m({
20
20
  inheritAttrs: !1,
21
21
  __name: "VDialog",
22
22
  props: /*@__PURE__*/ h({
@@ -46,47 +46,69 @@ var k = [
46
46
  }),
47
47
  emits: ["update:open"],
48
48
  setup(m, { expose: h }) {
49
- let I = m, L = e(), R = s(() => I.closeLabel ?? L.value.common.close), z = E(m, "open"), B = w(), V = b(null), H = T(), U = T(), W = s(() => I.persistentBackdrop ? I.persistentEscape ? "none" : "closerequest" : "any"), G = s(() => ({
50
- ...B,
51
- closedby: W.value
52
- })), K = b(z.value), q = Promise.resolve();
53
- function J() {
54
- return z.value = !0, _(() => q);
49
+ let F = m, I = e(), L = s(() => F.closeLabel ?? I.value.common.close), R = E(m, "open"), z = w(), B = b(null), V = T(), H = T(), U = s(() => F.persistentBackdrop ? F.persistentEscape ? "none" : "closerequest" : "any"), W = s(() => ({
50
+ ...z,
51
+ closedby: U.value
52
+ })), G = b(R.value), K = Promise.resolve();
53
+ function q() {
54
+ return R.value = !0, _(() => K);
55
55
  }
56
- function Y() {
57
- V.value?.close();
56
+ function J() {
57
+ B.value?.close();
58
58
  }
59
- let X = s(() => ({
60
- onClick: J,
59
+ let Y = s(() => ({
60
+ onClick: q,
61
61
  "aria-haspopup": "dialog"
62
62
  }));
63
- D(z, (e) => {
64
- e ? (K.value = !0, q = _(() => V.value?.showModal())) : (V.value?.close(), K.value = !1);
65
- }), v(() => {
66
- z.value && V.value?.showModal();
63
+ D(R, (e) => {
64
+ e ? (G.value = !0, K = _(() => B.value?.showModal())) : (B.value?.close(), G.value = !1);
65
+ }), ee(() => {
66
+ R.value && B.value?.showModal();
67
67
  });
68
- function Z() {
69
- z.value = !1;
68
+ function X(e) {
69
+ e.target === B.value && (R.value = !1);
70
+ }
71
+ v(() => {
72
+ R.value &&= !1;
73
+ });
74
+ let Z = () => !("closedBy" in HTMLDialogElement.prototype);
75
+ function ne(e) {
76
+ Z() && U.value === "none" && e.preventDefault();
77
+ }
78
+ function Q(e) {
79
+ let t = B.value?.getBoundingClientRect();
80
+ return t ? e.clientX < t.left || e.clientX > t.right || e.clientY < t.top || e.clientY > t.bottom : !1;
81
+ }
82
+ let $ = !1;
83
+ function re(e) {
84
+ $ = e.target === B.value && Q(e);
85
+ }
86
+ function ie(e) {
87
+ let t = $ && e.target === B.value && Q(e);
88
+ $ = !1, t && Z() && U.value === "any" && J();
70
89
  }
71
90
  return h({
72
- show: J,
73
- close: Y,
74
- el: V
75
- }), (e, s) => (y(), u(o, null, [x(e.$slots, "trigger", { triggerProps: X.value }), K.value ? (y(), u("dialog", g({
91
+ show: q,
92
+ close: J,
93
+ el: B
94
+ }), (e, s) => (y(), u(o, null, [x(e.$slots, "trigger", { triggerProps: Y.value }), G.value ? (y(), u("dialog", g({
76
95
  key: 0,
77
96
  ref_key: "dialogEl",
78
- ref: V
79
- }, G.value, {
97
+ ref: B,
98
+ "aria-labelledby": m.title ? C(V) : void 0,
99
+ "aria-describedby": m.subtitle ? C(H) : void 0
100
+ }, W.value, {
80
101
  class: "v-dialog",
81
102
  style: { "--dialog-width": C(t)(m.width) },
82
103
  role: m.role === "alertdialog" ? "alertdialog" : void 0,
83
- "aria-labelledby": m.title ? C(H) : void 0,
84
- "aria-describedby": m.subtitle ? C(U) : void 0,
85
- onClose: Z
104
+ onClose: X,
105
+ onCancel: ne,
106
+ onPointerdown: re,
107
+ onClick: ie
86
108
  }), [
87
- d("header", A, [x(e.$slots, "header", {}, () => [d("div", j, [m.title ? (y(), c(r, {
109
+ d("header", te, [x(e.$slots, "header", {}, () => [d("div", A, [m.title ? (y(), c(r, {
88
110
  key: 0,
89
- id: C(H),
111
+ id: C(V),
90
112
  as: "h2",
91
113
  variant: "heading-3",
92
114
  class: "v-dialog-title"
@@ -95,41 +117,41 @@ var k = [
95
117
  _: 1
96
118
  }, 8, ["id"])) : l("", !0), m.subtitle ? (y(), c(r, {
97
119
  key: 1,
98
- id: C(U),
120
+ id: C(H),
99
121
  variant: "subtitle",
100
122
  tone: "muted",
101
123
  class: "v-dialog-subtitle"
102
124
  }, {
103
125
  default: O(() => [f(S(m.subtitle), 1)]),
104
126
  _: 1
105
- }, 8, ["id"])) : l("", !0)])]), !m.hideClose || e.$slots["header-actions"] ? (y(), u("div", M, [x(e.$slots, "header-actions"), m.hideClose ? l("", !0) : (y(), c(i, {
127
+ }, 8, ["id"])) : l("", !0)])]), !m.hideClose || e.$slots["header-actions"] ? (y(), u("div", j, [x(e.$slots, "header-actions"), m.hideClose ? l("", !0) : (y(), c(i, {
106
128
  key: 0,
107
129
  class: "v-dialog-close",
108
- label: R.value,
130
+ label: L.value,
109
131
  variant: "ghost",
110
132
  tone: "neutral",
111
133
  size: "sm",
112
- onClick: Y
134
+ onClick: J
113
135
  }, {
114
136
  default: O(() => [p(n, { name: C(a) }, null, 8, ["name"])]),
115
137
  _: 1
116
138
  }, 8, ["label"]))])) : l("", !0)]),
117
- d("div", N, [
139
+ d("div", M, [
118
140
  s[0] ||= d("span", {
119
141
  class: "v-dialog-edge v-dialog-edge--top",
120
142
  "aria-hidden": "true"
121
143
  }, null, -1),
122
- d("div", P, [x(e.$slots, "default")]),
144
+ d("div", N, [x(e.$slots, "default")]),
123
145
  s[1] ||= d("span", {
124
146
  class: "v-dialog-edge v-dialog-edge--bottom",
125
147
  "aria-hidden": "true"
126
148
  }, null, -1)
127
149
  ]),
128
- e.$slots.footer ? (y(), u("footer", F, [x(e.$slots, "footer")])) : l("", !0)
150
+ e.$slots.footer ? (y(), u("footer", P, [x(e.$slots, "footer")])) : l("", !0)
129
151
  ], 16, k)) : l("", !0)], 64));
130
152
  }
131
153
  });
132
154
  //#endregion
133
- export { I as default };
155
+ export { F as default };
134
156
 
135
157
  //# sourceMappingURL=VDialog.vue_vue_type_script_setup_true_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"VDialog.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A modal dialog: it takes over the page until the reader answers it.\n *\n * Almost everything that makes a modal correct comes free with the native `<dialog>`\n * element opened as a modal — it is drawn above the whole page, the background is\n * dimmed and made unusable, the focus is trapped inside it and handed back to the\n * trigger on closing, and whether a click outside or Escape dismisses it is declared\n * with an attribute rather than coded.\n *\n * The ONLY behavioural JavaScript is the bridge between `v-model:open` and the two\n * methods the platform offers no declarative equivalent for: opening as a modal, and\n * closing.\n */\n\nimport { computed, nextTick, onMounted, ref, useAttrs, useId, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left\n * out, it takes the `--vectis-control-size-dialog-width` token, 400px by default. It is\n * never allowed to exceed the width of the viewport.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently — see\n * VDialogAlert, which is exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/**\n * Whether the dialog is showing. It starts closed, and it is BIDIRECTIONAL: the browser\n * writes back to it whenever the dialog closes on its own, through Escape or the backdrop,\n * so a consumer never has to reset it by hand.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It\n * stays rendered at all times, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\nconst dialogEl = ref<HTMLDialogElement | null>(null)\nconst titleId = useId()\nconst subtitleId = useId()\n\n/**\n * Which dismissals the browser itself accepts, declared as an attribute rather than\n * handled in code: everything, Escape alone, or nothing.\n *\n * One combination cannot be expressed that way — a click outside allowed while Escape\n * is not — and it falls back to allowing both. Nothing in the design system asks for\n * it, and it would be a strange thing to want.\n */\nconst closedby = computed(() =>\n props.persistentBackdrop ? (props.persistentEscape ? 'none' : 'closerequest') : 'any',\n)\n\n// @fallback\n// The `closedby` attribute is newer than the type definitions shipped with\n// TypeScript, so writing it directly in the template would be reported as an unknown\n// attribute. Passing it inside a bound object goes through the same path as any\n// forwarded attribute, which is not checked element by element. Vue still merges the\n// `class` and `style` it may contain with the static ones below.\nconst rootAttrs = computed(() => ({ ...attrs, closedby: closedby.value }))\n\n// The dialog and its content only exist while it is open, which also spares the page\n// whatever heavy thing it contains. Every opening therefore builds a BRAND NEW\n// element, so it is always opened as a modal from a clean slate, and closing removes\n// it entirely — nothing is left behind to swallow clicks, and reopening cannot race\n// with a closing that is still finishing. The trigger, by contrast, stays rendered at\n// all times.\nconst rendered = ref(open.value)\n\n/**\n * Settles once the dialog opened by the latest change of `open` is actually showing. The\n * opening waits for the element to be rendered, so it is always a tick behind the model.\n */\nlet opening: Promise<void> = Promise.resolve()\n\nfunction show(): Promise<void> {\n open.value = true\n // The watcher below only runs when the scheduler flushes, so `opening` is read from a\n // tick later: read at once, it would still be the promise of the previous opening.\n return nextTick(() => opening)\n}\n\nfunction requestClose() {\n // Closing the element is enough: the browser then fires its own close event, which\n // is what puts the model back in step below.\n dialogEl.value?.close()\n}\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\n// The `<dialog>` element is asymmetric in a way that shapes this whole bridge: it\n// announces every closing with an event, but announces nothing at all when it opens —\n// where a popover reports both. So the two directions are not written the same way:\n//\n// - opening: render the element, then, once Vue has actually put it in the document,\n// open it as a modal;\n// - closing: whatever closed it — the cross, our own call, Escape, a click outside —\n// the browser's close event is what puts the model back in step, and only then is\n// the element removed.\nwatch(open, (value) => {\n if (value) {\n rendered.value = true\n // the brand new <dialog> has to exist before it can be opened\n opening = nextTick(() => dialogEl.value?.showModal())\n } else {\n dialogEl.value?.close()\n rendered.value = false\n }\n})\n\n// @ssr\nonMounted(() => {\n // A watcher does not run during the server render, so a dialog asked to be open from\n // the start would never be opened. The element is already there — the flag above was\n // initialized from the model — it just has to be told to show itself.\n if (open.value) dialogEl.value?.showModal()\n})\n\n// The browser's own close event, whatever caused it, is what brings the model back in\n// step with reality.\nfunction onClose() {\n open.value = false\n}\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close: requestClose,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n @close=\"onClose\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"requestClose\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge v-dialog-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge v-dialog-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /* The width comes from the prop, set inline, or from the token when there is none.\n Whatever it asks for, the dialog is never allowed past the viewport, margins included.\n\n TRAP — the token is declared ON the element rather than written as a `var()` fallback.\n A custom property inherits, and a dialog opened from inside another one is its DOM\n descendant: with a fallback, an alert confirming something in an 800px dialog would\n read that dialog's inline width and open 800px wide itself, with no error anywhere. */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /* This puts the dialog back in the middle of the screen. The browser centres a\n modal with an automatic margin, which the design system's own reset — where\n every element loses its margins — had removed; restoring it here works because\n the component layer is stronger than the reset one. */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own,\n * but any display we declare above beats it — so without this, a closed dialog would\n * sit in the page at the top left, swallowing clicks.\n *\n * It also covers the two moments where the element exists without being open yet: the\n * frame between rendering it and opening it, and the server-rendered markup. The\n * popovers have the very same guard.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /* The ONLY part that scrolls. It takes whatever room is left between the header\n and the footer, which is what keeps the scrollbar confined to it. The zero\n minimum is load-bearing: without it a flex item refuses to shrink below its\n content, and nothing would ever scroll. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n /* This makes the box something its descendants can ask questions about — namely\n whether there is content hidden above or below. It adds no containment of size. */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling\n * area, and descendants of it, which is what allows them to ask about its scroll\n * state. Their negative margins mean they occupy no space at all. They are\n * transparent until the rules further down reveal them.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none; /* stays at the top, outside whatever scrolls */\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none; /* stays at the bottom, outside whatever scrolls */\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /*\n * The lines only appear when they mean something. Asking the scrolling area whether\n * it can still be scrolled upwards tells us content is hidden ABOVE, so the line\n * under the header is shown; the same question downwards reveals the one above the\n * footer. In other words each line is drawn exactly when content is passing behind\n * the edge it marks.\n *\n * This kind of query is Chrome 133 and later for now. Where it is missing the\n * sentinels simply stay transparent, which is the intended fallback.\n */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way IN and not on the way out. Since it is removed from\n * the page the moment it closes, there is nothing left to animate out — that is the\n * price of the clean slate the lazy rendering buys, and it also spares the extra\n * declarations an exit animation would need.\n *\n * The starting values below are where a freshly rendered dialog begins, before\n * settling into its open state.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEA,IAAM,IAAQ,GAWR,IAAI,EAAY,GAChB,IAAqB,QAAe,EAAM,cAAc,EAAE,MAAM,OAAO,KAAK,GAO5E,IAAO,EAAoB,GAAC,MAA0B,GA0BtD,IAAQ,EAAS,GACjB,IAAW,EAA8B,IAAI,GAC7C,IAAU,EAAM,GAChB,IAAa,EAAM,GAUnB,IAAW,QACf,EAAM,qBAAsB,EAAM,mBAAmB,SAAS,iBAAkB,KAClF,GAQM,IAAY,SAAgB;GAAE,GAAG;GAAO,UAAU,EAAS;EAAM,EAAE,GAQnE,IAAW,EAAI,EAAK,KAAK,GAM3B,IAAyB,QAAQ,QAAQ;EAE7C,SAAS,IAAsB;GAI7B,OAHA,EAAK,QAAQ,IAGN,QAAe,CAAO;EAC/B;EAEA,SAAS,IAAe;GAGtB,EAAS,OAAO,MAAM;EACxB;EAEA,IAAM,IAAe,SAAoC;GACvD,SAAS;GACT,iBAAiB;EACnB,EAAE;EAuBF,AAZA,EAAM,IAAO,MAAU;GACrB,AAAI,KACF,EAAS,QAAQ,IAEjB,IAAU,QAAe,EAAS,OAAO,UAAU,CAAC,MAEpD,EAAS,OAAO,MAAM,GACtB,EAAS,QAAQ;EAErB,CAAC,GAGD,QAAgB;GAId,AAAI,EAAK,SAAO,EAAS,OAAO,UAAU;EAC5C,CAAC;EAID,SAAS,IAAU;GACjB,EAAK,QAAQ;EACf;SAEA,EAAa;GAMX;GAKA,OAAO;GAEP,IAAI;EACN,CAAC,+BAIC,EAAqD,EAAA,QAAA,WAAA,EAA/B,cAAe,EAAA,MAAY,CAAA,GAEzC,EAAA,SAAA,EAAA,GADR,EAoES,UApET,EAoES;;YAlEH;GAAJ,KAAI;KACI,EAAA,OAAS;GACjB,OAAM;GACL,OAAK,EAAA,kBAAsB,EAAA,CAAA,CAAO,CAAC,EAAA,KAAK,EAAA;GACxC,MAAM,EAAA,SAAI,gBAAA,gBAAqC,KAAA;GAC/C,mBAAiB,EAAA,QAAQ,EAAA,CAAA,IAAU,KAAA;GACnC,oBAAkB,EAAA,WAAW,EAAA,CAAA,IAAa,KAAA;GACnC;;GAWR,EAqCS,UArCT,GAqCS,CApCP,EAqBO,EAAA,QAAA,UAAA,CAAA,SAAA,CApBL,EAmBM,OAnBN,GAmBM,CAjBI,EAAA,SAAA,EAAA,GADR,EAQc,GAAA;;IANX,IAAI,EAAA,CAAA;IACL,IAAG;IACH,SAAQ;IACR,OAAM;;qBAEK,CAAA,EAAA,EAAR,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;+BAGF,EAAA,YAAA,EAAA,GADR,EAQc,GAAA;;IANX,IAAI,EAAA,CAAA;IACL,SAAQ;IACR,MAAK;IACL,OAAM;;qBAEQ,CAAA,EAAA,EAAX,EAAA,QAAQ,GAAA,CAAA,CAAA,CAAA;;oCAIL,EAAA,aAAaA,EAAAA,OAAM,qBAAA,EAAA,GAA/B,EAaM,OAbN,GAaM,CAZJ,EAA8B,EAAA,QAAA,gBAAA,GAErB,EAAA,yBAAA,EAAA,GADT,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACR,SAAQ;IACR,MAAK;IACL,MAAK;IACJ,SAAO;;qBAEmB,CAA3B,EAA2B,GAAA,EAAnB,MAAM,EAAA,CAAA,EAAS,GAAA,MAAA,GAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;;GAI7B,EAMM,OANN,GAMM;aALJ,EAAoE,QAAA;KAA9D,OAAM;KAAmC,eAAY;;IAC3D,EAEM,OAFN,GAEM,CADJ,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA;aAEV,EAAuE,QAAA;KAAjE,OAAM;KAAsC,eAAY;;;GAElDA,EAAAA,OAAO,UAAA,EAAA,GAArB,EAES,UAFT,GAES,CADP,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA"}
1
+ {"version":3,"file":"VDialog.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A modal dialog: it takes over the page until the reader answers it.\n *\n * Almost everything that makes a modal correct comes free with the native `<dialog>`\n * element opened as a modal — it is drawn above the whole page, the background is\n * dimmed and made unusable, the focus is trapped inside it and handed back to the\n * trigger on closing, and whether a click outside or Escape dismisses it is declared\n * with an attribute rather than coded.\n *\n * The ONLY behavioural JavaScript is the bridge between `v-model:open` and the two\n * methods the platform offers no declarative equivalent for: opening as a modal, and\n * closing.\n */\n\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useAttrs, useId, watch } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left\n * out, it takes the `--vectis-control-size-dialog-width` token, 400px by default. It is\n * never allowed to exceed the width of the viewport.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently. VDialogAlert is\n * exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/**\n * Whether the dialog is showing. It starts closed, and it is BIDIRECTIONAL: the browser\n * writes back to it whenever the dialog closes on its own, through Escape or the backdrop,\n * so a consumer never has to reset it by hand.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It\n * stays rendered at all times, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst attrs = useAttrs()\nconst dialogEl = ref<HTMLDialogElement | null>(null)\nconst titleId = useId()\nconst subtitleId = useId()\n\n/**\n * Which dismissals the browser itself accepts, declared as an attribute rather than\n * handled in code: everything, Escape alone, or nothing.\n *\n * One combination cannot be expressed that way — a click outside allowed while Escape\n * is not — and it falls back to allowing both. Nothing in the design system asks for\n * it, and it would be a strange thing to want.\n */\nconst closedby = computed(() =>\n props.persistentBackdrop ? (props.persistentEscape ? 'none' : 'closerequest') : 'any',\n)\n\n// @fallback\n// The `closedby` attribute is newer than the type definitions shipped with\n// TypeScript, so writing it directly in the template would be reported as an unknown\n// attribute. Passing it inside a bound object goes through the same path as any\n// forwarded attribute, which is not checked element by element. Vue still merges the\n// `class` and `style` it may contain with the static ones below.\nconst rootAttrs = computed(() => ({ ...attrs, closedby: closedby.value }))\n\n// The dialog and its content only exist while it is open, which also spares the page\n// whatever heavy thing it contains. Every opening therefore builds a BRAND NEW\n// element, so it is always opened as a modal from a clean slate, and closing removes\n// it entirely — nothing is left behind to swallow clicks, and reopening cannot race\n// with a closing that is still finishing. The trigger, by contrast, stays rendered at\n// all times.\nconst rendered = ref(open.value)\n\n/**\n * Settles once the dialog opened by the latest change of `open` is actually showing. The\n * opening waits for the element to be rendered, so it is always a tick behind the model.\n */\nlet opening: Promise<void> = Promise.resolve()\n\nfunction show(): Promise<void> {\n open.value = true\n // The watcher below only runs when the scheduler flushes, so `opening` is read from a\n // tick later: read at once, it would still be the promise of the previous opening.\n return nextTick(() => opening)\n}\n\nfunction requestClose() {\n // Closing the element is enough: the browser then fires its own close event, which\n // is what puts the model back in step below.\n dialogEl.value?.close()\n}\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\n// The `<dialog>` element is asymmetric in a way that shapes this whole bridge: it\n// announces every closing with an event, but announces nothing at all when it opens —\n// where a popover reports both. So the two directions are not written the same way:\n//\n// - opening: render the element, then, once Vue has actually put it in the document,\n// open it as a modal;\n// - closing: whatever closed it — the cross, our own call, Escape, a click outside —\n// the browser's close event is what puts the model back in step, and only then is\n// the element removed.\nwatch(open, (value) => {\n if (value) {\n rendered.value = true\n // the brand new <dialog> has to exist before it can be opened\n opening = nextTick(() => dialogEl.value?.showModal())\n } else {\n dialogEl.value?.close()\n rendered.value = false\n }\n})\n\n// @ssr\nonMounted(() => {\n // A watcher does not run during the server render, so a dialog asked to be open from\n // the start would never be opened. The element is already there — the flag above was\n // initialized from the model — it just has to be told to show itself.\n if (open.value) dialogEl.value?.showModal()\n})\n\n// The browser's own close event, whatever caused it, is what brings the model back in\n// step with reality.\n//\n// TRAP — only the element on screen may do it. A close event is queued as a TASK, so the\n// one fired by the element a quick close-and-reopen has just replaced arrives after the new\n// element exists, and without the check it closed the dialog that had just opened.\nfunction onClose(event: Event) {\n if (event.target !== dialogEl.value) return\n open.value = false\n}\n\n// @core\n// An open dialog removed with its parent (a `v-if`, a route change) runs no close steps,\n// so no close event would come: the model is handed back closed here, or it stayed `true`\n// and the dialog reopened by itself the next time its parent was shown.\nonBeforeUnmount(() => {\n if (open.value) open.value = false\n})\n\n// @fallback\n/*\n * `closedby` is what applies `persistentBackdrop` and `persistentEscape`, and Safari does\n * not implement it: there Escape always closes and the backdrop never does. The two halves\n * are rebuilt below, and ONLY where the attribute is unknown, so a browser that has it is\n * never second-guessed. The support is asked at event time: there is no DOM during setup.\n */\nconst closedbyUnsupported = () => !('closedBy' in HTMLDialogElement.prototype)\n\nfunction onCancel(event: Event) {\n if (closedbyUnsupported() && closedby.value === 'none') event.preventDefault()\n}\n\n/**\n * Whether a point lies outside the dialog's box, which is where the backdrop is: a click on\n * the `::backdrop` is dispatched to the dialog element itself.\n */\nfunction outsideBox(event: MouseEvent) {\n const box = dialogEl.value?.getBoundingClientRect()\n if (!box) return false\n return (\n event.clientX < box.left ||\n event.clientX > box.right ||\n event.clientY < box.top ||\n event.clientY > box.bottom\n )\n}\n\n// A press that STARTED inside and ended on the backdrop (a text selection dragged past the\n// edge) is not a click on the backdrop, so the press is remembered, not only the release.\nlet pressedOnBackdrop = false\nfunction onPointerdown(event: PointerEvent) {\n pressedOnBackdrop = event.target === dialogEl.value && outsideBox(event)\n}\nfunction onBackdropClick(event: MouseEvent) {\n const onBackdrop = pressedOnBackdrop && event.target === dialogEl.value && outsideBox(event)\n pressedOnBackdrop = false\n if (onBackdrop && closedbyUnsupported() && closedby.value === 'any') requestClose()\n}\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close: requestClose,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"requestClose\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge v-dialog-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge v-dialog-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /* The width comes from the prop, set inline, or from the token when there is none.\n Whatever it asks for, the dialog is never allowed past the viewport, margins included.\n\n TRAP — the token is declared ON the element rather than written as a `var()` fallback.\n A custom property inherits, and a dialog opened from inside another one is its DOM\n descendant: with a fallback, an alert confirming something in an 800px dialog would\n read that dialog's inline width and open 800px wide itself, with no error anywhere. */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /* This puts the dialog back in the middle of the screen. The browser centres a\n modal with an automatic margin, which the design system's own reset — where\n every element loses its margins — had removed; restoring it here works because\n the component layer is stronger than the reset one. */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own,\n * but any display we declare above beats it — so without this, a closed dialog would\n * sit in the page at the top left, swallowing clicks.\n *\n * It also covers the two moments where the element exists without being open yet: the\n * frame between rendering it and opening it, and the server-rendered markup. The\n * popovers have the very same guard.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /* The ONLY part that scrolls. It takes whatever room is left between the header\n and the footer, which is what keeps the scrollbar confined to it. The zero\n minimum is load-bearing: without it a flex item refuses to shrink below its\n content, and nothing would ever scroll. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n /* A wheel reaching the end of the content stops there rather than scrolling the page\n behind the modal. */\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n /* This makes the box something its descendants can ask questions about — namely\n whether there is content hidden above or below. It adds no containment of size. */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling\n * area, and descendants of it, which is what allows them to ask about its scroll\n * state. Their negative margins mean they occupy no space at all. They are\n * transparent until the rules further down reveal them.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none; /* stays at the top, outside whatever scrolls */\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none; /* stays at the bottom, outside whatever scrolls */\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /*\n * The lines only appear when they mean something. Asking the scrolling area whether\n * it can still be scrolled upwards tells us content is hidden ABOVE, so the line\n * under the header is shown; the same question downwards reveals the one above the\n * footer. In other words each line is drawn exactly when content is passing behind\n * the edge it marks.\n *\n * This kind of query is Chrome 133 and later for now. Where it is missing the\n * sentinels simply stay transparent, which is the intended fallback.\n */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way IN and not on the way out. Since it is removed from\n * the page the moment it closes, there is nothing left to animate out — that is the\n * price of the clean slate the lazy rendering buys, and it also spares the extra\n * declarations an exit animation would need.\n *\n * The starting values below are where a freshly rendered dialog begins, before\n * settling into its open state.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n\n /* Windows forced colors flattens the dialog's background to Canvas and drops its shadow,\n which were its only edge: it would float over the page with no boundary at all. An\n outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-dialog {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEA,IAAM,IAAQ,GAWR,IAAI,EAAY,GAChB,IAAqB,QAAe,EAAM,cAAc,EAAE,MAAM,OAAO,KAAK,GAO5E,IAAO,EAAoB,GAAC,MAA0B,GA0BtD,IAAQ,EAAS,GACjB,IAAW,EAA8B,IAAI,GAC7C,IAAU,EAAM,GAChB,IAAa,EAAM,GAUnB,IAAW,QACf,EAAM,qBAAsB,EAAM,mBAAmB,SAAS,iBAAkB,KAClF,GAQM,IAAY,SAAgB;GAAE,GAAG;GAAO,UAAU,EAAS;EAAM,EAAE,GAQnE,IAAW,EAAI,EAAK,KAAK,GAM3B,IAAyB,QAAQ,QAAQ;EAE7C,SAAS,IAAsB;GAI7B,OAHA,EAAK,QAAQ,IAGN,QAAe,CAAO;EAC/B;EAEA,SAAS,IAAe;GAGtB,EAAS,OAAO,MAAM;EACxB;EAEA,IAAM,IAAe,SAAoC;GACvD,SAAS;GACT,iBAAiB;EACnB,EAAE;EAuBF,AAZA,EAAM,IAAO,MAAU;GACrB,AAAI,KACF,EAAS,QAAQ,IAEjB,IAAU,QAAe,EAAS,OAAO,UAAU,CAAC,MAEpD,EAAS,OAAO,MAAM,GACtB,EAAS,QAAQ;EAErB,CAAC,GAGD,SAAgB;GAId,AAAI,EAAK,SAAO,EAAS,OAAO,UAAU;EAC5C,CAAC;EAQD,SAAS,EAAQ,GAAc;GACzB,EAAM,WAAW,EAAS,UAC9B,EAAK,QAAQ;EACf;EAMA,QAAsB;GACpB,AAAgB,EAAK,UAAQ;EAC/B,CAAC;EASD,IAAM,UAA4B,EAAE,cAAc,kBAAkB;EAEpE,SAAS,GAAS,GAAc;GAC9B,AAAI,EAAoB,KAAK,EAAS,UAAU,UAAQ,EAAM,eAAe;EAC/E;EAMA,SAAS,EAAW,GAAmB;GACrC,IAAM,IAAM,EAAS,OAAO,sBAAsB;GAElD,OADK,IAEH,EAAM,UAAU,EAAI,QACpB,EAAM,UAAU,EAAI,SACpB,EAAM,UAAU,EAAI,OACpB,EAAM,UAAU,EAAI,SALL;EAOnB;EAIA,IAAI,IAAoB;EACxB,SAAS,GAAc,GAAqB;GAC1C,IAAoB,EAAM,WAAW,EAAS,SAAS,EAAW,CAAK;EACzE;EACA,SAAS,GAAgB,GAAmB;GAC1C,IAAM,IAAa,KAAqB,EAAM,WAAW,EAAS,SAAS,EAAW,CAAK;GAE3F,AADA,IAAoB,IAChB,KAAc,EAAoB,KAAK,EAAS,UAAU,SAAO,EAAa;EACpF;SAEA,EAAa;GAMX;GAKA,OAAO;GAEP,IAAI;EACN,CAAC,+BAIC,EAAqD,EAAA,QAAA,WAAA,EAA/B,cAAe,EAAA,MAAY,CAAA,GAEzC,EAAA,SAAA,EAAA,GADR,EAuES,UAvET,EAuES;;YArEH;GAAJ,KAAI;GACH,mBAAiB,EAAA,QAAQ,EAAA,CAAA,IAAU,KAAA;GACnC,oBAAkB,EAAA,WAAW,EAAA,CAAA,IAAa,KAAA;KACnC,EAAA,OAAS;GACjB,OAAM;GACL,OAAK,EAAA,kBAAsB,EAAA,CAAA,CAAO,CAAC,EAAA,KAAK,EAAA;GACxC,MAAM,EAAA,SAAI,gBAAA,gBAAqC,KAAA;GACxC;GACC;GACK;GACb,SAAO;;GAWR,EAqCS,UArCT,IAqCS,CApCP,EAqBO,EAAA,QAAA,UAAA,CAAA,SAAA,CApBL,EAmBM,OAnBN,GAmBM,CAjBI,EAAA,SAAA,EAAA,GADR,EAQc,GAAA;;IANX,IAAI,EAAA,CAAA;IACL,IAAG;IACH,SAAQ;IACR,OAAM;;qBAEK,CAAA,EAAA,EAAR,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;+BAGF,EAAA,YAAA,EAAA,GADR,EAQc,GAAA;;IANX,IAAI,EAAA,CAAA;IACL,SAAQ;IACR,MAAK;IACL,OAAM;;qBAEQ,CAAA,EAAA,EAAX,EAAA,QAAQ,GAAA,CAAA,CAAA,CAAA;;oCAIL,EAAA,aAAaA,EAAAA,OAAM,qBAAA,EAAA,GAA/B,EAaM,OAbN,GAaM,CAZJ,EAA8B,EAAA,QAAA,gBAAA,GAErB,EAAA,yBAAA,EAAA,GADT,EAUc,GAAA;;IARZ,OAAM;IACL,OAAO,EAAA;IACR,SAAQ;IACR,MAAK;IACL,MAAK;IACJ,SAAO;;qBAEmB,CAA3B,EAA2B,GAAA,EAAnB,MAAM,EAAA,CAAA,EAAS,GAAA,MAAA,GAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;;GAI7B,EAMM,OANN,GAMM;aALJ,EAAoE,QAAA;KAA9D,OAAM;KAAmC,eAAY;;IAC3D,EAEM,OAFN,GAEM,CADJ,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA;aAEV,EAAuE,QAAA;KAAjE,OAAM;KAAsC,eAAY;;;GAElDA,EAAAA,OAAO,UAAA,EAAA,GAArB,EAES,UAFT,GAES,CADP,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA"}
@@ -2,7 +2,31 @@ import { ChipSize } from '../VChip/VChip.js';
2
2
  import { IconSource } from '../VIcon/types.js';
3
3
  import { FileRejection } from '../../utils/file.js';
4
4
  /** How the chosen files are shown inside the field when several are allowed. */
5
- export type FileInputDisplay = 'text' | 'chip';
5
+ export type FileInputDisplay = 'chip' | 'text';
6
+ /** What the `#chip` slot receives. */
7
+ export interface FileInputChipSlotProps {
8
+ /** The file the chip stands for. */
9
+ file: File;
10
+ /** Its position in the selection. */
11
+ index: number;
12
+ /** Its name, already shortened in the middle so the extension survives. */
13
+ label: string;
14
+ /** Takes the file out of the selection, the only way to. */
15
+ remove: () => void;
16
+ /** The chip size worked out to sit inside the field, one step below it. */
17
+ size: ChipSize;
18
+ /** Whether the chip takes its compact height, to catch up below the smallest step. */
19
+ compact: boolean;
20
+ }
21
+ /** What the `#counter` slot receives. */
22
+ export interface FileInputCounterSlotProps {
23
+ /** How many files are selected. */
24
+ count: number;
25
+ /** Their total size, in bytes. */
26
+ bytes: number;
27
+ /** The sentence the counter shows by default, already translated. */
28
+ text: string;
29
+ }
6
30
  /** The height of the field: 32, 40 or 48 pixels. */
7
31
  export type FileInputSize = 'sm' | 'md' | 'lg';
8
32
  interface FileInputProps {
@@ -13,7 +37,7 @@ interface FileInputProps {
13
37
  *
14
38
  * It is applied TWICE, and it has to be: as an attribute, which is what filters the
15
39
  * system's file dialog, and again in code, which is the only thing that can filter a
16
- * file DROPPED on the component — the attribute has no say over a drop.
40
+ * file DROPPED on the component, the attribute having no say over a drop.
17
41
  */
18
42
  accept?: string;
19
43
  /**
@@ -45,7 +69,7 @@ interface FileInputProps {
45
69
  */
46
70
  readonly?: boolean;
47
71
  /**
48
- * Marks the field as invalid — for a rule of your own, since nothing here is checked
72
+ * Marks the field as invalid, for a rule of your own, since nothing here is checked
49
73
  * by the browser.
50
74
  */
51
75
  invalid?: boolean;
@@ -76,7 +100,7 @@ interface FileInputProps {
76
100
  */
77
101
  pickerIconLabel?: string;
78
102
  /**
79
- * Shows a spinner at the end of the field, in place of the attach icon — while an
103
+ * Shows a spinner at the end of the field, in place of the attach icon, while an
80
104
  * upload is under way, typically. It says that something is happening and changes
81
105
  * nothing else: files can still be dropped and the dialog still opens.
82
106
  */
@@ -85,7 +109,7 @@ interface FileInputProps {
85
109
  * What screen readers announce while the spinner turns. It falls back to the design
86
110
  * system dictionary.
87
111
  */
88
- loadingLabel?: string;
112
+ loadingText?: string;
89
113
  /**
90
114
  * Offers a cross that empties the selection. Worth turning on here more than on an
91
115
  * ordinary field: what a picker holds cannot be erased by typing, so the cross is the
@@ -103,7 +127,7 @@ type __VLS_Slots = {
103
127
  */
104
128
  start?(): unknown;
105
129
  /**
106
- * Controls of your own inside the field, placed before the ones the field owns — the
130
+ * Controls of your own inside the field, placed before the ones the field owns: the
107
131
  * clear cross and the icon that opens the file dialog. Those two are this component's
108
132
  * own affordance, which is why there is no `#end` here: it would replace them.
109
133
  */
@@ -113,31 +137,20 @@ type __VLS_Slots = {
113
137
  *
114
138
  * Three of the values it receives are what make it usable without regressions:
115
139
  * `label` is the name already shortened in the MIDDLE so that its extension survives,
116
- * `remove` is what takes the file out — without it the file could no longer be removed
117
- * at all — and the size and density are the ones worked out to sit inside the field,
140
+ * `remove` is what takes the file out (without it the file could no longer be removed
141
+ * at all), and the size and density are the ones worked out to sit inside the field,
118
142
  * which cannot be guessed from outside.
119
143
  */
120
- chip?(props: {
121
- file: File;
122
- index: number;
123
- label: string;
124
- remove: () => void;
125
- size: ChipSize;
126
- compact: boolean;
127
- }): unknown;
144
+ chip?(props: FileInputChipSlotProps): unknown;
128
145
  /**
129
146
  * Replaces the counter under the field. `text` is the sentence already built and
130
147
  * translated; the count and the total size in `bytes` are there for a wording of your own.
131
148
  */
132
- counter?(props: {
133
- count: number;
134
- bytes: number;
135
- text: string;
136
- }): unknown;
149
+ counter?(props: FileInputCounterSlotProps): unknown;
137
150
  };
138
151
  type __VLS_ModelProps = {
139
152
  /**
140
- * Always a LIST of files, whether or not several are allowed — never a file on its own.
153
+ * Always a LIST of files, whether or not several are allowed, and never a file on its own.
141
154
  * The shape of the value does not depend on a prop, so a consumer never has to narrow a
142
155
  * union TypeScript has no way of discriminating. With a single file it is simply a list
143
156
  * of at most one.
@@ -149,8 +162,8 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {
149
162
  /** Moves the focus to the visible field. */
150
163
  focus: (options?: FocusOptions) => void | undefined;
151
164
  /**
152
- * Opens the file dialog. It only works when called from something the reader did — a
153
- * click, a key press: browsers refuse to open a file dialog by themselves.
165
+ * Opens the file dialog. It only works when called from something the reader did (a
166
+ * click, a key press): browsers refuse to open a file dialog by themselves.
154
167
  */
155
168
  open: () => void;
156
169
  /**
@@ -186,7 +199,7 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {
186
199
  hint: string;
187
200
  readonly: boolean;
188
201
  iconStartLabel: string;
189
- loadingLabel: string;
202
+ loadingText: string;
190
203
  clearable: boolean;
191
204
  clearLabel: string;
192
205
  counter: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"VFileInput.js","names":[],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * File selection presented as an ordinary form field: a read-only VInput showing what was\n * picked, with the real file input hidden behind it — VDateInput's `readonly` shape, minus\n * the panel. The v-model is always a `File[]`, `multiple` or not, so a consumer never has to\n * narrow a union TypeScript cannot discriminate.\n *\n * The JS is imposed by the platform, not chosen. A dialog opens ONLY from a real click on a\n * file input, and a `FileList` cannot be written from a template — so the visible field can\n * never BE the native control, and that control is only ever a SOURCE of files.\n *\n * The rest follows: the relayed click, the reset after every change (without which the same\n * file cannot be picked twice running), and screening in code, `accept` having no say over\n * a drop.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'text' | 'chip'\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /**\n * Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).\n *\n * It is applied TWICE, and it has to be: as an attribute, which is what filters the\n * system's file dialog, and again in code, which is the only thing that can filter a\n * file DROPPED on the component — the attribute has no say over a drop.\n */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid — for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: 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, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. It is rendered before the chips rather than\n * in their place, so it survives a chip display. Decorative by default, it becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon — while an\n * upload is under way, typically. It says that something is happening and changes\n * nothing else: files can still be dropped and the dialog still opens.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingLabel?: string\n /**\n * Offers a cross that empties the selection. Worth turning on here more than on an\n * ordinary field: what a picker holds cannot be erased by typing, so the cross is the\n * only way back out of a wrong choice.\n */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingLabel: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection — added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns — the\n * clear cross and the icon that opens the file dialog. Those two are this component's\n * own affordance, which is why there is no `#end` here: it would replace them.\n */\n 'value-end'?(): unknown\n /**\n * Replaces the chip standing for one file.\n *\n * Three of the values it receives are what make it usable without regressions:\n * `label` is the name already shortened in the MIDDLE so that its extension survives,\n * `remove` is what takes the file out — without it the file could no longer be removed\n * at all — and the size and density are the ones worked out to sit inside the field,\n * which cannot be guessed from outside.\n */\n chip?(props: {\n file: File\n index: number\n label: string\n remove: () => void\n size: ChipSize\n compact: boolean\n }): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: { count: number; bytes: number; text: string }): unknown\n}>()\n\n/**\n * Always a LIST of files, whether or not several are allowed — never a file on its own.\n * The shape of the value does not depend on a prop, so a consumer never has to narrow a\n * union TypeScript has no way of discriminating. With a single file it is simply a list\n * of at most one.\n */\nconst model = defineModel<File[]>({ default: () => [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand — and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value, the removal of a file and the emptying of all of them live in `useFileField`,\n * shared with VFilePicker.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible\n * field: that is what they see, focus and click, so a consumer's own `<label for>` has\n * to point at it. The description attribute is pulled out of that bucket, because this\n * component re-assembles it further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n // Behind `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Chips only mean something for a list: a single file always shows as plain text.\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * Whether the clear cross is shown has to be answered EXPLICITLY here. The visible field\n * is read-only, and a read-only field hides its cross by default — rightly so, since its\n * text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * The icon disappears along with what it offers: a read-only field opens no dialog, so\n * an icon inviting one would be a lie — the same rule VDateInput follows for its\n * calendar.\n *\n * TRAP — its LABEL stays defined at all times, even when no icon is rendered. The helper\n * that detects a click handler on an icon warns AT SETUP if one is attached without a\n * label, and it has no way of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since\n * it is language; the SIZE is formatted by the platform, which knows the local\n * conventions; and the parentheses come from neither, being punctuation every language\n * shares — the same boundary VBadge's \"99+\" falls on.\n *\n * With nothing chosen it states the count alone: \"0 files ()\" would be noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here\n * rather than by VInput: the hint belongs to OUR row under the field, VInput having no\n * such row — its own counter sits inside the field.\n *\n * The counter is deliberately part of that list, unlike in VTextarea. When the files are\n * shown as chips the field's own text is EMPTY, and the counter is then the only spoken\n * summary of what has been chosen.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather\n * than on every render, which reads it three times per chip. The name is shortened in the\n * MIDDLE rather than cut off at the end, for two reasons.\n *\n * The chips WRAP, so there is no line for a long name to overflow: left whole it would\n * simply push the field onto two or three rows. And cutting the middle preserves the\n * extension, which is what tells the reader what kind of file it is — an ellipsis at the\n * end drops it first.\n *\n * The full name is never lost: the removal button is named with it, and the chip carries\n * it as a tooltip whenever the label was actually shortened.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // The field's own buttons already handle their clicks, and the click also reaches the\n // field itself. Without this guard the attach icon would open the dialog TWICE, and\n // the clear cross would reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/* Files may be dropped on the component itself; there is no separate drop area here —\n that is what VFilePicker is for. */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did — a\n * click, a key press: browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n readonly\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-label=\"loadingLabel\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n .v-file-input {\n position: relative;\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 real file input is a SOURCE of files and not a control anyone deals with: it\n wears `.v-hidden-input`, and taking it out of the tab order and hiding it from screen\n readers, in `useFileField`, is what leaves the visible field as the single stop and the\n single announcement. */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /* While a file is being dragged over it, the field is highlighted by redefining the\n very variable VInput uses for its own border colour — so the two can never disagree.\n\n TRAP — the selector weighs (0,4,0), one step above VInput's states, and it has to. The\n field inside is ALWAYS read-only, and VInput's read-only background weighs (0,3,0): at\n that weight the winner would be whichever of the two sheets a bundler put last. */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VFileInput.js","names":[],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * File selection presented as an ordinary form field: a read-only VInput showing what was\n * picked, with the real file input hidden behind it — VDateInput's `readonly` shape, minus\n * the panel. The v-model is always a `File[]`, `multiple` or not, so a consumer never has to\n * narrow a union TypeScript cannot discriminate.\n *\n * The JS is imposed by the platform, not chosen. A dialog opens ONLY from a real click on a\n * file input, and a `FileList` cannot be written from a template — so the visible field can\n * never BE the native control, and that control is only ever a SOURCE of files.\n *\n * The rest follows: the relayed click, the reset after every change (without which the same\n * file cannot be picked twice running), and screening in code, `accept` having no say over\n * a drop.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'chip' | 'text'\n\n/** What the `#chip` slot receives. */\nexport interface FileInputChipSlotProps {\n /** The file the chip stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Its name, already shortened in the middle so the extension survives. */\n label: string\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The chip size worked out to sit inside the field, one step below it. */\n size: ChipSize\n /** Whether the chip takes its compact height, to catch up below the smallest step. */\n compact: boolean\n}\n\n/** What the `#counter` slot receives. */\nexport interface FileInputCounterSlotProps {\n /** How many files are selected. */\n count: number\n /** Their total size, in bytes. */\n bytes: number\n /** The sentence the counter shows by default, already translated. */\n text: string\n}\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /**\n * Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).\n *\n * It is applied TWICE, and it has to be: as an attribute, which is what filters the\n * system's file dialog, and again in code, which is the only thing that can filter a\n * file DROPPED on the component, the attribute having no say over a drop.\n */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid, for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: 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, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. It is rendered before the chips rather than\n * in their place, so it survives a chip display. Decorative by default, it becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon, while an\n * upload is under way, typically. It says that something is happening and changes\n * nothing else: files can still be dropped and the dialog still opens.\n */\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 selection. Worth turning on here more than on an\n * ordinary field: what a picker holds cannot be erased by typing, so the cross is the\n * only way back out of a wrong choice.\n */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection: added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the\n * clear cross and the icon that opens the file dialog. Those two are this component's\n * own affordance, which is why there is no `#end` here: it would replace them.\n */\n 'value-end'?(): unknown\n /**\n * Replaces the chip standing for one file.\n *\n * Three of the values it receives are what make it usable without regressions:\n * `label` is the name already shortened in the MIDDLE so that its extension survives,\n * `remove` is what takes the file out (without it the file could no longer be removed\n * at all), and the size and density are the ones worked out to sit inside the field,\n * which cannot be guessed from outside.\n */\n chip?(props: FileInputChipSlotProps): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: FileInputCounterSlotProps): unknown\n}>()\n\n// `get` reads an explicit `null` as the empty list: the default applies only to a model\n// that was never given, and `files = ref(null)` is a common start in untyped code.\n/**\n * Always a LIST of files, whether or not several are allowed, and never a file on its own.\n * The shape of the value does not depend on a prop, so a consumer never has to narrow a\n * union TypeScript has no way of discriminating. With a single file it is simply a list\n * of at most one.\n */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand — and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value, the removal of a file and the emptying of all of them live in `useFileField`,\n * shared with VFilePicker.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible\n * field: that is what they see, focus and click, so a consumer's own `<label for>` has\n * to point at it. The description attribute is pulled out of that bucket, because this\n * component re-assembles it further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n // Behind `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Chips only mean something for a list: a single file always shows as plain text.\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * Whether the clear cross is shown has to be answered EXPLICITLY here. The visible field\n * is read-only, and a read-only field hides its cross by default — rightly so, since its\n * text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * The icon disappears along with what it offers: a read-only field opens no dialog, so\n * an icon inviting one would be a lie — the same rule VDateInput follows for its\n * calendar.\n *\n * TRAP — its LABEL stays defined at all times, even when no icon is rendered. The helper\n * that detects a click handler on an icon warns AT SETUP if one is attached without a\n * label, and it has no way of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since\n * it is language; the SIZE is formatted by the platform, which knows the local\n * conventions; and the parentheses come from neither, being punctuation every language\n * shares — the same boundary VBadge's \"99+\" falls on.\n *\n * With nothing chosen it states the count alone: \"0 files ()\" would be noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here\n * rather than by VInput: the hint belongs to OUR row under the field, VInput having no\n * such row — its own counter sits inside the field.\n *\n * The counter is deliberately part of that list, unlike in VTextarea. When the files are\n * shown as chips the field's own text is EMPTY, and the counter is then the only spoken\n * summary of what has been chosen.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather\n * than on every render, which reads it three times per chip. The name is shortened in the\n * MIDDLE rather than cut off at the end, for two reasons.\n *\n * The chips WRAP, so there is no line for a long name to overflow: left whole it would\n * simply push the field onto two or three rows. And cutting the middle preserves the\n * extension, which is what tells the reader what kind of file it is — an ellipsis at the\n * end drops it first.\n *\n * The full name is never lost: the removal button is named with it, and the chip carries\n * it as a tooltip whenever the label was actually shortened.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // The field's own buttons already handle their clicks, and the click also reaches the\n // field itself. Without this guard the attach icon would open the dialog TWICE, and\n // the clear cross would reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/* Files may be dropped on the component itself; there is no separate drop area here —\n that is what VFilePicker is for. */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did (a\n * click, a key press): browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n no-typing\n :readonly=\"readonly\"\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n .v-file-input {\n position: relative;\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 real file input is a SOURCE of files and not a control anyone deals with: it\n wears `.v-hidden-input`, and taking it out of the tab order and hiding it from screen\n readers, in `useFileField`, is what leaves the visible field as the single stop and the\n single announcement. */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /* While a file is being dragged over it, the field is highlighted by redefining the\n very variable VInput uses for its own border colour — so the two can never disagree.\n\n TRAP — the selector weighs (0,4,0), one step above VInput's states, and it has to. The\n field inside is ALWAYS read-only, and VInput's read-only background weighs (0,3,0): at\n that weight the winner would be whichever of the two sheets a bundler put last. */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":""}