vectis-ui 0.10.0 → 0.10.2

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 (334) hide show
  1. package/dist/components/VAccordion/VAccordion.js.map +1 -1
  2. package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
  3. package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
  4. package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
  5. package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/VAccordion/context.js.map +1 -1
  7. package/dist/components/VAvatar/VAvatar.d.ts +2 -12
  8. package/dist/components/VAvatar/VAvatar.js.map +1 -1
  9. package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  10. package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
  11. package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
  12. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
  13. package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  14. package/dist/components/VAvatar/context.d.ts +1 -5
  15. package/dist/components/VAvatar/context.js.map +1 -1
  16. package/dist/components/VBadge/VBadge.d.ts +13 -27
  17. package/dist/components/VBadge/VBadge.js.map +1 -1
  18. package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
  19. package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
  20. package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
  21. package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/components/VButton/VButton.d.ts +10 -17
  23. package/dist/components/VButton/VButton.js.map +1 -1
  24. package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  25. package/dist/components/VButton/VButtonGroup.d.ts +6 -15
  26. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  27. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VButton/context.d.ts +1 -9
  29. package/dist/components/VButton/context.js.map +1 -1
  30. package/dist/components/VCalendar/VCalendar.d.ts +9 -24
  31. package/dist/components/VCalendar/VCalendar.js.map +1 -1
  32. package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/components/VCalendar/VCalendarEvent.d.ts +4 -13
  34. package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
  35. package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
  36. package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
  37. package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
  38. package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
  39. package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
  40. package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
  41. package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
  42. package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  43. package/dist/components/VCalendar/color.d.ts +1 -7
  44. package/dist/components/VCalendar/color.js.map +1 -1
  45. package/dist/components/VCalendar/edgeStep.d.ts +5 -15
  46. package/dist/components/VCalendar/edgeStep.js.map +1 -1
  47. package/dist/components/VCalendar/gesture.d.ts +12 -36
  48. package/dist/components/VCalendar/gesture.js.map +1 -1
  49. package/dist/components/VCalendar/keyboard.d.ts +5 -42
  50. package/dist/components/VCalendar/keyboard.js.map +1 -1
  51. package/dist/components/VCalendar/layout.d.ts +38 -199
  52. package/dist/components/VCalendar/layout.js.map +1 -1
  53. package/dist/components/VCalendar/timeGrid.d.ts +3 -25
  54. package/dist/components/VCalendar/timeGrid.js.map +1 -1
  55. package/dist/components/VCalendar/types.d.ts +6 -23
  56. package/dist/components/VCarousel/VCarousel.d.ts +25 -96
  57. package/dist/components/VCarousel/VCarousel.js.map +1 -1
  58. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
  59. package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
  61. package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
  62. package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  63. package/dist/components/VCarousel/context.js.map +1 -1
  64. package/dist/components/VCheckbox/VCheckbox.d.ts +1 -5
  65. package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
  66. package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  67. package/dist/components/VChip/VChip.d.ts +2 -8
  68. package/dist/components/VChip/VChip.js.map +1 -1
  69. package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  70. package/dist/components/VCombobox/VCombobox.d.ts +21 -59
  71. package/dist/components/VCombobox/VCombobox.js.map +1 -1
  72. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
  73. package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  74. package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
  75. package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  76. package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
  77. package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
  78. package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
  79. package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  80. package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
  81. package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
  82. package/dist/components/VDataTable/VDataTable.d.ts +11 -36
  83. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  84. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
  85. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  86. package/dist/components/VDateInput/VDateInput.d.ts +12 -21
  87. package/dist/components/VDateInput/VDateInput.js.map +1 -1
  88. package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  89. package/dist/components/VDateInput/mask.d.ts +14 -59
  90. package/dist/components/VDateInput/mask.js.map +1 -1
  91. package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
  92. package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
  93. package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  94. package/dist/components/VDatePicker/keyboard.d.ts +10 -42
  95. package/dist/components/VDatePicker/keyboard.js.map +1 -1
  96. package/dist/components/VDatePicker/names.d.ts +8 -22
  97. package/dist/components/VDatePicker/names.js.map +1 -1
  98. package/dist/components/VDialog/VDialog.d.ts +5 -10
  99. package/dist/components/VDialog/VDialog.js.map +1 -1
  100. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
  101. package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
  102. package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
  103. package/dist/components/VDialog/VDialogAlert.js.map +1 -1
  104. package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
  105. package/dist/components/VFileInput/VFileInput.d.ts +11 -37
  106. package/dist/components/VFileInput/VFileInput.js.map +1 -1
  107. package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  108. package/dist/components/VFileInput/truncate.d.ts +3 -23
  109. package/dist/components/VFileInput/truncate.js.map +1 -1
  110. package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
  111. package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
  112. package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  113. package/dist/components/VFilePicker/fileKind.js.map +1 -1
  114. package/dist/components/VHotkeys/VHotkeys.d.ts +6 -15
  115. package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
  116. package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
  117. package/dist/components/VHotkeys/platform.d.ts +10 -49
  118. package/dist/components/VHotkeys/platform.js.map +1 -1
  119. package/dist/components/VIcon/VIcon.d.ts +7 -19
  120. package/dist/components/VIcon/VIcon.js.map +1 -1
  121. package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
  122. package/dist/components/VIcon/iconProps.d.ts +5 -19
  123. package/dist/components/VIcon/iconProps.js.map +1 -1
  124. package/dist/components/VIcon/resolver.d.ts +15 -43
  125. package/dist/components/VIcon/resolver.js.map +1 -1
  126. package/dist/components/VIcon/types.d.ts +7 -34
  127. package/dist/components/VIconButton/VIconButton.d.ts +2 -3
  128. package/dist/components/VIconButton/VIconButton.js.map +1 -1
  129. package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  130. package/dist/components/VInput/VInput.d.ts +14 -29
  131. package/dist/components/VInput/VInput.js.map +1 -1
  132. package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  133. package/dist/components/VInput/VInputGroup.js.map +1 -1
  134. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
  135. package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  136. package/dist/components/VInput/context.js.map +1 -1
  137. package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
  138. package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
  139. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
  140. package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
  141. package/dist/components/VMenu/VMenu.js.map +1 -1
  142. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  143. package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
  144. package/dist/components/VMenu/VMenuGroup.js.map +1 -1
  145. package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  146. package/dist/components/VMenu/VMenuItem.d.ts +0 -5
  147. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  148. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  149. package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
  150. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  151. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  152. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  153. package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
  154. package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  155. package/dist/components/VMenu/context.d.ts +3 -11
  156. package/dist/components/VMenu/context.js.map +1 -1
  157. package/dist/components/VPagination/VPagination.d.ts +9 -20
  158. package/dist/components/VPagination/VPagination.js.map +1 -1
  159. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  160. package/dist/components/VPopover/VPopover.d.ts +7 -18
  161. package/dist/components/VPopover/VPopover.js.map +1 -1
  162. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
  163. package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
  164. package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
  165. package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
  166. package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
  167. package/dist/components/VProgressLinear/VProgressLinear.d.ts +4 -15
  168. package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
  169. package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
  170. package/dist/components/VRadio/VRadio.d.ts +1 -5
  171. package/dist/components/VRadio/VRadio.js.map +1 -1
  172. package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  173. package/dist/components/VSeparator/VSeparator.d.ts +2 -15
  174. package/dist/components/VSeparator/VSeparator.js.map +1 -1
  175. package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
  176. package/dist/components/VSideNavigation/VSideNavigation.d.ts +1 -1
  177. package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
  178. package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
  179. package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
  180. package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
  181. package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  182. package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +1 -5
  183. package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
  184. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
  185. package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  186. package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
  187. package/dist/components/VSideNavigation/context.js.map +1 -1
  188. package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +6 -19
  189. package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
  190. package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
  191. package/dist/components/VSlider/VSlider.d.ts +5 -13
  192. package/dist/components/VSlider/VSlider.js.map +1 -1
  193. package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  194. package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
  195. package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
  196. package/dist/components/VSnackbar/state.d.ts +4 -17
  197. package/dist/components/VSnackbar/state.js.map +1 -1
  198. package/dist/components/VSpinner/VSpinner.d.ts +3 -5
  199. package/dist/components/VSpinner/VSpinner.js.map +1 -1
  200. package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
  201. package/dist/components/VSwitch/VSwitch.js.map +1 -1
  202. package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  203. package/dist/components/VTabs/VTab.d.ts +1 -5
  204. package/dist/components/VTabs/VTab.js.map +1 -1
  205. package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
  206. package/dist/components/VTabs/VTabPanel.js.map +1 -1
  207. package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  208. package/dist/components/VTabs/VTabs.d.ts +5 -17
  209. package/dist/components/VTabs/VTabs.js.map +1 -1
  210. package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
  211. package/dist/components/VTabs/context.js.map +1 -1
  212. package/dist/components/VTextarea/VTextarea.d.ts +13 -20
  213. package/dist/components/VTextarea/VTextarea.js.map +1 -1
  214. package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  215. package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
  216. package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
  217. package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  218. package/dist/components/VTimeInput/search.d.ts +3 -34
  219. package/dist/components/VTimeInput/search.js.map +1 -1
  220. package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
  221. package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
  222. package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  223. package/dist/components/VTimePicker/limits.d.ts +18 -45
  224. package/dist/components/VTimePicker/limits.js.map +1 -1
  225. package/dist/components/VToast/VToast.js.map +1 -1
  226. package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
  227. package/dist/components/VToast/VToaster.js.map +1 -1
  228. package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
  229. package/dist/components/VToast/state.d.ts +2 -7
  230. package/dist/components/VToast/state.js.map +1 -1
  231. package/dist/components/VToggle/VToggle.d.ts +5 -9
  232. package/dist/components/VToggle/VToggle.js.map +1 -1
  233. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  234. package/dist/components/VToggle/VToggleItem.d.ts +1 -9
  235. package/dist/components/VToggle/VToggleItem.js.map +1 -1
  236. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  237. package/dist/components/VToggle/context.js.map +1 -1
  238. package/dist/components/VTooltip/VTooltip.d.ts +3 -16
  239. package/dist/components/VTooltip/VTooltip.js.map +1 -1
  240. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
  241. package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  242. package/dist/components/VTypography/VTypography.d.ts +4 -8
  243. package/dist/components/VTypography/VTypography.js.map +1 -1
  244. package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
  245. package/dist/composables/useAriaLabel.js.map +1 -1
  246. package/dist/composables/useClearable.d.ts +2 -8
  247. package/dist/composables/useClearable.js.map +1 -1
  248. package/dist/composables/useControlShape.js.map +1 -1
  249. package/dist/composables/useDetailsOpen.d.ts +2 -10
  250. package/dist/composables/useDetailsOpen.js.map +1 -1
  251. package/dist/composables/useFieldIds.js.map +1 -1
  252. package/dist/composables/useFieldPanel.d.ts +4 -17
  253. package/dist/composables/useFieldPanel.js.map +1 -1
  254. package/dist/composables/useFileDrop.js.map +1 -1
  255. package/dist/composables/useFileField.d.ts +1 -5
  256. package/dist/composables/useFileField.js.map +1 -1
  257. package/dist/composables/useFocusoutDismiss.js.map +1 -1
  258. package/dist/composables/useIconClickHandlers.d.ts +4 -19
  259. package/dist/composables/useIconClickHandlers.js.map +1 -1
  260. package/dist/composables/useInertLink.d.ts +1 -1
  261. package/dist/composables/useInertLink.js.map +1 -1
  262. package/dist/composables/useLiveAnnouncer.js.map +1 -1
  263. package/dist/composables/useMaskedField.d.ts +12 -27
  264. package/dist/composables/useMaskedField.js.map +1 -1
  265. package/dist/composables/usePopover.d.ts +3 -11
  266. package/dist/composables/usePopover.js.map +1 -1
  267. package/dist/composables/useProgressValue.js.map +1 -1
  268. package/dist/composables/useRootAttrs.d.ts +0 -9
  269. package/dist/composables/useRootAttrs.js +11 -4
  270. package/dist/composables/useRootAttrs.js.map +1 -1
  271. package/dist/composables/useSlotNodes.d.ts +3 -14
  272. package/dist/composables/useSlotNodes.js.map +1 -1
  273. package/dist/composables/useTextLimit.d.ts +4 -16
  274. package/dist/composables/useTextLimit.js.map +1 -1
  275. package/dist/composables/useTimer.d.ts +2 -15
  276. package/dist/composables/useTimer.js.map +1 -1
  277. package/dist/i18n/en.js.map +1 -1
  278. package/dist/i18n/fr.js.map +1 -1
  279. package/dist/i18n/state.d.ts +9 -38
  280. package/dist/i18n/state.js.map +1 -1
  281. package/dist/i18n/types.d.ts +19 -81
  282. package/dist/icons.d.ts +2 -12
  283. package/dist/styles.css +1 -1
  284. package/dist/tokens/css.d.ts +3 -16
  285. package/dist/tokens/css.js.map +1 -1
  286. package/dist/tokens/index.d.ts +5 -0
  287. package/dist/tokens/index.js.map +1 -1
  288. package/dist/tokens/primitives.js.map +1 -1
  289. package/dist/tokens/semantic.d.ts +6 -23
  290. package/dist/tokens/semantic.js +5 -0
  291. package/dist/tokens/semantic.js.map +1 -1
  292. package/dist/tokens/themes/dark.js.map +1 -1
  293. package/dist/tokens/types.d.ts +2 -12
  294. package/dist/tokens/types.js.map +1 -1
  295. package/dist/tokens.json +14 -0
  296. package/dist/types.d.ts +3 -21
  297. package/dist/utils/array.d.ts +2 -4
  298. package/dist/utils/array.js.map +1 -1
  299. package/dist/utils/arrowNav.d.ts +2 -26
  300. package/dist/utils/arrowNav.js.map +1 -1
  301. package/dist/utils/chip.d.ts +1 -8
  302. package/dist/utils/chip.js.map +1 -1
  303. package/dist/utils/clock.d.ts +16 -39
  304. package/dist/utils/clock.js.map +1 -1
  305. package/dist/utils/css.d.ts +5 -16
  306. package/dist/utils/css.js.map +1 -1
  307. package/dist/utils/date.d.ts +17 -60
  308. package/dist/utils/date.js.map +1 -1
  309. package/dist/utils/direction.d.ts +2 -9
  310. package/dist/utils/direction.js.map +1 -1
  311. package/dist/utils/env.d.ts +2 -15
  312. package/dist/utils/env.js.map +1 -1
  313. package/dist/utils/file.d.ts +8 -46
  314. package/dist/utils/file.js.map +1 -1
  315. package/dist/utils/focus.d.ts +1 -14
  316. package/dist/utils/focus.js +1 -6
  317. package/dist/utils/focus.js.map +1 -1
  318. package/dist/utils/hostWarns.d.ts +3 -8
  319. package/dist/utils/hostWarns.js.map +1 -1
  320. package/dist/utils/ids.d.ts +2 -7
  321. package/dist/utils/ids.js.map +1 -1
  322. package/dist/utils/matcher.d.ts +1 -8
  323. package/dist/utils/matcher.js.map +1 -1
  324. package/dist/utils/memo.d.ts +1 -11
  325. package/dist/utils/memo.js.map +1 -1
  326. package/dist/utils/number.d.ts +1 -7
  327. package/dist/utils/number.js.map +1 -1
  328. package/dist/utils/text.d.ts +4 -23
  329. package/dist/utils/text.js.map +1 -1
  330. package/dist/utils/time.d.ts +4 -12
  331. package/dist/utils/time.js.map +1 -1
  332. package/dist/utils/vnode.d.ts +0 -10
  333. package/dist/utils/vnode.js.map +1 -1
  334. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"VFilePicker.js","names":[],"sources":["../../../src/components/VFilePicker/VFilePicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A drop zone: a dashed rectangle taking files dragged onto it or chosen from a dialog, with\n * an optional list of what it holds. The v-model is a plain `File[]`, and the division of\n * labour matters — this component SHOWS and SCREENS, uploading is the parent's business.\n *\n * Four things need JS, each forced by the platform. A dialog opens ONLY from a real click on\n * a file input, and a `FileList` cannot be written from a template, so the input is a SOURCE\n * of files rather than a mirror of the value. `accept` has no say over a DROP. The input has\n * to be reset after every change, or the same file cannot be chosen twice running. And a\n * thumbnail needs an object URL, which then has to be released.\n *\n * The KEYBOARD is deliberately not among them: with the browse button hidden the zone\n * becomes a real `<button>`, so Enter, Space, focus, role and a disabled control's inertness\n * all come from the browser. The other incarnation, a plain container, exists for one reason\n * — a button inside a button is invalid markup and unusable by keyboard. The drag highlight,\n * the \"or\" separator and the side-by-side layout are CSS too.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { audio_file as audioFileIcon } from '../VIcon/icons/audio_file'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport { cloud_upload as cloudUploadIcon } from '../VIcon/icons/cloud_upload'\nimport { code as codeIcon } from '../VIcon/icons/code'\nimport { description as descriptionIcon } from '../VIcon/icons/description'\nimport { folder_zip as folderZipIcon } from '../VIcon/icons/folder_zip'\nimport { image as imageIcon } from '../VIcon/icons/image'\nimport { picture_as_pdf as pictureAsPdfIcon } from '../VIcon/icons/picture_as_pdf'\nimport { table_chart as tableChartIcon } from '../VIcon/icons/table_chart'\nimport { video_file as videoFileIcon } from '../VIcon/icons/video_file'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { fileKind, type FilePickerKind } from './fileKind'\n\n/** What the `#browse` slot receives. */\nexport interface FilePickerBrowseSlotProps {\n /** Opens the file dialog; a button of your own has no other way to. */\n open: () => void\n /** Whether the picker is disabled, for your button to follow. */\n disabled: boolean\n}\n\n/** What the `#remove` slot receives. */\nexport interface FilePickerRemoveSlotProps {\n /** The file the row stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The ready-made accessible name of the control, the file's name included. */\n removeLabel: string\n}\n\n/** Where the files taken are listed: under the zone, beside it, or nowhere. */\nexport type FilePickerPreview = false | 'bottom' | 'end'\n\n/**\n * Everything known about one row of the list. The same object is handed to both slots\n * that can replace part of a row, so the two can never be given different information.\n */\nexport interface FilePickerRow {\n /** The file itself. */\n file: File\n /** Its position in the list. */\n index: number\n /** What kind of file it is, as worked out from its type or its extension. */\n kind: FilePickerKind\n /**\n * The address of its image thumbnail, when there is one.\n *\n * It is absent on the server AND on the browser's first render, since those addresses\n * only exist in a browser and creating them during the first render would make the two\n * markups differ. It is absent as well for anything the browser fails to decode. Fall\n * back to the icon in that case.\n */\n thumbnail: string | undefined\n /** The icon standing for that kind of file, a consumer's override included. */\n icon: IconSource\n /** Its size, already written out in the reader's conventions. */\n sizeText: string\n /** Takes this file out of the list. */\n remove: () => void\n}\n\ninterface FilePickerProps {\n /**\n * What the reader is being asked to drop, in one line. It is REQUIRED: a drop zone\n * with no instruction is just a rectangle.\n *\n * Note that this prop shadows the HTML attribute of the same name on the component\n * itself, an accepted trade-off: a tooltip on a drop zone would be redundant anyway.\n */\n title: string\n /** A second line under it, for the constraints in plain words: kinds, sizes, how many. */\n subtitle?: string\n /** The large icon at the top of the zone. */\n icon?: IconSource\n /**\n * Hides the separator and the browse button under the instruction. That changes the\n * zone's nature: it then becomes the control ITSELF, a real button, so Enter, Space\n * and the focus come from the platform rather than from a container that merely\n * reacts to clicks.\n */\n hideBrowse?: boolean\n /**\n * The wording drawn on the browse button, which is also its accessible name. It falls\n * back to the design system dictionary.\n */\n browseText?: string\n /**\n * Where the files taken are listed: under the zone, or beside it. Beside folds back\n * underneath when the COMPONENT is narrow, following the width it was given rather\n * than the width of the window.\n *\n * By default nothing is listed at all: the value is there for a parent that wants to\n * present it its own way.\n */\n preview?: FilePickerPreview\n /**\n * Shows the kind icon for every file in that list, images included: the way out when\n * a list holds many images, or very large ones.\n *\n * Left out, an image is shown as a thumbnail: it is given a temporary address, created\n * in the browser only and released as soon as the file leaves the list or the\n * component goes away.\n */\n hideThumbnails?: boolean\n /** Replaces the icon of one or more kinds of file. */\n typeIcons?: Partial<Record<FilePickerKind, IconSource>>\n /** The icon of the button removing a file from the list. */\n removeIcon?: IconSource\n /** Allows several files to be taken. 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 * DROPPED file, the attribute having no say over a drop.\n */\n accept?: string\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 taken at most. */\n maxFiles?: number\n /** Makes the zone unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was taken without allowing it to change: no dialog, no drop, no removal. Its\n * buttons stay reachable from the keyboard, announced as unavailable.\n */\n readonly?: boolean\n /**\n * Marks the zone as invalid, which colours its outline and is announced on the control\n * the reader reaches. It is for a rule of your own: nothing here is checked by the browser,\n * the real input being hidden.\n */\n invalid?: boolean\n /**\n * Shows a spinner in place of the zone icon, typically while an upload is under way.\n * It says that something is happening and changes nothing else: files can still be\n * dropped and the dialog still opens. `disabled` and `readonly` are the props that cut\n * those off.\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\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilePickerProps>(), {\n subtitle: undefined,\n icon: () => cloudUploadIcon,\n hideBrowse: false,\n browseText: undefined,\n preview: false,\n hideThumbnails: false,\n typeIcons: undefined,\n removeIcon: () => closeIcon,\n multiple: false,\n accept: undefined,\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n loading: false,\n loadingText: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection, by adding or removing something. */\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, and which one. It exists alongside `change`, which only says\n * what is LEFT: an upload already in flight is aborted from here, without having to\n * compare two lists to find out what went.\n */\n remove: [file: File, index: number]\n}>()\n\ndefineSlots<{\n /**\n * The large icon, for an illustration the icon props cannot express.\n *\n * It must stay non-interactive, and so must the two below: with the browse button\n * hidden, the zone IS a button, and nothing interactive may sit inside one.\n */\n icon?(): unknown\n /** The instruction. Text and inline elements only, for the same reason. */\n title?(): unknown\n /** The second line. Same contract as the instruction. */\n subtitle?(): unknown\n /**\n * The browse button. Call the `open` it receives: without it a button of your own\n * could no longer open the file dialog at all.\n */\n browse?(props: FilePickerBrowseSlotProps): unknown\n /**\n * A WHOLE row of the list, the way out for a row showing its own upload progress. It\n * receives everything the standard row was given, so nothing has to be worked out\n * again.\n */\n item?(props: FilePickerRow): unknown\n /**\n * The square at the start of a row alone: for a thumbnail produced by your server, a\n * video's poster frame, or a format the browser cannot decode.\n */\n thumbnail?(props: FilePickerRow): unknown\n /**\n * The control that removes a row. Two of the values it receives are not optional in\n * practice: `remove` is the only thing that can take the file out, and `removeLabel` is\n * the ready-made accessible name, the file's own included, without which the button\n * would be announced as nothing at all.\n */\n remove?(props: FilePickerRemoveSlotProps): 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\nconst m = useMessages()\nconst locale = useLocale()\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value and the removal of a file live in `useFileField`, shared with VFileInput.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the ZONE: that is\n * what they see, and what takes the focus once the browse button is hidden. Nothing is\n * pulled out of it, unlike in VFileInput — this component assembles no description of\n * its own.\n *\n * The zone becomes the control exactly when there is no browse button inside to be one,\n * which is what `hideBrowse` decides.\n */\nconst {\n fileEl,\n enabled: interactive,\n nativeInputAttrs,\n controlAttrs: zoneAttrs,\n acceptFiles,\n openPicker,\n removeAt: removeFile,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n // @a11y @devwarn\n // The name a consumer gives a plain container is ignored by assistive\n // technology, and axe reports it, so this one fails SILENTLY at runtime too. Behind\n // `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n !props.hideBrowse &&\n (attrs['aria-label'] !== undefined || attrs['aria-labelledby'] !== undefined) &&\n 'an aria-label on the zone is inert while the browse button is shown: the zone is a plain container then, and axe reports aria-prohibited-attr. Name the zone through `title`, or set `hide-browse` to make the zone itself the control.',\n ]\n : undefined,\n})\n\nconst zoneEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLUListElement | null>(null)\nconst showList = computed(() => props.preview !== false && model.value.length > 0)\n\n/**\n * Which icon stands for which kind of file. A consumer overrides it entry by entry, so\n * replacing one icon does not mean restating the other seven.\n */\nconst KIND_ICONS: Record<FilePickerKind, IconSource> = {\n image: imageIcon,\n pdf: pictureAsPdfIcon,\n audio: audioFileIcon,\n video: videoFileIcon,\n archive: folderZipIcon,\n spreadsheet: tableChartIcon,\n code: codeIcon,\n file: descriptionIcon,\n}\n\nconst iconForKind = (kind: FilePickerKind): IconSource =>\n props.typeIcons?.[kind] ?? KIND_ICONS[kind]\n\nfunction onZoneClick(event: MouseEvent) {\n // The browse button already handles its own click, and that click also reaches the\n // zone: without this guard it would open the dialog twice. The test is inert when the\n // zone IS the button, there being nothing inside it to match.\n if (!props.hideBrowse && (event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => interactive.value,\n acceptFiles,\n)\n\n// @a11y\n/**\n * What can be focused in the zone, whichever shape it has taken: the zone itself when it\n * is the button, and otherwise the first button inside it.\n *\n * It is found by searching the DOM rather than kept as a reference, because a consumer\n * may have replaced the browse button entirely — theirs is not ours to hold a reference\n * to, and it still has to be reachable.\n */\nconst rootEl = ref<HTMLElement | null>(null)\n\nfunction focusTarget(): HTMLElement | null {\n const zone = zoneEl.value\n if (!zone) return null\n return zone.matches('button') ? zone : zone.querySelector('button')\n}\n\n// @a11y @core\n/**\n * Takes a file out of the list, and catches the focus that was on its remove button.\n *\n * That button disappears with its row, so the focus would fall back to the page body.\n * It is moved instead to the row that takes its place, or to the browse control once\n * the list is empty. Waiting for the next tick is what makes the new row findable: the\n * page still shows the old one at the moment the value is written.\n */\nasync function removeAt(index: number) {\n if (!removeFile(index)) return\n\n await nextTick()\n const items = listEl.value?.querySelectorAll<HTMLElement>('.v-file-picker-item') ?? []\n const next = items[Math.min(index, items.length - 1)]?.querySelector('button')\n ;(next ?? focusTarget())?.focus()\n}\n\n/**\n * One temporary address per image, kept against THE FILE OBJECT ITSELF.\n *\n * Using the file as the key is safe, and that is worth knowing: Vue's reactivity only\n * wraps plain objects, arrays and collections, so a File travelling through the value\n * comes back with its identity intact — the exact opposite of VCombobox's option cache,\n * where comparing identities IS the bug. Keying by name would be wrong here: two files\n * picked from different folders can share one.\n *\n * The map is made reactive rather than held in a plain reference, which is what lets a\n * single row re-render when its address is ready, instead of replacing the whole map.\n */\nconst thumbUrls = reactive(new Map<File, string>())\n\n/**\n * Whether a thumbnail can be drawn at all, decided on the type the BROWSER reports and\n * not on the kind derived from the extension. A `.heic` file counts as an image and\n * deserves the image icon, but no engine will decode it.\n */\nconst isThumbable = (file: File) => file.type.startsWith('image/')\n\n// @ssr @core\n/**\n * Brings the map into line with the list: releases the addresses of files that have\n * gone, and creates them for files that have arrived.\n *\n * TRAP — it is never called from setup, from a derived value or during rendering, only\n * once mounted, from a watcher and from handlers. That is what keeps the component\n * renderable on a server: the server draws the kind icon, and so does the browser's\n * first render, the map being empty until then — the same reasoning as today's date in\n * VDatePicker.\n */\nfunction syncThumbnails() {\n // With no list rendered there is nothing to draw a thumbnail in, so no address is\n // created at all. This gate — and not a warning — is what makes the default\n // configuration cost strictly nothing.\n const wanted = new Set(\n !props.hideThumbnails && props.preview !== false\n ? model.value.filter((file) => isThumbable(file) && !undecodable.has(file))\n : [],\n )\n\n for (const [file, url] of thumbUrls) {\n if (wanted.has(file)) continue\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n }\n for (const file of wanted) {\n if (!thumbUrls.has(file)) thumbUrls.set(file, URL.createObjectURL(file))\n }\n}\n\n// @fallback\n/**\n * The image failed to decode — a format the browser does not read, a corrupt file, a\n * type that lied about itself. The address is released and the kind icon takes over: a\n * failure to draw must never leave an empty square in the list.\n */\nfunction dropThumbnail(file: File) {\n const url = thumbUrls.get(file)\n if (!url) return\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n undecodable.add(file)\n}\n\n/**\n * The files whose image failed once. They are still in the selection, so without this\n * `syncThumbnails` would hand each of them a new address on every change of the list, only\n * for the `<img>` to fail again. Weak, so a file that leaves takes its entry with it.\n */\nconst undecodable = new WeakSet<File>()\n\n// @ssr\n// The addresses are created in the browser only; the server and the browser's\n// first render both show the kind icon, so the two markups agree.\nonMounted(syncThumbnails)\n/*\n * Watching deeply costs nothing here and covers a consumer who ADDS to the list rather\n * than replacing it: the walk stops at a File, which is neither a plain object nor a\n * collection, so it never descends past the array itself.\n */\nwatch([model, () => props.hideThumbnails, () => props.preview], syncThumbnails, { deep: true })\n// Released BEFORE the component is torn down and never after — the same rule `useTimer`\n// and VHotkeys follow.\nonBeforeUnmount(() => {\n for (const url of thumbUrls.values()) URL.revokeObjectURL(url)\n thumbUrls.clear()\n})\n\n/**\n * Everything each row of the list needs, assembled in one place and once per change of the\n * list, the language, the icons or a thumbnail — not once per render. A drag over the zone\n * re-renders the component at the pointer's rate, and the rows would otherwise work out\n * their kind and their size again every time, for every slot that reads them.\n *\n * A Vue template has no way of declaring a reusable fragment, so this is also what keeps\n * the slots that can replace part of a row fed from a single source.\n */\nconst rows = computed<{ key: number; row: FilePickerRow; removeLabel: string }[]>(() =>\n showList.value\n ? model.value.map((file, index) => {\n const kind = fileKind(file)\n return {\n key: fileKey(file),\n row: {\n file,\n index,\n kind,\n thumbnail: thumbUrls.get(file),\n icon: iconForKind(kind),\n sizeText: formatBytes(file.size, locale.value),\n remove: () => removeAt(index),\n },\n removeLabel: m.value.common.remove(file.name),\n }\n })\n : [],\n)\n\ndefineExpose({\n /** Moves the focus to the browse control: the button, or the zone when it is one. */\n focus: (options?: FocusOptions) => focusTarget()?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did, a\n * click or a key press: browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The zone's own box, for what neither of the two above covers. It is the wrapper and\n * 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-picker\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-preview=\"preview || undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-invalid=\"invalid ? '' : 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-picker-native v-hidden-input\" />\n\n <!-- TRAP — this wrapper is not decoration. The root is what the layout asks about\n its own width, and an element cannot ask about ITSELF: whatever flips has to be\n a descendant of it. Deleting this element makes the side-by-side layout\n silently stop folding, with no error anywhere. -->\n <div class=\"v-file-picker-body\">\n <!-- A real button as soon as the zone IS the control: the role, Enter, Space, the\n focus and the complete inertness of a disabled control then come from the\n platform. A plain container otherwise — a button inside a button is invalid\n markup, and unreachable by keyboard. -->\n <component\n :is=\"hideBrowse ? 'button' : 'div'\"\n ref=\"zoneEl\"\n :aria-invalid=\"hideBrowse && invalid ? 'true' : undefined\"\n :aria-disabled=\"hideBrowse && readonly ? 'true' : undefined\"\n v-bind=\"zoneAttrs\"\n class=\"v-file-picker-zone\"\n :type=\"hideBrowse ? 'button' : undefined\"\n :disabled=\"hideBrowse && disabled ? true : undefined\"\n @click=\"onZoneClick\"\n >\n <span class=\"v-file-picker-icon\">\n <slot name=\"icon\">\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <VIcon v-else v-bind=\"iconProps(icon)\" />\n </slot>\n </span>\n\n <!-- Rendered as a span in BOTH shapes: a paragraph is not allowed inside a\n button, and the markup must not change form between the two — one shape\n means one set of CSS rules. -->\n <VTypography as=\"span\" variant=\"subtitle\" class=\"v-file-picker-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n\n <VTypography\n v-if=\"subtitle || $slots.subtitle\"\n as=\"span\"\n variant=\"body-sm\"\n class=\"v-file-picker-subtitle\"\n >\n <slot name=\"subtitle\">{{ subtitle }}</slot>\n </VTypography>\n\n <template v-if=\"!hideBrowse\">\n <!-- Deliberately not hidden from screen readers: \"or\" is real text, and it\n reads naturally between the instruction and the button. The two rules on\n either side of it are pseudo-elements, and therefore decorative by\n construction. -->\n <span class=\"v-file-picker-separator\">{{ m.filePicker.or }}</span>\n\n <slot name=\"browse\" :open=\"openPicker\" :disabled=\"!interactive\">\n <!-- Read-only leaves it reachable, as every read-only control of the design\n system is: unavailable rather than disabled. The invalid state is said on\n it, the control the reader reaches, the zone being a plain container. -->\n <VButton\n class=\"v-file-picker-browse\"\n variant=\"outline\"\n tone=\"neutral\"\n :disabled=\"disabled\"\n :aria-disabled=\"readonly ? 'true' : undefined\"\n :aria-invalid=\"invalid ? 'true' : undefined\"\n @click=\"openPicker\"\n >\n {{ browseText ?? m.filePicker.browse }}\n </VButton>\n </slot>\n </template>\n </component>\n\n <ul v-if=\"showList\" ref=\"listEl\" class=\"v-file-picker-list\" :aria-label=\"m.filePicker.list\">\n <li v-for=\"{ key, row, removeLabel } in rows\" :key=\"key\" class=\"v-file-picker-item\">\n <slot name=\"item\" v-bind=\"row\">\n <span class=\"v-file-picker-thumb\">\n <slot name=\"thumbnail\" v-bind=\"row\">\n <img\n v-if=\"row.thumbnail\"\n class=\"v-file-picker-image\"\n :src=\"row.thumbnail\"\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n @error=\"dropThumbnail(row.file)\"\n />\n <VIcon v-else v-bind=\"iconProps(row.icon)\" />\n </slot>\n </span>\n\n <span class=\"v-file-picker-info\">\n <span class=\"v-file-picker-name\" :title=\"row.file.name\">{{ row.file.name }}</span>\n <span class=\"v-file-picker-size\">{{ row.sizeText }}</span>\n </span>\n\n <slot\n name=\"remove\"\n :file=\"row.file\"\n :index=\"row.index\"\n :remove=\"row.remove\"\n :remove-label=\"removeLabel\"\n >\n <VIconButton\n class=\"v-file-picker-remove\"\n size=\"sm\"\n :icon=\"removeIcon\"\n :label=\"removeLabel\"\n :disabled=\"!interactive\"\n @click=\"row.remove\"\n />\n </slot>\n </slot>\n </li>\n </ul>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* These variables carry the component's name for a reason: they INHERIT, so a bare\n `--bg` or `--border-color` set by the host application on any ancestor would be\n picked up here instead — and nothing would catch it, since the unit tests evaluate\n no styles at all.\n\n They are declared on the ROOT so that the drag, disabled and read-only states each\n redefine them in ONE place, which is what makes those states structurally incapable\n of disagreeing with hover or focus. */\n .v-file-picker {\n --file-picker-border-color: var(--vectis-color-border-strong);\n --file-picker-bg: var(--vectis-color-surface-sunken);\n --file-picker-text: var(--vectis-color-text);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-raised);\n --file-picker-thumb-bg: var(--vectis-color-surface-muted);\n\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n position: relative;\n display: block;\n inline-size: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-file-picker-body {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-4);\n }\n\n /* One set of rules serves both shapes the zone can take. All that has to be undone is\n what the browser gives a button — its own appearance, font and margin — since\n everything else is declared here regardless. The full width is part of that: a\n button shrinks to fit its content, where a container does not. */\n .v-file-picker-zone {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--vectis-space-2);\n inline-size: 100%;\n min-block-size: var(--vectis-control-size-file-picker-min-block);\n margin: 0;\n padding: var(--vectis-space-6) var(--vectis-space-4);\n appearance: none;\n border: 1px dashed var(--file-picker-border-color);\n border-radius: var(--vectis-radius-surface);\n background: var(--file-picker-bg);\n color: var(--file-picker-text);\n font: inherit;\n text-align: center;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-file-picker:not([data-disabled]):not([data-readonly]) .v-file-picker-zone {\n cursor: pointer;\n }\n\n /* Not over an invalid zone: at (0,5,0) this rule outranks the invalid one below, and the\n pointer would paint the accent over the one cue that says something is wrong. */\n .v-file-picker:not([data-disabled]):not([data-readonly]):not([data-invalid])\n .v-file-picker-zone:hover {\n --file-picker-border-color: var(--vectis-color-accent);\n }\n\n /* Invalid redefines the variable the zone paints its outline with, the way the drag\n highlight does: the states can then never disagree about what colour the border is. */\n .v-file-picker[data-invalid] .v-file-picker-zone {\n --file-picker-border-color: var(--vectis-color-danger);\n }\n\n .v-file-picker-zone:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* While a file is being dragged over it, the zone is highlighted by redefining the\n very variables it paints itself with — so the highlight cannot disagree with the\n hover and focus states. */\n .v-file-picker[data-dragging] {\n --file-picker-border-color: var(--vectis-color-accent);\n --file-picker-bg: var(--vectis-color-accent-surface);\n }\n\n /* The large icon's size is set HERE, on its own wrapper, and never on the zone: set\n there it would inherit down and enlarge the browse button's icons as well. */\n .v-file-picker-icon {\n --vectis-icon-size: var(--vectis-control-size-file-picker-icon);\n\n /* The wrapper is floored at the icon's own size, whatever it holds. An illustration of\n your own in the `#icon` slot may be drawn smaller than the icon, and without that\n floor the zone would change height with it — the box has to be the icon's even when\n its content is not. A floor and not a fixed size, so a taller one still grows it\n rather than spilling out of it; the centring is what keeps a smaller content in the\n middle of the box instead of at its start. */\n min-inline-size: var(--vectis-control-size-file-picker-icon);\n min-block-size: var(--vectis-control-size-file-picker-icon);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--file-picker-text-muted);\n }\n\n /* The spinner standing in for the icon while loading takes the icon's box, the way it\n does in every field: a spinner's size is the box it occupies, and the icon size is\n set on this wrapper. */\n .v-file-picker-icon > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n .v-file-picker-subtitle {\n /* Set through VTypography's own colour variable rather than with a plain colour:\n there is then a single source, and the disabled state below overrides that same\n one instead of introducing a second. */\n --typography-color: var(--file-picker-text-muted);\n }\n\n /* The word set between two rules, entirely in CSS: the two lines are pseudo-elements\n sharing the row with the text between them. No extra element is added to the markup,\n and nothing decorative reaches the accessibility tree. */\n .v-file-picker-separator {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n inline-size: 100%;\n margin-block-start: var(--vectis-space-2);\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n }\n\n .v-file-picker-separator::before,\n .v-file-picker-separator::after {\n content: '';\n flex: 1 1 0;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-file-picker-list {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n .v-file-picker-item {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-2);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-row-bg);\n }\n\n .v-file-picker-thumb {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-size-file-picker-thumb);\n block-size: var(--vectis-control-size-file-picker-thumb);\n overflow: hidden;\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-thumb-bg);\n color: var(--file-picker-text-muted);\n }\n\n .v-file-picker-image {\n display: block;\n inline-size: 100%;\n block-size: 100%;\n object-fit: cover;\n }\n\n /* The zero minimum is what makes the ellipsis below possible at all: a flex item\n refuses by default to shrink below its own content, so without it a long file name\n would widen the row instead of being cut short. */\n .v-file-picker-info {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-inline-size: 0;\n color: var(--file-picker-text);\n }\n\n .v-file-picker-name {\n overflow: hidden;\n font-size: var(--vectis-text-body-sm-size);\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n\n /* Figures of equal width, so the sizes line up down the column instead of wobbling.\n That is not a typographic role, so it stays a local rule — the same reasoning as\n VFileInput's counter. */\n .v-file-picker-size {\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * Making the component a query container is reserved for the side-by-side layout,\n * since only its rule below depends on it — and it is not free.\n *\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * component would lose its natural width inside an automatically sized grid column or\n * an inline box. In the two other layouts it keeps it.\n */\n .v-file-picker[data-preview='end'] {\n container-type: inline-size;\n container-name: v-file-picker;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: row;\n align-items: flex-start;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-zone,\n .v-file-picker[data-preview='end'] .v-file-picker-list {\n flex: 1 1 0;\n min-inline-size: 0;\n }\n\n /*\n * The threshold is written as a literal length, a container query accepting no\n * variables. It is the width NEEDED to hold the two columns side by side, and not what\n * is left once they fold: the zone stops being readable below about 18rem — its\n * instruction wraps onto two lines and the button follows — a row needs about 15rem to\n * show a thumbnail, a name worth reading and its remove button, and there is a gutter\n * between them.\n */\n @container v-file-picker (max-width: 34rem) {\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: column;\n }\n }\n\n /* A disabled zone greys out through the colour tokens and never through opacity.\n\n The pairing used is the design system's own for a disabled surface, exactly as\n VInput sets it: a lighter text would look more convincingly disabled, and would fall\n below the required contrast against that background — which the accessibility checks\n refuse.\n\n Both text variables take the SAME value here, so that the secondary line does not\n sink below the title on a surface that is already dimmed. */\n .v-file-picker[data-disabled] {\n --file-picker-border-color: var(--vectis-color-border);\n --file-picker-bg: var(--vectis-color-surface-muted);\n --file-picker-text: var(--vectis-color-text-muted);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-muted);\n }\n\n .v-file-picker[data-readonly] {\n --file-picker-border-color: var(--vectis-color-border);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-file-picker-zone {\n transition: none;\n }\n }\n\n /*\n * Under Windows forced colours the backgrounds are dropped and every border colour is\n * replaced, so the drag highlight — which is nothing but colour — would disappear\n * entirely. A change of border STYLE survives that: the dashed outline becomes solid\n * for as long as a file hovers over the zone.\n */\n @media (forced-colors: active) {\n .v-file-picker[data-dragging] .v-file-picker-zone {\n border-style: solid;\n }\n\n .v-file-picker-thumb {\n border: 1px solid;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VFilePicker.js","names":[],"sources":["../../../src/components/VFilePicker/VFilePicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native file input supplies the dialog and form value; JavaScript screens dropped files\n * because accept does not constrain drops.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { audio_file as audioFileIcon } from '../VIcon/icons/audio_file'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport { cloud_upload as cloudUploadIcon } from '../VIcon/icons/cloud_upload'\nimport { code as codeIcon } from '../VIcon/icons/code'\nimport { description as descriptionIcon } from '../VIcon/icons/description'\nimport { folder_zip as folderZipIcon } from '../VIcon/icons/folder_zip'\nimport { image as imageIcon } from '../VIcon/icons/image'\nimport { picture_as_pdf as pictureAsPdfIcon } from '../VIcon/icons/picture_as_pdf'\nimport { table_chart as tableChartIcon } from '../VIcon/icons/table_chart'\nimport { video_file as videoFileIcon } from '../VIcon/icons/video_file'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { fileKind, type FilePickerKind } from './fileKind'\n\n/** What the `#browse` slot receives. */\nexport interface FilePickerBrowseSlotProps {\n /** Opens the file dialog; a button of your own has no other way to. */\n open: () => void\n /** Whether the picker is disabled, for your button to follow. */\n disabled: boolean\n}\n\n/** What the `#remove` slot receives. */\nexport interface FilePickerRemoveSlotProps {\n /** The file the row stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The ready-made accessible name of the control, the file's name included. */\n removeLabel: string\n}\n\n/** Where the files taken are listed: under the zone, beside it, or nowhere. */\nexport type FilePickerPreview = false | 'bottom' | 'end'\n\n/**\n * Everything known about one row of the list. The same object is handed to both slots\n * that can replace part of a row, so the two can never be given different information.\n */\nexport interface FilePickerRow {\n /** The file itself. */\n file: File\n /** Its position in the list. */\n index: number\n /** What kind of file it is, as worked out from its type or its extension. */\n kind: FilePickerKind\n /** The address of its image thumbnail, when there is one. Fall back to the icon in that case. */\n thumbnail: string | undefined\n /** The icon standing for that kind of file, a consumer's override included. */\n icon: IconSource\n /** Its size, already written out in the reader's conventions. */\n sizeText: string\n /** Takes this file out of the list. */\n remove: () => void\n}\n\ninterface FilePickerProps {\n /** What the reader is being asked to drop, in one line. */\n title: string\n /** A second line under it, for the constraints in plain words: kinds, sizes, how many. */\n subtitle?: string\n /** The large icon at the top of the zone. */\n icon?: IconSource\n /** Hides the separator and the browse button under the instruction. */\n hideBrowse?: boolean\n /**\n * The wording drawn on the browse button, which is also its accessible name. It falls\n * back to the design system dictionary.\n */\n browseText?: string\n /**\n * Where the files taken are listed: under the zone, or beside it. By default nothing is\n * listed at all: the value is there for a parent that wants to present it its own way.\n */\n preview?: FilePickerPreview\n /**\n * Shows the kind icon for every file in that list, images included: the way out when\n * a list holds many images, or very large ones.\n *\n * Left out, an image is shown as a thumbnail: it is given a temporary address, created\n * in the browser only and released as soon as the file leaves the list or the\n * component goes away.\n */\n hideThumbnails?: boolean\n /** Replaces the icon of one or more kinds of file. */\n typeIcons?: Partial<Record<FilePickerKind, IconSource>>\n /** The icon of the button removing a file from the list. */\n removeIcon?: IconSource\n /** Allows several files to be taken. With one only, every extra file is turned away. */\n multiple?: boolean\n /** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */\n accept?: string\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 taken at most. */\n maxFiles?: number\n /** Makes the zone unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was taken without allowing it to change: no dialog, no drop, no removal. Its\n * buttons stay reachable from the keyboard, announced as unavailable.\n */\n readonly?: boolean\n /**\n * Marks the zone as invalid, which colours its outline and is announced on the control the\n * reader reaches.\n */\n invalid?: boolean\n /**\n * Shows a spinner in place of the zone icon, typically while an upload is under way.\n * `disabled` and `readonly` are the props that cut those off.\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\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilePickerProps>(), {\n subtitle: undefined,\n icon: () => cloudUploadIcon,\n hideBrowse: false,\n browseText: undefined,\n preview: false,\n hideThumbnails: false,\n typeIcons: undefined,\n removeIcon: () => closeIcon,\n multiple: false,\n accept: undefined,\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n loading: false,\n loadingText: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection, by adding or removing something. */\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 /** One file was taken out, and which one. */\n remove: [file: File, index: number]\n}>()\n\ndefineSlots<{\n /** The large icon, for an illustration the icon props cannot express. */\n icon?(): unknown\n /** The instruction. Text and inline elements only, for the same reason. */\n title?(): unknown\n /** The second line. Same contract as the instruction. */\n subtitle?(): unknown\n /** The browse button. */\n browse?(props: FilePickerBrowseSlotProps): unknown\n /**\n * A whole row of the list, the way out for a row showing its own upload progress. It receives\n * everything the standard row was given, so nothing has to be worked out again.\n */\n item?(props: FilePickerRow): unknown\n /**\n * The square at the start of a row alone: for a thumbnail produced by your server, a\n * video's poster frame, or a format the browser cannot decode.\n */\n thumbnail?(props: FilePickerRow): unknown\n /** The control that removes a row. */\n remove?(props: FilePickerRemoveSlotProps): 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/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\nconst m = useMessages()\nconst locale = useLocale()\n\n/*\n * Here, the bucket meant for \"the control the user deals with\" goes to the ZONE: that is what\n * they see, and what takes the focus once the browse button is hidden. Nothing is pulled out of\n * it, unlike in VFileInput; this component assembles no description of its own.\n */\nconst {\n fileEl,\n enabled: interactive,\n nativeInputAttrs,\n controlAttrs: zoneAttrs,\n acceptFiles,\n openPicker,\n removeAt: removeFile,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n // @a11y @devwarn\n // The name a consumer gives a plain container is ignored by assistive\n // technology, and axe reports it, so this one fails SILENTLY at runtime too. Behind\n // `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n !props.hideBrowse &&\n (attrs['aria-label'] !== undefined || attrs['aria-labelledby'] !== undefined) &&\n 'an aria-label on the zone is inert while the browse button is shown: the zone is a plain container then, and axe reports aria-prohibited-attr. Name the zone through `title`, or set `hide-browse` to make the zone itself the control.',\n ]\n : undefined,\n})\n\nconst zoneEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLUListElement | null>(null)\nconst showList = computed(() => props.preview !== false && model.value.length > 0)\n\n/**\n * Which icon stands for which kind of file. A consumer overrides it entry by entry, so\n * replacing one icon does not mean restating the other seven.\n */\nconst KIND_ICONS: Record<FilePickerKind, IconSource> = {\n image: imageIcon,\n pdf: pictureAsPdfIcon,\n audio: audioFileIcon,\n video: videoFileIcon,\n archive: folderZipIcon,\n spreadsheet: tableChartIcon,\n code: codeIcon,\n file: descriptionIcon,\n}\n\nconst iconForKind = (kind: FilePickerKind): IconSource =>\n props.typeIcons?.[kind] ?? KIND_ICONS[kind]\n\nfunction onZoneClick(event: MouseEvent) {\n // The browse button already handles its own click, and that click also reaches the\n // zone: without this guard it would open the dialog twice. The test is inert when the\n // zone IS the button, there being nothing inside it to match.\n if (!props.hideBrowse && (event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => interactive.value,\n acceptFiles,\n)\n\n// @a11y\n/**\n * What can be focused in the zone, whichever shape it has taken: the zone itself when it is the\n * button, and otherwise the first button inside it.\n */\nconst rootEl = ref<HTMLElement | null>(null)\n\nfunction focusTarget(): HTMLElement | null {\n const zone = zoneEl.value\n if (!zone) return null\n return zone.matches('button') ? zone : zone.querySelector('button')\n}\n\n// @a11y @core\n/**\n * Takes a file out of the list, and catches the focus that was on its remove button. That\n * button disappears with its row, so the focus would fall back to the page body.\n */\nasync function removeAt(index: number) {\n if (!removeFile(index)) return\n\n await nextTick()\n const items = listEl.value?.querySelectorAll<HTMLElement>('.v-file-picker-item') ?? []\n const next = items[Math.min(index, items.length - 1)]?.querySelector('button')\n ;(next ?? focusTarget())?.focus()\n}\n\n/**\n * One temporary address per image, kept against THE FILE OBJECT ITSELF. The map is made\n * reactive rather than held in a plain reference, which lets a single row re-render when its\n * address is ready, instead of replacing the whole map.\n */\nconst thumbUrls = reactive(new Map<File, string>())\n\n/**\n * Whether a thumbnail can be drawn at all, decided on the type the browser reports and not on\n * the kind derived from the extension.\n */\nconst isThumbable = (file: File) => file.type.startsWith('image/')\n\n// @ssr @core\n/**\n * It is never called from setup, from a derived value or during rendering, only once mounted,\n * from a watcher and from handlers. That is what keeps the component renderable on a server:\n * the server draws the kind icon, and so does the browser's first render, the map being empty\n * until then; the same reasoning as today's date in VDatePicker.\n */\nfunction syncThumbnails() {\n // With no list rendered there is nothing to draw a thumbnail in, so no address is created at\n // all. This gate; and not a warning; is what makes the default configuration cost strictly\n // nothing.\n const wanted = new Set(\n !props.hideThumbnails && props.preview !== false\n ? model.value.filter((file) => isThumbable(file) && !undecodable.has(file))\n : [],\n )\n\n for (const [file, url] of thumbUrls) {\n if (wanted.has(file)) continue\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n }\n for (const file of wanted) {\n if (!thumbUrls.has(file)) thumbUrls.set(file, URL.createObjectURL(file))\n }\n}\n\n// @fallback\n/**\n * The image failed to decode; a format the browser does not read, a corrupt file, a type that\n * lied about itself.\n */\nfunction dropThumbnail(file: File) {\n const url = thumbUrls.get(file)\n if (!url) return\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n undecodable.add(file)\n}\n\n/** The files whose image failed once. */\nconst undecodable = new WeakSet<File>()\n\n// @ssr\n// The addresses are created in the browser only; the server and the browser's\n// first render both show the kind icon, so the two markups agree.\nonMounted(syncThumbnails)\n/*\n * Watching deeply costs nothing here and covers a consumer who ADDS to the list rather\n * than replacing it: the walk stops at a File, which is neither a plain object nor a\n * collection, so it never descends past the array itself.\n */\nwatch([model, () => props.hideThumbnails, () => props.preview], syncThumbnails, { deep: true })\n// Released before the component is torn down and never after; the same rule `useTimer` and\n// VHotkeys follow.\nonBeforeUnmount(() => {\n for (const url of thumbUrls.values()) URL.revokeObjectURL(url)\n thumbUrls.clear()\n})\n\n/**\n * Everything each row of the list needs, assembled in one place and once per change of the\n * list, the language, the icons or a thumbnail; not once per render.\n */\nconst rows = computed<{ key: number; row: FilePickerRow; removeLabel: string }[]>(() =>\n showList.value\n ? model.value.map((file, index) => {\n const kind = fileKind(file)\n return {\n key: fileKey(file),\n row: {\n file,\n index,\n kind,\n thumbnail: thumbUrls.get(file),\n icon: iconForKind(kind),\n sizeText: formatBytes(file.size, locale.value),\n remove: () => removeAt(index),\n },\n removeLabel: m.value.common.remove(file.name),\n }\n })\n : [],\n)\n\ndefineExpose({\n /** Moves the focus to the browse control: the button, or the zone when it is one. */\n focus: (options?: FocusOptions) => focusTarget()?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did, a\n * click or a key press: browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The zone's own box, for what neither of the two above covers. It is the wrapper and\n * 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-picker\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-preview=\"preview || undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-invalid=\"invalid ? '' : 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-picker-native v-hidden-input\" />\n\n <!--\n This wrapper is not decoration. The root is what the layout asks about its own width, and\n an element cannot ask about ITSELF: whatever flips has to be a descendant of it.\n -->\n <div class=\"v-file-picker-body\">\n <!--\n A real button as soon as the zone IS the control: the role, Enter, Space, the focus and\n the complete inertness of a disabled control then come from the platform. A plain\n container otherwise; a button inside a button is invalid markup, and unreachable by\n keyboard.\n -->\n <component\n :is=\"hideBrowse ? 'button' : 'div'\"\n ref=\"zoneEl\"\n :aria-invalid=\"hideBrowse && invalid ? 'true' : undefined\"\n :aria-disabled=\"hideBrowse && readonly ? 'true' : undefined\"\n v-bind=\"zoneAttrs\"\n class=\"v-file-picker-zone\"\n :type=\"hideBrowse ? 'button' : undefined\"\n :disabled=\"hideBrowse && disabled ? true : undefined\"\n @click=\"onZoneClick\"\n >\n <span class=\"v-file-picker-icon\">\n <slot name=\"icon\">\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <VIcon v-else v-bind=\"iconProps(icon)\" />\n </slot>\n </span>\n\n <!--\n Rendered as a span in both shapes: a paragraph is not allowed inside a button, and the\n markup must not change form between the two; one shape means one set of CSS rules.\n -->\n <VTypography as=\"span\" variant=\"subtitle\" class=\"v-file-picker-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n\n <VTypography\n v-if=\"subtitle || $slots.subtitle\"\n as=\"span\"\n variant=\"body-sm\"\n class=\"v-file-picker-subtitle\"\n >\n <slot name=\"subtitle\">{{ subtitle }}</slot>\n </VTypography>\n\n <template v-if=\"!hideBrowse\">\n <!-- Deliberately not hidden from screen readers: \"or\" is real text, and it\n reads naturally between the instruction and the button. The two rules on\n either side of it are pseudo-elements, and therefore decorative by\n construction. -->\n <span class=\"v-file-picker-separator\">{{ m.filePicker.or }}</span>\n\n <slot name=\"browse\" :open=\"openPicker\" :disabled=\"!interactive\">\n <!-- Read-only leaves it reachable, as every read-only control of the design\n system is: unavailable rather than disabled. The invalid state is said on\n it, the control the reader reaches, the zone being a plain container. -->\n <VButton\n class=\"v-file-picker-browse\"\n variant=\"outline\"\n tone=\"neutral\"\n :disabled=\"disabled\"\n :aria-disabled=\"readonly ? 'true' : undefined\"\n :aria-invalid=\"invalid ? 'true' : undefined\"\n @click=\"openPicker\"\n >\n {{ browseText ?? m.filePicker.browse }}\n </VButton>\n </slot>\n </template>\n </component>\n\n <ul v-if=\"showList\" ref=\"listEl\" class=\"v-file-picker-list\" :aria-label=\"m.filePicker.list\">\n <li v-for=\"{ key, row, removeLabel } in rows\" :key=\"key\" class=\"v-file-picker-item\">\n <slot name=\"item\" v-bind=\"row\">\n <span class=\"v-file-picker-thumb\">\n <slot name=\"thumbnail\" v-bind=\"row\">\n <img\n v-if=\"row.thumbnail\"\n class=\"v-file-picker-image\"\n :src=\"row.thumbnail\"\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n @error=\"dropThumbnail(row.file)\"\n />\n <VIcon v-else v-bind=\"iconProps(row.icon)\" />\n </slot>\n </span>\n\n <span class=\"v-file-picker-info\">\n <span class=\"v-file-picker-name\" :title=\"row.file.name\">{{ row.file.name }}</span>\n <span class=\"v-file-picker-size\">{{ row.sizeText }}</span>\n </span>\n\n <slot\n name=\"remove\"\n :file=\"row.file\"\n :index=\"row.index\"\n :remove=\"row.remove\"\n :remove-label=\"removeLabel\"\n >\n <VIconButton\n class=\"v-file-picker-remove\"\n size=\"sm\"\n :icon=\"removeIcon\"\n :label=\"removeLabel\"\n :disabled=\"!interactive\"\n @click=\"row.remove\"\n />\n </slot>\n </slot>\n </li>\n </ul>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * These variables carry the component's name for a reason: they INHERIT, so a bare `--bg` or\n * `--border-color` set by the host application on any ancestor would be picked up here\n * instead; and nothing would catch it, since the unit tests evaluate no styles at all. They\n * are declared on the ROOT so that the drag, disabled and read-only states each redefine them\n * in ONE place, which makes those states structurally incapable of disagreeing with hover or\n * focus.\n */\n .v-file-picker {\n --file-picker-border-color: var(--vectis-color-border-strong);\n --file-picker-bg: var(--vectis-color-surface-sunken);\n --file-picker-text: var(--vectis-color-text);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-raised);\n --file-picker-thumb-bg: var(--vectis-color-surface-muted);\n\n position: relative;\n display: block;\n inline-size: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-file-picker-body {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-4);\n }\n\n /*\n * The full width is part of that: a button shrinks to fit its content, where a container does\n * not.\n */\n .v-file-picker-zone {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--vectis-space-2);\n inline-size: 100%;\n min-block-size: var(--vectis-control-size-file-picker-min-block);\n margin: 0;\n padding: var(--vectis-space-6) var(--vectis-space-4);\n appearance: none;\n border: 1px dashed var(--file-picker-border-color);\n border-radius: var(--vectis-radius-surface);\n background: var(--file-picker-bg);\n color: var(--file-picker-text);\n font: inherit;\n text-align: center;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-file-picker:not([data-disabled]):not([data-readonly]) .v-file-picker-zone {\n cursor: pointer;\n }\n\n /* Not over an invalid zone: at (0,5,0) this rule outranks the invalid one below, and the\n pointer would paint the accent over the one cue that says something is wrong. */\n .v-file-picker:not([data-disabled]):not([data-readonly]):not([data-invalid])\n .v-file-picker-zone:hover {\n --file-picker-border-color: var(--vectis-color-accent);\n }\n\n /* Invalid redefines the variable the zone paints its outline with, the way the drag\n highlight does: the states can then never disagree about what colour the border is. */\n .v-file-picker[data-invalid] .v-file-picker-zone {\n --file-picker-border-color: var(--vectis-color-danger);\n }\n\n .v-file-picker-zone:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /*\n * While a file is being dragged over it, the zone is highlighted by redefining the very\n * variables it paints itself with; so the highlight cannot disagree with the hover and focus\n * states.\n */\n .v-file-picker[data-dragging] {\n --file-picker-border-color: var(--vectis-color-accent);\n --file-picker-bg: var(--vectis-color-accent-surface);\n }\n\n /*\n * The large icon's size is set here, on its own wrapper, and never on the zone: set there it\n * would inherit down and enlarge the browse button's icons as well.\n */\n .v-file-picker-icon {\n --vectis-icon-size: var(--vectis-control-size-file-picker-icon);\n\n /*\n * An illustration of your own in the `#icon` slot may be drawn smaller than the icon, and\n * without that floor the zone would change height with it; the box has to be the icon's\n * even when its content is not. A floor and not a fixed size, so a taller one still grows\n * it rather than spilling out of it; the centring is what keeps a smaller content in the\n * middle of the box instead of at its start.\n */\n min-inline-size: var(--vectis-control-size-file-picker-icon);\n min-block-size: var(--vectis-control-size-file-picker-icon);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--file-picker-text-muted);\n }\n\n /* The spinner standing in for the icon while loading takes the icon's box, the way it\n does in every field: a spinner's size is the box it occupies, and the icon size is\n set on this wrapper. */\n .v-file-picker-icon > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n .v-file-picker-subtitle {\n /* Set through VTypography's own colour variable rather than with a plain colour:\n there is then a single source, and the disabled state below overrides that same\n one instead of introducing a second. */\n --typography-color: var(--file-picker-text-muted);\n }\n\n /* The word set between two rules, entirely in CSS: the two lines are pseudo-elements\n sharing the row with the text between them. No extra element is added to the markup,\n and nothing decorative reaches the accessibility tree. */\n .v-file-picker-separator {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n inline-size: 100%;\n margin-block-start: var(--vectis-space-2);\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n }\n\n .v-file-picker-separator::before,\n .v-file-picker-separator::after {\n content: '';\n flex: 1 1 0;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-file-picker-list {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n .v-file-picker-item {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-2);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-row-bg);\n }\n\n .v-file-picker-thumb {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-size-file-picker-thumb);\n block-size: var(--vectis-control-size-file-picker-thumb);\n overflow: hidden;\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-thumb-bg);\n color: var(--file-picker-text-muted);\n }\n\n .v-file-picker-image {\n display: block;\n inline-size: 100%;\n block-size: 100%;\n object-fit: cover;\n }\n\n /* The zero minimum is what makes the ellipsis below possible at all: a flex item\n refuses by default to shrink below its own content, so without it a long file name\n would widen the row instead of being cut short. */\n .v-file-picker-info {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-inline-size: 0;\n color: var(--file-picker-text);\n }\n\n .v-file-picker-name {\n overflow: hidden;\n font-size: var(--vectis-text-body-sm-size);\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n\n /*\n * Figures of equal width, so the sizes line up down the column instead of wobbling. That is\n * not a typographic role, so it stays a local rule; the same reasoning as VFileInput's\n * counter.\n */\n .v-file-picker-size {\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * component would lose its natural width inside an automatically sized grid column or an\n * inline box. In the two other layouts it keeps it.\n */\n .v-file-picker[data-preview='end'] {\n container-type: inline-size;\n container-name: v-file-picker;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: row;\n align-items: flex-start;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-zone,\n .v-file-picker[data-preview='end'] .v-file-picker-list {\n flex: 1 1 0;\n min-inline-size: 0;\n }\n\n /*\n * The threshold is written as a literal length, a container query accepting no variables. It\n * is the width NEEDED to hold the two columns side by side, and not what is left once they\n * fold: the zone stops being readable below about 18rem; its instruction wraps onto two lines\n * and the button follows; a row needs about 15rem to show a thumbnail, a name worth reading\n * and its remove button, and there is a gutter between them.\n */\n @container v-file-picker (max-width: 34rem) {\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: column;\n }\n }\n\n /*\n * A disabled zone greys out through the colour tokens and never through opacity. The pairing\n * used is the design system's own for a disabled surface, exactly as VInput sets it: a\n * lighter text would look more convincingly disabled, and would fall below the required\n * contrast against that background; which the accessibility checks refuse.\n */\n .v-file-picker[data-disabled] {\n --file-picker-border-color: var(--vectis-color-border);\n --file-picker-bg: var(--vectis-color-surface-muted);\n --file-picker-text: var(--vectis-color-text-muted);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-muted);\n }\n\n .v-file-picker[data-readonly] {\n --file-picker-border-color: var(--vectis-color-border);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-file-picker-zone {\n transition: none;\n }\n }\n\n /*\n * Under Windows forced colours the backgrounds are dropped and every border colour is\n * replaced, so the drag highlight; which is nothing but colour; would disappear entirely. A\n * change of border STYLE survives that: the dashed outline becomes solid for as long as a\n * file hovers over the zone.\n */\n @media (forced-colors: active) {\n .v-file-picker[data-dragging] .v-file-picker-zone {\n border-style: solid;\n }\n\n .v-file-picker-thumb {\n border: 1px solid;\n }\n }\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"VFilePicker.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VFilePicker/VFilePicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A drop zone: a dashed rectangle taking files dragged onto it or chosen from a dialog, with\n * an optional list of what it holds. The v-model is a plain `File[]`, and the division of\n * labour matters — this component SHOWS and SCREENS, uploading is the parent's business.\n *\n * Four things need JS, each forced by the platform. A dialog opens ONLY from a real click on\n * a file input, and a `FileList` cannot be written from a template, so the input is a SOURCE\n * of files rather than a mirror of the value. `accept` has no say over a DROP. The input has\n * to be reset after every change, or the same file cannot be chosen twice running. And a\n * thumbnail needs an object URL, which then has to be released.\n *\n * The KEYBOARD is deliberately not among them: with the browse button hidden the zone\n * becomes a real `<button>`, so Enter, Space, focus, role and a disabled control's inertness\n * all come from the browser. The other incarnation, a plain container, exists for one reason\n * — a button inside a button is invalid markup and unusable by keyboard. The drag highlight,\n * the \"or\" separator and the side-by-side layout are CSS too.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { audio_file as audioFileIcon } from '../VIcon/icons/audio_file'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport { cloud_upload as cloudUploadIcon } from '../VIcon/icons/cloud_upload'\nimport { code as codeIcon } from '../VIcon/icons/code'\nimport { description as descriptionIcon } from '../VIcon/icons/description'\nimport { folder_zip as folderZipIcon } from '../VIcon/icons/folder_zip'\nimport { image as imageIcon } from '../VIcon/icons/image'\nimport { picture_as_pdf as pictureAsPdfIcon } from '../VIcon/icons/picture_as_pdf'\nimport { table_chart as tableChartIcon } from '../VIcon/icons/table_chart'\nimport { video_file as videoFileIcon } from '../VIcon/icons/video_file'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { fileKind, type FilePickerKind } from './fileKind'\n\n/** What the `#browse` slot receives. */\nexport interface FilePickerBrowseSlotProps {\n /** Opens the file dialog; a button of your own has no other way to. */\n open: () => void\n /** Whether the picker is disabled, for your button to follow. */\n disabled: boolean\n}\n\n/** What the `#remove` slot receives. */\nexport interface FilePickerRemoveSlotProps {\n /** The file the row stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The ready-made accessible name of the control, the file's name included. */\n removeLabel: string\n}\n\n/** Where the files taken are listed: under the zone, beside it, or nowhere. */\nexport type FilePickerPreview = false | 'bottom' | 'end'\n\n/**\n * Everything known about one row of the list. The same object is handed to both slots\n * that can replace part of a row, so the two can never be given different information.\n */\nexport interface FilePickerRow {\n /** The file itself. */\n file: File\n /** Its position in the list. */\n index: number\n /** What kind of file it is, as worked out from its type or its extension. */\n kind: FilePickerKind\n /**\n * The address of its image thumbnail, when there is one.\n *\n * It is absent on the server AND on the browser's first render, since those addresses\n * only exist in a browser and creating them during the first render would make the two\n * markups differ. It is absent as well for anything the browser fails to decode. Fall\n * back to the icon in that case.\n */\n thumbnail: string | undefined\n /** The icon standing for that kind of file, a consumer's override included. */\n icon: IconSource\n /** Its size, already written out in the reader's conventions. */\n sizeText: string\n /** Takes this file out of the list. */\n remove: () => void\n}\n\ninterface FilePickerProps {\n /**\n * What the reader is being asked to drop, in one line. It is REQUIRED: a drop zone\n * with no instruction is just a rectangle.\n *\n * Note that this prop shadows the HTML attribute of the same name on the component\n * itself, an accepted trade-off: a tooltip on a drop zone would be redundant anyway.\n */\n title: string\n /** A second line under it, for the constraints in plain words: kinds, sizes, how many. */\n subtitle?: string\n /** The large icon at the top of the zone. */\n icon?: IconSource\n /**\n * Hides the separator and the browse button under the instruction. That changes the\n * zone's nature: it then becomes the control ITSELF, a real button, so Enter, Space\n * and the focus come from the platform rather than from a container that merely\n * reacts to clicks.\n */\n hideBrowse?: boolean\n /**\n * The wording drawn on the browse button, which is also its accessible name. It falls\n * back to the design system dictionary.\n */\n browseText?: string\n /**\n * Where the files taken are listed: under the zone, or beside it. Beside folds back\n * underneath when the COMPONENT is narrow, following the width it was given rather\n * than the width of the window.\n *\n * By default nothing is listed at all: the value is there for a parent that wants to\n * present it its own way.\n */\n preview?: FilePickerPreview\n /**\n * Shows the kind icon for every file in that list, images included: the way out when\n * a list holds many images, or very large ones.\n *\n * Left out, an image is shown as a thumbnail: it is given a temporary address, created\n * in the browser only and released as soon as the file leaves the list or the\n * component goes away.\n */\n hideThumbnails?: boolean\n /** Replaces the icon of one or more kinds of file. */\n typeIcons?: Partial<Record<FilePickerKind, IconSource>>\n /** The icon of the button removing a file from the list. */\n removeIcon?: IconSource\n /** Allows several files to be taken. 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 * DROPPED file, the attribute having no say over a drop.\n */\n accept?: string\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 taken at most. */\n maxFiles?: number\n /** Makes the zone unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was taken without allowing it to change: no dialog, no drop, no removal. Its\n * buttons stay reachable from the keyboard, announced as unavailable.\n */\n readonly?: boolean\n /**\n * Marks the zone as invalid, which colours its outline and is announced on the control\n * the reader reaches. It is for a rule of your own: nothing here is checked by the browser,\n * the real input being hidden.\n */\n invalid?: boolean\n /**\n * Shows a spinner in place of the zone icon, typically while an upload is under way.\n * It says that something is happening and changes nothing else: files can still be\n * dropped and the dialog still opens. `disabled` and `readonly` are the props that cut\n * those off.\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\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilePickerProps>(), {\n subtitle: undefined,\n icon: () => cloudUploadIcon,\n hideBrowse: false,\n browseText: undefined,\n preview: false,\n hideThumbnails: false,\n typeIcons: undefined,\n removeIcon: () => closeIcon,\n multiple: false,\n accept: undefined,\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n loading: false,\n loadingText: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection, by adding or removing something. */\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, and which one. It exists alongside `change`, which only says\n * what is LEFT: an upload already in flight is aborted from here, without having to\n * compare two lists to find out what went.\n */\n remove: [file: File, index: number]\n}>()\n\ndefineSlots<{\n /**\n * The large icon, for an illustration the icon props cannot express.\n *\n * It must stay non-interactive, and so must the two below: with the browse button\n * hidden, the zone IS a button, and nothing interactive may sit inside one.\n */\n icon?(): unknown\n /** The instruction. Text and inline elements only, for the same reason. */\n title?(): unknown\n /** The second line. Same contract as the instruction. */\n subtitle?(): unknown\n /**\n * The browse button. Call the `open` it receives: without it a button of your own\n * could no longer open the file dialog at all.\n */\n browse?(props: FilePickerBrowseSlotProps): unknown\n /**\n * A WHOLE row of the list, the way out for a row showing its own upload progress. It\n * receives everything the standard row was given, so nothing has to be worked out\n * again.\n */\n item?(props: FilePickerRow): unknown\n /**\n * The square at the start of a row alone: for a thumbnail produced by your server, a\n * video's poster frame, or a format the browser cannot decode.\n */\n thumbnail?(props: FilePickerRow): unknown\n /**\n * The control that removes a row. Two of the values it receives are not optional in\n * practice: `remove` is the only thing that can take the file out, and `removeLabel` is\n * the ready-made accessible name, the file's own included, without which the button\n * would be announced as nothing at all.\n */\n remove?(props: FilePickerRemoveSlotProps): 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\nconst m = useMessages()\nconst locale = useLocale()\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value and the removal of a file live in `useFileField`, shared with VFileInput.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the ZONE: that is\n * what they see, and what takes the focus once the browse button is hidden. Nothing is\n * pulled out of it, unlike in VFileInput — this component assembles no description of\n * its own.\n *\n * The zone becomes the control exactly when there is no browse button inside to be one,\n * which is what `hideBrowse` decides.\n */\nconst {\n fileEl,\n enabled: interactive,\n nativeInputAttrs,\n controlAttrs: zoneAttrs,\n acceptFiles,\n openPicker,\n removeAt: removeFile,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n // @a11y @devwarn\n // The name a consumer gives a plain container is ignored by assistive\n // technology, and axe reports it, so this one fails SILENTLY at runtime too. Behind\n // `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n !props.hideBrowse &&\n (attrs['aria-label'] !== undefined || attrs['aria-labelledby'] !== undefined) &&\n 'an aria-label on the zone is inert while the browse button is shown: the zone is a plain container then, and axe reports aria-prohibited-attr. Name the zone through `title`, or set `hide-browse` to make the zone itself the control.',\n ]\n : undefined,\n})\n\nconst zoneEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLUListElement | null>(null)\nconst showList = computed(() => props.preview !== false && model.value.length > 0)\n\n/**\n * Which icon stands for which kind of file. A consumer overrides it entry by entry, so\n * replacing one icon does not mean restating the other seven.\n */\nconst KIND_ICONS: Record<FilePickerKind, IconSource> = {\n image: imageIcon,\n pdf: pictureAsPdfIcon,\n audio: audioFileIcon,\n video: videoFileIcon,\n archive: folderZipIcon,\n spreadsheet: tableChartIcon,\n code: codeIcon,\n file: descriptionIcon,\n}\n\nconst iconForKind = (kind: FilePickerKind): IconSource =>\n props.typeIcons?.[kind] ?? KIND_ICONS[kind]\n\nfunction onZoneClick(event: MouseEvent) {\n // The browse button already handles its own click, and that click also reaches the\n // zone: without this guard it would open the dialog twice. The test is inert when the\n // zone IS the button, there being nothing inside it to match.\n if (!props.hideBrowse && (event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => interactive.value,\n acceptFiles,\n)\n\n// @a11y\n/**\n * What can be focused in the zone, whichever shape it has taken: the zone itself when it\n * is the button, and otherwise the first button inside it.\n *\n * It is found by searching the DOM rather than kept as a reference, because a consumer\n * may have replaced the browse button entirely — theirs is not ours to hold a reference\n * to, and it still has to be reachable.\n */\nconst rootEl = ref<HTMLElement | null>(null)\n\nfunction focusTarget(): HTMLElement | null {\n const zone = zoneEl.value\n if (!zone) return null\n return zone.matches('button') ? zone : zone.querySelector('button')\n}\n\n// @a11y @core\n/**\n * Takes a file out of the list, and catches the focus that was on its remove button.\n *\n * That button disappears with its row, so the focus would fall back to the page body.\n * It is moved instead to the row that takes its place, or to the browse control once\n * the list is empty. Waiting for the next tick is what makes the new row findable: the\n * page still shows the old one at the moment the value is written.\n */\nasync function removeAt(index: number) {\n if (!removeFile(index)) return\n\n await nextTick()\n const items = listEl.value?.querySelectorAll<HTMLElement>('.v-file-picker-item') ?? []\n const next = items[Math.min(index, items.length - 1)]?.querySelector('button')\n ;(next ?? focusTarget())?.focus()\n}\n\n/**\n * One temporary address per image, kept against THE FILE OBJECT ITSELF.\n *\n * Using the file as the key is safe, and that is worth knowing: Vue's reactivity only\n * wraps plain objects, arrays and collections, so a File travelling through the value\n * comes back with its identity intact — the exact opposite of VCombobox's option cache,\n * where comparing identities IS the bug. Keying by name would be wrong here: two files\n * picked from different folders can share one.\n *\n * The map is made reactive rather than held in a plain reference, which is what lets a\n * single row re-render when its address is ready, instead of replacing the whole map.\n */\nconst thumbUrls = reactive(new Map<File, string>())\n\n/**\n * Whether a thumbnail can be drawn at all, decided on the type the BROWSER reports and\n * not on the kind derived from the extension. A `.heic` file counts as an image and\n * deserves the image icon, but no engine will decode it.\n */\nconst isThumbable = (file: File) => file.type.startsWith('image/')\n\n// @ssr @core\n/**\n * Brings the map into line with the list: releases the addresses of files that have\n * gone, and creates them for files that have arrived.\n *\n * TRAP — it is never called from setup, from a derived value or during rendering, only\n * once mounted, from a watcher and from handlers. That is what keeps the component\n * renderable on a server: the server draws the kind icon, and so does the browser's\n * first render, the map being empty until then — the same reasoning as today's date in\n * VDatePicker.\n */\nfunction syncThumbnails() {\n // With no list rendered there is nothing to draw a thumbnail in, so no address is\n // created at all. This gate — and not a warning — is what makes the default\n // configuration cost strictly nothing.\n const wanted = new Set(\n !props.hideThumbnails && props.preview !== false\n ? model.value.filter((file) => isThumbable(file) && !undecodable.has(file))\n : [],\n )\n\n for (const [file, url] of thumbUrls) {\n if (wanted.has(file)) continue\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n }\n for (const file of wanted) {\n if (!thumbUrls.has(file)) thumbUrls.set(file, URL.createObjectURL(file))\n }\n}\n\n// @fallback\n/**\n * The image failed to decode — a format the browser does not read, a corrupt file, a\n * type that lied about itself. The address is released and the kind icon takes over: a\n * failure to draw must never leave an empty square in the list.\n */\nfunction dropThumbnail(file: File) {\n const url = thumbUrls.get(file)\n if (!url) return\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n undecodable.add(file)\n}\n\n/**\n * The files whose image failed once. They are still in the selection, so without this\n * `syncThumbnails` would hand each of them a new address on every change of the list, only\n * for the `<img>` to fail again. Weak, so a file that leaves takes its entry with it.\n */\nconst undecodable = new WeakSet<File>()\n\n// @ssr\n// The addresses are created in the browser only; the server and the browser's\n// first render both show the kind icon, so the two markups agree.\nonMounted(syncThumbnails)\n/*\n * Watching deeply costs nothing here and covers a consumer who ADDS to the list rather\n * than replacing it: the walk stops at a File, which is neither a plain object nor a\n * collection, so it never descends past the array itself.\n */\nwatch([model, () => props.hideThumbnails, () => props.preview], syncThumbnails, { deep: true })\n// Released BEFORE the component is torn down and never after — the same rule `useTimer`\n// and VHotkeys follow.\nonBeforeUnmount(() => {\n for (const url of thumbUrls.values()) URL.revokeObjectURL(url)\n thumbUrls.clear()\n})\n\n/**\n * Everything each row of the list needs, assembled in one place and once per change of the\n * list, the language, the icons or a thumbnail — not once per render. A drag over the zone\n * re-renders the component at the pointer's rate, and the rows would otherwise work out\n * their kind and their size again every time, for every slot that reads them.\n *\n * A Vue template has no way of declaring a reusable fragment, so this is also what keeps\n * the slots that can replace part of a row fed from a single source.\n */\nconst rows = computed<{ key: number; row: FilePickerRow; removeLabel: string }[]>(() =>\n showList.value\n ? model.value.map((file, index) => {\n const kind = fileKind(file)\n return {\n key: fileKey(file),\n row: {\n file,\n index,\n kind,\n thumbnail: thumbUrls.get(file),\n icon: iconForKind(kind),\n sizeText: formatBytes(file.size, locale.value),\n remove: () => removeAt(index),\n },\n removeLabel: m.value.common.remove(file.name),\n }\n })\n : [],\n)\n\ndefineExpose({\n /** Moves the focus to the browse control: the button, or the zone when it is one. */\n focus: (options?: FocusOptions) => focusTarget()?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did, a\n * click or a key press: browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The zone's own box, for what neither of the two above covers. It is the wrapper and\n * 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-picker\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-preview=\"preview || undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-invalid=\"invalid ? '' : 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-picker-native v-hidden-input\" />\n\n <!-- TRAP — this wrapper is not decoration. The root is what the layout asks about\n its own width, and an element cannot ask about ITSELF: whatever flips has to be\n a descendant of it. Deleting this element makes the side-by-side layout\n silently stop folding, with no error anywhere. -->\n <div class=\"v-file-picker-body\">\n <!-- A real button as soon as the zone IS the control: the role, Enter, Space, the\n focus and the complete inertness of a disabled control then come from the\n platform. A plain container otherwise — a button inside a button is invalid\n markup, and unreachable by keyboard. -->\n <component\n :is=\"hideBrowse ? 'button' : 'div'\"\n ref=\"zoneEl\"\n :aria-invalid=\"hideBrowse && invalid ? 'true' : undefined\"\n :aria-disabled=\"hideBrowse && readonly ? 'true' : undefined\"\n v-bind=\"zoneAttrs\"\n class=\"v-file-picker-zone\"\n :type=\"hideBrowse ? 'button' : undefined\"\n :disabled=\"hideBrowse && disabled ? true : undefined\"\n @click=\"onZoneClick\"\n >\n <span class=\"v-file-picker-icon\">\n <slot name=\"icon\">\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <VIcon v-else v-bind=\"iconProps(icon)\" />\n </slot>\n </span>\n\n <!-- Rendered as a span in BOTH shapes: a paragraph is not allowed inside a\n button, and the markup must not change form between the two — one shape\n means one set of CSS rules. -->\n <VTypography as=\"span\" variant=\"subtitle\" class=\"v-file-picker-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n\n <VTypography\n v-if=\"subtitle || $slots.subtitle\"\n as=\"span\"\n variant=\"body-sm\"\n class=\"v-file-picker-subtitle\"\n >\n <slot name=\"subtitle\">{{ subtitle }}</slot>\n </VTypography>\n\n <template v-if=\"!hideBrowse\">\n <!-- Deliberately not hidden from screen readers: \"or\" is real text, and it\n reads naturally between the instruction and the button. The two rules on\n either side of it are pseudo-elements, and therefore decorative by\n construction. -->\n <span class=\"v-file-picker-separator\">{{ m.filePicker.or }}</span>\n\n <slot name=\"browse\" :open=\"openPicker\" :disabled=\"!interactive\">\n <!-- Read-only leaves it reachable, as every read-only control of the design\n system is: unavailable rather than disabled. The invalid state is said on\n it, the control the reader reaches, the zone being a plain container. -->\n <VButton\n class=\"v-file-picker-browse\"\n variant=\"outline\"\n tone=\"neutral\"\n :disabled=\"disabled\"\n :aria-disabled=\"readonly ? 'true' : undefined\"\n :aria-invalid=\"invalid ? 'true' : undefined\"\n @click=\"openPicker\"\n >\n {{ browseText ?? m.filePicker.browse }}\n </VButton>\n </slot>\n </template>\n </component>\n\n <ul v-if=\"showList\" ref=\"listEl\" class=\"v-file-picker-list\" :aria-label=\"m.filePicker.list\">\n <li v-for=\"{ key, row, removeLabel } in rows\" :key=\"key\" class=\"v-file-picker-item\">\n <slot name=\"item\" v-bind=\"row\">\n <span class=\"v-file-picker-thumb\">\n <slot name=\"thumbnail\" v-bind=\"row\">\n <img\n v-if=\"row.thumbnail\"\n class=\"v-file-picker-image\"\n :src=\"row.thumbnail\"\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n @error=\"dropThumbnail(row.file)\"\n />\n <VIcon v-else v-bind=\"iconProps(row.icon)\" />\n </slot>\n </span>\n\n <span class=\"v-file-picker-info\">\n <span class=\"v-file-picker-name\" :title=\"row.file.name\">{{ row.file.name }}</span>\n <span class=\"v-file-picker-size\">{{ row.sizeText }}</span>\n </span>\n\n <slot\n name=\"remove\"\n :file=\"row.file\"\n :index=\"row.index\"\n :remove=\"row.remove\"\n :remove-label=\"removeLabel\"\n >\n <VIconButton\n class=\"v-file-picker-remove\"\n size=\"sm\"\n :icon=\"removeIcon\"\n :label=\"removeLabel\"\n :disabled=\"!interactive\"\n @click=\"row.remove\"\n />\n </slot>\n </slot>\n </li>\n </ul>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* These variables carry the component's name for a reason: they INHERIT, so a bare\n `--bg` or `--border-color` set by the host application on any ancestor would be\n picked up here instead — and nothing would catch it, since the unit tests evaluate\n no styles at all.\n\n They are declared on the ROOT so that the drag, disabled and read-only states each\n redefine them in ONE place, which is what makes those states structurally incapable\n of disagreeing with hover or focus. */\n .v-file-picker {\n --file-picker-border-color: var(--vectis-color-border-strong);\n --file-picker-bg: var(--vectis-color-surface-sunken);\n --file-picker-text: var(--vectis-color-text);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-raised);\n --file-picker-thumb-bg: var(--vectis-color-surface-muted);\n\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n position: relative;\n display: block;\n inline-size: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-file-picker-body {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-4);\n }\n\n /* One set of rules serves both shapes the zone can take. All that has to be undone is\n what the browser gives a button — its own appearance, font and margin — since\n everything else is declared here regardless. The full width is part of that: a\n button shrinks to fit its content, where a container does not. */\n .v-file-picker-zone {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--vectis-space-2);\n inline-size: 100%;\n min-block-size: var(--vectis-control-size-file-picker-min-block);\n margin: 0;\n padding: var(--vectis-space-6) var(--vectis-space-4);\n appearance: none;\n border: 1px dashed var(--file-picker-border-color);\n border-radius: var(--vectis-radius-surface);\n background: var(--file-picker-bg);\n color: var(--file-picker-text);\n font: inherit;\n text-align: center;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-file-picker:not([data-disabled]):not([data-readonly]) .v-file-picker-zone {\n cursor: pointer;\n }\n\n /* Not over an invalid zone: at (0,5,0) this rule outranks the invalid one below, and the\n pointer would paint the accent over the one cue that says something is wrong. */\n .v-file-picker:not([data-disabled]):not([data-readonly]):not([data-invalid])\n .v-file-picker-zone:hover {\n --file-picker-border-color: var(--vectis-color-accent);\n }\n\n /* Invalid redefines the variable the zone paints its outline with, the way the drag\n highlight does: the states can then never disagree about what colour the border is. */\n .v-file-picker[data-invalid] .v-file-picker-zone {\n --file-picker-border-color: var(--vectis-color-danger);\n }\n\n .v-file-picker-zone:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* While a file is being dragged over it, the zone is highlighted by redefining the\n very variables it paints itself with — so the highlight cannot disagree with the\n hover and focus states. */\n .v-file-picker[data-dragging] {\n --file-picker-border-color: var(--vectis-color-accent);\n --file-picker-bg: var(--vectis-color-accent-surface);\n }\n\n /* The large icon's size is set HERE, on its own wrapper, and never on the zone: set\n there it would inherit down and enlarge the browse button's icons as well. */\n .v-file-picker-icon {\n --vectis-icon-size: var(--vectis-control-size-file-picker-icon);\n\n /* The wrapper is floored at the icon's own size, whatever it holds. An illustration of\n your own in the `#icon` slot may be drawn smaller than the icon, and without that\n floor the zone would change height with it — the box has to be the icon's even when\n its content is not. A floor and not a fixed size, so a taller one still grows it\n rather than spilling out of it; the centring is what keeps a smaller content in the\n middle of the box instead of at its start. */\n min-inline-size: var(--vectis-control-size-file-picker-icon);\n min-block-size: var(--vectis-control-size-file-picker-icon);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--file-picker-text-muted);\n }\n\n /* The spinner standing in for the icon while loading takes the icon's box, the way it\n does in every field: a spinner's size is the box it occupies, and the icon size is\n set on this wrapper. */\n .v-file-picker-icon > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n .v-file-picker-subtitle {\n /* Set through VTypography's own colour variable rather than with a plain colour:\n there is then a single source, and the disabled state below overrides that same\n one instead of introducing a second. */\n --typography-color: var(--file-picker-text-muted);\n }\n\n /* The word set between two rules, entirely in CSS: the two lines are pseudo-elements\n sharing the row with the text between them. No extra element is added to the markup,\n and nothing decorative reaches the accessibility tree. */\n .v-file-picker-separator {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n inline-size: 100%;\n margin-block-start: var(--vectis-space-2);\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n }\n\n .v-file-picker-separator::before,\n .v-file-picker-separator::after {\n content: '';\n flex: 1 1 0;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-file-picker-list {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n .v-file-picker-item {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-2);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-row-bg);\n }\n\n .v-file-picker-thumb {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-size-file-picker-thumb);\n block-size: var(--vectis-control-size-file-picker-thumb);\n overflow: hidden;\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-thumb-bg);\n color: var(--file-picker-text-muted);\n }\n\n .v-file-picker-image {\n display: block;\n inline-size: 100%;\n block-size: 100%;\n object-fit: cover;\n }\n\n /* The zero minimum is what makes the ellipsis below possible at all: a flex item\n refuses by default to shrink below its own content, so without it a long file name\n would widen the row instead of being cut short. */\n .v-file-picker-info {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-inline-size: 0;\n color: var(--file-picker-text);\n }\n\n .v-file-picker-name {\n overflow: hidden;\n font-size: var(--vectis-text-body-sm-size);\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n\n /* Figures of equal width, so the sizes line up down the column instead of wobbling.\n That is not a typographic role, so it stays a local rule — the same reasoning as\n VFileInput's counter. */\n .v-file-picker-size {\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * Making the component a query container is reserved for the side-by-side layout,\n * since only its rule below depends on it — and it is not free.\n *\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * component would lose its natural width inside an automatically sized grid column or\n * an inline box. In the two other layouts it keeps it.\n */\n .v-file-picker[data-preview='end'] {\n container-type: inline-size;\n container-name: v-file-picker;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: row;\n align-items: flex-start;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-zone,\n .v-file-picker[data-preview='end'] .v-file-picker-list {\n flex: 1 1 0;\n min-inline-size: 0;\n }\n\n /*\n * The threshold is written as a literal length, a container query accepting no\n * variables. It is the width NEEDED to hold the two columns side by side, and not what\n * is left once they fold: the zone stops being readable below about 18rem — its\n * instruction wraps onto two lines and the button follows — a row needs about 15rem to\n * show a thumbnail, a name worth reading and its remove button, and there is a gutter\n * between them.\n */\n @container v-file-picker (max-width: 34rem) {\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: column;\n }\n }\n\n /* A disabled zone greys out through the colour tokens and never through opacity.\n\n The pairing used is the design system's own for a disabled surface, exactly as\n VInput sets it: a lighter text would look more convincingly disabled, and would fall\n below the required contrast against that background — which the accessibility checks\n refuse.\n\n Both text variables take the SAME value here, so that the secondary line does not\n sink below the title on a surface that is already dimmed. */\n .v-file-picker[data-disabled] {\n --file-picker-border-color: var(--vectis-color-border);\n --file-picker-bg: var(--vectis-color-surface-muted);\n --file-picker-text: var(--vectis-color-text-muted);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-muted);\n }\n\n .v-file-picker[data-readonly] {\n --file-picker-border-color: var(--vectis-color-border);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-file-picker-zone {\n transition: none;\n }\n }\n\n /*\n * Under Windows forced colours the backgrounds are dropped and every border colour is\n * replaced, so the drag highlight — which is nothing but colour — would disappear\n * entirely. A change of border STYLE survives that: the dashed outline becomes solid\n * for as long as a file hovers over the zone.\n */\n @media (forced-colors: active) {\n .v-file-picker[data-dragging] .v-file-picker-zone {\n border-style: solid;\n }\n\n .v-file-picker-thumb {\n border: 1px solid;\n }\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"VFilePicker.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VFilePicker/VFilePicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native file input supplies the dialog and form value; JavaScript screens dropped files\n * because accept does not constrain drops.\n */\nimport { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { audio_file as audioFileIcon } from '../VIcon/icons/audio_file'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport { cloud_upload as cloudUploadIcon } from '../VIcon/icons/cloud_upload'\nimport { code as codeIcon } from '../VIcon/icons/code'\nimport { description as descriptionIcon } from '../VIcon/icons/description'\nimport { folder_zip as folderZipIcon } from '../VIcon/icons/folder_zip'\nimport { image as imageIcon } from '../VIcon/icons/image'\nimport { picture_as_pdf as pictureAsPdfIcon } from '../VIcon/icons/picture_as_pdf'\nimport { table_chart as tableChartIcon } from '../VIcon/icons/table_chart'\nimport { video_file as videoFileIcon } from '../VIcon/icons/video_file'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { fileKind, type FilePickerKind } from './fileKind'\n\n/** What the `#browse` slot receives. */\nexport interface FilePickerBrowseSlotProps {\n /** Opens the file dialog; a button of your own has no other way to. */\n open: () => void\n /** Whether the picker is disabled, for your button to follow. */\n disabled: boolean\n}\n\n/** What the `#remove` slot receives. */\nexport interface FilePickerRemoveSlotProps {\n /** The file the row stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The ready-made accessible name of the control, the file's name included. */\n removeLabel: string\n}\n\n/** Where the files taken are listed: under the zone, beside it, or nowhere. */\nexport type FilePickerPreview = false | 'bottom' | 'end'\n\n/**\n * Everything known about one row of the list. The same object is handed to both slots\n * that can replace part of a row, so the two can never be given different information.\n */\nexport interface FilePickerRow {\n /** The file itself. */\n file: File\n /** Its position in the list. */\n index: number\n /** What kind of file it is, as worked out from its type or its extension. */\n kind: FilePickerKind\n /** The address of its image thumbnail, when there is one. Fall back to the icon in that case. */\n thumbnail: string | undefined\n /** The icon standing for that kind of file, a consumer's override included. */\n icon: IconSource\n /** Its size, already written out in the reader's conventions. */\n sizeText: string\n /** Takes this file out of the list. */\n remove: () => void\n}\n\ninterface FilePickerProps {\n /** What the reader is being asked to drop, in one line. */\n title: string\n /** A second line under it, for the constraints in plain words: kinds, sizes, how many. */\n subtitle?: string\n /** The large icon at the top of the zone. */\n icon?: IconSource\n /** Hides the separator and the browse button under the instruction. */\n hideBrowse?: boolean\n /**\n * The wording drawn on the browse button, which is also its accessible name. It falls\n * back to the design system dictionary.\n */\n browseText?: string\n /**\n * Where the files taken are listed: under the zone, or beside it. By default nothing is\n * listed at all: the value is there for a parent that wants to present it its own way.\n */\n preview?: FilePickerPreview\n /**\n * Shows the kind icon for every file in that list, images included: the way out when\n * a list holds many images, or very large ones.\n *\n * Left out, an image is shown as a thumbnail: it is given a temporary address, created\n * in the browser only and released as soon as the file leaves the list or the\n * component goes away.\n */\n hideThumbnails?: boolean\n /** Replaces the icon of one or more kinds of file. */\n typeIcons?: Partial<Record<FilePickerKind, IconSource>>\n /** The icon of the button removing a file from the list. */\n removeIcon?: IconSource\n /** Allows several files to be taken. With one only, every extra file is turned away. */\n multiple?: boolean\n /** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */\n accept?: string\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 taken at most. */\n maxFiles?: number\n /** Makes the zone unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was taken without allowing it to change: no dialog, no drop, no removal. Its\n * buttons stay reachable from the keyboard, announced as unavailable.\n */\n readonly?: boolean\n /**\n * Marks the zone as invalid, which colours its outline and is announced on the control the\n * reader reaches.\n */\n invalid?: boolean\n /**\n * Shows a spinner in place of the zone icon, typically while an upload is under way.\n * `disabled` and `readonly` are the props that cut those off.\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\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilePickerProps>(), {\n subtitle: undefined,\n icon: () => cloudUploadIcon,\n hideBrowse: false,\n browseText: undefined,\n preview: false,\n hideThumbnails: false,\n typeIcons: undefined,\n removeIcon: () => closeIcon,\n multiple: false,\n accept: undefined,\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n loading: false,\n loadingText: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection, by adding or removing something. */\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 /** One file was taken out, and which one. */\n remove: [file: File, index: number]\n}>()\n\ndefineSlots<{\n /** The large icon, for an illustration the icon props cannot express. */\n icon?(): unknown\n /** The instruction. Text and inline elements only, for the same reason. */\n title?(): unknown\n /** The second line. Same contract as the instruction. */\n subtitle?(): unknown\n /** The browse button. */\n browse?(props: FilePickerBrowseSlotProps): unknown\n /**\n * A whole row of the list, the way out for a row showing its own upload progress. It receives\n * everything the standard row was given, so nothing has to be worked out again.\n */\n item?(props: FilePickerRow): unknown\n /**\n * The square at the start of a row alone: for a thumbnail produced by your server, a\n * video's poster frame, or a format the browser cannot decode.\n */\n thumbnail?(props: FilePickerRow): unknown\n /** The control that removes a row. */\n remove?(props: FilePickerRemoveSlotProps): 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/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\nconst m = useMessages()\nconst locale = useLocale()\n\n/*\n * Here, the bucket meant for \"the control the user deals with\" goes to the ZONE: that is what\n * they see, and what takes the focus once the browse button is hidden. Nothing is pulled out of\n * it, unlike in VFileInput; this component assembles no description of its own.\n */\nconst {\n fileEl,\n enabled: interactive,\n nativeInputAttrs,\n controlAttrs: zoneAttrs,\n acceptFiles,\n openPicker,\n removeAt: removeFile,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n // @a11y @devwarn\n // The name a consumer gives a plain container is ignored by assistive\n // technology, and axe reports it, so this one fails SILENTLY at runtime too. Behind\n // `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n !props.hideBrowse &&\n (attrs['aria-label'] !== undefined || attrs['aria-labelledby'] !== undefined) &&\n 'an aria-label on the zone is inert while the browse button is shown: the zone is a plain container then, and axe reports aria-prohibited-attr. Name the zone through `title`, or set `hide-browse` to make the zone itself the control.',\n ]\n : undefined,\n})\n\nconst zoneEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLUListElement | null>(null)\nconst showList = computed(() => props.preview !== false && model.value.length > 0)\n\n/**\n * Which icon stands for which kind of file. A consumer overrides it entry by entry, so\n * replacing one icon does not mean restating the other seven.\n */\nconst KIND_ICONS: Record<FilePickerKind, IconSource> = {\n image: imageIcon,\n pdf: pictureAsPdfIcon,\n audio: audioFileIcon,\n video: videoFileIcon,\n archive: folderZipIcon,\n spreadsheet: tableChartIcon,\n code: codeIcon,\n file: descriptionIcon,\n}\n\nconst iconForKind = (kind: FilePickerKind): IconSource =>\n props.typeIcons?.[kind] ?? KIND_ICONS[kind]\n\nfunction onZoneClick(event: MouseEvent) {\n // The browse button already handles its own click, and that click also reaches the\n // zone: without this guard it would open the dialog twice. The test is inert when the\n // zone IS the button, there being nothing inside it to match.\n if (!props.hideBrowse && (event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => interactive.value,\n acceptFiles,\n)\n\n// @a11y\n/**\n * What can be focused in the zone, whichever shape it has taken: the zone itself when it is the\n * button, and otherwise the first button inside it.\n */\nconst rootEl = ref<HTMLElement | null>(null)\n\nfunction focusTarget(): HTMLElement | null {\n const zone = zoneEl.value\n if (!zone) return null\n return zone.matches('button') ? zone : zone.querySelector('button')\n}\n\n// @a11y @core\n/**\n * Takes a file out of the list, and catches the focus that was on its remove button. That\n * button disappears with its row, so the focus would fall back to the page body.\n */\nasync function removeAt(index: number) {\n if (!removeFile(index)) return\n\n await nextTick()\n const items = listEl.value?.querySelectorAll<HTMLElement>('.v-file-picker-item') ?? []\n const next = items[Math.min(index, items.length - 1)]?.querySelector('button')\n ;(next ?? focusTarget())?.focus()\n}\n\n/**\n * One temporary address per image, kept against THE FILE OBJECT ITSELF. The map is made\n * reactive rather than held in a plain reference, which lets a single row re-render when its\n * address is ready, instead of replacing the whole map.\n */\nconst thumbUrls = reactive(new Map<File, string>())\n\n/**\n * Whether a thumbnail can be drawn at all, decided on the type the browser reports and not on\n * the kind derived from the extension.\n */\nconst isThumbable = (file: File) => file.type.startsWith('image/')\n\n// @ssr @core\n/**\n * It is never called from setup, from a derived value or during rendering, only once mounted,\n * from a watcher and from handlers. That is what keeps the component renderable on a server:\n * the server draws the kind icon, and so does the browser's first render, the map being empty\n * until then; the same reasoning as today's date in VDatePicker.\n */\nfunction syncThumbnails() {\n // With no list rendered there is nothing to draw a thumbnail in, so no address is created at\n // all. This gate; and not a warning; is what makes the default configuration cost strictly\n // nothing.\n const wanted = new Set(\n !props.hideThumbnails && props.preview !== false\n ? model.value.filter((file) => isThumbable(file) && !undecodable.has(file))\n : [],\n )\n\n for (const [file, url] of thumbUrls) {\n if (wanted.has(file)) continue\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n }\n for (const file of wanted) {\n if (!thumbUrls.has(file)) thumbUrls.set(file, URL.createObjectURL(file))\n }\n}\n\n// @fallback\n/**\n * The image failed to decode; a format the browser does not read, a corrupt file, a type that\n * lied about itself.\n */\nfunction dropThumbnail(file: File) {\n const url = thumbUrls.get(file)\n if (!url) return\n URL.revokeObjectURL(url)\n thumbUrls.delete(file)\n undecodable.add(file)\n}\n\n/** The files whose image failed once. */\nconst undecodable = new WeakSet<File>()\n\n// @ssr\n// The addresses are created in the browser only; the server and the browser's\n// first render both show the kind icon, so the two markups agree.\nonMounted(syncThumbnails)\n/*\n * Watching deeply costs nothing here and covers a consumer who ADDS to the list rather\n * than replacing it: the walk stops at a File, which is neither a plain object nor a\n * collection, so it never descends past the array itself.\n */\nwatch([model, () => props.hideThumbnails, () => props.preview], syncThumbnails, { deep: true })\n// Released before the component is torn down and never after; the same rule `useTimer` and\n// VHotkeys follow.\nonBeforeUnmount(() => {\n for (const url of thumbUrls.values()) URL.revokeObjectURL(url)\n thumbUrls.clear()\n})\n\n/**\n * Everything each row of the list needs, assembled in one place and once per change of the\n * list, the language, the icons or a thumbnail; not once per render.\n */\nconst rows = computed<{ key: number; row: FilePickerRow; removeLabel: string }[]>(() =>\n showList.value\n ? model.value.map((file, index) => {\n const kind = fileKind(file)\n return {\n key: fileKey(file),\n row: {\n file,\n index,\n kind,\n thumbnail: thumbUrls.get(file),\n icon: iconForKind(kind),\n sizeText: formatBytes(file.size, locale.value),\n remove: () => removeAt(index),\n },\n removeLabel: m.value.common.remove(file.name),\n }\n })\n : [],\n)\n\ndefineExpose({\n /** Moves the focus to the browse control: the button, or the zone when it is one. */\n focus: (options?: FocusOptions) => focusTarget()?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did, a\n * click or a key press: browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The zone's own box, for what neither of the two above covers. It is the wrapper and\n * 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-picker\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-preview=\"preview || undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-invalid=\"invalid ? '' : 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-picker-native v-hidden-input\" />\n\n <!--\n This wrapper is not decoration. The root is what the layout asks about its own width, and\n an element cannot ask about ITSELF: whatever flips has to be a descendant of it.\n -->\n <div class=\"v-file-picker-body\">\n <!--\n A real button as soon as the zone IS the control: the role, Enter, Space, the focus and\n the complete inertness of a disabled control then come from the platform. A plain\n container otherwise; a button inside a button is invalid markup, and unreachable by\n keyboard.\n -->\n <component\n :is=\"hideBrowse ? 'button' : 'div'\"\n ref=\"zoneEl\"\n :aria-invalid=\"hideBrowse && invalid ? 'true' : undefined\"\n :aria-disabled=\"hideBrowse && readonly ? 'true' : undefined\"\n v-bind=\"zoneAttrs\"\n class=\"v-file-picker-zone\"\n :type=\"hideBrowse ? 'button' : undefined\"\n :disabled=\"hideBrowse && disabled ? true : undefined\"\n @click=\"onZoneClick\"\n >\n <span class=\"v-file-picker-icon\">\n <slot name=\"icon\">\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <VIcon v-else v-bind=\"iconProps(icon)\" />\n </slot>\n </span>\n\n <!--\n Rendered as a span in both shapes: a paragraph is not allowed inside a button, and the\n markup must not change form between the two; one shape means one set of CSS rules.\n -->\n <VTypography as=\"span\" variant=\"subtitle\" class=\"v-file-picker-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n\n <VTypography\n v-if=\"subtitle || $slots.subtitle\"\n as=\"span\"\n variant=\"body-sm\"\n class=\"v-file-picker-subtitle\"\n >\n <slot name=\"subtitle\">{{ subtitle }}</slot>\n </VTypography>\n\n <template v-if=\"!hideBrowse\">\n <!-- Deliberately not hidden from screen readers: \"or\" is real text, and it\n reads naturally between the instruction and the button. The two rules on\n either side of it are pseudo-elements, and therefore decorative by\n construction. -->\n <span class=\"v-file-picker-separator\">{{ m.filePicker.or }}</span>\n\n <slot name=\"browse\" :open=\"openPicker\" :disabled=\"!interactive\">\n <!-- Read-only leaves it reachable, as every read-only control of the design\n system is: unavailable rather than disabled. The invalid state is said on\n it, the control the reader reaches, the zone being a plain container. -->\n <VButton\n class=\"v-file-picker-browse\"\n variant=\"outline\"\n tone=\"neutral\"\n :disabled=\"disabled\"\n :aria-disabled=\"readonly ? 'true' : undefined\"\n :aria-invalid=\"invalid ? 'true' : undefined\"\n @click=\"openPicker\"\n >\n {{ browseText ?? m.filePicker.browse }}\n </VButton>\n </slot>\n </template>\n </component>\n\n <ul v-if=\"showList\" ref=\"listEl\" class=\"v-file-picker-list\" :aria-label=\"m.filePicker.list\">\n <li v-for=\"{ key, row, removeLabel } in rows\" :key=\"key\" class=\"v-file-picker-item\">\n <slot name=\"item\" v-bind=\"row\">\n <span class=\"v-file-picker-thumb\">\n <slot name=\"thumbnail\" v-bind=\"row\">\n <img\n v-if=\"row.thumbnail\"\n class=\"v-file-picker-image\"\n :src=\"row.thumbnail\"\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n @error=\"dropThumbnail(row.file)\"\n />\n <VIcon v-else v-bind=\"iconProps(row.icon)\" />\n </slot>\n </span>\n\n <span class=\"v-file-picker-info\">\n <span class=\"v-file-picker-name\" :title=\"row.file.name\">{{ row.file.name }}</span>\n <span class=\"v-file-picker-size\">{{ row.sizeText }}</span>\n </span>\n\n <slot\n name=\"remove\"\n :file=\"row.file\"\n :index=\"row.index\"\n :remove=\"row.remove\"\n :remove-label=\"removeLabel\"\n >\n <VIconButton\n class=\"v-file-picker-remove\"\n size=\"sm\"\n :icon=\"removeIcon\"\n :label=\"removeLabel\"\n :disabled=\"!interactive\"\n @click=\"row.remove\"\n />\n </slot>\n </slot>\n </li>\n </ul>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * These variables carry the component's name for a reason: they INHERIT, so a bare `--bg` or\n * `--border-color` set by the host application on any ancestor would be picked up here\n * instead; and nothing would catch it, since the unit tests evaluate no styles at all. They\n * are declared on the ROOT so that the drag, disabled and read-only states each redefine them\n * in ONE place, which makes those states structurally incapable of disagreeing with hover or\n * focus.\n */\n .v-file-picker {\n --file-picker-border-color: var(--vectis-color-border-strong);\n --file-picker-bg: var(--vectis-color-surface-sunken);\n --file-picker-text: var(--vectis-color-text);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-raised);\n --file-picker-thumb-bg: var(--vectis-color-surface-muted);\n\n position: relative;\n display: block;\n inline-size: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-file-picker-body {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-4);\n }\n\n /*\n * The full width is part of that: a button shrinks to fit its content, where a container does\n * not.\n */\n .v-file-picker-zone {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--vectis-space-2);\n inline-size: 100%;\n min-block-size: var(--vectis-control-size-file-picker-min-block);\n margin: 0;\n padding: var(--vectis-space-6) var(--vectis-space-4);\n appearance: none;\n border: 1px dashed var(--file-picker-border-color);\n border-radius: var(--vectis-radius-surface);\n background: var(--file-picker-bg);\n color: var(--file-picker-text);\n font: inherit;\n text-align: center;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-file-picker:not([data-disabled]):not([data-readonly]) .v-file-picker-zone {\n cursor: pointer;\n }\n\n /* Not over an invalid zone: at (0,5,0) this rule outranks the invalid one below, and the\n pointer would paint the accent over the one cue that says something is wrong. */\n .v-file-picker:not([data-disabled]):not([data-readonly]):not([data-invalid])\n .v-file-picker-zone:hover {\n --file-picker-border-color: var(--vectis-color-accent);\n }\n\n /* Invalid redefines the variable the zone paints its outline with, the way the drag\n highlight does: the states can then never disagree about what colour the border is. */\n .v-file-picker[data-invalid] .v-file-picker-zone {\n --file-picker-border-color: var(--vectis-color-danger);\n }\n\n .v-file-picker-zone:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /*\n * While a file is being dragged over it, the zone is highlighted by redefining the very\n * variables it paints itself with; so the highlight cannot disagree with the hover and focus\n * states.\n */\n .v-file-picker[data-dragging] {\n --file-picker-border-color: var(--vectis-color-accent);\n --file-picker-bg: var(--vectis-color-accent-surface);\n }\n\n /*\n * The large icon's size is set here, on its own wrapper, and never on the zone: set there it\n * would inherit down and enlarge the browse button's icons as well.\n */\n .v-file-picker-icon {\n --vectis-icon-size: var(--vectis-control-size-file-picker-icon);\n\n /*\n * An illustration of your own in the `#icon` slot may be drawn smaller than the icon, and\n * without that floor the zone would change height with it; the box has to be the icon's\n * even when its content is not. A floor and not a fixed size, so a taller one still grows\n * it rather than spilling out of it; the centring is what keeps a smaller content in the\n * middle of the box instead of at its start.\n */\n min-inline-size: var(--vectis-control-size-file-picker-icon);\n min-block-size: var(--vectis-control-size-file-picker-icon);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--file-picker-text-muted);\n }\n\n /* The spinner standing in for the icon while loading takes the icon's box, the way it\n does in every field: a spinner's size is the box it occupies, and the icon size is\n set on this wrapper. */\n .v-file-picker-icon > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n .v-file-picker-subtitle {\n /* Set through VTypography's own colour variable rather than with a plain colour:\n there is then a single source, and the disabled state below overrides that same\n one instead of introducing a second. */\n --typography-color: var(--file-picker-text-muted);\n }\n\n /* The word set between two rules, entirely in CSS: the two lines are pseudo-elements\n sharing the row with the text between them. No extra element is added to the markup,\n and nothing decorative reaches the accessibility tree. */\n .v-file-picker-separator {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n inline-size: 100%;\n margin-block-start: var(--vectis-space-2);\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n }\n\n .v-file-picker-separator::before,\n .v-file-picker-separator::after {\n content: '';\n flex: 1 1 0;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-file-picker-list {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n .v-file-picker-item {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-2);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-row-bg);\n }\n\n .v-file-picker-thumb {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-size-file-picker-thumb);\n block-size: var(--vectis-control-size-file-picker-thumb);\n overflow: hidden;\n border-radius: var(--vectis-radius-interactive);\n background: var(--file-picker-thumb-bg);\n color: var(--file-picker-text-muted);\n }\n\n .v-file-picker-image {\n display: block;\n inline-size: 100%;\n block-size: 100%;\n object-fit: cover;\n }\n\n /* The zero minimum is what makes the ellipsis below possible at all: a flex item\n refuses by default to shrink below its own content, so without it a long file name\n would widen the row instead of being cut short. */\n .v-file-picker-info {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-inline-size: 0;\n color: var(--file-picker-text);\n }\n\n .v-file-picker-name {\n overflow: hidden;\n font-size: var(--vectis-text-body-sm-size);\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n\n /*\n * Figures of equal width, so the sizes line up down the column instead of wobbling. That is\n * not a typographic role, so it stays a local rule; the same reasoning as VFileInput's\n * counter.\n */\n .v-file-picker-size {\n color: var(--file-picker-text-muted);\n font-size: var(--vectis-text-caption-size);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * component would lose its natural width inside an automatically sized grid column or an\n * inline box. In the two other layouts it keeps it.\n */\n .v-file-picker[data-preview='end'] {\n container-type: inline-size;\n container-name: v-file-picker;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: row;\n align-items: flex-start;\n }\n\n .v-file-picker[data-preview='end'] .v-file-picker-zone,\n .v-file-picker[data-preview='end'] .v-file-picker-list {\n flex: 1 1 0;\n min-inline-size: 0;\n }\n\n /*\n * The threshold is written as a literal length, a container query accepting no variables. It\n * is the width NEEDED to hold the two columns side by side, and not what is left once they\n * fold: the zone stops being readable below about 18rem; its instruction wraps onto two lines\n * and the button follows; a row needs about 15rem to show a thumbnail, a name worth reading\n * and its remove button, and there is a gutter between them.\n */\n @container v-file-picker (max-width: 34rem) {\n .v-file-picker[data-preview='end'] .v-file-picker-body {\n flex-direction: column;\n }\n }\n\n /*\n * A disabled zone greys out through the colour tokens and never through opacity. The pairing\n * used is the design system's own for a disabled surface, exactly as VInput sets it: a\n * lighter text would look more convincingly disabled, and would fall below the required\n * contrast against that background; which the accessibility checks refuse.\n */\n .v-file-picker[data-disabled] {\n --file-picker-border-color: var(--vectis-color-border);\n --file-picker-bg: var(--vectis-color-surface-muted);\n --file-picker-text: var(--vectis-color-text-muted);\n --file-picker-text-muted: var(--vectis-color-text-muted);\n --file-picker-row-bg: var(--vectis-color-surface-muted);\n }\n\n .v-file-picker[data-readonly] {\n --file-picker-border-color: var(--vectis-color-border);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-file-picker-zone {\n transition: none;\n }\n }\n\n /*\n * Under Windows forced colours the backgrounds are dropped and every border colour is\n * replaced, so the drag highlight; which is nothing but colour; would disappear entirely. A\n * change of border STYLE survives that: the dashed outline becomes solid for as long as a\n * file hovers over the zone.\n */\n @media (forced-colors: active) {\n .v-file-picker[data-dragging] .v-file-picker-zone {\n border-style: solid;\n }\n\n .v-file-picker-thumb {\n border: 1px solid;\n }\n }\n}\n</style>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fileKind.js","names":[],"sources":["../../../src/components/VFilePicker/fileKind.ts"],"sourcesContent":["/**\n * What KIND of thing a file is, for the sole purpose of choosing an icon. It is\n * deliberately coarse: the question is which of eight pictures to show, not how to\n * classify every type in existence.\n *\n * The answer comes from the type the browser reports FIRST, and from the extension\n * afterwards. That order is the contract: the reported type is authoritative when there\n * IS one, but it is empty far more often than one expects — an extension the operating\n * system does not know, certain Linux setups — which is why the extension has to be\n * there as a fallback rather than as a first resort.\n *\n * The module is pure and knows nothing of Vue. It lives in the component's folder for\n * the usual reason: a single consumer, so it does not qualify as shared code — promote\n * it the day a second one appears.\n *\n * What is NOT here is which icon each kind takes. That belongs to VFilePicker, which\n * exposes it as a prop a consumer can override.\n */\n\nimport type { FileCandidate } from '../../utils/file'\n\nexport type FilePickerKind =\n 'image' | 'pdf' | 'audio' | 'video' | 'archive' | 'spreadsheet' | 'code' | 'file'\n\nconst MIME_KINDS: Record<string, FilePickerKind> = {\n 'application/pdf': 'pdf',\n 'application/zip': 'archive',\n 'application/x-zip-compressed': 'archive',\n 'application/vnd.rar': 'archive',\n 'application/x-7z-compressed': 'archive',\n 'application/x-tar': 'archive',\n 'application/gzip': 'archive',\n 'text/csv': 'spreadsheet',\n 'application/vnd.ms-excel': 'spreadsheet',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'spreadsheet',\n 'application/vnd.oasis.opendocument.spreadsheet': 'spreadsheet',\n 'application/json': 'code',\n 'application/xml': 'code',\n 'text/xml': 'code',\n 'text/html': 'code',\n 'text/css': 'code',\n 'text/javascript': 'code',\n 'application/javascript': 'code',\n}\n\nconst EXTENSION_KINDS: Record<string, FilePickerKind> = {\n avif: 'image',\n bmp: 'image',\n gif: 'image',\n heic: 'image',\n heif: 'image',\n ico: 'image',\n jpeg: 'image',\n jpg: 'image',\n png: 'image',\n svg: 'image',\n tif: 'image',\n tiff: 'image',\n webp: 'image',\n\n pdf: 'pdf',\n\n aac: 'audio',\n flac: 'audio',\n m4a: 'audio',\n mp3: 'audio',\n ogg: 'audio',\n wav: 'audio',\n\n avi: 'video',\n mkv: 'video',\n mov: 'video',\n mp4: 'video',\n webm: 'video',\n\n '7z': 'archive',\n bz2: 'archive',\n gz: 'archive',\n iso: 'archive',\n rar: 'archive',\n tar: 'archive',\n xz: 'archive',\n zip: 'archive',\n\n csv: 'spreadsheet',\n ods: 'spreadsheet',\n tsv: 'spreadsheet',\n xls: 'spreadsheet',\n xlsx: 'spreadsheet',\n\n css: 'code',\n go: 'code',\n html: 'code',\n java: 'code',\n js: 'code',\n json: 'code',\n jsx: 'code',\n py: 'code',\n rb: 'code',\n rs: 'code',\n scss: 'code',\n sh: 'code',\n ts: 'code',\n tsx: 'code',\n xml: 'code',\n yaml: 'code',\n yml: 'code',\n}\n\n/** Works out which kind a file belongs to. */\nexport function fileKind(file: FileCandidate): FilePickerKind {\n const type = file.type.toLowerCase()\n\n // The whole families first: matching on the family covers every image, audio and\n // video format, including the ones no table will ever list.\n if (type.startsWith('image/')) return 'image'\n if (type.startsWith('audio/')) return 'audio'\n if (type.startsWith('video/')) return 'video'\n\n const byMime = lookup(MIME_KINDS, type)\n if (byMime) return byMime\n\n // TRAP — the dot must be found PAST the first character and not at it: a file named\n // `.gitignore` has no extension at all, it has a name beginning with a dot.\n const dot = file.name.lastIndexOf('.')\n const extension = dot > 0 ? file.name.slice(dot + 1).toLowerCase() : ''\n return lookup(EXTENSION_KINDS, extension) ?? 'file'\n}\n\n/**\n * TRAP — the tables are object literals, so a bare index also answers for what they\n * INHERIT: a file named `x.constructor` would get a function back, and the component would\n * look its icon up with it. Only a table's own keys are kinds.\n */\nconst lookup = (table: Record<string, FilePickerKind>, key: string): FilePickerKind | undefined =>\n Object.hasOwn(table, key) ? table[key] : undefined\n"],"mappings":";AAwBA,IAAM,IAA6C;CACjD,mBAAmB;CACnB,mBAAmB;CACnB,gCAAgC;CAChC,uBAAuB;CACvB,+BAA+B;CAC/B,qBAAqB;CACrB,oBAAoB;CACpB,YAAY;CACZ,4BAA4B;CAC5B,qEAAqE;CACrE,kDAAkD;CAClD,oBAAoB;CACpB,mBAAmB;CACnB,YAAY;CACZ,aAAa;CACb,YAAY;CACZ,mBAAmB;CACnB,0BAA0B;AAC5B,GAEM,IAAkD;CACtD,MAAM;CACN,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAM;CACN,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAM;CAEN,KAAK;CAEL,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CAEL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CAEN,MAAM;CACN,KAAK;CACL,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,IAAI;CACJ,KAAK;CAEL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CAEN,KAAK;CACL,IAAI;CACJ,MAAM;CACN,MAAM;CACN,IAAI;CACJ,MAAM;CACN,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,IAAI;CACJ,IAAI;CACJ,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;AACP;AAGA,SAAgB,EAAS,GAAqC;CAC5D,IAAM,IAAO,EAAK,KAAK,YAAY;CAInC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CAEtC,IAAM,IAAS,EAAO,GAAY,CAAI;CACtC,IAAI,GAAQ,OAAO;CAInB,IAAM,IAAM,EAAK,KAAK,YAAY,GAAG;CAErC,OAAO,EAAO,GADI,IAAM,IAAI,EAAK,KAAK,MAAM,IAAM,CAAC,CAAC,CAAC,YAAY,IAAI,EAC7B,KAAK;AAC/C;AAOA,IAAM,KAAU,GAAuC,MACrD,OAAO,OAAO,GAAO,CAAG,IAAI,EAAM,KAAO,KAAA"}
1
+ {"version":3,"file":"fileKind.js","names":[],"sources":["../../../src/components/VFilePicker/fileKind.ts"],"sourcesContent":["/**\n * Prefer reported MIME types and fall back to filename extensions when the browser provides no\n * type.\n */\n\nimport type { FileCandidate } from '../../utils/file'\n\nexport type FilePickerKind =\n 'image' | 'pdf' | 'audio' | 'video' | 'archive' | 'spreadsheet' | 'code' | 'file'\n\nconst MIME_KINDS: Record<string, FilePickerKind> = {\n 'application/pdf': 'pdf',\n 'application/zip': 'archive',\n 'application/x-zip-compressed': 'archive',\n 'application/vnd.rar': 'archive',\n 'application/x-7z-compressed': 'archive',\n 'application/x-tar': 'archive',\n 'application/gzip': 'archive',\n 'text/csv': 'spreadsheet',\n 'application/vnd.ms-excel': 'spreadsheet',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'spreadsheet',\n 'application/vnd.oasis.opendocument.spreadsheet': 'spreadsheet',\n 'application/json': 'code',\n 'application/xml': 'code',\n 'text/xml': 'code',\n 'text/html': 'code',\n 'text/css': 'code',\n 'text/javascript': 'code',\n 'application/javascript': 'code',\n}\n\nconst EXTENSION_KINDS: Record<string, FilePickerKind> = {\n avif: 'image',\n bmp: 'image',\n gif: 'image',\n heic: 'image',\n heif: 'image',\n ico: 'image',\n jpeg: 'image',\n jpg: 'image',\n png: 'image',\n svg: 'image',\n tif: 'image',\n tiff: 'image',\n webp: 'image',\n\n pdf: 'pdf',\n\n aac: 'audio',\n flac: 'audio',\n m4a: 'audio',\n mp3: 'audio',\n ogg: 'audio',\n wav: 'audio',\n\n avi: 'video',\n mkv: 'video',\n mov: 'video',\n mp4: 'video',\n webm: 'video',\n\n '7z': 'archive',\n bz2: 'archive',\n gz: 'archive',\n iso: 'archive',\n rar: 'archive',\n tar: 'archive',\n xz: 'archive',\n zip: 'archive',\n\n csv: 'spreadsheet',\n ods: 'spreadsheet',\n tsv: 'spreadsheet',\n xls: 'spreadsheet',\n xlsx: 'spreadsheet',\n\n css: 'code',\n go: 'code',\n html: 'code',\n java: 'code',\n js: 'code',\n json: 'code',\n jsx: 'code',\n py: 'code',\n rb: 'code',\n rs: 'code',\n scss: 'code',\n sh: 'code',\n ts: 'code',\n tsx: 'code',\n xml: 'code',\n yaml: 'code',\n yml: 'code',\n}\n\n/** Works out which kind a file belongs to. */\nexport function fileKind(file: FileCandidate): FilePickerKind {\n const type = file.type.toLowerCase()\n\n // The whole families first: matching on the family covers every image, audio and\n // video format, including the ones no table will ever list.\n if (type.startsWith('image/')) return 'image'\n if (type.startsWith('audio/')) return 'audio'\n if (type.startsWith('video/')) return 'video'\n\n const byMime = lookup(MIME_KINDS, type)\n if (byMime) return byMime\n\n // The dot must be found PAST the first character and not at it: a file named `.gitignore` has\n // no extension at all, it has a name beginning with a dot.\n const dot = file.name.lastIndexOf('.')\n const extension = dot > 0 ? file.name.slice(dot + 1).toLowerCase() : ''\n return lookup(EXTENSION_KINDS, extension) ?? 'file'\n}\n\n/**\n * The tables are object literals, so a bare index also answers for what they INHERIT: a file\n * named `x.constructor` would get a function back, and the component would look its icon up\n * with it. Only a table's own keys are kinds.\n */\nconst lookup = (table: Record<string, FilePickerKind>, key: string): FilePickerKind | undefined =>\n Object.hasOwn(table, key) ? table[key] : undefined\n"],"mappings":";AAUA,IAAM,IAA6C;CACjD,mBAAmB;CACnB,mBAAmB;CACnB,gCAAgC;CAChC,uBAAuB;CACvB,+BAA+B;CAC/B,qBAAqB;CACrB,oBAAoB;CACpB,YAAY;CACZ,4BAA4B;CAC5B,qEAAqE;CACrE,kDAAkD;CAClD,oBAAoB;CACpB,mBAAmB;CACnB,YAAY;CACZ,aAAa;CACb,YAAY;CACZ,mBAAmB;CACnB,0BAA0B;AAC5B,GAEM,IAAkD;CACtD,MAAM;CACN,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAM;CACN,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAM;CAEN,KAAK;CAEL,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CAEL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CAEN,MAAM;CACN,KAAK;CACL,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,IAAI;CACJ,KAAK;CAEL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CAEN,KAAK;CACL,IAAI;CACJ,MAAM;CACN,MAAM;CACN,IAAI;CACJ,MAAM;CACN,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,IAAI;CACJ,IAAI;CACJ,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;AACP;AAGA,SAAgB,EAAS,GAAqC;CAC5D,IAAM,IAAO,EAAK,KAAK,YAAY;CAInC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAI,EAAK,WAAW,QAAQ,GAAG,OAAO;CAEtC,IAAM,IAAS,EAAO,GAAY,CAAI;CACtC,IAAI,GAAQ,OAAO;CAInB,IAAM,IAAM,EAAK,KAAK,YAAY,GAAG;CAErC,OAAO,EAAO,GADI,IAAM,IAAI,EAAK,KAAK,MAAM,IAAM,CAAC,CAAC,CAAC,YAAY,IAAI,EAC7B,KAAK;AAC/C;AAOA,IAAM,KAAU,GAAuC,MACrD,OAAO,OAAO,GAAO,CAAG,IAAI,EAAM,KAAO,KAAA"}
@@ -4,22 +4,14 @@ export type HotkeysVariant = 'soft' | 'outline' | 'elevated';
4
4
  /** The size of the caps, from the two smallest steps of the control scale. */
5
5
  export type HotkeysSize = 'xs' | 'sm';
6
6
  interface HotkeysProps {
7
- /**
8
- * The combination, `+`-separated: `mod+k`, `ctrl+shift+p`, `alt+enter`, `esc`.
9
- * Case- and space-insensitive. `mod` is the CROSS-PLATFORM modifier: ⌘ on
10
- * macOS, Ctrl everywhere else; `meta` is the literal Command/Windows/Super key.
11
- * Aliases: cmd/command/win/super → meta, control → ctrl, option/opt → alt,
12
- * return → enter, escape → esc, del → delete, arrowup… → up…. An unknown token
13
- * is displayed as declared (`k` → K, `f5` → F5). The `+` key is `plus`.
14
- */
7
+ /** The combination, `+`-separated: `mod+k`, `ctrl+shift+p`, `alt+enter`, `esc`. */
15
8
  keys: string;
16
9
  /** How a key cap is drawn: tinted (`soft`), outlined (`outline`), or raised off the page (`elevated`). */
17
10
  variant?: HotkeysVariant;
18
11
  /**
19
- * Draws the whole combination as a SINGLE key rather than as several: the decoration
20
- * moves from each cap to the shortcut as a whole, so the separator ends up inside
21
- * the key instead of between two of them. It is purely visual: the markup and the
22
- * announced name are identical either way.
12
+ * Draws the whole combination as a SINGLE key rather than as several: the decoration moves
13
+ * from each cap to the shortcut as a whole, so the separator ends up inside the key instead
14
+ * of between two of them.
23
15
  */
24
16
  attached?: boolean;
25
17
  /** The size of the caps, `xs` by default: a shortcut is chrome beside other text. */
@@ -44,9 +36,8 @@ interface HotkeysProps {
44
36
  */
45
37
  listen?: boolean;
46
38
  /**
47
- * While listening, lets the browser go on doing whatever the combination normally
48
- * does. Left out, the browser is stopped, which is the entire point of taking over
49
- * something like ⌘K. Escape is never stopped: it has to stay the close request of dialogs.
39
+ * While listening, lets the browser go on doing whatever the combination normally does. Left
40
+ * out, the browser is stopped, which is the entire point of taking over something like ⌘K.
50
41
  */
51
42
  allowDefault?: boolean;
52
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"VHotkeys.js","names":[],"sources":["../../../src/components/VHotkeys/VHotkeys.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A keyboard shortcut, DISPLAYED. Written as a plain string — `mod+k` — and rendered with\n * the symbols of the system the reader is on: ⌘K on a Mac, Ctrl+K elsewhere.\n *\n * Two things here are unique in the library. It carries the ONLY `navigator` read, in\n * `onMounted` and nowhere else: the server cannot know the OS and the first client render\n * has to match what it sent, so a Mac visitor pays one frame of `Ctrl`. The `platform` prop\n * is the escape hatch, and what makes stories and tests deterministic.\n *\n * And `listen` attaches the library's ONLY document-level listener, which is why it must be\n * asked for: a component whose job is to display a shortcut must not silently capture the\n * page's keyboard.\n *\n * It is not interactive and not focusable, so it carries no hover, active or focus rule, and\n * therefore no transition and no reduced-motion block. That absence is deliberate — do not\n * copy VButton's state rules into it.\n */\n\nimport { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'\n\nimport {\n DEFAULT_PLATFORM,\n capLabel,\n detectPlatform,\n isEditableTarget,\n matchesEvent,\n parseHotkeys,\n resolveKeys,\n} from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useMessages } from '../../i18n/state'\n\n/** How a key cap is drawn. */\nexport type HotkeysVariant = 'soft' | 'outline' | 'elevated'\n/** The size of the caps, from the two smallest steps of the control scale. */\nexport type HotkeysSize = 'xs' | 'sm'\n\ninterface HotkeysProps {\n /**\n * The combination, `+`-separated: `mod+k`, `ctrl+shift+p`, `alt+enter`, `esc`.\n * Case- and space-insensitive. `mod` is the CROSS-PLATFORM modifier: ⌘ on\n * macOS, Ctrl everywhere else; `meta` is the literal Command/Windows/Super key.\n * Aliases: cmd/command/win/super → meta, control → ctrl, option/opt → alt,\n * return → enter, escape → esc, del → delete, arrowup… → up…. An unknown token\n * is displayed as declared (`k` → K, `f5` → F5). The `+` key is `plus`.\n */\n keys: string\n /** How a key cap is drawn: tinted (`soft`), outlined (`outline`), or raised off the page (`elevated`). */\n variant?: HotkeysVariant\n /**\n * Draws the whole combination as a SINGLE key rather than as several: the decoration\n * moves from each cap to the shortcut as a whole, so the separator ends up inside\n * the key instead of between two of them. It is purely visual: the markup and the\n * announced name are identical either way.\n */\n attached?: boolean\n /** The size of the caps, `xs` by default: a shortcut is chrome beside other text. */\n size?: HotkeysSize\n /** Takes 4px off the height, leaving the padding and the text as they are. */\n compact?: boolean\n /**\n * Forces the keyboard's OS instead of detecting it, for a deterministic\n * rendering (stories, tests, a table showing all three) or a host that already\n * knows (Electron, Tauri, a server reading the User-Agent).\n */\n platform?: HotkeysPlatform\n /**\n * What is written between two caps. An empty string gives the macOS convention,\n * where the symbols simply follow one another: ⇧⌘K.\n */\n separator?: string\n /**\n * Actually listens for the combination and reports it. It is off by default: a\n * component whose job is to display a shortcut must not capture the page's keyboard\n * without being asked.\n */\n listen?: boolean\n /**\n * While listening, lets the browser go on doing whatever the combination normally\n * does. Left out, the browser is stopped, which is the entire point of taking over\n * something like ⌘K. Escape is never stopped: it has to stay the close request of dialogs.\n */\n allowDefault?: boolean\n /**\n * While listening, fires even when the reader is typing in a field. It is off by\n * default, so a shortcut cannot fire in the middle of a sentence.\n */\n allowInInput?: boolean\n /**\n * What screen readers announce, \"Keyboard shortcut: Ctrl + K\" by default, from the\n * design system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<HotkeysProps>(), {\n variant: 'soft',\n attached: false,\n size: 'xs',\n compact: false,\n platform: undefined,\n separator: '+',\n listen: false,\n allowDefault: false,\n allowInInput: false,\n label: undefined,\n})\n\nconst emit = defineEmits<{\n /** The combination was pressed. It is only ever emitted while `listen` is on. */\n trigger: [event: KeyboardEvent]\n}>()\n\nconst m = useMessages()\n\n// @ssr — the library's ONLY reading of the visitor's platform, and the reason it\n// happens after mounting.\n/* The operating system is read once the component is in the page and never during\n setup: a server has nothing to read it from, so rendering the detected value\n directly would make the two markups differ.\n\n TRAP — the value is held PER INSTANCE and not at module level. A shared one, having\n already resolved to macOS, would make a component hydrated later — a Nuxt island, a\n route loaded on demand — render ⌘ on its very first client pass where the server had\n written Ctrl. */\nconst detected = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n detected.value = detectPlatform()\n if (props.listen) attach()\n})\n\nconst platform = computed(() => props.platform ?? detected.value)\nconst tokens = computed(() => parseHotkeys(props.keys))\nconst resolved = computed(() => resolveKeys(tokens.value, platform.value))\n\n// @a11y\n/* The same table is read twice, in opposite directions. On SCREEN the symbol wins, ⌘\n being what is engraved on the key; in the ANNOUNCED NAME the word does, because that\n symbol is either passed over in silence or read out as \"place of interest sign\",\n depending on the screen reader. Inverting the preference is what lets one table\n serve both. */\nconst caps = computed(() =>\n resolved.value.map(\n (key) => key.glyph ?? (key.word ? m.value.hotkeys[key.word] : capLabel(key.token)),\n ),\n)\nconst spoken = computed(() =>\n resolved.value\n .map((key) => (key.word ? m.value.hotkeys[key.word] : (key.glyph ?? capLabel(key.token))))\n .join(' + '),\n)\nconst resolvedLabel = computed(() => props.label ?? m.value.hotkeys.label(spoken.value))\n\n// @core\n/* A plain variable and not a reactive one: nothing renders it, and making it reactive\n would cause renders for no reason — the same reasoning as in `useTimer`.\n\n TRAP — it is called `listening` and NOT `attached`. Every top-level binding of a\n `<script setup>` is exposed to the template, where it SHADOWS the prop of the same\n name: called `attached`, this flag would silently be what the template read instead\n of the prop, and the joined rendering would follow whether a listener happened to be\n installed. */\nlet listening = false\n\nfunction attach() {\n if (listening || typeof document === 'undefined') return\n document.addEventListener('keydown', onKeydown)\n listening = true\n}\n\nfunction detach() {\n if (!listening) return\n document.removeEventListener('keydown', onKeydown)\n listening = false\n}\n\n// @keyboard @core — the listening half. Deciding whether a key event IS the\n// combination is pure and lives in `platform.ts`.\nfunction onKeydown(event: KeyboardEvent) {\n /* A held-down combination repeats at the system's auto-repeat rate, which would\n reopen the consumer's palette a dozen times a second; only the first press\n counts. */\n if (!props.listen || event.repeat) return\n /* A key another handler has already dealt with (a menu's arrow, a field's Enter) is not\n a shortcut any more. And the Enter that confirms an input method's candidate is the\n end of a word being typed, not a command. */\n if (event.defaultPrevented || event.isComposing) return\n /* The document sees a key typed inside a shadow root as coming from the shadow HOST, a\n plain element: the field itself is the first entry of the composed path. */\n if (!props.allowInInput && isEditableTarget(event.composedPath()[0] ?? event.target)) return\n if (!matchesEvent(event, tokens.value, platform.value)) return\n /* TRAP — Escape is never cancelled. A cancelled Escape is not turned into a close request,\n so every open dialog and light-dismiss popover on the page would stop closing on it. */\n if (!props.allowDefault && !tokens.value.includes('esc')) event.preventDefault()\n emit('trigger', event)\n}\n\n/* Attaching the listener once and simply returning early inside it would be shorter to\n write, but a documentation page listing fifty shortcuts would then install fifty\n document listeners that do nothing. Following the prop is what keeps that at zero. */\nwatch(\n () => props.listen,\n (on) => (on ? attach() : detach()),\n)\nonBeforeUnmount(detach)\n/* A view kept alive by <KeepAlive> is never unmounted, only put aside: its shortcut must\n stop with it, or a hidden page would go on answering the keyboard. */\nonDeactivated(detach)\nonActivated(() => {\n if (props.listen) attach()\n})\n</script>\n\n<template>\n <kbd\n class=\"v-hotkeys v-control\"\n :data-variant=\"variant\"\n :data-attached=\"attached ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-platform=\"platform\"\n >\n <span class=\"v-hotkeys-keys\" aria-hidden=\"true\">\n <template v-for=\"(cap, index) in caps\" :key=\"index\">\n <span v-if=\"index > 0 && separator\" class=\"v-hotkeys-separator\">{{ separator }}</span>\n <kbd class=\"v-hotkeys-key\">{{ cap }}</kbd>\n </template>\n </span>\n <span class=\"v-visually-hidden\">{{ resolvedLabel }}</span>\n </kbd>\n</template>\n\n<style>\n@layer vectis.components {\n /* TRAP — the browser gives a `<kbd>` a monospaced family AND a smaller size, and\n that reduction COMPOUNDS when one `<kbd>` sits inside another, as it does here:\n the caps would come out at roughly 69% of the surrounding text. Both are reset\n here and again on the caps themselves, and deleting either line silently\n miniaturizes the whole component.\n\n The vertical alignment is the second correction: a box laid out this way sits on\n the baseline of its FIRST item, which drops the caps below the line of prose\n around them. */\n .v-hotkeys {\n display: inline-flex;\n align-items: center;\n /* Centres the content in the cases where the minimum width wins over it: a\n single-character combination drawn as one key. */\n justify-content: center;\n vertical-align: middle;\n font-family: var(--vectis-text-family);\n font-size: var(--control-font-size);\n --hotkeys-pad: var(--control-padding-inline);\n }\n\n /* The size comes from the shared class set on the ROOT: its variables inherit down,\n and it is the CAPS that read the height from them. The scale itself holds five\n steps; the component's type restricts it to the two smallest, a key cap being\n smaller than a control. */\n .v-hotkeys-keys {\n display: inline-flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-hotkeys-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: inherit;\n font-weight: var(--vectis-text-control-weight);\n line-height: var(--vectis-text-control-leading);\n }\n\n /* The three variants are tinted (`soft`, the word VButton and VChip use for a tinted\n surface, where `flat` names the absence of decoration on VTabs, VAccordion and\n VDataTable), outlined and raised, and EVERY paint here derives\n from `currentcolor` rather than from a surface token — the argument the separator\n at the bottom of this sheet already makes, carried through to the caps themselves.\n\n A shortcut is chrome, so it sits in whatever surrounds it: a paragraph, a field, a\n menu row, a tooltip painted on an inverse surface. Greys chosen against the page\n are wrong in half of those, and the failure is silent in the worst way — the caps\n kept the page's text colour while the ground under them was the opposite one, so\n on a light theme a shortcut inside a tooltip wrote dark keys on a dark panel.\n Deriving the whole cap from the inherited colour is what makes the component\n ground-independent, and it is the same reason there is no tone table at all.\n\n They set VARIABLES rather than declaring the look straight away, because that look\n has two possible carriers (see below). The names are qualified on purpose: these\n variables inherit, so a bare `--bg` would be captured by any ancestor in the host\n application that happened to define one. `currentcolor` inside them is resolved on\n whichever element ends up USING them, which is exactly what is wanted here. */\n .v-hotkeys[data-variant='soft'] {\n --hotkeys-bg: color-mix(in oklab, currentcolor, transparent 90%);\n --hotkeys-border: transparent;\n --hotkeys-shadow: none;\n }\n\n .v-hotkeys[data-variant='outline'] {\n --hotkeys-bg: transparent;\n --hotkeys-border: color-mix(in oklab, currentcolor, transparent 70%);\n --hotkeys-shadow: none;\n }\n\n /* Lifted rather than settled into its ground, so the tint is the lighter of the two\n and the shadow does the work. That shadow is the one paint left that is not\n derived: it is a cast rather than a colour, so on a dark ground it simply has\n nothing to fall on, and the tint is what still separates the cap there. */\n .v-hotkeys[data-variant='elevated'] {\n --hotkeys-bg: color-mix(in oklab, currentcolor, transparent 94%);\n --hotkeys-border: transparent;\n --hotkeys-shadow: var(--vectis-shadow-sm);\n }\n\n /* When one key holds the WHOLE combination, its ends take the same breathing room as\n the gaps inside it, which gives the key a single rhythm. The usual control padding\n is sized to wrap ONE short label; around three runs of text already spaced from\n one another, it reads as slack at the edges. */\n .v-hotkeys[data-attached] {\n --hotkeys-pad: var(--control-gap);\n }\n\n /* THE key recipe, written once for its two possible carriers: every cap by default,\n and the whole shortcut alone when it is drawn as a single key — which is exactly\n what puts the separator inside the key rather than between two of them. Written\n out twice, the two renderings would drift apart at the first token change. */\n .v-hotkeys[data-attached],\n .v-hotkeys:not([data-attached]) .v-hotkeys-key {\n height: var(--control-height);\n /* A minimum equal to the height makes a single-character key read as a square\n rather than as a sliver. */\n min-inline-size: var(--control-height);\n padding-inline: var(--hotkeys-pad);\n border: 1px solid var(--hotkeys-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--hotkeys-bg);\n box-shadow: var(--hotkeys-shadow);\n }\n\n /* Softened rather than full strength: the separator is punctuation between two\n caps, and at the same weight as them it competes with what it separates. */\n .v-hotkeys-separator {\n color: color-mix(in oklab, currentcolor, transparent 40%);\n }\n\n /* Forced colours flatten the tint and drop the shadow, which is all a soft or an\n elevated cap is made of. Every variant takes the edge of the outlined one, in the\n system text colour. At (0,2,0) and later in the sheet, this beats the variant rules. */\n @media (forced-colors: active) {\n .v-hotkeys[data-variant] {\n --hotkeys-border: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VHotkeys.js","names":[],"sources":["../../../src/components/VHotkeys/VHotkeys.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Render platform-specific shortcut glyphs. Detect the platform after mount for SSR; attach\n * keyboard listeners only when listen is enabled.\n */\n\nimport { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'\n\nimport {\n DEFAULT_PLATFORM,\n capLabel,\n detectPlatform,\n isEditableTarget,\n matchesEvent,\n parseHotkeys,\n resolveKeys,\n} from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useMessages } from '../../i18n/state'\n\n/** How a key cap is drawn. */\nexport type HotkeysVariant = 'soft' | 'outline' | 'elevated'\n/** The size of the caps, from the two smallest steps of the control scale. */\nexport type HotkeysSize = 'xs' | 'sm'\n\ninterface HotkeysProps {\n /** The combination, `+`-separated: `mod+k`, `ctrl+shift+p`, `alt+enter`, `esc`. */\n keys: string\n /** How a key cap is drawn: tinted (`soft`), outlined (`outline`), or raised off the page (`elevated`). */\n variant?: HotkeysVariant\n /**\n * Draws the whole combination as a SINGLE key rather than as several: the decoration moves\n * from each cap to the shortcut as a whole, so the separator ends up inside the key instead\n * of between two of them.\n */\n attached?: boolean\n /** The size of the caps, `xs` by default: a shortcut is chrome beside other text. */\n size?: HotkeysSize\n /** Takes 4px off the height, leaving the padding and the text as they are. */\n compact?: boolean\n /**\n * Forces the keyboard's OS instead of detecting it, for a deterministic\n * rendering (stories, tests, a table showing all three) or a host that already\n * knows (Electron, Tauri, a server reading the User-Agent).\n */\n platform?: HotkeysPlatform\n /**\n * What is written between two caps. An empty string gives the macOS convention,\n * where the symbols simply follow one another: ⇧⌘K.\n */\n separator?: string\n /**\n * Actually listens for the combination and reports it. It is off by default: a\n * component whose job is to display a shortcut must not capture the page's keyboard\n * without being asked.\n */\n listen?: boolean\n /**\n * While listening, lets the browser go on doing whatever the combination normally does. Left\n * out, the browser is stopped, which is the entire point of taking over something like ⌘K.\n */\n allowDefault?: boolean\n /**\n * While listening, fires even when the reader is typing in a field. It is off by\n * default, so a shortcut cannot fire in the middle of a sentence.\n */\n allowInInput?: boolean\n /**\n * What screen readers announce, \"Keyboard shortcut: Ctrl + K\" by default, from the\n * design system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<HotkeysProps>(), {\n variant: 'soft',\n attached: false,\n size: 'xs',\n compact: false,\n platform: undefined,\n separator: '+',\n listen: false,\n allowDefault: false,\n allowInInput: false,\n label: undefined,\n})\n\nconst emit = defineEmits<{\n /** The combination was pressed. It is only ever emitted while `listen` is on. */\n trigger: [event: KeyboardEvent]\n}>()\n\nconst m = useMessages()\n\n// @ssr\n/*\n * The value is held per instance and not at module level. A shared one, having already resolved\n * to macOS, would make a component hydrated later; a Nuxt island, a route loaded on demand;\n * render ⌘ on its very first client pass where the server had written Ctrl.\n */\nconst detected = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n detected.value = detectPlatform()\n if (props.listen) attach()\n})\n\nconst platform = computed(() => props.platform ?? detected.value)\nconst tokens = computed(() => parseHotkeys(props.keys))\nconst resolved = computed(() => resolveKeys(tokens.value, platform.value))\n\n// @a11y\n/*\n * On SCREEN the symbol wins, ⌘ being what is engraved on the key; in the ANNOUNCED NAME the\n * word does, because that symbol is either passed over in silence or read out as \"place of\n * interest sign\", depending on the screen reader. Inverting the preference is what lets one\n * table serve both.\n */\nconst caps = computed(() =>\n resolved.value.map(\n (key) => key.glyph ?? (key.word ? m.value.hotkeys[key.word] : capLabel(key.token)),\n ),\n)\nconst spoken = computed(() =>\n resolved.value\n .map((key) => (key.word ? m.value.hotkeys[key.word] : (key.glyph ?? capLabel(key.token))))\n .join(' + '),\n)\nconst resolvedLabel = computed(() => props.label ?? m.value.hotkeys.label(spoken.value))\n\n// @core\n/*\n * It is called `listening` and not `attached`. Every top-level binding of a `<script setup>` is\n * exposed to the template, where it SHADOWS the prop of the same name: called `attached`, this\n * flag would silently be what the template read instead of the prop, and the joined rendering\n * would follow whether a listener happened to be installed.\n */\nlet listening = false\n\nfunction attach() {\n if (listening || typeof document === 'undefined') return\n document.addEventListener('keydown', onKeydown)\n listening = true\n}\n\nfunction detach() {\n if (!listening) return\n document.removeEventListener('keydown', onKeydown)\n listening = false\n}\n\n// @keyboard @core\nfunction onKeydown(event: KeyboardEvent) {\n /* A held-down combination repeats at the system's auto-repeat rate, which would\n reopen the consumer's palette a dozen times a second; only the first press\n counts. */\n if (!props.listen || event.repeat) return\n /* A key another handler has already dealt with (a menu's arrow, a field's Enter) is not\n a shortcut any more. And the Enter that confirms an input method's candidate is the\n end of a word being typed, not a command. */\n if (event.defaultPrevented || event.isComposing) return\n /* The document sees a key typed inside a shadow root as coming from the shadow HOST, a\n plain element: the field itself is the first entry of the composed path. */\n if (!props.allowInInput && isEditableTarget(event.composedPath()[0] ?? event.target)) return\n if (!matchesEvent(event, tokens.value, platform.value)) return\n /*\n * Escape is never cancelled. A cancelled Escape is not turned into a close request, so every\n * open dialog and light-dismiss popover on the page would stop closing on it.\n */\n if (!props.allowDefault && !tokens.value.includes('esc')) event.preventDefault()\n emit('trigger', event)\n}\n\n/* Attaching the listener once and simply returning early inside it would be shorter to\n write, but a documentation page listing fifty shortcuts would then install fifty\n document listeners that do nothing. Following the prop is what keeps that at zero. */\nwatch(\n () => props.listen,\n (on) => (on ? attach() : detach()),\n)\nonBeforeUnmount(detach)\n/* A view kept alive by <KeepAlive> is never unmounted, only put aside: its shortcut must\n stop with it, or a hidden page would go on answering the keyboard. */\nonDeactivated(detach)\nonActivated(() => {\n if (props.listen) attach()\n})\n</script>\n\n<template>\n <kbd\n class=\"v-hotkeys v-control\"\n :data-variant=\"variant\"\n :data-attached=\"attached ? '' : undefined\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-platform=\"platform\"\n >\n <span class=\"v-hotkeys-keys\" aria-hidden=\"true\">\n <template v-for=\"(cap, index) in caps\" :key=\"index\">\n <span v-if=\"index > 0 && separator\" class=\"v-hotkeys-separator\">{{ separator }}</span>\n <kbd class=\"v-hotkeys-key\">{{ cap }}</kbd>\n </template>\n </span>\n <span class=\"v-visually-hidden\">{{ resolvedLabel }}</span>\n </kbd>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The browser gives a `<kbd>` a monospaced family and a smaller size, and that reduction\n * COMPOUNDS when one `<kbd>` sits inside another, as it does here: the caps would come out at\n * roughly 69% of the surrounding text. Both are reset here and again on the caps themselves,\n * and deleting either line silently miniaturizes the whole component.\n */\n .v-hotkeys {\n display: inline-flex;\n align-items: center;\n /* Centres the content in the cases where the minimum width wins over it: a\n single-character combination drawn as one key. */\n justify-content: center;\n vertical-align: middle;\n font-family: var(--vectis-text-family);\n font-size: var(--control-font-size);\n --hotkeys-pad: var(--control-padding-inline);\n }\n\n /* The size comes from the shared class set on the ROOT: its variables inherit down,\n and it is the CAPS that read the height from them. The scale itself holds five\n steps; the component's type restricts it to the two smallest, a key cap being\n smaller than a control. */\n .v-hotkeys-keys {\n display: inline-flex;\n align-items: center;\n gap: var(--control-gap);\n }\n\n .v-hotkeys-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: inherit;\n font-weight: var(--vectis-text-control-weight);\n line-height: var(--vectis-text-control-leading);\n }\n\n /*\n * Deriving the whole cap from the inherited colour is what makes the component\n * ground-independent, and it is the same reason there is no tone table at all. They set\n * VARIABLES rather than declaring the look straight away, because that look has two possible\n * carriers (see below).\n */\n .v-hotkeys[data-variant='soft'] {\n --hotkeys-bg: color-mix(in oklab, currentcolor, transparent 90%);\n --hotkeys-border: transparent;\n --hotkeys-shadow: none;\n }\n\n .v-hotkeys[data-variant='outline'] {\n --hotkeys-bg: transparent;\n --hotkeys-border: color-mix(in oklab, currentcolor, transparent 70%);\n --hotkeys-shadow: none;\n }\n\n /* Lifted rather than settled into its ground, so the tint is the lighter of the two\n and the shadow does the work. That shadow is the one paint left that is not\n derived: it is a cast rather than a colour, so on a dark ground it simply has\n nothing to fall on, and the tint is what still separates the cap there. */\n .v-hotkeys[data-variant='elevated'] {\n --hotkeys-bg: color-mix(in oklab, currentcolor, transparent 94%);\n --hotkeys-border: transparent;\n --hotkeys-shadow: var(--vectis-shadow-sm);\n }\n\n /*\n * When one key holds the whole combination, its ends take the same breathing room as the gaps\n * inside it, which gives the key a single rhythm. The usual control padding is sized to wrap\n * ONE short label; around three runs of text already spaced from one another, it reads as\n * slack at the edges.\n */\n .v-hotkeys[data-attached] {\n --hotkeys-pad: var(--control-gap);\n }\n\n /*\n * THE key recipe, written once for its two possible carriers: every cap by default, and the\n * whole shortcut alone when it is drawn as a single key; which is exactly what puts the\n * separator inside the key rather than between two of them. Written out twice, the two\n * renderings would drift apart at the first token change.\n */\n .v-hotkeys[data-attached],\n .v-hotkeys:not([data-attached]) .v-hotkeys-key {\n height: var(--control-height);\n /* A minimum equal to the height makes a single-character key read as a square\n rather than as a sliver. */\n min-inline-size: var(--control-height);\n padding-inline: var(--hotkeys-pad);\n border: 1px solid var(--hotkeys-border);\n border-radius: var(--vectis-radius-interactive);\n background: var(--hotkeys-bg);\n box-shadow: var(--hotkeys-shadow);\n }\n\n /* Softened rather than full strength: the separator is punctuation between two\n caps, and at the same weight as them it competes with what it separates. */\n .v-hotkeys-separator {\n color: color-mix(in oklab, currentcolor, transparent 40%);\n }\n\n /*\n * Forced colours flatten the tint and drop the shadow, which is all a soft or an elevated cap\n * is made of. Every variant takes the edge of the outlined one, in the system text colour.\n */\n @media (forced-colors: active) {\n .v-hotkeys[data-variant] {\n --hotkeys-border: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}