vectis-ui 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/VAccordion/VAccordion.js.map +1 -1
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
- 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/VAvatar/VAvatar.d.ts +2 -12
- package/dist/components/VAvatar/VAvatar.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
- package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/context.d.ts +1 -5
- package/dist/components/VAvatar/context.js.map +1 -1
- package/dist/components/VBadge/VBadge.d.ts +13 -27
- package/dist/components/VBadge/VBadge.js.map +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
- 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 +10 -17
- package/dist/components/VButton/VButton.js.map +1 -1
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +6 -15
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/context.d.ts +1 -9
- package/dist/components/VButton/context.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.d.ts +9 -24
- 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.d.ts +4 -13
- 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.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
- package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
- 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.d.ts +1 -7
- package/dist/components/VCalendar/color.js.map +1 -1
- package/dist/components/VCalendar/edgeStep.d.ts +5 -15
- package/dist/components/VCalendar/edgeStep.js.map +1 -1
- package/dist/components/VCalendar/gesture.d.ts +12 -36
- package/dist/components/VCalendar/gesture.js.map +1 -1
- package/dist/components/VCalendar/keyboard.d.ts +5 -42
- package/dist/components/VCalendar/keyboard.js.map +1 -1
- package/dist/components/VCalendar/layout.d.ts +38 -199
- package/dist/components/VCalendar/layout.js.map +1 -1
- package/dist/components/VCalendar/timeGrid.d.ts +3 -25
- package/dist/components/VCalendar/timeGrid.js.map +1 -1
- package/dist/components/VCalendar/types.d.ts +6 -23
- package/dist/components/VCarousel/VCarousel.d.ts +25 -96
- package/dist/components/VCarousel/VCarousel.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
- 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/VCheckbox.d.ts +1 -5
- 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 +2 -8
- package/dist/components/VChip/VChip.js.map +1 -1
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +21 -59
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
- package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.d.ts +11 -36
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.d.ts +12 -21
- package/dist/components/VDateInput/VDateInput.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/mask.d.ts +14 -59
- package/dist/components/VDateInput/mask.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/keyboard.d.ts +10 -42
- package/dist/components/VDatePicker/keyboard.js.map +1 -1
- package/dist/components/VDatePicker/names.d.ts +8 -22
- package/dist/components/VDatePicker/names.js.map +1 -1
- package/dist/components/VDialog/VDialog.d.ts +5 -10
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
- 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/VFileInput/VFileInput.d.ts +11 -37
- package/dist/components/VFileInput/VFileInput.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/truncate.d.ts +3 -23
- package/dist/components/VFileInput/truncate.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
- package/dist/components/VFilePicker/VFilePicker.js.map +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 +6 -15
- package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VHotkeys/platform.d.ts +10 -49
- package/dist/components/VHotkeys/platform.js.map +1 -1
- package/dist/components/VIcon/VIcon.d.ts +7 -19
- 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.d.ts +5 -19
- package/dist/components/VIcon/iconProps.js.map +1 -1
- package/dist/components/VIcon/resolver.d.ts +15 -43
- package/dist/components/VIcon/resolver.js.map +1 -1
- package/dist/components/VIcon/types.d.ts +7 -34
- package/dist/components/VIconButton/VIconButton.d.ts +2 -3
- package/dist/components/VIconButton/VIconButton.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInput.d.ts +14 -29
- package/dist/components/VInput/VInput.js.map +1 -1
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInputGroup.js.map +1 -1
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/context.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
- package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
- 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.d.ts +0 -5
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- 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.d.ts +3 -11
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.d.ts +9 -20
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VPopover/VPopover.d.ts +7 -18
- package/dist/components/VPopover/VPopover.js.map +1 -1
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
- 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.d.ts +4 -15
- 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.d.ts +1 -5
- 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/VSeparator/VSeparator.d.ts +2 -15
- 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 +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
- 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.d.ts +1 -5
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
- 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 +6 -19
- package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSlider/VSlider.d.ts +5 -13
- package/dist/components/VSlider/VSlider.js.map +1 -1
- 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.d.ts +4 -17
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.d.ts +3 -5
- 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/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.d.ts +1 -5
- package/dist/components/VTabs/VTab.js.map +1 -1
- 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.d.ts +5 -17
- package/dist/components/VTabs/VTabs.js.map +1 -1
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/context.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.d.ts +13 -20
- package/dist/components/VTextarea/VTextarea.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
- package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/search.d.ts +3 -34
- package/dist/components/VTimeInput/search.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/limits.d.ts +18 -45
- package/dist/components/VTimePicker/limits.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.map +1 -1
- package/dist/components/VToast/state.d.ts +2 -7
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +5 -9
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.d.ts +1 -9
- 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.d.ts +3 -16
- package/dist/components/VTooltip/VTooltip.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTypography/VTypography.d.ts +4 -8
- 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/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useClearable.d.ts +2 -8
- package/dist/composables/useClearable.js.map +1 -1
- package/dist/composables/useControlShape.js.map +1 -1
- package/dist/composables/useDetailsOpen.d.ts +2 -10
- package/dist/composables/useDetailsOpen.js.map +1 -1
- package/dist/composables/useFieldIds.js.map +1 -1
- package/dist/composables/useFieldPanel.d.ts +4 -17
- package/dist/composables/useFieldPanel.js.map +1 -1
- package/dist/composables/useFileDrop.js.map +1 -1
- package/dist/composables/useFileField.d.ts +1 -5
- package/dist/composables/useFileField.js.map +1 -1
- package/dist/composables/useFocusoutDismiss.js.map +1 -1
- package/dist/composables/useIconClickHandlers.d.ts +4 -19
- package/dist/composables/useIconClickHandlers.js.map +1 -1
- package/dist/composables/useInertLink.d.ts +1 -1
- package/dist/composables/useInertLink.js.map +1 -1
- package/dist/composables/useLiveAnnouncer.js.map +1 -1
- package/dist/composables/useMaskedField.d.ts +12 -27
- package/dist/composables/useMaskedField.js.map +1 -1
- package/dist/composables/usePopover.d.ts +3 -11
- package/dist/composables/usePopover.js.map +1 -1
- package/dist/composables/useProgressValue.js.map +1 -1
- package/dist/composables/useRootAttrs.d.ts +0 -9
- package/dist/composables/useRootAttrs.js +11 -4
- package/dist/composables/useRootAttrs.js.map +1 -1
- package/dist/composables/useSlotNodes.d.ts +3 -14
- package/dist/composables/useSlotNodes.js.map +1 -1
- package/dist/composables/useTextLimit.d.ts +4 -16
- package/dist/composables/useTextLimit.js.map +1 -1
- package/dist/composables/useTimer.d.ts +2 -15
- package/dist/composables/useTimer.js.map +1 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/state.d.ts +9 -38
- package/dist/i18n/state.js.map +1 -1
- package/dist/i18n/types.d.ts +19 -81
- package/dist/icons.d.ts +2 -12
- package/dist/styles.css +1 -1
- package/dist/tokens/css.d.ts +3 -16
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.d.ts +5 -0
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +6 -23
- package/dist/tokens/semantic.js +5 -0
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens/themes/dark.js.map +1 -1
- package/dist/tokens/types.d.ts +2 -12
- package/dist/tokens/types.js.map +1 -1
- package/dist/tokens.json +14 -0
- package/dist/types.d.ts +3 -21
- package/dist/utils/array.d.ts +2 -4
- package/dist/utils/array.js.map +1 -1
- package/dist/utils/arrowNav.d.ts +2 -26
- package/dist/utils/arrowNav.js.map +1 -1
- package/dist/utils/chip.d.ts +1 -8
- package/dist/utils/chip.js.map +1 -1
- package/dist/utils/clock.d.ts +16 -39
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/css.d.ts +5 -16
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/date.d.ts +17 -60
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/direction.d.ts +2 -9
- package/dist/utils/direction.js.map +1 -1
- package/dist/utils/env.d.ts +2 -15
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/file.d.ts +8 -46
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/focus.d.ts +1 -14
- package/dist/utils/focus.js +1 -6
- package/dist/utils/focus.js.map +1 -1
- package/dist/utils/hostWarns.d.ts +3 -8
- package/dist/utils/hostWarns.js.map +1 -1
- package/dist/utils/ids.d.ts +2 -7
- package/dist/utils/ids.js.map +1 -1
- package/dist/utils/matcher.d.ts +1 -8
- package/dist/utils/matcher.js.map +1 -1
- package/dist/utils/memo.d.ts +1 -11
- package/dist/utils/memo.js.map +1 -1
- package/dist/utils/number.d.ts +1 -7
- package/dist/utils/number.js.map +1 -1
- package/dist/utils/text.d.ts +4 -23
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/time.d.ts +4 -12
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/vnode.d.ts +0 -10
- package/dist/utils/vnode.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VTextarea.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTextarea/VTextarea.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A multi-line field, with the same label, hint, icons, counter, loading state and clear\n * cross as VInput, arranged around a real `<textarea>`.\n *\n * Two things differ. It grows with what is typed, which is pure CSS (`field-sizing`) — a\n * browser without it keeps a fixed height and its usual scrollbar. And the counter goes\n * UNDER the field rather than inside it, where several lines of text would run into it.\n *\n * As in VInput: the attrs split, validation stays the browser's, and the only behavioural JS\n * is the v-model and the clear cross, which hands focus back to the field it just emptied.\n */\nimport { computed, ref } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconName, iconProps } from '../VIcon/iconProps'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport type { IconSource } from '../VIcon/types'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useClearable } from '../../composables/useClearable'\nimport { useFieldIds } from '../../composables/useFieldIds'\nimport { useIconClickHandlers } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useTextLimit } from '../../composables/useTextLimit'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of one line: 32, 40 or 48 pixels. */\nexport type TextareaSize = 'sm' | 'md' | 'lg'\n\ninterface TextareaProps {\n /** The size of the field, which sets its padding, its type scale and its icons. */\n size?: TextareaSize\n /**\n * Takes 4px off the field by tightening its padding, leaving the number of lines,\n * the type and the icons alone.\n */\n compact?: boolean\n /**\n * How many lines of text the field shows: the native `rows` attribute, which is\n * what gives the field its height. Anything under 1 is raised to 1, and at 1 the\n * field is exactly as tall as a VInput of the same size.\n */\n rows?: number\n /**\n * Lets the field grow as the text is typed, instead of scrolling inside the height\n * `rows` gives it, which stays its starting height. It is pure CSS; where the\n * browser does not support it, the field simply behaves like an ordinary textarea.\n */\n autoGrow?: boolean\n /**\n * Marks the field as invalid whatever the browser thinks: the route for a rule\n * only the server can check.\n */\n invalid?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the text without allowing it to be changed. The field can still be focused\n * and copied from, and the clear button is hidden.\n */\n readonly?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, tied to the textarea for assistive technology so\n * that it is read out along with the label.\n */\n hint?: string\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case\n * it needs `iconStartLabel`. The `#start` slot is rendered after it, so the two can\n * be given together.\n */\n iconStart?: IconSource\n /**\n * The same at the end of the field. The `#end` slot replaces it, and the loading\n * spinner takes its place while it turns.\n */\n iconEnd?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /** What the end icon does, in words, once it is clickable. */\n iconEndLabel?: string\n /** Shows a spinner at the end of the field, in place of the end icon or slot. */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the field. It appears when there is something to\n * clear and the field can be edited.\n */\n clearable?: boolean\n /**\n * Decides whether the cross is shown, instead of letting the field work it out from its\n * own content (a read-only field included). It is the same escape hatch VInput offers,\n * for the same reason: a component built on top of this one may hold what there is to\n * clear somewhere other than the text.\n */\n clearVisible?: boolean\n /** What the clear button does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The maximum number of characters. By default this is the browser's own limit,\n * which simply refuses anything beyond it.\n */\n maxlength?: number\n /**\n * Turns that limit into a soft one: the reader may type past it, and the field goes\n * into error instead of silently refusing the keystrokes. It is reported through\n * the native validity, so a form cannot be submitted over the limit.\n */\n softLimit?: boolean\n /**\n * Shows how much has been typed, under the field: \"12/80\" against a limit, or just\n * \"12\" without one.\n */\n counter?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TextareaProps>(), {\n size: 'md',\n compact: false,\n rows: 5,\n autoGrow: false,\n invalid: false,\n disabled: false,\n readonly: false,\n label: undefined,\n hint: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconStartLabel: undefined,\n iconEndLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearVisible: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n softLimit: false,\n counter: false,\n})\n\nconst emit = defineEmits<{\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n /** The end icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-end': [event: MouseEvent]\n /** The clear button was pressed. The field has already been emptied. */\n clear: []\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its\n * place, as in VInput.\n */\n start?(): unknown\n /**\n * Content at the end of the field, which replaces `iconEnd`. It is hidden while the\n * field is loading, the spinner taking that place.\n */\n end?(): unknown\n /**\n * Controls of your own inside the field, placed before the field's own: the clear cross\n * and the end icon. It is the slot for something that acts on the VALUE rather than on\n * the field, and it is VInput's slot of the same name.\n */\n 'value-end'?(): unknown\n}>()\n\n/** The text in the field, empty to begin with. */\nconst model = defineModel<string>({ default: '' })\n\n// HTML wants `rows` to be a positive integer, and it drops anything else on the floor:\n// a 0, a negative or a fractional value would leave the browser falling back to its own\n// default of two rows, which is neither what was asked for nor this component's default.\n// Rounding and flooring here is what keeps the prop's contract out of the parser's hands.\nconst resolvedRows = computed(() => Math.max(1, Math.round(props.rows)))\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the\n// field; every other attribute goes to the textarea, where it does something.\nconst { attrs, rootClass, rootStyle, forwardedAttrs: restAttrs } = useRootAttrs()\n\n// The prop keeps priority; what it falls back to is the dictionary, so the default\n// wording follows the language the design system is set to.\nconst m = useMessages()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.common.clear)\n\nconst { fieldId, hintId, counterId, describedBy } = useFieldIds(\n attrs,\n () => !!props.hint,\n () => props.counter,\n)\n\n// Every length measurement goes through this projection: a value straight from an API or\n// a database is often `null`, and reading `.length` on it would throw at mount.\nconst modelText = computed(() => model.value ?? '')\n\nconst { hasIconStartHandler, hasIconEndHandler } = useIconClickHandlers({\n name: 'VTextarea',\n iconStartLabel: props.iconStartLabel,\n iconEndLabel: props.iconEndLabel,\n})\n\nconst controlEl = ref<HTMLTextAreaElement | null>(null)\n\nconst { showClear, onClear } = useClearable({\n clearable: () => props.clearable,\n clearVisible: () => props.clearVisible,\n disabled: () => props.disabled,\n readonly: () => props.readonly,\n text: () => modelText.value,\n controlEl,\n onCleared: () => {\n model.value = ''\n emit('clear')\n },\n})\n\nconst { counterText, over } = useTextLimit({\n el: controlEl,\n text: () => modelText.value,\n maxlength: () => props.maxlength,\n softLimit: () => props.softLimit,\n})\n\ndefineExpose({\n /** Moves the focus to the real textarea. */\n focus: (options?: FocusOptions) => controlEl.value?.focus(options),\n /** Selects everything in the field. */\n select: () => controlEl.value?.select(),\n /** The real `<textarea>`, for what neither of the two above covers. */\n el: controlEl,\n})\n</script>\n\n<template>\n <div\n class=\"v-textarea v-control\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <VTypography v-if=\"label\" as=\"label\" variant=\"label\" class=\"v-textarea-label\" :for=\"fieldId\">\n {{ label }}\n </VTypography>\n\n <div\n class=\"v-textarea-field\"\n :style=\"{ '--textarea-rows': resolvedRows }\"\n :data-auto-grow=\"autoGrow ? '' : undefined\"\n >\n <!-- The start icon is rendered BEFORE the slot, where the end icon is the slot's own\n fallback. The asymmetry is VInput's, mirrored here so the two fields answer the\n same way: what a composed field puts in that zone is OTHER content beside the\n icon, not another way of drawing it. -->\n <button\n v-if=\"iconStart && hasIconStartHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-start\"\n :aria-label=\"iconStartLabel ?? iconName(iconStart)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-start', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconStart)\" />\n </button>\n <VIcon v-else-if=\"iconStart\" v-bind=\"iconProps(iconStart)\" class=\"v-textarea-icon-start\" />\n <slot name=\"start\" />\n\n <textarea\n :id=\"fieldId\"\n ref=\"controlEl\"\n v-model=\"model\"\n :aria-invalid=\"invalid || undefined\"\n v-bind=\"restAttrs\"\n class=\"v-textarea-control\"\n :rows=\"resolvedRows\"\n :maxlength=\"softLimit ? undefined : maxlength\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-describedby=\"describedBy\"\n />\n\n <slot name=\"value-end\" />\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-clear\"\n :aria-label=\"resolvedClearLabel\"\n @click=\"onClear\"\n >\n <VIcon :name=\"closeIcon\" />\n </button>\n\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <slot v-else name=\"end\">\n <button\n v-if=\"iconEnd && hasIconEndHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-end\"\n :aria-label=\"iconEndLabel ?? iconName(iconEnd)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-end', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconEnd)\" />\n </button>\n <VIcon v-else-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" class=\"v-textarea-icon-end\" />\n </slot>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-textarea-meta v-field-meta\">\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-textarea-hint\">\n {{ hint }}\n </VTypography>\n <span\n v-if=\"counter\"\n :id=\"counterId\"\n class=\"v-textarea-counter v-field-counter\"\n :data-over=\"over ? '' : undefined\"\n >\n {{ counterText }}\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-textarea {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The label and the hint are rendered by VTypography, which carries their type. The\n .v-textarea-label and .v-textarea-hint classes remain as hooks: a consumer\n overrides through them, and the disabled state below reaches them that way. */\n\n /* This is the box that carries the border, the background, the focus ring and the\n resize handle — which is why it also hides its overflow, `resize` having no\n effect on a box whose overflow is visible. `--field-border-color` is the single\n source of truth for its colour, and the hover, error and disabled states do\n nothing but redefine it. */\n .v-textarea-field {\n --field-border-color: var(--vectis-color-border-strong);\n\n /*\n * Nothing here restates the size scale: the `--control-*` variables are inherited\n * from the v-control root (styles/control-size.css), the icon context included.\n *\n * The field carries NO height of its own: it is the `rows` attribute on the\n * textarea that sets one, and the field is simply as tall as the control it wraps.\n * That is what makes dragging the resize handle behave like a bare textarea — the\n * field shrinks and the control scrolls — instead of stopping at a CSS floor.\n */\n\n /* The leading is named ONCE and feeds two things that have to agree by\n construction: the field's own `line-height`, and the first line box below,\n against which everything sitting beside the text is offset. */\n --textarea-leading: var(--vectis-text-body-md-leading);\n\n /* The height of the FIRST line box, the row the icons and the buttons align on.\n\n TRAP — it is DERIVED and must never be written as `1lh`. The `lh` unit resolves\n against the element the property is USED on, and the children do not share the\n field's font: an icon carries `--vectis-icon-size` (20px against the field's\n 14px), so `1lh` measured a 30px line there and dropped every icon 4.5px below\n the text and the buttons it is supposed to line up with. The buttons happened to\n be right only because the reset gives them `font: inherit`. */\n --textarea-line: calc(var(--control-font-size) * var(--textarea-leading));\n\n display: flex;\n align-items: flex-start;\n gap: var(--control-gap);\n padding-inline: var(--control-padding-inline-field);\n /* Half of what is left of the control height once its two 1px borders and one line\n box are taken out — so at `rows=\"1\"` the field is exactly a VInput of the same\n size, and every further row adds precisely one line box.\n\n It reads --control-height and not --control-height-base, which is what carries\n the density here: with no height of its own left to shrink, the padding is the\n only lever `compact` has, and 2px off each edge is the -4px the whole scale\n promises. */\n padding-block: calc((var(--control-height) - 2px - var(--textarea-line)) / 2);\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--field-border-color);\n /* TRAP — the cap is what carries the VInput identity above into the OTHER row counts,\n and it has to stay derived from --control-height. A browser scales down any radius it\n cannot fit, so on a one-row field a pill override is already painted as half the\n control height; min() applies that same reduction to a field that is taller. Written\n bare, the identical override paints half of a FIVE-row box instead: 60px corners on a\n 120px field, an ellipse sitting beside buttons that kept 20px, and the same again on\n every drag of the resize handle. At the shipped 6px it resolves to 6px on every size,\n compact included, so nothing at the default value can see this line go — only the\n PillRadius play function can. */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-size: var(--control-font-size);\n /* Text running over several lines takes the body line height. The `control` type\n role, whose lines are set tight against one another, only makes sense for a\n single-line label. */\n line-height: var(--textarea-leading);\n resize: vertical;\n overflow: hidden;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-textarea-control {\n flex: 1;\n min-width: 0;\n align-self: stretch;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n outline: none; /* the focus ring is drawn by the field around it, not here */\n resize: none; /* the resize handle belongs to the field, so there is only one */\n }\n\n .v-textarea-control::placeholder {\n color: var(--vectis-color-text-subtle);\n }\n\n /* The icons and buttons align with the FIRST line of text rather than with the\n middle of a field whose height varies: half the difference between the line box and\n the element centres it there. On a single line that comes out identical to the\n centring VInput gets from its fixed height, which is the point.\n\n The measure is `--textarea-line` and never `1lh` — see the trap where it is\n declared: `lh` would be resolved against each child's own font, which differs from\n the field's.\n\n TRAP — the margin is SYMMETRIC, the VToast rule. It looks redundant under\n `align-items: flex-start`, where only the start side moves anything, but the end\n side is what keeps the item's OUTER box down to one line box. Left off, a clear\n cross (24px against a 21px line) would be the tallest thing in the flex line and\n the field would be 1.5px taller than the rows asked for — 2px at `lg` — which is\n exactly the gap that stops `rows=\"1\"` from being a VInput. */\n .v-textarea-field > .v-icon,\n .v-textarea-field > .v-spinner {\n margin-block: calc((var(--textarea-line) - var(--vectis-icon-size)) / 2);\n }\n\n .v-textarea-field > .v-textarea-action {\n margin-block: calc((var(--textarea-line) - var(--control-action-size)) / 2);\n }\n\n /* A decorative icon is drawn in a muted grey, so it stays quieter than the text\n being typed beside it. */\n .v-textarea-field > .v-icon {\n color: var(--vectis-color-text-muted);\n }\n\n .v-textarea-field > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n /* A read-only field sinks slightly into the page but keeps its text at full\n strength — the value is there to be read — and still shows the accent focus ring.\n The state is read from [data-readonly] and never from `:read-only`, which the\n browser also matches on a disabled field.\n\n TRAP — this block must stay FIRST in the sequence of states: read-only, then\n hover, focus, invalid and disabled. The read-only, focus, invalid and disabled\n selectors all weigh (0,3,0), `:has()` taking the specificity of what it contains, so\n nothing but the source order arbitrates between them. Hover alone weighs more, (0,6,0):\n its `:not()` keeps it off a focused, invalid or disabled field, and on a read-only\n one it darkens the border as on any other. Moved further down, this rule would repaint the\n error border and the accent focus ring grey, with no error anywhere. What belongs\n to a read-only field is its BASE colour alone. */\n .v-textarea[data-readonly] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-sunken);\n }\n\n .v-textarea-field:hover:not(:has(.v-textarea-control:focus)):not(\n :has(\n .v-textarea-control:disabled,\n .v-textarea-control:user-invalid,\n .v-textarea-control[aria-invalid='true']\n )\n ) {\n --field-border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /* The focused field appears to have a two-pixel border: it is really its own 1px\n border plus a 1px shadow of the same colour just outside it, which costs no\n layout and therefore makes nothing jump.\n\n The selector watches the CONTROL's focus and not `:focus-within`, so that when one\n of the field's own buttons takes keyboard focus, only that button's outline\n lights up: two indicators at once would be unreadable. And it is `:focus` rather\n than `:focus-visible`, because a text field shows its focus even when it was\n reached with the mouse.\n\n The transparent outline is the safety net for Windows forced colours, which drop\n box-shadows entirely; an outline survives and keeps the field marked. */\n .v-textarea-field:has(.v-textarea-control:focus) {\n --field-border-color: var(--vectis-color-accent);\n\n box-shadow: 0 0 0 1px var(--field-border-color);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n /* The invalid state, from the browser's own verdict first and from the `invalid`\n prop second. Only the colour variable is changed, which is why the border AND the\n focus ring both turn red without either being restated. */\n .v-textarea-field:has(.v-textarea-control:user-invalid),\n .v-textarea-field:has(.v-textarea-control[aria-invalid='true']) {\n --field-border-color: var(--vectis-color-danger);\n }\n\n /* A disabled field greys out through the colour tokens, the same ones VCheckbox and\n VRadio use, and never through opacity. It comes LAST in the sequence of states,\n which at equal specificity is what makes it win over all of them, the error\n included: a disabled field is not submitted, so it has nothing to report.\n\n The text here is `text-muted` and not the `text-subtle` used by the label and the\n hint below, because those sit on the page surface where this sits on\n `surface-muted` — against which subtle falls to a 4.4:1 contrast. The control\n itself is exempt from that rule, being natively disabled, but the icons the field\n CONTAINS are not, and they inherit this colour. */\n .v-textarea[data-disabled] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n cursor: not-allowed;\n resize: none;\n }\n\n .v-textarea[data-disabled] .v-textarea-label,\n .v-textarea[data-disabled] .v-textarea-hint,\n .v-textarea[data-disabled] .v-textarea-counter {\n color: var(--vectis-color-text-subtle);\n }\n\n .v-textarea[data-disabled] .v-textarea-action,\n .v-textarea[data-disabled] .v-textarea-field > .v-icon {\n color: inherit;\n cursor: not-allowed;\n }\n\n .v-textarea-control:disabled {\n cursor: not-allowed;\n }\n\n /* Growing with the content is pure CSS: `field-sizing` makes the textarea's height\n follow what is typed, and the field around it follows in turn. The resize handle\n goes, there being nothing left to resize by hand. A browser without support keeps\n the fixed height and its scrollbar, which is the intended fallback. */\n .v-textarea-field[data-auto-grow] {\n resize: none;\n }\n\n /* `field-sizing: content` sizes the control to what has been typed and ignores `rows`\n outright, so an auto-growing field would open one line tall whatever the prop said.\n The floor hands the STARTING height back to `rows` and leaves the growing to the\n browser. It is scoped to this branch and must not be lifted onto the control at\n large: a floor there would stop the control shrinking with the resize handle, and\n the field, which hides its overflow, would clip it instead of scrolling. */\n .v-textarea-field[data-auto-grow] .v-textarea-control {\n field-sizing: content;\n min-block-size: calc(var(--textarea-rows) * var(--textarea-line));\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-textarea-field {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VTextarea.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTextarea/VTextarea.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * Native textarea owns editing and validity. JavaScript bridges the model, text limits and\n * clear-button focus; CSS controls optional content-based height.\n */\nimport { computed, ref } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconName, iconProps } from '../VIcon/iconProps'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport type { IconSource } from '../VIcon/types'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { useClearable } from '../../composables/useClearable'\nimport { useFieldIds } from '../../composables/useFieldIds'\nimport { useIconClickHandlers } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useTextLimit } from '../../composables/useTextLimit'\nimport { useMessages } from '../../i18n/state'\n\n/** The height of one line: 32, 40 or 48 pixels. */\nexport type TextareaSize = 'sm' | 'md' | 'lg'\n\ninterface TextareaProps {\n /** The size of the field, which sets its padding, its type scale and its icons. */\n size?: TextareaSize\n /**\n * Takes 4px off the field by tightening its padding, leaving the number of lines,\n * the type and the icons alone.\n */\n compact?: boolean\n /**\n * How many lines of text the field shows: the native `rows` attribute, which gives the field\n * its height.\n */\n rows?: number\n /**\n * Lets the field grow as the text is typed, instead of scrolling inside the height `rows`\n * gives it, which stays its starting height.\n */\n autoGrow?: boolean\n /**\n * Marks the field as invalid whatever the browser thinks: the route for a rule\n * only the server can check.\n */\n invalid?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the text without allowing it to be changed. The field can still be focused\n * and copied from, and the clear button is hidden.\n */\n readonly?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, tied to the textarea for assistive technology so\n * that it is read out along with the label.\n */\n hint?: string\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a real\n * button as soon as a `@click:icon-start` listener is attached, in which case it needs\n * `iconStartLabel`.\n */\n iconStart?: IconSource\n /**\n * The same at the end of the field. The `#end` slot replaces it, and the loading\n * spinner takes its place while it turns.\n */\n iconEnd?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /** What the end icon does, in words, once it is clickable. */\n iconEndLabel?: string\n /** Shows a spinner at the end of the field, in place of the end icon or slot. */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the field. It appears when there is something to\n * clear and the field can be edited.\n */\n clearable?: boolean\n /**\n * Decides whether the cross is shown, instead of letting the field work it out from its own\n * content (a read-only field included).\n */\n clearVisible?: boolean\n /** What the clear button does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The maximum number of characters. By default this is the browser's own limit,\n * which simply refuses anything beyond it.\n */\n maxlength?: number\n /**\n * Turns that limit into a soft one: the reader may type past it, and the field goes into\n * error instead of silently refusing the keystrokes.\n */\n softLimit?: boolean\n /**\n * Shows how much has been typed, under the field: \"12/80\" against a limit, or just\n * \"12\" without one.\n */\n counter?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TextareaProps>(), {\n size: 'md',\n compact: false,\n rows: 5,\n autoGrow: false,\n invalid: false,\n disabled: false,\n readonly: false,\n label: undefined,\n hint: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconStartLabel: undefined,\n iconEndLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearVisible: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n softLimit: false,\n counter: false,\n})\n\nconst emit = defineEmits<{\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n /** The end icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-end': [event: MouseEvent]\n /** The clear button was pressed. The field has already been emptied. */\n clear: []\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its\n * place, as in VInput.\n */\n start?(): unknown\n /**\n * Content at the end of the field, which replaces `iconEnd`. It is hidden while the\n * field is loading, the spinner taking that place.\n */\n end?(): unknown\n /**\n * Controls of your own inside the field, placed before the field's own: the clear cross and\n * the end icon.\n */\n 'value-end'?(): unknown\n}>()\n\n/** The text in the field, empty to begin with. */\nconst model = defineModel<string>({ default: '' })\n\n// HTML wants `rows` to be a positive integer, and it drops anything else on the floor:\n// a 0, a negative or a fractional value would leave the browser falling back to its own\n// default of two rows, which is neither what was asked for nor this component's default.\n// Rounding and flooring here is what keeps the prop's contract out of the parser's hands.\nconst resolvedRows = computed(() => Math.max(1, Math.round(props.rows)))\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the\n// field; every other attribute goes to the textarea, where it does something.\nconst { attrs, rootClass, rootStyle, forwardedAttrs: restAttrs } = useRootAttrs()\n\n// The prop keeps priority; what it falls back to is the dictionary, so the default\n// wording follows the language the design system is set to.\nconst m = useMessages()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.common.clear)\n\nconst { fieldId, hintId, counterId, describedBy } = useFieldIds(\n attrs,\n () => !!props.hint,\n () => props.counter,\n)\n\n// Every length measurement goes through this projection: a value straight from an API or\n// a database is often `null`, and reading `.length` on it would throw at mount.\nconst modelText = computed(() => model.value ?? '')\n\nconst { hasIconStartHandler, hasIconEndHandler } = useIconClickHandlers({\n name: 'VTextarea',\n iconStartLabel: props.iconStartLabel,\n iconEndLabel: props.iconEndLabel,\n})\n\nconst controlEl = ref<HTMLTextAreaElement | null>(null)\n\nconst { showClear, onClear } = useClearable({\n clearable: () => props.clearable,\n clearVisible: () => props.clearVisible,\n disabled: () => props.disabled,\n readonly: () => props.readonly,\n text: () => modelText.value,\n controlEl,\n onCleared: () => {\n model.value = ''\n emit('clear')\n },\n})\n\nconst { counterText, over } = useTextLimit({\n el: controlEl,\n text: () => modelText.value,\n maxlength: () => props.maxlength,\n softLimit: () => props.softLimit,\n})\n\ndefineExpose({\n /** Moves the focus to the real textarea. */\n focus: (options?: FocusOptions) => controlEl.value?.focus(options),\n /** Selects everything in the field. */\n select: () => controlEl.value?.select(),\n /** The real `<textarea>`, for what neither of the two above covers. */\n el: controlEl,\n})\n</script>\n\n<template>\n <div\n class=\"v-textarea v-control\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n >\n <VTypography v-if=\"label\" as=\"label\" variant=\"label\" class=\"v-textarea-label\" :for=\"fieldId\">\n {{ label }}\n </VTypography>\n\n <div\n class=\"v-textarea-field\"\n :style=\"{ '--textarea-rows': resolvedRows }\"\n :data-auto-grow=\"autoGrow ? '' : undefined\"\n >\n <!--\n The start icon is rendered before the slot, where the end icon is the slot's own\n fallback. The asymmetry is VInput's, mirrored here so the two fields answer the same\n way: what a composed field puts in that zone is OTHER content beside the icon, not\n another way of drawing it.\n -->\n <button\n v-if=\"iconStart && hasIconStartHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-start\"\n :aria-label=\"iconStartLabel ?? iconName(iconStart)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-start', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconStart)\" />\n </button>\n <VIcon v-else-if=\"iconStart\" v-bind=\"iconProps(iconStart)\" class=\"v-textarea-icon-start\" />\n <slot name=\"start\" />\n\n <textarea\n :id=\"fieldId\"\n ref=\"controlEl\"\n v-model=\"model\"\n :aria-invalid=\"invalid || undefined\"\n v-bind=\"restAttrs\"\n class=\"v-textarea-control\"\n :rows=\"resolvedRows\"\n :maxlength=\"softLimit ? undefined : maxlength\"\n :disabled=\"disabled\"\n :readonly=\"readonly || undefined\"\n :aria-describedby=\"describedBy\"\n />\n\n <slot name=\"value-end\" />\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-clear\"\n :aria-label=\"resolvedClearLabel\"\n @click=\"onClear\"\n >\n <VIcon :name=\"closeIcon\" />\n </button>\n\n <VSpinner v-if=\"loading\" :label=\"loadingText\" />\n <slot v-else name=\"end\">\n <button\n v-if=\"iconEnd && hasIconEndHandler\"\n type=\"button\"\n class=\"v-textarea-action v-field-action v-textarea-icon-end\"\n :aria-label=\"iconEndLabel ?? iconName(iconEnd)\"\n :disabled=\"disabled\"\n @click=\"emit('click:icon-end', $event)\"\n >\n <VIcon v-bind=\"iconProps(iconEnd)\" />\n </button>\n <VIcon v-else-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" class=\"v-textarea-icon-end\" />\n </slot>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-textarea-meta v-field-meta\">\n <VTypography v-if=\"hint\" :id=\"hintId\" variant=\"caption\" tone=\"muted\" class=\"v-textarea-hint\">\n {{ hint }}\n </VTypography>\n <span\n v-if=\"counter\"\n :id=\"counterId\"\n class=\"v-textarea-counter v-field-counter\"\n :data-over=\"over ? '' : undefined\"\n >\n {{ counterText }}\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-textarea {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The .v-textarea-label and .v-textarea-hint classes remain as hooks: a consumer overrides\n * through them, and the disabled state below reaches them that way.\n */\n\n /*\n * This is the box that carries the border, the background, the focus ring and the resize\n * handle; which is why it also hides its overflow, `resize` having no effect on a box whose\n * overflow is visible. `--field-border-color` is the single source of truth for its colour,\n * and the hover, error and disabled states do nothing but redefine it.\n */\n .v-textarea-field {\n --field-border-color: var(--vectis-color-border-strong);\n\n /*\n * Nothing here restates the size scale: the `--control-*` variables are inherited from the\n * v-control root (styles/control-size.css), the icon context included. The field carries no\n * height of its own: it is the `rows` attribute on the textarea that sets one, and the\n * field is simply as tall as the control it wraps.\n */\n\n /*\n * The leading is named once and feeds two things that have to agree by construction: the\n * field's own `line-height`, and the first line box below, against which everything sitting\n * beside the text is offset.\n */\n --textarea-leading: var(--vectis-text-body-md-leading);\n\n /*\n * It is DERIVED and must never be written as `1lh`. The buttons happened to be right only\n * because the reset gives them `font: inherit`.\n */\n --textarea-line: calc(var(--control-font-size) * var(--textarea-leading));\n\n display: flex;\n align-items: flex-start;\n gap: var(--control-gap);\n padding-inline: var(--control-padding-inline-field);\n /*\n * Derive padding from control height minus borders and one line. Read the compact-adjusted\n * height so rows=1 matches VInput and each additional row adds one line.\n */\n padding-block: calc((var(--control-height) - 2px - var(--textarea-line)) / 2);\n background: var(--vectis-color-surface);\n color: var(--vectis-color-text);\n border: 1px solid var(--field-border-color);\n /*\n * The cap is what carries the VInput identity above into the OTHER row counts, and it has\n * to stay derived from --control-height. A browser scales down any radius it cannot fit, so\n * on a one-row field a pill override is already painted as half the control height; min()\n * applies that same reduction to a field that is taller.\n */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-size: var(--control-font-size);\n /* Text running over several lines takes the body line height. The `control` type\n role, whose lines are set tight against one another, only makes sense for a\n single-line label. */\n line-height: var(--textarea-leading);\n resize: vertical;\n overflow: hidden;\n transition:\n border-color var(--vectis-duration-fast) var(--vectis-ease-default),\n background-color var(--vectis-duration-fast) var(--vectis-ease-default),\n box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-textarea-control {\n flex: 1;\n min-width: 0;\n align-self: stretch;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n outline: none; /* The focus ring is drawn by the field around it, not here */\n resize: none;\n }\n\n .v-textarea-control::placeholder {\n color: var(--vectis-color-text-subtle);\n }\n\n /*\n * The margin is SYMMETRIC, the VToast rule. It looks redundant under\n * `align-items: flex-start`, where only the start side moves anything, but the end side is\n * what keeps the item's OUTER box down to one line box.\n */\n .v-textarea-field > .v-icon,\n .v-textarea-field > .v-spinner {\n margin-block: calc((var(--textarea-line) - var(--vectis-icon-size)) / 2);\n }\n\n .v-textarea-field > .v-textarea-action {\n margin-block: calc((var(--textarea-line) - var(--control-action-size)) / 2);\n }\n\n /* A decorative icon is drawn in a muted grey, so it stays quieter than the text\n being typed beside it. */\n .v-textarea-field > .v-icon {\n color: var(--vectis-color-text-muted);\n }\n\n .v-textarea-field > .v-spinner {\n font-size: var(--vectis-icon-size);\n }\n\n /*\n * This block must stay first in the sequence of states: read-only, then hover, focus, invalid\n * and disabled. The read-only, focus, invalid and disabled selectors all weigh (0,3,0),\n * `:has()` taking the specificity of what it contains, so nothing but the source order\n * arbitrates between them.\n */\n .v-textarea[data-readonly] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-sunken);\n }\n\n .v-textarea-field:hover:not(:has(.v-textarea-control:focus)):not(\n :has(\n .v-textarea-control:disabled,\n .v-textarea-control:user-invalid,\n .v-textarea-control[aria-invalid='true']\n )\n ) {\n --field-border-color: color-mix(\n in oklab,\n var(--vectis-color-border-strong),\n var(--vectis-color-text) 15%\n );\n }\n\n /*\n * The selector watches the control's focus and not `:focus-within`, so that when one of the\n * field's own buttons takes keyboard focus, only that button's outline lights up: two\n * indicators at once would be unreadable. And it is `:focus` rather than `:focus-visible`,\n * because a text field shows its focus even when it was reached with the mouse.\n */\n .v-textarea-field:has(.v-textarea-control:focus) {\n --field-border-color: var(--vectis-color-accent);\n\n box-shadow: 0 0 0 1px var(--field-border-color);\n outline: var(--vectis-focus-ring-width) solid transparent;\n }\n\n /*\n * Only the colour variable is changed, which is why the border and the focus ring both turn\n * red without either being restated.\n */\n .v-textarea-field:has(.v-textarea-control:user-invalid),\n .v-textarea-field:has(.v-textarea-control[aria-invalid='true']) {\n --field-border-color: var(--vectis-color-danger);\n }\n\n /*\n * A disabled field greys out through the colour tokens, the same ones VCheckbox and VRadio\n * use, and never through opacity. It comes last in the sequence of states, which at equal\n * specificity is what makes it win over all of them, the error included: a disabled field is\n * not submitted, so it has nothing to report.\n */\n .v-textarea[data-disabled] .v-textarea-field {\n --field-border-color: var(--vectis-color-border);\n\n background: var(--vectis-color-surface-muted);\n color: var(--vectis-color-text-muted);\n cursor: not-allowed;\n resize: none;\n }\n\n .v-textarea[data-disabled] .v-textarea-label,\n .v-textarea[data-disabled] .v-textarea-hint,\n .v-textarea[data-disabled] .v-textarea-counter {\n color: var(--vectis-color-text-subtle);\n }\n\n .v-textarea[data-disabled] .v-textarea-action,\n .v-textarea[data-disabled] .v-textarea-field > .v-icon {\n color: inherit;\n cursor: not-allowed;\n }\n\n .v-textarea-control:disabled {\n cursor: not-allowed;\n }\n\n /*\n * A browser without support keeps the fixed height and its scrollbar, which is the intended\n * fallback.\n */\n .v-textarea-field[data-auto-grow] {\n resize: none;\n }\n\n /*\n * It is scoped to this branch and must not be lifted onto the control at large: a floor there\n * would stop the control shrinking with the resize handle, and the field, which hides its\n * overflow, would clip it instead of scrolling.\n */\n .v-textarea-field[data-auto-grow] .v-textarea-control {\n field-sizing: content;\n min-block-size: calc(var(--textarea-rows) * var(--textarea-line));\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-textarea-field {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -32,11 +32,8 @@ interface TimeInputProps {
|
|
|
32
32
|
*/
|
|
33
33
|
mode?: TimeInputMode;
|
|
34
34
|
/**
|
|
35
|
-
* Offers the picker beside a field one can type into: an icon at the end of the field,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* It means nothing in `picker` mode, where the clock is already the only way to choose,
|
|
39
|
-
* nor in `list` mode, which has a panel of its own. VDateInput reads it the same way.
|
|
35
|
+
* Offers the picker beside a field one can type into: an icon at the end of the field, and a
|
|
36
|
+
* panel it opens.
|
|
40
37
|
*/
|
|
41
38
|
showPicker?: boolean;
|
|
42
39
|
/**
|
|
@@ -79,10 +76,8 @@ interface TimeInputProps {
|
|
|
79
76
|
/** Makes the field unusable, greyed out through the colour tokens. */
|
|
80
77
|
disabled?: boolean;
|
|
81
78
|
/**
|
|
82
|
-
* Shows the time without letting it be changed: nothing can be typed, there is no
|
|
83
|
-
*
|
|
84
|
-
* is what separates it from `disabled`, and it is a different question from `mode`,
|
|
85
|
-
* which says how a field that CAN be changed is filled in.
|
|
79
|
+
* Shows the time without letting it be changed: nothing can be typed, there is no clock and
|
|
80
|
+
* no clear cross.
|
|
86
81
|
*/
|
|
87
82
|
readonly?: boolean;
|
|
88
83
|
/** Marks the field as invalid, for a rule of your own. */
|
|
@@ -132,21 +127,13 @@ type __VLS_Slots = {
|
|
|
132
127
|
*/
|
|
133
128
|
start?(): unknown;
|
|
134
129
|
/**
|
|
135
|
-
* Controls of your own inside the field, after the AM/PM button when there is one and
|
|
136
|
-
*
|
|
137
|
-
* Those two are this component's own affordance, which is why there is no `#end` here.
|
|
130
|
+
* Controls of your own inside the field, after the AM/PM button when there is one and before
|
|
131
|
+
* the ones the field owns: the clear cross and the icon that opens the panel.
|
|
138
132
|
*/
|
|
139
133
|
'value-end'?(): unknown;
|
|
140
134
|
/**
|
|
141
|
-
* The strip at the foot of the clock, which REPLACES the Cancel and OK buttons rather
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* It receives both actions, and they are what make the slot usable: the clock writes a
|
|
145
|
-
* DRAFT that only `confirm` commits, so a footer of your own without it would leave the
|
|
146
|
-
* value unchangeable through the panel. `cancel` drops the draft and closes, and `close`
|
|
147
|
-
* is the same function under the name VDateInput's footer hands out.
|
|
148
|
-
*
|
|
149
|
-
* It is not rendered in `list` mode, which has no panel of this component's own.
|
|
135
|
+
* The strip at the foot of the clock, which REPLACES the Cancel and OK buttons rather than
|
|
136
|
+
* joining them.
|
|
150
137
|
*/
|
|
151
138
|
footer?(props: TimeInputFooterSlotProps): unknown;
|
|
152
139
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VTimeInput.js","names":[],"sources":["../../../src/components/VTimeInput/VTimeInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * A field for choosing a time. Three forms, and only two of them are built here.\n *\n * `input` types hours and minutes behind a mask, optionally with a clock beside it, and\n * `picker` makes that clock the only way in. What the field is FOR is a different question\n * from the `readonly` prop, which freezes whatever it holds by every route at once. Both use the VDateInput shell\n * (`useFieldPanel`): the panel is opened imperatively, so the focus can be moved into it,\n * and closes on `focusout` or Escape. The CLOCK works on a draft only OK writes — Cancel,\n * Escape and focusout all discard it — because dragging a hand passes over dozens of times\n * nobody meant.\n *\n * `list` offers the times at a fixed interval, which suits booking a slot far better than\n * pointing at a dial. It is a VCombobox outright, so a time is FOUND by typing as much as\n * scrolled to, and the whole listbox pattern comes from there rather than being written\n * again. Choosing a row commits at once: picking from a list is a single gesture.\n *\n * Whatever is displayed, the value is always a canonical 24-hour `'HH:mm'`.\n */\n\nimport { computed, inject, provide, ref, watchEffect } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VCombobox from '../VCombobox/VCombobox.vue'\nimport type { ComboboxOption } from '../VCombobox/VCombobox.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport { schedule as scheduleIcon } from '../VIcon/icons/schedule'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport VPopover from '../VPopover/VPopover.vue'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VTimePicker from '../VTimePicker/VTimePicker.vue'\nimport type { TimePickerFormat } from '../VTimePicker/VTimePicker.vue'\nimport {\n isTimeAllowed,\n limitsProblem,\n nearestAllowedTime,\n resolveLimits,\n} from '../VTimePicker/limits'\nimport type { TimePickerAllowed } from '../VTimePicker/limits'\nimport {\n formatTime,\n formatTimeDisplay,\n hourCycleFor,\n isValidTime,\n parseTime,\n} from '../../utils/time'\nimport {\n formatTimeMask,\n parseTimeMask,\n snapMinute,\n timeCaret,\n timeList,\n timeToMask,\n to12h,\n withMeridiem,\n} from '../../utils/clock'\nimport type { Meridiem } from '../../utils/time'\nimport type { TimeOption } from '../../utils/clock'\nimport { readWrittenTime, timeMatches } from './search'\nimport { isDev } from '../../utils/env'\nimport { hostWarnsKey } from '../../utils/hostWarns'\nimport { digitsOf } from '../../utils/text'\n\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useFieldPanel } from '../../composables/useFieldPanel'\nimport { canClear } from '../../composables/useClearable'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useMaskedField } from '../../composables/useMaskedField'\nimport { useMessages, useResolvedLocale } from '../../i18n/state'\n\n/**\n * Whether the field can be typed into, only filled from the clock, or picked from a list of\n * times.\n */\nexport type TimeInputMode = 'picker' | 'input' | 'list'\n\n/** What the `#footer` slot receives. */\nexport interface TimeInputFooterSlotProps {\n /** Writes the time on the clock into the value, then closes. */\n confirm: () => void\n /** Closes and drops the time on the clock, the value left as it was. */\n cancel: () => void\n /** The same as `cancel`, under the name VDateInput's footer uses. */\n close: () => void\n}\n\nconst MODES: TimeInputMode[] = ['picker', 'input', 'list']\n\n/** Where the panel opens relative to the field, whether it holds the clock or the list. */\nexport type TimeInputPlacement =\n 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end'\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type TimeInputSize = 'sm' | 'md' | 'lg'\n\ninterface TimeInputProps {\n /**\n * Whether times are shown on a 12- or a 24-hour clock. Left out, the reader's language\n * decides, which is almost always what one wants.\n */\n format?: TimePickerFormat\n /**\n * Which form the field takes: one that can be TYPED into, a `picker` one where the\n * clock is the only way in (so the picker is forced on there), or a searchable LIST of\n * times at a fixed interval, where a picker would make no sense.\n */\n mode?: TimeInputMode\n /**\n * Offers the picker beside a field one can type into: an icon at the end of the field,\n * and a panel it opens.\n *\n * It means nothing in `picker` mode, where the clock is already the only way to choose,\n * nor in `list` mode, which has a panel of its own. VDateInput reads it the same way.\n */\n showPicker?: boolean\n /**\n * The interval between two times that can be chosen. It applies to the picker, to the\n * arrow keys and to the rows of the list.\n */\n minuteStep?: number\n /**\n * The earliest time that can be chosen, inclusive, as a canonical 24-hour `'HH:mm'`.\n * The picker and the list both LEAVE OUT what it rules out, and a time typed past it\n * makes the field invalid.\n */\n min?: string\n /** The latest time that can be chosen, inclusive, written like `min`. */\n max?: string\n /**\n * Which hours can be chosen: the list of them, or a rule answering for one. The hour\n * handed to a rule is always the 24-hour one, whichever clock is on display.\n */\n allowedHours?: TimePickerAllowed\n /** Which minutes can be chosen: the list of them, or a rule answering for one. */\n allowedMinutes?: TimePickerAllowed\n /**\n * A BCP 47 locale, which decides the clock and how a time is written out. It TAKES\n * PRECEDENCE over the design system's global locale and falls back to it, which is why\n * it has no literal default: `undefined` has to stay recognizable for the global locale\n * to have its chance.\n */\n locale?: string\n // From here on: the field.\n /** The label above the field. */\n label?: string\n /** A line of help under the field. */\n hint?: string\n /** What the field says while empty. */\n placeholder?: string\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: TimeInputSize\n /** Takes 4px off the height. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the time without letting it be changed: nothing can be typed, there is no\n * clock and no clear cross. The field stays focusable and can be copied from, which\n * is what separates it from `disabled`, and it is a different question from `mode`,\n * which says how a field that CAN be changed is filled in.\n */\n readonly?: boolean\n /** Marks the field as invalid, for a rule of your own. */\n invalid?: boolean\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the clock button does, in words. It names the button `pickerIcon` renders,\n * and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the clock icon. It says that\n * something is being loaded and changes nothing else: the field can still be typed\n * into and the panel still opens.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** Offers a cross that empties the value, shown before the end icon. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The icon that opens the PICKER, at the end of the field. It has no effect on the list\n * form, whose chevron follows the combobox convention. The clear cross appears to its\n * left rather than in its place.\n */\n pickerIcon?: IconSource\n /** Where the panel opens relative to the field, whether it holds the clock or the list. */\n placement?: TimeInputPlacement\n}\n\nconst props = withDefaults(defineProps<TimeInputProps>(), {\n format: undefined,\n // Deliberately left undefined rather than defaulted to the typed mode: that is what\n // distinguishes \"the prop was not given\" from \"the prop was given this value\", and\n // therefore what allows warning ONLY the consumer who explicitly asked for something\n // that cannot work.\n mode: undefined,\n showPicker: false,\n minuteStep: 1,\n min: undefined,\n max: undefined,\n allowedHours: undefined,\n allowedMinutes: undefined,\n locale: undefined,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n pickerIcon: () => scheduleIcon,\n placement: 'bottom-start',\n})\n\n/**\n * The time, always as a 24-hour \"HH:mm\" string whatever clock is displayed. A consumer\n * therefore never has to know which clock the reader's language uses.\n */\nconst model = defineModel<string | null>({ default: null })\n\nconst emit = defineEmits<{\n /** The clear cross emptied the field. The value has already been reset. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its place,\n * as in VInput.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, after the AM/PM button when there is one and\n * before the ones the field owns: the clear cross and the icon that opens the panel.\n * Those two are this component's own affordance, which is why there is no `#end` here.\n */\n 'value-end'?(): unknown\n /**\n * The strip at the foot of the clock, which REPLACES the Cancel and OK buttons rather\n * than joining them.\n *\n * It receives both actions, and they are what make the slot usable: the clock writes a\n * DRAFT that only `confirm` commits, so a footer of your own without it would leave the\n * value unchangeable through the panel. `cancel` drops the draft and closes, and `close`\n * is the same function under the name VDateInput's footer hands out.\n *\n * It is not rendered in `list` mode, which has no panel of this component's own.\n */\n footer?(props: TimeInputFooterSlotProps): unknown\n}>()\n\n// `class` and `style` stay on the wrapper; everything else goes down to the text field,\n// which is what a consumer's label points at and what assistive technology deals with.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand — and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\n/** What reaches the field on screen — the masked VInput or the list's VCombobox. */\nconst fieldAttrs = computed(() => ({ ...forwardedAttrs.value, ...iconStartClick }))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelRef = ref<InstanceType<typeof VPopover> | null>(null)\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst listRef = ref<InstanceType<typeof VCombobox> | null>(null)\nconst pickerRef = ref<InstanceType<typeof VTimePicker> | null>(null)\n\n/** The real input inside the field, which the mask and the caret work on. */\nconst fieldEl = computed<HTMLInputElement | null>(() => inputRef.value?.el ?? null)\n\nconst resolvedMode = computed<TimeInputMode>(() => {\n // TRAP — the default is applied BEFORE the value is checked. A prop that was never\n // given is not an unknown value, and treating it as one would warn on a bare\n // `<VTimeInput />`.\n const mode = props.mode ?? 'input'\n return MODES.includes(mode) ? mode : 'input'\n})\nconst typing = computed(() => resolvedMode.value === 'input')\nconst isList = computed(() => resolvedMode.value === 'list')\n/**\n * Whether this component builds a panel of its own, which is the clock's: the list form\n * hands that job to VCombobox. It is forced on a `picker` field, where nothing else could\n * fill it, and offered beside a field one types into. A frozen field has none: there is\n * nothing left for a panel to do.\n */\nconst hasPanel = computed(\n () => !props.readonly && (resolvedMode.value === 'picker' || (typing.value && props.showPicker)),\n)\n\nconst resolvedLocale = useResolvedLocale(() => props.locale)\n\nconst resolvedFormat = computed<TimePickerFormat>(\n () => props.format ?? hourCycleFor(resolvedLocale.value),\n)\n\n/**\n * The restrictions, resolved once. VTimePicker resolves the same props again for itself:\n * they are ITS contract, and this component only consults them for the two things the\n * picker knows nothing about — which rows the list offers, and whether a typed time is\n * one the field should be showing as invalid.\n */\nconst limits = computed(() => resolveLimits(props))\n\n/** Whether the value is one the restrictions allow. Nothing at all is not a breach. */\nconst valueAllowed = computed(() => {\n const parts = modelParts.value\n return !parts || isTimeAllowed(parts.hour, parts.minute, limits.value)\n})\n\n// @devwarn\nif (isDev) {\n // The clock receives `minuteStep` and the restrictions as they stand, so what it would say\n // about them is said here, under the name the consumer wrote (utils/hostWarns).\n provide(hostWarnsKey, true)\n watchEffect(() => {\n if (!Number.isInteger(props.minuteStep) || props.minuteStep < 1 || 60 % props.minuteStep !== 0)\n console.warn(`[VTimeInput] minuteStep ${props.minuteStep} — a divisor of 60 is expected.`)\n if (props.mode !== undefined && !MODES.includes(props.mode))\n console.warn(\n `[VTimeInput] unknown mode \"${props.mode}\": use \"input\" (the default), \"picker\" or \"list\".`,\n )\n if (isList.value && props.showPicker)\n console.warn(\n '[VTimeInput] showPicker is ignored in \"list\" mode: the list of times is the only panel.',\n )\n // The list form is a VCombobox, which draws the end of the field itself: its own\n // chevron in place of the clock. The two props that describe THIS component's end icon\n // therefore reach nothing at all, and their absence is invisible on screen.\n // `loadingText` is not among them: the combobox announces the loading with it.\n const inert = ([] as string[]).concat(\n props.pickerIcon !== scheduleIcon ? 'pickerIcon' : [],\n props.pickerIconLabel ? 'pickerIconLabel' : [],\n )\n const isAre = inert.length > 1 ? 'are' : 'is'\n if (isList.value && inert.length > 0)\n console.warn(\n `[VTimeInput] ${inert.join(', ')} ${isAre} ignored in \"list\" mode: the list draws its own chevron.`,\n )\n // A field one types into without the picker has no end icon either, as in VDateInput.\n // A read-only field is left out: it is a state that comes and goes, not a configuration\n // to correct.\n if (typing.value && !props.showPicker && inert.length > 0)\n console.warn(\n `[VTimeInput] ${inert.join(', ')} ${isAre} ignored without showPicker: a field one types into has no clock icon unless it offers the clock.`,\n )\n const problem = limitsProblem(limits.value)\n if (problem) console.warn(`[VTimeInput] ${problem}`)\n if (isList.value && props.minuteStep < 5)\n console.warn(\n `[VTimeInput] minuteStep ${props.minuteStep} in \"list\" mode would render ${Math.ceil(1440 / props.minuteStep)} rows: a step of 15 or 30 minutes is expected.`,\n )\n })\n}\n\n/**\n * The time being built on the clock, in the same canonical form as the value itself.\n * Nothing here reaches the value until OK is pressed — which is the whole reason the\n * picker is bound to THIS rather than to the model.\n */\nconst pickerDraft = ref<string | null>(null)\n\nconst modelParts = computed(() => parseTime(model.value))\n\n/** The half of the day chosen while no time is set: VTimePicker's own `pendingMeridiem`. */\nconst pendingMeridiem = ref<Meridiem>('AM')\n\n/**\n * The half of the day the FIELD is showing. It is what the mask needs: typing \"9:30\" says\n * nothing about the morning or the afternoon, so the answer has to be readable while the\n * panel is shut.\n */\nconst currentMeridiem = computed<Meridiem>(() => {\n // With the picker open it is the DRAFT that is on screen, and this control has to read\n // the same thing: two controls showing opposite halves of the day until OK is pressed\n // is the one state that must not happen.\n const parts = open.value && hasPanel.value ? parseTime(pickerDraft.value) : modelParts.value\n return parts ? to12h(parts.hour).meridiem : pendingMeridiem.value\n})\n\n/**\n * The button beside the value flips the half of the day. There is no third state to reach,\n * so it names the half it is showing and swaps to the other one on a click.\n */\nfunction toggleMeridiem() {\n const next: Meridiem = currentMeridiem.value === 'PM' ? 'AM' : 'PM'\n pendingMeridiem.value = next\n // With no time set there is nothing to convert, so the choice is simply REMEMBERED\n // and applies to the first time typed or chosen.\n const moved = withMeridiem(model.value, next)\n if (moved) model.value = moved\n // With the picker open the draft has to follow as well, or OK would write back the\n // half of the day the reader has just changed.\n const pending = open.value && hasPanel.value ? withMeridiem(pickerDraft.value, next) : null\n if (pending) pickerDraft.value = pending\n}\n\n/**\n * Whether the button is rendered at all. Only the typed form needs it: the `picker` form\n * reaches the clock, which carries its own AM/PM, and the list already spells the half of\n * the day out on every row. It writes the value, so a frozen field renders none.\n */\nconst hasMeridiem = computed(\n () => typing.value && !props.readonly && resolvedFormat.value === '12h',\n)\n\nconst hasValue = computed(() => !!modelParts.value)\nconst displayText = computed(() =>\n model.value ? formatTimeDisplay(model.value, resolvedLocale.value, resolvedFormat.value) : '',\n)\n\n// @a11y\n// Where the focus goes when the panel opens. The picker publishes where its own focus\n// belongs, so nothing here has to know that the target is a slider — the same arrangement\n// VDateInput has with VDatePicker.\nfunction focusInPanel() {\n pickerRef.value?.focus()\n}\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). Nothing else is needed here: the\n// AM/PM button lives INSIDE the field, so a 12 hour field is one box like any other and\n// the row has nothing of its own to join.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n// The whole \"field plus panel\" shell, shared with VDateInput. What is specific to this\n// component is only what happens around it: preparing the draft as the panel opens, and\n// clearing the announcement as it closes.\nconst {\n open,\n panelId,\n closeAndFocus,\n focusField,\n onFieldFocus,\n toggleFromIcon,\n onControlClick,\n onFocusout,\n onKeydown,\n onPanelMousedown,\n} = useFieldPanel({\n rootEl,\n panelRef,\n field: inputRef,\n // With no panel there is nothing to open. This is the composable's SINGLE cut-off\n // point, and every way in passes through it — clicking the field, focusing it, the\n // down arrow, Enter, the icon.\n disabled: () => resolvedDisabled.value || !hasPanel.value,\n focusInPanel,\n // Beside a field one types into, the panel opens on focus WITHOUT taking it, so typing\n // carries on.\n openOnFocus: () => typing.value,\n onOpen: () => {\n const parts = modelParts.value\n if (parts) pickerDraft.value = formatTime(parts.hour, parts.minute)\n // A clock opened BESIDE a field one types into starts empty: it opened because the\n // field took the focus, not because a time was asked for, and a draft set to the\n // current time would hand its half of the day to the digits about to be typed, so\n // \"0930\" typed in the afternoon would become 21:30.\n else if (typing.value) pickerDraft.value = null\n else {\n // Opening on the current time when none is set. Reading the clock is safe here:\n // this runs from a handler, hence in a browser, never during a render — and it is\n // why the picker itself never reads it, so that it stays identical on both sides\n // of hydration. The instant is taken ONCE: two reads could straddle a minute.\n const now = new Date()\n const wanted = snapMinute(now.getMinutes(), props.minuteStep)\n // Pulled to the nearest time the restrictions allow, so that the panel never opens\n // on one the reader would be refused. Null is a set of restrictions that allows\n // nothing at all, which the warning above has already reported.\n pickerDraft.value =\n nearestAllowedTime(now.getHours(), wanted, limits.value) ??\n formatTime(now.getHours(), wanted)\n }\n pickerRef.value?.reset()\n },\n onClose: () => {\n pickerRef.value?.reset()\n },\n})\n\n/** OK: the ONE route by which the picker's draft becomes the value. */\nfunction confirm() {\n if (pickerDraft.value) model.value = pickerDraft.value\n closeAndFocus()\n}\n\n/** Cancel: the draft is simply dropped with the panel. */\nconst cancel = closeAndFocus\n\n// @a11y\n// TRAP — the popup wiring is spread OVER the forwarded attributes, never bound as four\n// attributes after them. A binding written after `v-bind` wins even when it is\n// `undefined`, so a field with no panel would erase the consumer's own `role` or\n// `aria-controls`; spread, the wiring carries no key at all when there is no panel, and\n// wins over the consumer's when there is one, which is what a combobox needs.\nconst inputAttrs = computed(() =>\n hasPanel.value\n ? {\n ...fieldAttrs.value,\n role: 'combobox',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': open.value,\n 'aria-controls': panelId,\n }\n : fieldAttrs.value,\n)\n\n/*\n * Emptying the value, called by the field as it emits its clear event. The focus is taken\n * through `focusField`, whose note says why that stops the panel reopening.\n */\nfunction clearValue() {\n model.value = null\n // The typed text is emptied EXPLICITLY: when the value was already empty, nothing\n // changes, and the guard that keeps the field and the value from chasing each other\n // would leave the text where it was.\n if (typing.value) writeField('')\n focusField()\n emit('clear')\n}\n\n// Escape and the focus leaving both close WITHOUT committing, which is the picker's Cancel\n// behaviour: closing never writes the value, and only OK does.\n\nconst m = useMessages()\n\n// @core\n/**\n * Saying no to a time that is not one the restrictions allow.\n *\n * The value is committed all the same — the field shows what was typed, and a consumer\n * bound to it reads the same thing — and the refusal is carried by the control's OWN\n * validity, which is what turns the field red through `:user-invalid` and what stops a\n * form being submitted with it. That is the design system's route for a rule of its own\n * making (`useTextLimit`), never an event of its own invention.\n *\n * It is written unconditionally rather than only while typing: the browser BARS a\n * read-only control from constraint validation, so the read-only form settles it without\n * a branch here. The list form never reaches this at all, having no control of this\n * component's own to write on.\n */\nwatchEffect(\n () => {\n const el = fieldEl.value\n if (!el) return\n const own = m.value.timeInput.unavailable\n if (!valueAllowed.value) el.setCustomValidity(own)\n // Only the component's own message is taken back: a consumer's verdict written over it,\n // a server saying the slot is taken, stays until the consumer clears it.\n else if (el.validationMessage === own) el.setCustomValidity('')\n },\n { flush: 'post' },\n)\n\n/* From here on: everything the typed field needs. */\n\n/*\n * The mask machinery — the text being typed, the bridge to the value, the reformatting\n * that preserves the caret, the commit and the silent revert — is shared with VDateInput\n * and lives in `useMaskedField`.\n *\n * The time vocabulary is the simpler of the two: four digits, and a separator that is the\n * SAME in every language. That is why the caret can be computed outright here, where a\n * date has to look for a separator it cannot predict; and why nothing distinguishes a\n * final commit from a live one, there being no equivalent of expanding a two-digit year.\n */\nconst {\n draft,\n fieldModel,\n writeField,\n commitOrRevert,\n onFieldInput,\n onKeydown: onMaskKeydown,\n onPaste,\n} = useMaskedField({\n fieldEl,\n typing: () => typing.value,\n displayText: () => displayText.value,\n readValue: () => model.value,\n writeValue: (time) => {\n model.value = time\n // With the clock open beside the field, it follows what is typed: otherwise OK would\n // write the clock's time back over the one the reader has just typed.\n if (open.value && hasPanel.value) pickerDraft.value = time\n },\n maxDigits: () => 4,\n format: formatTimeMask,\n caret: (_text, digitsBefore, inserting) => timeCaret(digitsBefore, inserting),\n parse: (text) => parseTimeMask(text, resolvedFormat.value, currentMeridiem.value),\n toMask: (time) => timeToMask(time, resolvedFormat.value),\n})\n\n// @keyboard\n// What the mask's keys mean for a time: typing anything that is not a digit,\n// the separator included so that \"9:30\" can be typed exactly as it reads, completes the\n// hour with a leading zero and moves on to the minutes; the down arrow is the one explicit\n// way from the field into the picker.\nfunction onFieldKeydown(event: KeyboardEvent) {\n onMaskKeydown(event, {\n onSeparator: (el) => {\n const digits = digitsOf(el.value)\n if (digits.length === 1) writeField(formatTimeMask(`0${digits}`), 3)\n },\n onArrowDown: () => {\n if (!open.value) return false\n focusInPanel()\n return true\n },\n onEnter: closeAndFocus,\n })\n}\n\n/**\n * Pasting. A time recognizable as a whole is adopted as it stands — and a pasted 24-hour\n * time is read as one whatever clock is on display: \"19:05\" means seven in the evening\n * even in a field showing a 12-hour clock. Anything else contributes its digits alone.\n */\nfunction onFieldPaste(event: ClipboardEvent) {\n onPaste(event, (pasted) =>\n isValidTime(pasted)\n ? pasted\n : readWrittenTime(pasted, resolvedFormat.value, currentMeridiem.value),\n )\n}\n\n/*\n * TRAP — the list form must not reach these two at all, and being panel-less is not\n * enough to keep it out. `useFieldPanel.onKeydown` cancels the default on Enter BEFORE\n * asking whether a panel can open, so an Enter travelling up from the VCombobox would\n * stop a surrounding form from being submitted with nothing to show for it.\n */\nfunction onRootKeydown(event: KeyboardEvent) {\n if (!isList.value) onKeydown(event)\n}\n\nfunction onRootFocusout(event: FocusEvent) {\n if (!isList.value) onFocusout(event)\n}\n\n/*\n * From here on: everything the list form needs.\n *\n * The list is a VCombobox, so the whole ARIA listbox pattern — the panel, the highlight,\n * the scroll onto the chosen row, the commit and the dismissal — comes from there and\n * NONE of it is written here. What is left is the three things a combobox cannot know\n * about times: which rows to offer, how a search matches one, and the fact that this\n * component's value is a nullable string where a combobox's is a plain one.\n */\n\n/**\n * The rows the step and the restrictions offer, which the value does not enter into: a\n * change of value must not format up to 1440 times again.\n */\nconst baseOptions = computed<TimeOption[]>(() => {\n if (!isList.value) return []\n return timeList(props.minuteStep, resolvedLocale.value, resolvedFormat.value).filter((row) => {\n // A list is READ before it is chosen from, so a time it may not take has no reason to be\n // in it, which is the picker's own rule for its numerals.\n const parts = parseTime(row.value)\n return !parts || isTimeAllowed(parts.hour, parts.minute, limits.value)\n })\n})\n\n/**\n * The rows on offer.\n *\n * A value that is not ON the step is added to them, in order: it is a real value, so the\n * field has to be able to write it out and the panel to open on it. Without it the field\n * would fall back to showing the raw canonical string — \"09:07\" in a field that spells\n * every other row \"9:07 AM\" — since a combobox labels a value through the option carrying\n * it.\n */\nconst options = computed<TimeOption[]>(() => {\n const rows = baseOptions.value\n const current = model.value\n if (!isValidTime(current) || rows.some((row) => row.value === current)) return rows\n const at = rows.findIndex((row) => row.value > current)\n const extra: TimeOption = {\n value: current,\n label: formatTimeDisplay(current, resolvedLocale.value, resolvedFormat.value),\n }\n return at < 0 ? [...rows, extra] : [...rows.slice(0, at), extra, ...rows.slice(at)]\n})\n\n/**\n * The search, which accepts the digit run as well as the words — \"930\" finds 9:30, the way\n * the typed form's mask reads what it is given. The rule itself is pure and lives in\n * `./search`, which says why it is not in `utils/time.ts`.\n */\nconst matchTime = (option: ComboboxOption, query: string) =>\n timeMatches(option.label, String(option.value), query)\n\n/**\n * The bridge to VCombobox, whose value is a plain string where this component's is\n * nullable: an emptied combobox says so with an empty string, which is not a time.\n */\nconst listModel = computed<string | string[]>({\n get: () => model.value ?? '',\n set: (value) => {\n model.value = typeof value === 'string' && value ? value : null\n },\n})\n\n/*\n * The clear cross and the icon at the end of the field, for the two forms this component\n * builds itself; the list form gets both from VCombobox.\n *\n * The cross is the field's own, which renders it BEFORE the end icon rather than in its\n * place — the convention every field in the design system follows.\n *\n * Whether the cross is shown has to be answered explicitly here: outside the typed mode\n * the field is read-only and would hide it, while the value comes from the panel. The\n * `readonly` PROP is the one case where that default answer was right: frozen, the field\n * offers no route to a new value, so it offers no route to none either.\n */\nconst clearVisible = computed(() =>\n canClear(props, resolvedDisabled.value, hasValue.value || (typing.value && !!draft.value)),\n)\nconst endIcon = computed<IconSource | undefined>(() =>\n hasPanel.value ? props.pickerIcon : undefined,\n)\n// @a11y @devwarn\n/*\n * TRAP — the LABEL is passed at all times, even when no icon is rendered at all.\n *\n * The helper detecting a click handler on an icon warns AT SETUP if one is attached\n * without a label, and it has no way of knowing whether an icon exists. Since the\n * listener here is attached permanently and that detection is static, making the label\n * conditional would produce a false warning every time a typed field without a picker is\n * mounted.\n */\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.timeInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.timeInput.clear)\n\n/*\n * The three of them go through whichever field is on screen: in the list form there is no\n * VInput of this component's own, the VCombobox owning the one the reader sees. Exposing\n * the same trio as the other fields is what keeps a consumer's `ref` working whatever the\n * mode, which is a runtime prop they may well be binding.\n */\nconst field = computed(() => (isList.value ? listRef.value : inputRef.value))\n\ndefineExpose({\n /** Moves the focus to the text field. */\n focus: (options?: FocusOptions) => field.value?.focus(options),\n /** Selects what the field is showing. */\n select: () => field.value?.select(),\n /** The real `<input>` behind the field, for what neither of the two above covers. */\n el: computed(() => field.value?.el ?? null),\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-time-input\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-open=\"open ? '' : undefined\"\n :data-mode=\"resolvedMode\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n @focusout=\"onRootFocusout\"\n @keydown=\"onRootKeydown\"\n >\n <!-- The list form is a VCombobox and nothing else: a list one picks a value from and\n narrows by typing IS the combobox pattern, down to the chevron, the clear cross\n and the dismissal. All this component adds is the three things a combobox cannot\n know about times — the rows, the search, and a value that may be nothing. -->\n <VCombobox\n v-if=\"isList\"\n ref=\"listRef\"\n v-model=\"listModel\"\n v-bind=\"fieldAttrs\"\n :options=\"options\"\n :filter=\"matchTime\"\n :label=\"label\"\n :hint=\"hint\"\n :placeholder=\"placeholder\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :readonly=\"readonly\"\n :invalid=\"invalid\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :clearable=\"clearable\"\n :clear-label=\"clearLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :placement=\"placement\"\n @clear=\"emit('clear')\"\n />\n\n <div v-else class=\"v-time-input-control\" @click=\"onControlClick\">\n <!-- The field is declared a combobox rather than left as the plain text box it\n implicitly is, because a text box may not carry the attribute saying whether\n something is expanded — the same reasoning as in VDateInput. -->\n <VInput\n ref=\"inputRef\"\n v-model=\"fieldModel\"\n :inputmode=\"typing ? 'numeric' : undefined\"\n :autocomplete=\"typing ? 'off' : undefined\"\n v-bind=\"inputAttrs\"\n :readonly=\"readonly\"\n :no-typing=\"!typing\"\n :label=\"label\"\n :hint=\"hint\"\n :placeholder=\"placeholder ?? (typing ? m.timeInput.maskPlaceholder : undefined)\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n @click:icon-end=\"toggleFromIcon\"\n @clear=\"clearValue\"\n @focus=\"onFieldFocus\"\n @input=\"onFieldInput\"\n @change=\"commitOrRevert\"\n @blur=\"commitOrRevert\"\n @keydown=\"onFieldKeydown\"\n @paste=\"onFieldPaste\"\n >\n <!-- The AM/PM button sits beside the value it qualifies, before the controls that\n clear the field and open the panel. It carries `.v-input-action`, which is\n what the field's own buttons wear AND what tells the shell that a click here\n is that button's business rather than a reason to open the panel.\n\n The rule after it separates what acts on the VALUE from what acts on the\n FIELD. It is dropped when nothing follows it, or it would trail off the end\n of an otherwise bare field. -->\n <template v-if=\"$slots.start\" #start><slot name=\"start\" /></template>\n <template v-if=\"hasMeridiem || $slots['value-end']\" #value-end>\n <button\n v-if=\"hasMeridiem\"\n type=\"button\"\n class=\"v-input-action v-field-action v-time-input-meridiem\"\n :disabled=\"resolvedDisabled\"\n :aria-label=\"\n m.timeInput.meridiemValue(\n currentMeridiem === 'PM' ? m.timePicker.pm : m.timePicker.am,\n )\n \"\n @click=\"toggleMeridiem\"\n >\n {{ currentMeridiem === 'PM' ? m.timePicker.pm : m.timePicker.am }}\n </button>\n <VSeparator\n v-if=\"hasMeridiem && (clearVisible || endIcon)\"\n orientation=\"vertical\"\n class=\"v-time-input-divider\"\n />\n <slot name=\"value-end\" />\n </template>\n </VInput>\n </div>\n\n <!-- With no panel rendered there is nothing to hold a reference to, and the open state\n — which is fed by the panel's own events — can no longer become true. The absence\n of a panel is therefore self-enforcing. -->\n <VPopover\n v-if=\"hasPanel\"\n :id=\"panelId\"\n ref=\"panelRef\"\n v-model:open=\"open\"\n mode=\"manual\"\n anchor=\"--time-input-anchor\"\n :placement=\"placement\"\n role=\"dialog\"\n class=\"v-time-input-panel\"\n :aria-label=\"label ?? m.timeInput.pickerLabel\"\n @mousedown=\"onPanelMousedown\"\n >\n <!-- The picker works on the DRAFT and not on the value, which is what makes OK the\n only way in: Cancel, Escape and the focus leaving simply drop it. The two\n actions are handed to its own footer slot, since they belong to this panel\n rather than to a clock shown on its own. -->\n <VTimePicker\n ref=\"pickerRef\"\n v-model=\"pickerDraft\"\n :format=\"resolvedFormat\"\n :locale=\"resolvedLocale\"\n :minute-step=\"minuteStep\"\n :min=\"min\"\n :max=\"max\"\n :allowed-hours=\"allowedHours\"\n :allowed-minutes=\"allowedMinutes\"\n @confirm=\"confirm\"\n >\n <!-- The slot REPLACES the two buttons rather than sitting beside them, so it is\n handed both actions: the clock writes a draft, and without `confirm` a footer\n of one's own could never commit it — the value would become unchangeable\n through the panel. The `remove` of VCombobox's `#chip` slot exists for the\n same reason. -->\n <template #footer>\n <slot name=\"footer\" :confirm=\"confirm\" :cancel=\"cancel\" :close=\"cancel\">\n <VButton variant=\"ghost\" tone=\"neutral\" @click=\"cancel\">{{ m.common.cancel }}</VButton>\n <VButton @click=\"confirm\">{{ m.common.confirm }}</VButton>\n </slot>\n </template>\n </VTimePicker>\n </VPopover>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-time-input {\n /* Confines the anchor name to this instance. It is declared on the root because that\n is the common ancestor of the field and the panel. */\n anchor-scope: --time-input-anchor;\n display: block;\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The pointer says \"this opens a panel\", which a read-only or disabled field no longer\n does: the VFileInput gating. */\n .v-time-input:not([data-disabled]):not([data-readonly]) .v-time-input-control {\n cursor: pointer;\n }\n\n /* The anchor is the FIELD's box and not this wrapper's, which also holds the label and\n the hint: anchored to the wrapper, the panel opens a hint's height below the field, and\n a label's height above it once there is no room below and `flip-block` turns it over.\n Against the field it covers whichever of the two it lands on, which is what a panel\n belonging to a control is supposed to do.\n\n TRAP — the selector has to start at the wrapper and never at the root. In `list` mode\n that wrapper is not rendered at all and the field on screen belongs to a VCombobox,\n which names it `--combobox-anchor` from its own sheet at the same (0,2,0): written\n `.v-time-input .v-input-field`, this rule would put a SECOND `anchor-name` on that very\n element, and which of the two survived would be decided by the order the consumer's\n bundler gave the two sheets. The list's panel would then lose its anchor and paint\n itself at the viewport origin, with nothing in the console to say why. */\n .v-time-input-control .v-input-field {\n anchor-name: --time-input-anchor;\n }\n\n /* A field one types into shows the text cursor rather than the pointer a clickable\n control shows. Its figures are also given equal widths: with proportional ones the\n text shifts as digits are typed, and the caret appears to jitter. */\n .v-time-input[data-mode='input']:not([data-disabled]):not([data-readonly]) .v-time-input-control {\n cursor: text;\n }\n\n .v-time-input[data-mode='input'] .v-input-control {\n font-variant-numeric: tabular-nums;\n }\n\n /* The AM/PM button wears the field's own action recipe — the box, the muted colour that\n lights up on hover, the focus ring and the disabled state all come from\n `.v-field-action` — and changes only what a WORD needs that a glyph does not: room to\n be read, and the field's type rather than the browser's default button font.\n\n The floor keeps the hit target at the size of its neighbours, so the two spellings do\n not resize the field between them.\n\n The padding is DERIVED and not picked from the spacing scale: `.v-field-action` carries\n a negative inline margin that cancels the slack around a glyph inside its box, so that\n the row's gap is measured from the INK. A word fills its own box, so it has to put that\n slack back or it would sit a couple of pixels off the rhythm its neighbours keep —\n which reads as an asymmetry around the rule, whose two sides are otherwise identical.\n\n The selector compounds the two classes because they are the same specificity in two\n different sheets, and which one the consumer's bundler puts last is not ours to\n decide. */\n .v-field-action.v-time-input-meridiem {\n inline-size: auto;\n min-inline-size: var(--control-action-size);\n padding-inline: calc((var(--control-action-size) - var(--vectis-icon-size)) / 2);\n font: inherit;\n }\n\n /* The rule between the AM/PM button and the field's own controls, which is what keeps a\n word from reading as a third icon in the row.\n\n A vertical VSeparator stretches to its flex line by contract, which here is the full\n height of the field: it would meet the border at both ends and read as a division of\n the box rather than a break in the row. It is brought back to the height of the\n buttons on either side of it.\n\n `align-self` and the height are declared against the ORIENTATION branch, which is the\n one that stretches, and beating it takes the extra class: the compound alone would tie\n with it. */\n .v-separator[data-orientation='vertical'].v-time-input-divider {\n align-self: center;\n block-size: var(--control-action-size);\n }\n\n /* The anchoring and the panel's surface both come from VPopover. The panel's OWN padding\n is cancelled: VTimePicker pads itself, as VDatePicker does, so both pickers keep the\n same room on a page and in a panel. The picker brings its own layout too — the gap\n between its parts and the centring of the face — so nothing of that is declared here.\n\n NO `display` here either: the column layout is `.v-panel`'s, and a panel declaring\n its own is what the doubled-class `.v-overlay` guard exists to overrule.\n\n The selector compounds two classes VPopover puts on the same element, because the\n padding is also declared by the shared panel class: at equal specificity the winner\n would be whichever sheet the consumer's bundler put last. The size attribute cannot\n serve that purpose here — the picker's panel carries none. */\n .v-popover-panel.v-time-input-panel {\n width: max-content;\n padding: 0;\n }\n\n /* The list form is a VCombobox and takes everything from it — this is the one\n declaration that does not belong to a combobox in general.\n\n A column of times read down the panel is a column of FIGURES, and proportional ones\n slide the colon from row to row. The selector is (0,3,0) against the panel's own\n (0,1,0), so it wins on specificity and not on the order the consumer's bundler\n happens to give the two sheets. */\n .v-time-input[data-mode='list'] .v-combobox-panel {\n font-variant-numeric: tabular-nums;\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VTimeInput.js","names":[],"sources":["../../../src/components/VTimeInput/VTimeInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Compose typing and clock modes from VInput and VTimePicker; list mode delegates to VCombobox.\n * JavaScript supplies masking and manual panel focus because text inputs cannot use\n * popovertarget.\n */\n\nimport { computed, inject, provide, ref, watchEffect } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VCombobox from '../VCombobox/VCombobox.vue'\nimport type { ComboboxOption } from '../VCombobox/VCombobox.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport { schedule as scheduleIcon } from '../VIcon/icons/schedule'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport VPopover from '../VPopover/VPopover.vue'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VTimePicker from '../VTimePicker/VTimePicker.vue'\nimport type { TimePickerFormat } from '../VTimePicker/VTimePicker.vue'\nimport {\n isTimeAllowed,\n limitsProblem,\n nearestAllowedTime,\n resolveLimits,\n} from '../VTimePicker/limits'\nimport type { TimePickerAllowed } from '../VTimePicker/limits'\nimport {\n formatTime,\n formatTimeDisplay,\n hourCycleFor,\n isValidTime,\n parseTime,\n} from '../../utils/time'\nimport {\n formatTimeMask,\n parseTimeMask,\n snapMinute,\n timeCaret,\n timeList,\n timeToMask,\n to12h,\n withMeridiem,\n} from '../../utils/clock'\nimport type { Meridiem } from '../../utils/time'\nimport type { TimeOption } from '../../utils/clock'\nimport { readWrittenTime, timeMatches } from './search'\nimport { isDev } from '../../utils/env'\nimport { hostWarnsKey } from '../../utils/hostWarns'\nimport { digitsOf } from '../../utils/text'\n\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useFieldPanel } from '../../composables/useFieldPanel'\nimport { canClear } from '../../composables/useClearable'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useMaskedField } from '../../composables/useMaskedField'\nimport { useMessages, useResolvedLocale } from '../../i18n/state'\n\n/**\n * Whether the field can be typed into, only filled from the clock, or picked from a list of\n * times.\n */\nexport type TimeInputMode = 'picker' | 'input' | 'list'\n\n/** What the `#footer` slot receives. */\nexport interface TimeInputFooterSlotProps {\n /** Writes the time on the clock into the value, then closes. */\n confirm: () => void\n /** Closes and drops the time on the clock, the value left as it was. */\n cancel: () => void\n /** The same as `cancel`, under the name VDateInput's footer uses. */\n close: () => void\n}\n\nconst MODES: TimeInputMode[] = ['picker', 'input', 'list']\n\n/** Where the panel opens relative to the field, whether it holds the clock or the list. */\nexport type TimeInputPlacement =\n 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end'\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type TimeInputSize = 'sm' | 'md' | 'lg'\n\ninterface TimeInputProps {\n /**\n * Whether times are shown on a 12- or a 24-hour clock. Left out, the reader's language\n * decides, which is almost always what one wants.\n */\n format?: TimePickerFormat\n /**\n * Which form the field takes: one that can be TYPED into, a `picker` one where the\n * clock is the only way in (so the picker is forced on there), or a searchable LIST of\n * times at a fixed interval, where a picker would make no sense.\n */\n mode?: TimeInputMode\n /**\n * Offers the picker beside a field one can type into: an icon at the end of the field, and a\n * panel it opens.\n */\n showPicker?: boolean\n /**\n * The interval between two times that can be chosen. It applies to the picker, to the\n * arrow keys and to the rows of the list.\n */\n minuteStep?: number\n /**\n * The earliest time that can be chosen, inclusive, as a canonical 24-hour `'HH:mm'`.\n * The picker and the list both LEAVE OUT what it rules out, and a time typed past it\n * makes the field invalid.\n */\n min?: string\n /** The latest time that can be chosen, inclusive, written like `min`. */\n max?: string\n /**\n * Which hours can be chosen: the list of them, or a rule answering for one. The hour\n * handed to a rule is always the 24-hour one, whichever clock is on display.\n */\n allowedHours?: TimePickerAllowed\n /** Which minutes can be chosen: the list of them, or a rule answering for one. */\n allowedMinutes?: TimePickerAllowed\n /**\n * A BCP 47 locale, which decides the clock and how a time is written out. It TAKES\n * PRECEDENCE over the design system's global locale and falls back to it, which is why\n * it has no literal default: `undefined` has to stay recognizable for the global locale\n * to have its chance.\n */\n locale?: string\n /** The label above the field. */\n label?: string\n /** A line of help under the field. */\n hint?: string\n /** What the field says while empty. */\n placeholder?: string\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: TimeInputSize\n /** Takes 4px off the height. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows the time without letting it be changed: nothing can be typed, there is no clock and\n * no clear cross.\n */\n readonly?: boolean\n /** Marks the field as invalid, for a rule of your own. */\n invalid?: boolean\n /**\n * An icon inside the field, at the start. It is decorative by default and becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the clock button does, in words. It names the button `pickerIcon` renders,\n * and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the clock icon. It says that\n * something is being loaded and changes nothing else: the field can still be typed\n * into and the panel still opens.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** Offers a cross that empties the value, shown before the end icon. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The icon that opens the PICKER, at the end of the field. It has no effect on the list\n * form, whose chevron follows the combobox convention. The clear cross appears to its\n * left rather than in its place.\n */\n pickerIcon?: IconSource\n /** Where the panel opens relative to the field, whether it holds the clock or the list. */\n placement?: TimeInputPlacement\n}\n\nconst props = withDefaults(defineProps<TimeInputProps>(), {\n format: undefined,\n // Deliberately left undefined rather than defaulted to the typed mode: that is what\n // distinguishes \"the prop was not given\" from \"the prop was given this value\", and therefore\n // what allows warning only the consumer who explicitly asked for something that cannot work.\n mode: undefined,\n showPicker: false,\n minuteStep: 1,\n min: undefined,\n max: undefined,\n allowedHours: undefined,\n allowedMinutes: undefined,\n locale: undefined,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n pickerIcon: () => scheduleIcon,\n placement: 'bottom-start',\n})\n\n/**\n * The time, always as a 24-hour \"HH:mm\" string whatever clock is displayed. A consumer\n * therefore never has to know which clock the reader's language uses.\n */\nconst model = defineModel<string | null>({ default: null })\n\nconst emit = defineEmits<{\n /** The clear cross emptied the field. The value has already been reset. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after `iconStart` rather than in its place,\n * as in VInput.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, after the AM/PM button when there is one and before\n * the ones the field owns: the clear cross and the icon that opens the panel.\n */\n 'value-end'?(): unknown\n /**\n * The strip at the foot of the clock, which REPLACES the Cancel and OK buttons rather than\n * joining them.\n */\n footer?(props: TimeInputFooterSlotProps): unknown\n}>()\n\n// `class` and `style` stay on the wrapper; everything else goes down to the text field, which a\n// consumer's label points at and what assistive technology deals with.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand; and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\n/** What reaches the field on screen; the masked VInput or the list's VCombobox. */\nconst fieldAttrs = computed(() => ({ ...forwardedAttrs.value, ...iconStartClick }))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelRef = ref<InstanceType<typeof VPopover> | null>(null)\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst listRef = ref<InstanceType<typeof VCombobox> | null>(null)\nconst pickerRef = ref<InstanceType<typeof VTimePicker> | null>(null)\n\n/** The real input inside the field, which the mask and the caret work on. */\nconst fieldEl = computed<HTMLInputElement | null>(() => inputRef.value?.el ?? null)\n\nconst resolvedMode = computed<TimeInputMode>(() => {\n // The default is applied before the value is checked. A prop that was never given is not an\n // unknown value, and treating it as one would warn on a bare `<VTimeInput />`.\n const mode = props.mode ?? 'input'\n return MODES.includes(mode) ? mode : 'input'\n})\nconst typing = computed(() => resolvedMode.value === 'input')\nconst isList = computed(() => resolvedMode.value === 'list')\n/**\n * Whether this component builds a panel of its own, which is the clock's: the list form hands\n * that job to VCombobox.\n */\nconst hasPanel = computed(\n () => !props.readonly && (resolvedMode.value === 'picker' || (typing.value && props.showPicker)),\n)\n\nconst resolvedLocale = useResolvedLocale(() => props.locale)\n\nconst resolvedFormat = computed<TimePickerFormat>(\n () => props.format ?? hourCycleFor(resolvedLocale.value),\n)\n\n/** The restrictions, resolved once. */\nconst limits = computed(() => resolveLimits(props))\n\n/** Whether the value is one the restrictions allow. Nothing at all is not a breach. */\nconst valueAllowed = computed(() => {\n const parts = modelParts.value\n return !parts || isTimeAllowed(parts.hour, parts.minute, limits.value)\n})\n\n// @devwarn\nif (isDev) {\n // The clock receives `minuteStep` and the restrictions as they stand, so what it would say\n // about them is said here, under the name the consumer wrote (utils/hostWarns).\n provide(hostWarnsKey, true)\n watchEffect(() => {\n if (!Number.isInteger(props.minuteStep) || props.minuteStep < 1 || 60 % props.minuteStep !== 0)\n console.warn(`[VTimeInput] minuteStep ${props.minuteStep} — a divisor of 60 is expected.`)\n if (props.mode !== undefined && !MODES.includes(props.mode))\n console.warn(\n `[VTimeInput] unknown mode \"${props.mode}\": use \"input\" (the default), \"picker\" or \"list\".`,\n )\n if (isList.value && props.showPicker)\n console.warn(\n '[VTimeInput] showPicker is ignored in \"list\" mode: the list of times is the only panel.',\n )\n // The list form is a VCombobox, which draws the end of the field itself: its own chevron in\n // place of the clock. The two props that describe this component's end icon therefore reach\n // nothing at all, and their absence is invisible on screen.\n const inert = ([] as string[]).concat(\n props.pickerIcon !== scheduleIcon ? 'pickerIcon' : [],\n props.pickerIconLabel ? 'pickerIconLabel' : [],\n )\n const isAre = inert.length > 1 ? 'are' : 'is'\n if (isList.value && inert.length > 0)\n console.warn(\n `[VTimeInput] ${inert.join(', ')} ${isAre} ignored in \"list\" mode: the list draws its own chevron.`,\n )\n // A field one types into without the picker has no end icon either, as in VDateInput.\n // A read-only field is left out: it is a state that comes and goes, not a configuration\n // to correct.\n if (typing.value && !props.showPicker && inert.length > 0)\n console.warn(\n `[VTimeInput] ${inert.join(', ')} ${isAre} ignored without showPicker: a field one types into has no clock icon unless it offers the clock.`,\n )\n const problem = limitsProblem(limits.value)\n if (problem) console.warn(`[VTimeInput] ${problem}`)\n if (isList.value && props.minuteStep < 5)\n console.warn(\n `[VTimeInput] minuteStep ${props.minuteStep} in \"list\" mode would render ${Math.ceil(1440 / props.minuteStep)} rows: a step of 15 or 30 minutes is expected.`,\n )\n })\n}\n\n/** The time being built on the clock, in the same canonical form as the value itself. */\nconst pickerDraft = ref<string | null>(null)\n\nconst modelParts = computed(() => parseTime(model.value))\n\n/** The half of the day chosen while no time is set: VTimePicker's own `pendingMeridiem`. */\nconst pendingMeridiem = ref<Meridiem>('AM')\n\n/**\n * The half of the day the FIELD is showing. It is what the mask needs: typing \"9:30\" says\n * nothing about the morning or the afternoon, so the answer has to be readable while the\n * panel is shut.\n */\nconst currentMeridiem = computed<Meridiem>(() => {\n // With the picker open it is the DRAFT that is on screen, and this control has to read\n // the same thing: two controls showing opposite halves of the day until OK is pressed\n // is the one state that must not happen.\n const parts = open.value && hasPanel.value ? parseTime(pickerDraft.value) : modelParts.value\n return parts ? to12h(parts.hour).meridiem : pendingMeridiem.value\n})\n\n/**\n * The button beside the value flips the half of the day. There is no third state to reach,\n * so it names the half it is showing and swaps to the other one on a click.\n */\nfunction toggleMeridiem() {\n const next: Meridiem = currentMeridiem.value === 'PM' ? 'AM' : 'PM'\n pendingMeridiem.value = next\n // With no time set there is nothing to convert, so the choice is simply REMEMBERED\n // and applies to the first time typed or chosen.\n const moved = withMeridiem(model.value, next)\n if (moved) model.value = moved\n // With the picker open the draft has to follow as well, or OK would write back the\n // half of the day the reader has just changed.\n const pending = open.value && hasPanel.value ? withMeridiem(pickerDraft.value, next) : null\n if (pending) pickerDraft.value = pending\n}\n\n/** Whether the button is rendered at all. */\nconst hasMeridiem = computed(\n () => typing.value && !props.readonly && resolvedFormat.value === '12h',\n)\n\nconst hasValue = computed(() => !!modelParts.value)\nconst displayText = computed(() =>\n model.value ? formatTimeDisplay(model.value, resolvedLocale.value, resolvedFormat.value) : '',\n)\n\n// @a11y\n// Where the focus goes when the panel opens. The picker publishes where its own focus belongs,\n// so nothing here has to know that the target is a slider; the same arrangement VDateInput has\n// with VDatePicker.\nfunction focusInPanel() {\n pickerRef.value?.focus()\n}\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). Nothing else is needed here: the\n// AM/PM button lives INSIDE the field, so a 12 hour field is one box like any other and\n// the row has nothing of its own to join.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n// The whole \"field plus panel\" shell, shared with VDateInput. What is specific to this\n// component is only what happens around it: preparing the draft as the panel opens, and\n// clearing the announcement as it closes.\nconst {\n open,\n panelId,\n closeAndFocus,\n focusField,\n onFieldFocus,\n toggleFromIcon,\n onControlClick,\n onFocusout,\n onKeydown,\n onPanelMousedown,\n} = useFieldPanel({\n rootEl,\n panelRef,\n field: inputRef,\n // With no panel there is nothing to open. This is the composable's SINGLE cut-off point, and\n // every way in passes through it; clicking the field, focusing it, the down arrow, Enter, the\n // icon.\n disabled: () => resolvedDisabled.value || !hasPanel.value,\n focusInPanel,\n // Beside a field one types into, the panel opens on focus WITHOUT taking it, so typing\n // carries on.\n openOnFocus: () => typing.value,\n onOpen: () => {\n const parts = modelParts.value\n if (parts) pickerDraft.value = formatTime(parts.hour, parts.minute)\n // A clock opened BESIDE a field one types into starts empty: it opened because the\n // field took the focus, not because a time was asked for, and a draft set to the\n // current time would hand its half of the day to the digits about to be typed, so\n // \"0930\" typed in the afternoon would become 21:30.\n else if (typing.value) pickerDraft.value = null\n else {\n // Reading the clock is safe here: this runs from a handler, hence in a browser, never\n // during a render; and it is why the picker itself never reads it, so that it stays\n // identical on both sides of hydration. The instant is taken once: two reads could\n // straddle a minute.\n const now = new Date()\n const wanted = snapMinute(now.getMinutes(), props.minuteStep)\n // Pulled to the nearest time the restrictions allow, so that the panel never opens\n // on one the reader would be refused. Null is a set of restrictions that allows\n // nothing at all, which the warning above has already reported.\n pickerDraft.value =\n nearestAllowedTime(now.getHours(), wanted, limits.value) ??\n formatTime(now.getHours(), wanted)\n }\n pickerRef.value?.reset()\n },\n onClose: () => {\n pickerRef.value?.reset()\n },\n})\n\n/** OK: the ONE route by which the picker's draft becomes the value. */\nfunction confirm() {\n if (pickerDraft.value) model.value = pickerDraft.value\n closeAndFocus()\n}\n\n/** Cancel: the draft is simply dropped with the panel. */\nconst cancel = closeAndFocus\n\n// @a11y\n// Spread popup wiring over forwarded attrs: absent panels must contribute no keys, while\n// present comboboxes require their own role and controls reference.\nconst inputAttrs = computed(() =>\n hasPanel.value\n ? {\n ...fieldAttrs.value,\n role: 'combobox',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': open.value,\n 'aria-controls': panelId,\n }\n : fieldAttrs.value,\n)\n\n/*\n * Emptying the value, called by the field as it emits its clear event. The focus is taken\n * through `focusField`, whose note says why that stops the panel reopening.\n */\nfunction clearValue() {\n model.value = null\n // The typed text is emptied EXPLICITLY: when the value was already empty, nothing\n // changes, and the guard that keeps the field and the value from chasing each other\n // would leave the text where it was.\n if (typing.value) writeField('')\n focusField()\n emit('clear')\n}\n\n// Escape and the focus leaving both close WITHOUT committing, which is the picker's Cancel\n// behaviour: closing never writes the value, and only OK does.\n\nconst m = useMessages()\n\n// @core\n/**\n * Saying no to a time that is not one the restrictions allow. It is written unconditionally\n * rather than only while typing: the browser BARS a read-only control from constraint\n * validation, so the read-only form settles it without a branch here.\n */\nwatchEffect(\n () => {\n const el = fieldEl.value\n if (!el) return\n const own = m.value.timeInput.unavailable\n if (!valueAllowed.value) el.setCustomValidity(own)\n // Only the component's own message is taken back: a consumer's verdict written over it,\n // a server saying the slot is taken, stays until the consumer clears it.\n else if (el.validationMessage === own) el.setCustomValidity('')\n },\n { flush: 'post' },\n)\n\n/*\n * That is why the caret can be computed outright here, where a date has to look for a separator\n * it cannot predict; and why nothing distinguishes a final commit from a live one, there being\n * no equivalent of expanding a two-digit year.\n */\nconst {\n draft,\n fieldModel,\n writeField,\n commitOrRevert,\n onFieldInput,\n onKeydown: onMaskKeydown,\n onPaste,\n} = useMaskedField({\n fieldEl,\n typing: () => typing.value,\n displayText: () => displayText.value,\n readValue: () => model.value,\n writeValue: (time) => {\n model.value = time\n // With the clock open beside the field, it follows what is typed: otherwise OK would\n // write the clock's time back over the one the reader has just typed.\n if (open.value && hasPanel.value) pickerDraft.value = time\n },\n maxDigits: () => 4,\n format: formatTimeMask,\n caret: (_text, digitsBefore, inserting) => timeCaret(digitsBefore, inserting),\n parse: (text) => parseTimeMask(text, resolvedFormat.value, currentMeridiem.value),\n toMask: (time) => timeToMask(time, resolvedFormat.value),\n})\n\n// @keyboard\n// What the mask's keys mean for a time: typing anything that is not a digit,\n// the separator included so that \"9:30\" can be typed exactly as it reads, completes the\n// hour with a leading zero and moves on to the minutes; the down arrow is the one explicit\n// way from the field into the picker.\nfunction onFieldKeydown(event: KeyboardEvent) {\n onMaskKeydown(event, {\n onSeparator: (el) => {\n const digits = digitsOf(el.value)\n if (digits.length === 1) writeField(formatTimeMask(`0${digits}`), 3)\n },\n onArrowDown: () => {\n if (!open.value) return false\n focusInPanel()\n return true\n },\n onEnter: closeAndFocus,\n })\n}\n\n/** Pasting. */\nfunction onFieldPaste(event: ClipboardEvent) {\n onPaste(event, (pasted) =>\n isValidTime(pasted)\n ? pasted\n : readWrittenTime(pasted, resolvedFormat.value, currentMeridiem.value),\n )\n}\n\n/*\n * The list form must not reach these two at all, and being panel-less is not enough to keep it\n * out. `useFieldPanel.onKeydown` cancels the default on Enter before asking whether a panel can\n * open, so an Enter travelling up from the VCombobox would stop a surrounding form from being\n * submitted with nothing to show for it.\n */\nfunction onRootKeydown(event: KeyboardEvent) {\n if (!isList.value) onKeydown(event)\n}\n\nfunction onRootFocusout(event: FocusEvent) {\n if (!isList.value) onFocusout(event)\n}\n\n/*\n * What is left is the three things a combobox cannot know about times: which rows to offer, how\n * a search matches one, and the fact that this component's value is a nullable string where a\n * combobox's is a plain one.\n */\n\n/**\n * The rows the step and the restrictions offer, which the value does not enter into: a\n * change of value must not format up to 1440 times again.\n */\nconst baseOptions = computed<TimeOption[]>(() => {\n if (!isList.value) return []\n return timeList(props.minuteStep, resolvedLocale.value, resolvedFormat.value).filter((row) => {\n // A list is read before it is chosen from, so a time it may not take has no reason to be in\n // it, which is the picker's own rule for its numerals.\n const parts = parseTime(row.value)\n return !parts || isTimeAllowed(parts.hour, parts.minute, limits.value)\n })\n})\n\n/**\n * The rows on offer. Without it the field would fall back to showing the raw canonical string;\n * \"09:07\" in a field that spells every other row \"9:07 AM\"; since a combobox labels a value\n * through the option carrying it.\n */\nconst options = computed<TimeOption[]>(() => {\n const rows = baseOptions.value\n const current = model.value\n if (!isValidTime(current) || rows.some((row) => row.value === current)) return rows\n const at = rows.findIndex((row) => row.value > current)\n const extra: TimeOption = {\n value: current,\n label: formatTimeDisplay(current, resolvedLocale.value, resolvedFormat.value),\n }\n return at < 0 ? [...rows, extra] : [...rows.slice(0, at), extra, ...rows.slice(at)]\n})\n\n/**\n * The search, which accepts the digit run as well as the words; \"930\" finds 9:30, the way the\n * typed form's mask reads what it is given.\n */\nconst matchTime = (option: ComboboxOption, query: string) =>\n timeMatches(option.label, String(option.value), query)\n\n/**\n * The bridge to VCombobox, whose value is a plain string where this component's is\n * nullable: an emptied combobox says so with an empty string, which is not a time.\n */\nconst listModel = computed<string | string[]>({\n get: () => model.value ?? '',\n set: (value) => {\n model.value = typeof value === 'string' && value ? value : null\n },\n})\n\n/*\n * Whether the cross is shown has to be answered explicitly here: outside the typed mode the\n * field is read-only and would hide it, while the value comes from the panel. The `readonly`\n * PROP is the one case where that default answer was right: frozen, the field offers no route\n * to a new value, so it offers no route to none either.\n */\nconst clearVisible = computed(() =>\n canClear(props, resolvedDisabled.value, hasValue.value || (typing.value && !!draft.value)),\n)\nconst endIcon = computed<IconSource | undefined>(() =>\n hasPanel.value ? props.pickerIcon : undefined,\n)\n// @a11y @devwarn\n/*\n * The label is passed always, even when no icon is rendered at all. The helper detecting a\n * click handler on an icon warns AT setup if one is attached without a label, and it has no way\n * of knowing whether an icon exists.\n */\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.timeInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.timeInput.clear)\n\n/*\n * The three of them go through whichever field is on screen: in the list form there is no\n * VInput of this component's own, the VCombobox owning the one the reader sees. Exposing\n * the same trio as the other fields is what keeps a consumer's `ref` working whatever the\n * mode, which is a runtime prop they may well be binding.\n */\nconst field = computed(() => (isList.value ? listRef.value : inputRef.value))\n\ndefineExpose({\n /** Moves the focus to the text field. */\n focus: (options?: FocusOptions) => field.value?.focus(options),\n /** Selects what the field is showing. */\n select: () => field.value?.select(),\n /** The real `<input>` behind the field, for what neither of the two above covers. */\n el: computed(() => field.value?.el ?? null),\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-time-input\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n :data-open=\"open ? '' : undefined\"\n :data-mode=\"resolvedMode\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n @focusout=\"onRootFocusout\"\n @keydown=\"onRootKeydown\"\n >\n <!--\n All this component adds is the three things a combobox cannot know about times; the rows,\n the search, and a value that may be nothing.\n -->\n <VCombobox\n v-if=\"isList\"\n ref=\"listRef\"\n v-model=\"listModel\"\n v-bind=\"fieldAttrs\"\n :options=\"options\"\n :filter=\"matchTime\"\n :label=\"label\"\n :hint=\"hint\"\n :placeholder=\"placeholder\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :readonly=\"readonly\"\n :invalid=\"invalid\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :clearable=\"clearable\"\n :clear-label=\"clearLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :placement=\"placement\"\n @clear=\"emit('clear')\"\n />\n\n <div v-else class=\"v-time-input-control\" @click=\"onControlClick\">\n <!--\n The field is declared a combobox rather than left as the plain text box it implicitly\n is, because a text box may not carry the attribute saying whether something is expanded;\n the same reasoning as in VDateInput.\n -->\n <VInput\n ref=\"inputRef\"\n v-model=\"fieldModel\"\n :inputmode=\"typing ? 'numeric' : undefined\"\n :autocomplete=\"typing ? 'off' : undefined\"\n v-bind=\"inputAttrs\"\n :readonly=\"readonly\"\n :no-typing=\"!typing\"\n :label=\"label\"\n :hint=\"hint\"\n :placeholder=\"placeholder ?? (typing ? m.timeInput.maskPlaceholder : undefined)\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n @click:icon-end=\"toggleFromIcon\"\n @clear=\"clearValue\"\n @focus=\"onFieldFocus\"\n @input=\"onFieldInput\"\n @change=\"commitOrRevert\"\n @blur=\"commitOrRevert\"\n @keydown=\"onFieldKeydown\"\n @paste=\"onFieldPaste\"\n >\n <!--\n It is dropped when nothing follows it, or it would trail off the end of an otherwise\n bare field.\n -->\n <template v-if=\"$slots.start\" #start><slot name=\"start\" /></template>\n <template v-if=\"hasMeridiem || $slots['value-end']\" #value-end>\n <button\n v-if=\"hasMeridiem\"\n type=\"button\"\n class=\"v-input-action v-field-action v-time-input-meridiem\"\n :disabled=\"resolvedDisabled\"\n :aria-label=\"\n m.timeInput.meridiemValue(\n currentMeridiem === 'PM' ? m.timePicker.pm : m.timePicker.am,\n )\n \"\n @click=\"toggleMeridiem\"\n >\n {{ currentMeridiem === 'PM' ? m.timePicker.pm : m.timePicker.am }}\n </button>\n <VSeparator\n v-if=\"hasMeridiem && (clearVisible || endIcon)\"\n orientation=\"vertical\"\n class=\"v-time-input-divider\"\n />\n <slot name=\"value-end\" />\n </template>\n </VInput>\n </div>\n\n <!-- The absence of a panel is therefore self-enforcing. -->\n <VPopover\n v-if=\"hasPanel\"\n :id=\"panelId\"\n ref=\"panelRef\"\n v-model:open=\"open\"\n mode=\"manual\"\n anchor=\"--time-input-anchor\"\n :placement=\"placement\"\n role=\"dialog\"\n class=\"v-time-input-panel\"\n :aria-label=\"label ?? m.timeInput.pickerLabel\"\n @mousedown=\"onPanelMousedown\"\n >\n <!--\n The picker works on the DRAFT and not on the value, which makes OK the only way in:\n Cancel, Escape and the focus leaving simply drop it. The two actions are handed to its\n own footer slot, since they belong to this panel rather than to a clock shown on its\n own.\n -->\n <VTimePicker\n ref=\"pickerRef\"\n v-model=\"pickerDraft\"\n :format=\"resolvedFormat\"\n :locale=\"resolvedLocale\"\n :minute-step=\"minuteStep\"\n :min=\"min\"\n :max=\"max\"\n :allowed-hours=\"allowedHours\"\n :allowed-minutes=\"allowedMinutes\"\n @confirm=\"confirm\"\n >\n <!--\n The slot REPLACES the two buttons rather than sitting beside them, so it is handed\n both actions: the clock writes a draft, and without `confirm` a footer of one's own\n could never commit it; the value would become unchangeable through the panel. The\n `remove` of VCombobox's `#chip` slot exists for the same reason.\n -->\n <template #footer>\n <slot name=\"footer\" :confirm=\"confirm\" :cancel=\"cancel\" :close=\"cancel\">\n <VButton variant=\"ghost\" tone=\"neutral\" @click=\"cancel\">{{ m.common.cancel }}</VButton>\n <VButton @click=\"confirm\">{{ m.common.confirm }}</VButton>\n </slot>\n </template>\n </VTimePicker>\n </VPopover>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-time-input {\n /* Confines the anchor name to this instance. It is declared on the root because that\n is the common ancestor of the field and the panel. */\n anchor-scope: --time-input-anchor;\n display: block;\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-time-input:not([data-disabled]):not([data-readonly]) .v-time-input-control {\n cursor: pointer;\n }\n\n /*\n * Anchor through the clock/input wrapper only; list mode belongs to VCombobox and must retain\n * its own anchor independently of stylesheet order.\n */\n .v-time-input-control .v-input-field {\n anchor-name: --time-input-anchor;\n }\n\n /* A field one types into shows the text cursor rather than the pointer a clickable\n control shows. Its figures are also given equal widths: with proportional ones the\n text shifts as digits are typed, and the caret appears to jitter. */\n .v-time-input[data-mode='input']:not([data-disabled]):not([data-readonly]) .v-time-input-control {\n cursor: text;\n }\n\n .v-time-input[data-mode='input'] .v-input-control {\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * Give the AM/PM action room for text while preserving the neighbouring icon buttons' minimum\n * hit target.\n */\n .v-field-action.v-time-input-meridiem {\n inline-size: auto;\n min-inline-size: var(--control-action-size);\n padding-inline: calc((var(--control-action-size) - var(--vectis-icon-size)) / 2);\n font: inherit;\n }\n\n /*\n * The rule between the AM/PM button and the field's own controls, which keeps a word from\n * reading as a third icon in the row. A vertical VSeparator stretches to its flex line by\n * contract, which here is the full height of the field: it would meet the border at both ends\n * and read as a division of the box rather than a break in the row.\n */\n .v-separator[data-orientation='vertical'].v-time-input-divider {\n align-self: center;\n block-size: var(--control-action-size);\n }\n\n /*\n * No `display` here either: the column layout is `.v-panel`'s, and a panel declaring its own\n * is what the doubled-class `.v-overlay` guard exists to overrule. The selector compounds two\n * classes VPopover puts on the same element, because the padding is also declared by the\n * shared panel class: at equal specificity the winner would be whichever sheet the consumer's\n * bundler put last.\n */\n .v-popover-panel.v-time-input-panel {\n width: max-content;\n padding: 0;\n }\n\n /*\n * The list form is a VCombobox and takes everything from it; this is the one declaration that\n * does not belong to a combobox in general. A column of times read down the panel is a column\n * of FIGURES, and proportional ones slide the colon from row to row.\n */\n .v-time-input[data-mode='list'] .v-combobox-panel {\n font-variant-numeric: tabular-nums;\n }\n}\n</style>\n"],"mappings":""}
|