vectis-ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/VAccordion/VAccordion.css +1 -1
- package/dist/components/VAccordion/VAccordion.d.ts +0 -6
- package/dist/components/VAccordion/VAccordion.js.map +1 -1
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js +3 -8
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/context.js.map +1 -1
- package/dist/components/VAlert/VAlert.js.map +1 -1
- package/dist/components/VAlert/VAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.d.ts +0 -3
- package/dist/components/VAvatar/VAvatar.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js +21 -27
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.d.ts +0 -6
- package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +5 -19
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/context.d.ts +0 -1
- package/dist/components/VAvatar/context.js.map +1 -1
- package/dist/components/VBadge/VBadge.js.map +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButton.d.ts +0 -6
- package/dist/components/VButton/VButton.js.map +1 -1
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js +11 -17
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +0 -6
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js +0 -7
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/context.d.ts +0 -2
- package/dist/components/VButton/context.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js +5 -17
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js +19 -31
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/color.js.map +1 -1
- package/dist/components/VCalendar/edgeStep.js +1 -5
- package/dist/components/VCalendar/edgeStep.js.map +1 -1
- package/dist/components/VCalendar/gesture.js +4 -12
- package/dist/components/VCalendar/gesture.js.map +1 -1
- package/dist/components/VCalendar/keyboard.js.map +1 -1
- package/dist/components/VCalendar/layout.js +8 -12
- package/dist/components/VCalendar/layout.js.map +1 -1
- package/dist/components/VCalendar/timeGrid.js +5 -5
- package/dist/components/VCalendar/timeGrid.js.map +1 -1
- package/dist/components/VCard/VCard.js.map +1 -1
- package/dist/components/VCard/VCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/context.js.map +1 -1
- package/dist/components/VCheckbox/VCheckMark.js.map +1 -1
- package/dist/components/VCheckbox/VCheckMark.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VChip/VChip.d.ts +0 -3
- package/dist/components/VChip/VChip.js.map +1 -1
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VColorInput/VColorInput.d.ts +0 -3
- package/dist/components/VColorInput/VColorInput.js.map +1 -1
- package/dist/components/VColorInput/VColorInput.vue_vue_type_script_setup_true_lang.js +3 -9
- package/dist/components/VColorInput/VColorInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VColorPicker/VColorPicker.js.map +1 -1
- package/dist/components/VColorPicker/VColorPicker.vue_vue_type_script_setup_true_lang.js +1 -5
- package/dist/components/VColorPicker/VColorPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VColorPicker/color.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +0 -3
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +100 -119
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPalette.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPalette.vue_vue_type_script_setup_true_lang.js +0 -1
- package/dist/components/VCommandPalette/VCommandPalette.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPaletteItem.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPaletteItem.vue_vue_type_script_setup_true_lang.js +1 -5
- package/dist/components/VCommandPalette/VCommandPaletteItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VContextMenu/VContextMenu.d.ts +0 -3
- package/dist/components/VContextMenu/VContextMenu.js.map +1 -1
- package/dist/components/VContextMenu/VContextMenu.vue_vue_type_script_setup_true_lang.js +3 -11
- package/dist/components/VContextMenu/VContextMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.css +1 -1
- package/dist/components/VDataTable/VDataTable.d.ts +0 -2
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +69 -88
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.d.ts +0 -3
- package/dist/components/VDateInput/VDateInput.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js +5 -10
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/mask.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/keyboard.js.map +1 -1
- package/dist/components/VDatePicker/names.js.map +1 -1
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDrawer/VDrawer.js.map +1 -1
- package/dist/components/VDrawer/VDrawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VEmptyState/VEmptyState.js.map +1 -1
- package/dist/components/VEmptyState/VEmptyState.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VField/VField.js.map +1 -1
- package/dist/components/VField/VField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VField/VFieldAnnouncer.js.map +1 -1
- package/dist/components/VField/VFieldAnnouncer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VField/VFieldMessage.js.map +1 -1
- package/dist/components/VField/VFieldMessage.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFieldset/VFieldset.js.map +1 -1
- package/dist/components/VFieldset/VFieldset.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.d.ts +0 -5
- package/dist/components/VFileInput/VFileInput.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js +54 -65
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/truncate.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFilePicker/fileKind.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.d.ts +0 -3
- package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VHotkeys/platform.js +1 -1
- package/dist/components/VHotkeys/platform.js.map +1 -1
- package/dist/components/VHoverCard/VHoverCard.js.map +1 -1
- package/dist/components/VHoverCard/VHoverCard.vue_vue_type_script_setup_true_lang.js +2 -12
- package/dist/components/VHoverCard/VHoverCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/VIcon.js.map +1 -1
- package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/iconProps.js.map +1 -1
- package/dist/components/VIcon/icons/add.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_downward.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_downward_alt.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_drop_down.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_drop_up.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_left_alt.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_right_alt.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_upward.js.map +1 -1
- package/dist/components/VIcon/icons/arrow_upward_alt.js.map +1 -1
- package/dist/components/VIcon/icons/attach_file.js.map +1 -1
- package/dist/components/VIcon/icons/audio_file.js.map +1 -1
- package/dist/components/VIcon/icons/calendar_today.js.map +1 -1
- package/dist/components/VIcon/icons/check.js.map +1 -1
- package/dist/components/VIcon/icons/check_circle.js.map +1 -1
- package/dist/components/VIcon/icons/chevron_left.js.map +1 -1
- package/dist/components/VIcon/icons/chevron_right.js.map +1 -1
- package/dist/components/VIcon/icons/close.js.map +1 -1
- package/dist/components/VIcon/icons/cloud_upload.js.map +1 -1
- package/dist/components/VIcon/icons/code.js.map +1 -1
- package/dist/components/VIcon/icons/colorize.js.map +1 -1
- package/dist/components/VIcon/icons/description.js.map +1 -1
- package/dist/components/VIcon/icons/error.js.map +1 -1
- package/dist/components/VIcon/icons/expand_less.js.map +1 -1
- package/dist/components/VIcon/icons/expand_more.js.map +1 -1
- package/dist/components/VIcon/icons/first_page.js.map +1 -1
- package/dist/components/VIcon/icons/folder_zip.js.map +1 -1
- package/dist/components/VIcon/icons/image.js.map +1 -1
- package/dist/components/VIcon/icons/inbox.js.map +1 -1
- package/dist/components/VIcon/icons/index.js.map +1 -1
- package/dist/components/VIcon/icons/info.js.map +1 -1
- package/dist/components/VIcon/icons/last_page.js.map +1 -1
- package/dist/components/VIcon/icons/more_horiz.js.map +1 -1
- package/dist/components/VIcon/icons/names.js.map +1 -1
- package/dist/components/VIcon/icons/notifications.js.map +1 -1
- package/dist/components/VIcon/icons/open_in_new.js.map +1 -1
- package/dist/components/VIcon/icons/picture_as_pdf.js.map +1 -1
- package/dist/components/VIcon/icons/priority_high.js.map +1 -1
- package/dist/components/VIcon/icons/remove.js.map +1 -1
- package/dist/components/VIcon/icons/schedule.js.map +1 -1
- package/dist/components/VIcon/icons/search.js.map +1 -1
- package/dist/components/VIcon/icons/search_off.js.map +1 -1
- package/dist/components/VIcon/icons/star.js.map +1 -1
- package/dist/components/VIcon/icons/swap_vert.js.map +1 -1
- package/dist/components/VIcon/icons/table_chart.js.map +1 -1
- package/dist/components/VIcon/icons/video_file.js.map +1 -1
- package/dist/components/VIcon/icons/viewBox.js.map +1 -1
- package/dist/components/VIcon/icons/warning.js.map +1 -1
- package/dist/components/VIcon/resolver.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.d.ts +0 -3
- package/dist/components/VIconButton/VIconButton.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInput.css +1 -1
- package/dist/components/VInput/VInput.d.ts +0 -3
- package/dist/components/VInput/VInput.js.map +1 -1
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js +2 -8
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInputGroup.d.ts +0 -3
- package/dist/components/VInput/VInputGroup.js.map +1 -1
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +0 -7
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/context.d.ts +0 -2
- package/dist/components/VInput/context.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.d.ts +0 -3
- package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +2 -14
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VLink/VLink.js.map +1 -1
- package/dist/components/VLink/VLink.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxChevron.js.map +1 -1
- package/dist/components/VListbox/VListboxChevron.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxGroup.js.map +1 -1
- package/dist/components/VListbox/VListboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxOption.js.map +1 -1
- package/dist/components/VListbox/VListboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxPanel.d.ts +0 -3
- package/dist/components/VListbox/VListboxPanel.js.map +1 -1
- package/dist/components/VListbox/VListboxPanel.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VListbox/VListboxPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxSeparator.js.map +1 -1
- package/dist/components/VListbox/VListboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VListbox/VListboxValues.js.map +1 -1
- package/dist/components/VListbox/VListboxValues.vue_vue_type_script_setup_true_lang.js +4 -14
- package/dist/components/VListbox/VListboxValues.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenu.d.ts +0 -3
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +6 -12
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +0 -3
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +9 -16
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VMeter/VMeter.js.map +1 -1
- package/dist/components/VMeter/VMeter.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VNumberInput/VNumberInput.d.ts +0 -3
- package/dist/components/VNumberInput/VNumberInput.js.map +1 -1
- package/dist/components/VNumberInput/VNumberInput.vue_vue_type_script_setup_true_lang.js +85 -90
- package/dist/components/VNumberInput/VNumberInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VNumberInput/number.js.map +1 -1
- package/dist/components/VPagination/VPagination.d.ts +0 -3
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VPopover/VPopover.js.map +1 -1
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VRadio/VRadio.js.map +1 -1
- package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VRating/VRating.js.map +1 -1
- package/dist/components/VRating/VRating.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VResizable/VResizable.js.map +1 -1
- package/dist/components/VResizable/VResizable.vue_vue_type_script_setup_true_lang.js +8 -6
- package/dist/components/VResizable/VResizable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VResizable/VResizablePanel.js.map +1 -1
- package/dist/components/VResizable/VResizablePanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VResizable/context.js.map +1 -1
- package/dist/components/VResizable/layout.js.map +1 -1
- package/dist/components/VSelect/VSelect.d.ts +0 -3
- package/dist/components/VSelect/VSelect.js.map +1 -1
- package/dist/components/VSelect/VSelect.vue_vue_type_script_setup_true_lang.js +80 -90
- package/dist/components/VSelect/VSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.d.ts +0 -6
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js +2 -11
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +1 -5
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
- package/dist/components/VSideNavigation/context.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +0 -3
- package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSlider/VSlider.js.map +1 -1
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js +107 -112
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSplitButton/VSplitButton.d.ts +0 -3
- package/dist/components/VSplitButton/VSplitButton.js.map +1 -1
- package/dist/components/VSplitButton/VSplitButton.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VSplitButton/VSplitButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VStepper/VStepper.js.map +1 -1
- package/dist/components/VStepper/VStepper.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTab.js.map +1 -1
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js +0 -2
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabs.css +1 -1
- package/dist/components/VTabs/VTabs.d.ts +0 -3
- package/dist/components/VTabs/VTabs.js.map +1 -1
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js +2 -15
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/context.d.ts +0 -2
- package/dist/components/VTabs/context.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.d.ts +0 -6
- package/dist/components/VTextarea/VTextarea.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js +0 -6
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.d.ts +0 -3
- package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js +197 -204
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/search.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/limits.js.map +1 -1
- package/dist/components/VTimeline/VTimeline.js.map +1 -1
- package/dist/components/VTimeline/VTimeline.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeline/VTimelineItem.js.map +1 -1
- package/dist/components/VTimeline/VTimelineItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeline/context.js.map +1 -1
- package/dist/components/VTimeline/datetime.js.map +1 -1
- package/dist/components/VToast/VToast.js.map +1 -1
- package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/VToaster.js.map +1 -1
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +0 -3
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +1 -13
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/context.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTreeView/VTreeView.js.map +1 -1
- package/dist/components/VTreeView/VTreeView.vue_vue_type_script_setup_true_lang.js +1 -5
- package/dist/components/VTreeView/VTreeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTypography/VTypography.js.map +1 -1
- package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VVirtualList/VVirtualList.js.map +1 -1
- package/dist/components/VVirtualList/VVirtualList.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useChoice.js.map +1 -1
- package/dist/composables/useClearable.js.map +1 -1
- package/dist/composables/useControlShape.d.ts +1 -5
- package/dist/composables/useControlShape.js +0 -1
- package/dist/composables/useControlShape.js.map +1 -1
- package/dist/composables/useDebouncedSearch.js.map +1 -1
- package/dist/composables/useDetailsOpen.js.map +1 -1
- package/dist/composables/useEscapeDismiss.js.map +1 -1
- package/dist/composables/useFieldIds.js.map +1 -1
- package/dist/composables/useFieldPanel.js +3 -7
- package/dist/composables/useFieldPanel.js.map +1 -1
- package/dist/composables/useFileDrop.js.map +1 -1
- package/dist/composables/useFileField.js +2 -2
- package/dist/composables/useFileField.js.map +1 -1
- package/dist/composables/useFocusoutDismiss.js.map +1 -1
- package/dist/composables/useHotkeyListener.js.map +1 -1
- package/dist/composables/useIconClickHandlers.js.map +1 -1
- package/dist/composables/useInertLink.js.map +1 -1
- package/dist/composables/useInfiniteScroll.js +2 -2
- package/dist/composables/useInfiniteScroll.js.map +1 -1
- package/dist/composables/useListbox.d.ts +0 -1
- package/dist/composables/useListbox.js +4 -10
- package/dist/composables/useListbox.js.map +1 -1
- package/dist/composables/useLiveAnnouncer.js.map +1 -1
- package/dist/composables/useMaskedField.js +28 -48
- package/dist/composables/useMaskedField.js.map +1 -1
- package/dist/composables/useModalDialog.js +1 -1
- package/dist/composables/useModalDialog.js.map +1 -1
- package/dist/composables/usePopover.js +1 -1
- package/dist/composables/usePopover.js.map +1 -1
- package/dist/composables/useProgressValue.js.map +1 -1
- package/dist/composables/useRootAttrs.js.map +1 -1
- package/dist/composables/useSlotNodes.js.map +1 -1
- package/dist/composables/useTextLimit.js.map +1 -1
- package/dist/composables/useTimer.js.map +1 -1
- package/dist/composables/useTypeahead.js.map +1 -1
- package/dist/composables/useVirtualList.js.map +1 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/state.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.d.ts +3 -0
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +3 -0
- package/dist/tokens/semantic.js +6 -5
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens/themes/dark.js.map +1 -1
- package/dist/tokens/types.js.map +1 -1
- package/dist/tokens.json +12 -5
- package/dist/types.d.ts +0 -2
- package/dist/utils/array.js.map +1 -1
- package/dist/utils/arrowNav.js.map +1 -1
- package/dist/utils/attrs.js.map +1 -1
- package/dist/utils/chip.d.ts +4 -6
- package/dist/utils/chip.js +8 -5
- package/dist/utils/chip.js.map +1 -1
- package/dist/utils/clock.js +3 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/css.js +1 -1
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/direction.js.map +1 -1
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/file.js +13 -31
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/focus.js.map +1 -1
- package/dist/utils/hostWarns.js.map +1 -1
- package/dist/utils/ids.js.map +1 -1
- package/dist/utils/matcher.js.map +1 -1
- package/dist/utils/memo.js.map +1 -1
- package/dist/utils/number.js.map +1 -1
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/vnode.js.map +1 -1
- package/package.json +19 -19
|
@@ -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 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
|
+
{"version":3,"file":"VFilePicker.js","names":["_sfc_main","VFilePicker_vue_vue_type_script_setup_true_lang_default","VFilePicker_default"],"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":""}
|
|
@@ -103,7 +103,7 @@ var N = [
|
|
|
103
103
|
file: u
|
|
104
104
|
}, Le = (e) => x.typeIcons?.[e] ?? Ie[e];
|
|
105
105
|
function Re(e) {
|
|
106
|
-
|
|
106
|
+
(x.hideBrowse || !e.target.closest("button")) && V();
|
|
107
107
|
}
|
|
108
108
|
let { dragging: ze, onDragEnter: G, onDragOver: K, onDragLeave: q, onDrop: J } = re(() => B.value, Pe), Y = w(null);
|
|
109
109
|
function X() {
|
|
@@ -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 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
|
+
{"version":3,"file":"VFilePicker.vue_vue_type_script_setup_true_lang.js","names":["$slots","cloudUploadIcon","closeIcon","_useModel","imageIcon","pictureAsPdfIcon","audioFileIcon","videoFileIcon","folderZipIcon","tableChartIcon","codeIcon","descriptionIcon","_openBlock","_createElementBlock","_normalizeClass","_unref","_normalizeStyle","_createElementVNode","_mergeProps","_createBlock","_resolveDynamicComponent","_withCtx","_renderSlot","VSpinner","VIcon","_normalizeProps","_createVNode","VTypography","_createTextVNode","_toDisplayString","_createCommentVNode","_Fragment","VButton","_renderList","VIconButton","VIcon_default","VTypography_default","VSpinner_default","VButton_default","VIconButton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","VFilePicker_vue_vue_type_script_setup_true_lang_default","cloud_upload","close","\"modelValue\"","\"modelModifiers\"","__expose","__emit","props","__props","emit","model","useModel","files","useRootAttrs","m","useMessages","locale","useLocale","interactive","zoneAttrs","removeFile","useFileField","isDev","attrs","zoneEl","ref","listEl","showList","computed","KIND_ICONS","picture_as_pdf","audio_file","video_file","folder_zip","table_chart","description","iconForKind","kind","onZoneClick","openPicker","useFileDrop","acceptFiles","rootEl","focusTarget","zone","removeAt","index","nextTick","items","thumbUrls","reactive","isThumbable","file","syncThumbnails","wanted","undecodable","url","dropThumbnail","onMounted","watch","onBeforeUnmount","rows","fileKind","fileKey","formatBytes","options","openBlock","createElementBlock","normalizeClass","unref","rootClass","normalizeStyle","rootStyle","dragging","_cache","args","onDragEnter","onDragOver","onDragLeave","onDrop","createElementVNode","mergeProps","nativeInputAttrs","fileEl","createBlock","resolveDynamicComponent","withCtx","renderSlot","_ctx","normalizeProps","iconProps","createVNode","createTextVNode","toDisplayString","createCommentVNode","Fragment","renderList","row","removeLabel"],"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 * 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'
|
|
1
|
+
{"version":3,"file":"fileKind.js","names":["MIME_KINDS","EXTENSION_KINDS","fileKind","type","file","byMime","lookup","dot","table","key"],"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'\n | 'pdf'\n | 'audio'\n | 'video'\n | 'archive'\n | 'spreadsheet'\n | 'code'\n | '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":";AAiBA,IAAMA,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,GAEMC,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,SAAgBC,EAAS,GAAqC;CAC5D,IAAMC,IAAOC,EAAK,KAAK,YAAY;CAInC,IAAID,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAIA,EAAK,WAAW,QAAQ,GAAG,OAAO;CACtC,IAAIA,EAAK,WAAW,QAAQ,GAAG,OAAO;CAEtC,IAAME,IAASC,EAAON,GAAYG,CAAI;CACtC,IAAIE,GAAQ,OAAOA;CAInB,IAAME,IAAMH,EAAK,KAAK,YAAY,GAAG;CAErC,OAAOE,EAAOL,GADIM,IAAM,IAAIH,EAAK,KAAK,MAAMG,IAAM,CAAC,CAAC,CAAC,YAAY,IAAI,EAC7B,KAAK;AAC/C;AAOA,IAAMD,KAAU,GAAuC,MACrD,OAAO,OAAOE,GAAOC,CAAG,IAAID,EAAMC,KAAO,KAAA"}
|
|
@@ -16,8 +16,6 @@ interface HotkeysProps {
|
|
|
16
16
|
attached?: boolean;
|
|
17
17
|
/** The size of the caps, `xs` by default: a shortcut is chrome beside other text. */
|
|
18
18
|
size?: HotkeysSize;
|
|
19
|
-
/** Takes 4px off the height, leaving the padding and the text as they are. */
|
|
20
|
-
compact?: boolean;
|
|
21
19
|
/**
|
|
22
20
|
* Forces the keyboard's OS instead of detecting it, for a deterministic
|
|
23
21
|
* rendering (stories, tests, a table showing all three) or a host that already
|
|
@@ -59,7 +57,6 @@ declare const __VLS_export: import('vue').DefineComponent<HotkeysProps, {}, {},
|
|
|
59
57
|
label: string;
|
|
60
58
|
size: HotkeysSize;
|
|
61
59
|
variant: HotkeysVariant;
|
|
62
|
-
compact: boolean;
|
|
63
60
|
separator: string;
|
|
64
61
|
attached: boolean;
|
|
65
62
|
platform: HotkeysPlatform;
|
|
@@ -1 +1 @@
|
|
|
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, onMounted, ref } from 'vue'\n\nimport { DEFAULT_PLATFORM, capLabel, detectPlatform, parseHotkeys, resolveKeys } from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\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
|
|
1
|
+
{"version":3,"file":"VHotkeys.js","names":["_sfc_main","VHotkeys_vue_vue_type_script_setup_true_lang_default","VHotkeys_default"],"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, onMounted, ref } from 'vue'\n\nimport { DEFAULT_PLATFORM, capLabel, detectPlatform, parseHotkeys, resolveKeys } from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\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 /**\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 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})\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\nuseHotkeyListener({\n tokens: () => tokens.value,\n platform: () => platform.value,\n enabled: () => props.listen,\n allowDefault: () => props.allowDefault,\n allowInInput: () => props.allowInInput,\n onTrigger: (event) => emit('trigger', event),\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-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 }\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 * 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(--control-padding-inline);\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":""}
|
|
@@ -7,7 +7,6 @@ var v = [
|
|
|
7
7
|
"data-variant",
|
|
8
8
|
"data-attached",
|
|
9
9
|
"data-size",
|
|
10
|
-
"data-compact",
|
|
11
10
|
"data-platform"
|
|
12
11
|
], y = {
|
|
13
12
|
class: "v-hotkeys-keys",
|
|
@@ -25,10 +24,6 @@ var v = [
|
|
|
25
24
|
default: !1
|
|
26
25
|
},
|
|
27
26
|
size: { default: "xs" },
|
|
28
|
-
compact: {
|
|
29
|
-
type: Boolean,
|
|
30
|
-
default: !1
|
|
31
|
-
},
|
|
32
27
|
platform: { default: void 0 },
|
|
33
28
|
separator: { default: "+" },
|
|
34
29
|
listen: {
|
|
@@ -64,7 +59,6 @@ var v = [
|
|
|
64
59
|
"data-variant": f.variant,
|
|
65
60
|
"data-attached": f.attached ? "" : void 0,
|
|
66
61
|
"data-size": f.size,
|
|
67
|
-
"data-compact": f.compact ? "" : void 0,
|
|
68
62
|
"data-platform": O.value
|
|
69
63
|
}, [d("span", y, [(m(!0), u(s, null, g(j.value, (e, t) => (m(), u(s, { key: t }, [t > 0 && f.separator ? (m(), u("span", b, _(f.separator), 1)) : l("", !0), d("kbd", x, _(e), 1)], 64))), 128))]), d("span", S, _(N.value), 1)], 8, v));
|
|
70
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VHotkeys.vue_vue_type_script_setup_true_lang.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, onMounted, ref } from 'vue'\n\nimport { DEFAULT_PLATFORM, capLabel, detectPlatform, parseHotkeys, resolveKeys } from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\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})\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\nuseHotkeyListener({\n tokens: () => tokens.value,\n platform: () => platform.value,\n enabled: () => props.listen,\n allowDefault: () => props.allowDefault,\n allowInInput: () => props.allowInInput,\n onTrigger: (event) => emit('trigger', event),\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 }\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 * 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(--control-padding-inline);\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmEA,IAAM,IAAQ,GAaR,IAAO,GAKP,IAAI,EAAY,GAQhB,IAAW,EAAqB,CAAgB;EACtD,QAAgB;GACd,EAAS,QAAQ,EAAe;EAClC,CAAC;EAED,IAAM,IAAW,QAAe,EAAM,YAAY,EAAS,KAAK,GAC1D,IAAS,QAAe,EAAa,EAAM,IAAI,CAAC,GAChD,IAAW,QAAe,EAAY,EAAO,OAAO,EAAS,KAAK,CAAC,GASnE,IAAO,QACX,EAAS,MAAM,KACZ,MAAQ,EAAI,UAAU,EAAI,OAAO,EAAE,MAAM,QAAQ,EAAI,QAAQ,EAAS,EAAI,KAAK,EAClF,CACF,GACM,IAAS,QACb,EAAS,MACN,KAAK,MAAS,EAAI,OAAO,EAAE,MAAM,QAAQ,EAAI,QAAS,EAAI,SAAS,EAAS,EAAI,KAAK,CAAG,CAAA,CACxF,KAAK,KAAK,CACf,GACM,IAAgB,QAAe,EAAM,SAAS,EAAE,MAAM,QAAQ,MAAM,EAAO,KAAK,CAAC;SAEvF,EAAkB;GAChB,cAAc,EAAO;GACrB,gBAAgB,EAAS;GACzB,eAAe,EAAM;GACrB,oBAAoB,EAAM;GAC1B,oBAAoB,EAAM;GAC1B,YAAY,MAAU,EAAK,WAAW,CAAK;EAC7C,CAAC,mBAIC,EAeM,OAAA;GAdJ,OAAM;GACL,gBAAc,EAAA;GACd,iBAAe,EAAA,WAAQ,KAAQ,KAAA;GAC/B,aAAW,EAAA;GACX,gBAAc,EAAA,UAAO,KAAQ,KAAA;GAC7B,iBAAe,EAAA;MAEhB,EAKO,QALP,GAKO,EAAA,EAAA,EAAA,GAJL,EAGW,GAAA,MAAA,EAHsB,EAAA,QAAf,GAAK,wBAAsB,EAAK,GAAA,CACpC,IAAK,KAAQ,EAAA,aAAA,EAAA,GAAzB,EAAsF,QAAtF,GAAsF,EAAnB,EAAA,SAAS,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAC5E,EAA0C,OAA1C,GAA0C,EAAZ,CAAG,GAAA,CAAA,CAAA,GAAA,EAAA,cAGrC,EAA0D,QAA1D,GAA0D,EAAvB,EAAA,KAAa,GAAA,CAAA,CAAA,GAAA,GAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"VHotkeys.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_createElementVNode","_Fragment","_renderList","_toDisplayString","_createCommentVNode","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","VHotkeys_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","m","useMessages","detected","ref","DEFAULT_PLATFORM","detectPlatform","platform","computed","tokens","parseHotkeys","resolved","resolveKeys","caps","key","capLabel","spoken","resolvedLabel","useHotkeyListener","event","openBlock","createElementBlock","createElementVNode","Fragment","renderList","index","toDisplayString","createCommentVNode","cap"],"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, onMounted, ref } from 'vue'\n\nimport { DEFAULT_PLATFORM, capLabel, detectPlatform, parseHotkeys, resolveKeys } from './platform'\nimport type { HotkeysPlatform } from './platform'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\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 /**\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 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})\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\nuseHotkeyListener({\n tokens: () => tokens.value,\n platform: () => platform.value,\n enabled: () => props.listen,\n allowDefault: () => props.allowDefault,\n allowInInput: () => props.allowInInput,\n onTrigger: (event) => emit('trigger', event),\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-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 }\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 * 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(--control-padding-inline);\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEA,IAAMc,IAAQC,GAYRC,IAAOH,GAKPI,IAAIC,EAAY,GAQhBC,IAAWC,EAAqBC,CAAgB;EACtD,QAAgB;GACd,EAAS,QAAQC,EAAe;EAClC,CAAC;EAED,IAAMC,IAAWC,QAAeV,EAAM,YAAYK,EAAS,KAAK,GAC1DM,IAASD,QAAeE,EAAaZ,EAAM,IAAI,CAAC,GAChDa,IAAWH,QAAeI,EAAYH,EAAO,OAAOF,EAAS,KAAK,CAAC,GASnEM,IAAOL,QACXG,EAAS,MAAM,KACZ,MAAQG,EAAI,UAAUA,EAAI,OAAOb,EAAE,MAAM,QAAQa,EAAI,QAAQC,EAASD,EAAI,KAAK,EAClF,CACF,GACME,IAASR,QACbG,EAAS,MACN,KAAK,MAASG,EAAI,OAAOb,EAAE,MAAM,QAAQa,EAAI,QAASA,EAAI,SAASC,EAASD,EAAI,KAAK,CAAG,CAAA,CACxF,KAAK,KAAK,CACf,GACMG,IAAgBT,QAAeV,EAAM,SAASG,EAAE,MAAM,QAAQ,MAAMe,EAAO,KAAK,CAAC;SAEvFE,EAAkB;GAChB,cAAcT,EAAO;GACrB,gBAAgBF,EAAS;GACzB,eAAeT,EAAM;GACrB,oBAAoBA,EAAM;GAC1B,oBAAoBA,EAAM;GAC1B,YAAY,MAAUE,EAAK,WAAWmB,CAAK;EAC7C,CAAC,cAICnC,EAAA,GAAAC,EAcM,OAAA;GAbJ,OAAM;GACL,gBAAcc,EAAA;GACd,iBAAeA,EAAA,WAAQ,KAAQ,KAAA;GAC/B,aAAWA,EAAA;GACX,iBAAeQ,EAAA;EAEhBrB,GAAAA,CAAAA,EAKO,QALPM,GAKO,EAJLR,EAAA,EAAA,GAAAC,EAGWE,GAAA,MAAAC,EAHsByB,EAAA,QAAf,GAAK,OAAsB7B,EAAA,GAAAC,EAAAE,GAAA,EAAA,KAAAsC,EAAK,GAAA,CACpCA,IAAK,KAAQ1B,EAAA,aAAzBf,EAAA,GAAAC,EAAsF,QAAtFQ,GAAsFJ,EAAnBU,EAAA,SAAS,GAAA,CAAA,KAAA4B,EAAA,IAAA,EAAA,GAC5EzC,EAA0C,OAA1CQ,GAA0CL,EAAZuC,CAAG,GAAA,CAAA,CAAA,GAAA,EAAA,EAGrC1C,GAAAA,GAAAA,EAAAA,CAAAA,GAAAA,EAA0D,QAA1DS,GAA0DN,EAAvB4B,EAAA,KAAa,GAAA,CAAA,CAAA,GAAA,GAAA1B,CAAA"}
|
|
@@ -97,7 +97,7 @@ function p(e, t) {
|
|
|
97
97
|
return e.filter((e) => {
|
|
98
98
|
if (!u(e)) return !0;
|
|
99
99
|
let r = e === "mod" ? t === "mac" ? "meta" : "ctrl" : e;
|
|
100
|
-
return n.has(r)
|
|
100
|
+
return !n.has(r) && (n.add(r), !0);
|
|
101
101
|
}).map((e) => ({
|
|
102
102
|
token: e,
|
|
103
103
|
glyph: (t === "mac" ? i[e] : void 0) ?? a[e],
|