vectis-ui 0.10.1 → 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/tokens/css.d.ts +3 -16
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +1 -23
- 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/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":"VCalendarMonth.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCalendar/VCalendarMonth.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"E extends CalendarEvent\">\n// @a11y @keyboard @core\n/**\n * The month view: the weeks stacked one under another, each day a square holding as many of\n * its events as will fit.\n *\n * It is internal to VCalendar and has no story of its own — its documentation lives with\n * the component that renders it. Like the time grid it computes nothing about dates: it is\n * handed the weeks already cut and filtered, and turns them into boxes.\n *\n * A day here is not a column of hours but a SUMMARY, so the events are drawn as chips in\n * the order the day would be read out — the all-day ones first, then the rest by when they\n * start — and a day with more than it can show says how many are left rather than silently\n * dropping them.\n *\n * The drag and the keyboard grab are `gesture.ts`'s. What stays here is what a square means:\n * the point under the pointer turned into a day, and a vertical step read as a whole week.\n */\nimport { computed, ref, useId } from 'vue'\n\nimport { formatDateDisplay } from '../../utils/date'\nimport { clamp } from '../../utils/number'\nimport { formatTimeDisplay } from '../../utils/time'\n\nimport { useMessages } from '../../i18n/state'\n\nimport VCalendarEvent from './VCalendarEvent.vue'\nimport { gridGeometryOf, useCalendarGesture, type GestureBase, type GrabStep } from './gesture'\nimport { calendarIntent } from './keyboard'\nimport {\n eventsByDay,\n isAllDayEvent,\n moveEventToDay,\n pointToMonthCell,\n sameTimes,\n timesOf,\n type MonthCell,\n} from './layout'\nimport type {\n ActivatedCell,\n CalendarEvent,\n CalendarEventId,\n CalendarEventSlotProps,\n CalendarEventTimes,\n CalendarFormat,\n} from './types'\n\nexport interface CalendarMonthProps<T> {\n /** The weeks, already padded to a fixed height and filtered to the visible weekdays. */\n weeks: MonthCell[][]\n events: T[]\n locale: string\n format: CalendarFormat\n today: string | null\n /** How many events a day shows before it starts counting the rest. */\n monthEventLimit: number\n /** Whether events can be moved from one day to another. */\n editable: boolean\n /** Whether the whole calendar is frozen, which also takes its cards out of the tab order. */\n disabled: boolean\n /** The node telling a reader how a chip can be moved, shared by every one of them. */\n hintId: string\n /** How long a drag rests against an edge before the month turns. Zero turns paging off. */\n edgeStepDelay: number\n label: string\n}\n\nconst props = defineProps<CalendarMonthProps<E>>()\n\nconst emit = defineEmits<{\n /** A day was asked to be opened on its own. */\n 'day-activate': [iso: string]\n /** An empty part of a day was activated. A month has no hours, so the time is left to the calendar. */\n 'cell-activate': [cell: ActivatedCell]\n 'event-activate': [event: E]\n /** An event was dropped on another day. A month has no hours, so it is only ever a move. */\n 'event-drop': [id: CalendarEventId, times: CalendarEventTimes, kind: 'move']\n /** Something happened that a reader who cannot see the grid needs told. */\n announce: [message: string]\n step: [delta: -1 | 1]\n}>()\n\n/** Which day holds the tab stop. Only the date matters here; the hour is the grid's affair. */\nconst focused = defineModel<string>('focused', { required: true })\n\ndefineSlots<{\n event?(props: CalendarEventSlotProps<E>): unknown\n}>()\n\nconst m = useMessages()\nconst uid = useId()\n\nconst cellId = (iso: string) => `${uid}-m-${iso}`\n\n/** The flattened list of days: what the arrows travel along, and what the events are filed by. */\nconst flat = computed(() => props.weeks.flat().map((cell) => cell.iso))\nconst columnCount = computed(() => props.weeks[0]?.length ?? 1)\n\n/** The column headings, taken from the first week so they always match the columns drawn. */\nconst weekdayNames = computed(() =>\n (props.weeks[0] ?? []).map((cell) =>\n formatDateDisplay(cell.iso, props.locale, { weekday: 'short' }),\n ),\n)\n\n/*\n * The two labels every square carries, worked out once per cell rather than once per USE.\n *\n * `longDay` is asked for twice in the template — the square's own `aria-label`, then the\n * name of the button that opens the day — so the 42 cells cost 126 `Intl` formats a render,\n * and a drag re-renders the whole grid each time it carries a chip onto another day. The map\n * depends on the weeks and the locale alone, so it survives every frame of a drag.\n */\nconst dayLabels = computed(() => {\n const map = new Map<string, { number: string; long: string }>()\n for (const iso of flat.value) {\n map.set(iso, {\n number: formatDateDisplay(iso, props.locale, { day: 'numeric' }),\n long: formatDateDisplay(iso, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n }),\n })\n }\n return map\n})\n\nconst dayNumber = (iso: string) => dayLabels.value.get(iso)?.number ?? ''\n\n/*\n * A day outside the grid can reach this — the announcement after a move names the day the\n * event landed on, which may be the next month — so it falls back to formatting rather than\n * assuming the map holds it.\n */\nconst longDay = (iso: string) =>\n dayLabels.value.get(iso)?.long ??\n formatDateDisplay(iso, props.locale, { weekday: 'long', day: 'numeric', month: 'long' })\n\n/**\n * A chip being carried from one day to another, by pointer or by keyboard.\n *\n * A month has no hours, so there is one kind and no resize: only which day an event starts\n * on moves, and its length in days and its times come along untouched.\n */\ninterface Gesture extends GestureBase {\n /** Which square the pointer took hold in, so a long bar does not jump under it. */\n grabIndex: number\n}\n\nconst gridEl = ref<HTMLElement | null>(null)\n/** The view's own box, which a drag is measured against to know whether it has left it. */\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst {\n gesture,\n withPreview,\n edgeCue,\n isRtl,\n start,\n endLastGesture,\n consumeDrag,\n onPointermove,\n onPointerup,\n onPointercancel,\n onPointerleave,\n onFocusout,\n onCardKeydown,\n focusCell,\n idOfCard,\n cardState,\n} = useCalendarGesture<E, Gesture>({\n events: () => props.events,\n // The finished gesture and the weeks it was measured against are not kept past it.\n onRelease: () => {\n lastApplied = null\n },\n eventOf: (id): E | undefined => eventsById.value.get(id),\n // Measured against the ROOT and not against `gridEl`, whose rect leaves out the row of\n // weekday names above it.\n rootEl,\n // Captured on the GRID rather than on the chip, because a chip is redrawn on another day the\n // moment the drag starts and a captured element that moves takes the pointer with it.\n captureEl: gridEl,\n measureEl: gridEl,\n columns: () => columnCount.value,\n editable: () => props.editable,\n hintId: () => props.hintId,\n // The step is irrelevant to a month, which has no hours: 1 keeps the table honest without\n // asking the calendar for a number this view never uses.\n slotMinutes: () => 1,\n edgeStepDelay: () => props.edgeStepDelay,\n layout: () => props.weeks,\n applyPoint,\n onDrop: (state) => emit('event-drop', state.id, state.preview, 'move'),\n grab: (item) =>\n props.editable\n ? {\n id: item.id,\n origin: timesOf(item),\n pointerId: null,\n originX: 0,\n originY: 0,\n // There is no pointer to remember; the arrows work off the preview, not a position.\n grabIndex: 0,\n }\n : null,\n onGrabStep,\n step: (delta) => emit('step', delta),\n announce: (message) => emit('announce', message),\n})\n\n/**\n * The events as the month should currently DRAW them — the model, with the one being carried\n * put wherever it now is. Running the preview through the same grouping as everything else is\n * what makes the day it leaves and the day it arrives at both redraw with no special case.\n *\n * Nothing has to be held out of a layout for the echo, unlike the time grid: a month cell\n * simply stacks its chips, so the echo takes its own line in the day it came from and disturbs\n * nothing.\n */\nconst drawnEvents = computed<E[]>(() => (gesture.value ? withPreview(props.events) : props.events))\n\n/** Every day's events, worked out once rather than once per chip. */\nconst byDay = computed(() => eventsByDay(drawnEvents.value, flat.value))\n\nconst eventsById = computed(\n () => new Map<CalendarEventId, E>(drawnEvents.value.map((item) => [item.id, item])),\n)\n\n/**\n * A chip says when it happens only when that is not obvious: an all-day event has no time\n * to give, and the day it sits in already says which day it is.\n *\n * Memoized by start time inside the computed, so a change of locale or hour format throws the\n * cache away whole — the twin in VCalendarTimeGrid.vue carries the trap that makes reading\n * the computed, and not a cache beside it, the only correct shape.\n */\nconst timeTextOf = computed(() => {\n const { locale, format } = props\n const cache = new Map<string, string>()\n return (event: E): string => {\n if (isAllDayEvent(event)) return ''\n let text = cache.get(event.startTime)\n if (text === undefined) {\n text = formatTimeDisplay(event.startTime, locale, format)\n cache.set(event.startTime, text)\n }\n return text\n }\n})\n\n/** One chip as the template draws it, its time already written out. */\ninterface Chip {\n event: E\n timeText: string\n}\n\n/**\n * What each square draws, and how many it had to leave out — one Map, built in a single pass.\n *\n * It is the `dayLabels` treatment. Written as template functions these returned a NEW array\n * per square per render, and since one of them is the source of a `v-for`, Vue rediffed all\n * 42 lists each time; each chip also formatted its own time. The map is rebuilt with `byDay`,\n * which a drag changes only when it carries its chip onto another day (`applyPoint` writes\n * nothing otherwise), and survives every other render.\n */\nconst dayEvents = computed(() => {\n const map = new Map<string, { shown: Chip[]; hidden: number }>()\n const textOf = timeTextOf.value\n // Below zero, a limit would count hidden events on every empty square.\n const limit = Math.max(0, props.monthEventLimit)\n for (const [iso, all] of byDay.value) {\n const shown = all.length > limit ? all.slice(0, limit) : all\n map.set(iso, {\n shown: shown.map((event) => ({ event, timeText: textOf(event) })),\n hidden: Math.max(0, all.length - limit),\n })\n }\n return map\n})\n\n/** A square with nothing on it, shared rather than allocated per empty cell. */\nconst NO_EVENTS: { shown: Chip[]; hidden: number } = Object.freeze({ shown: [], hidden: 0 })\nconst dayEventsOf = (iso: string) => dayEvents.value.get(iso) ?? NO_EVENTS\n\nfunction moveFocusTo(iso: string) {\n focused.value = iso\n // @a11y — the model decides which cell is tabbable, so the focus can only follow once the\n // render that applied it has run.\n focusCell(cellId(iso), true)\n}\n\nconst tabbable = computed(() =>\n flat.value.includes(focused.value) ? focused.value : (flat.value[0] ?? null),\n)\n\n/**\n * The same table serves both views, so its two axes are read differently here: a sideways step\n * is one day, and a vertical one is a whole week rather than an hour. That is the only place the\n * month reinterprets it, and it is why the table returns a direction rather than a date.\n */\nconst dayStep = (days: number, minutes: number) =>\n days !== 0 ? days : Math.sign(minutes) * columnCount.value\n\nfunction onKeydown(event: KeyboardEvent) {\n const target = event.target as HTMLElement | null\n\n // A chip answers its own table, delegated here on the same element as the pointer so the\n // two input paths cannot drift apart.\n const card = target?.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n onCardKeydown(event, card)\n return\n }\n\n const cell = target?.closest<HTMLElement>('.v-calendar-month-cell')\n if (!cell) return\n\n const intent = calendarIntent(event, 'cell', 1, isRtl())\n if (!intent) return\n\n const iso = cell.dataset.iso!\n const index = flat.value.indexOf(iso)\n const columns = columnCount.value\n\n if (intent.kind === 'moveFocus') {\n const next =\n flat.value[clamp(index + dayStep(intent.days, intent.minutes), 0, flat.value.length - 1)]\n if (!next) return\n event.preventDefault()\n moveFocusTo(next)\n return\n }\n\n if (intent.kind === 'rowEdge') {\n const row = Math.floor(index / columns)\n const edge = intent.edge === 'start' ? row * columns : row * columns + columns - 1\n const next = flat.value[clamp(edge, 0, flat.value.length - 1)]\n if (!next) return\n event.preventDefault()\n moveFocusTo(next)\n return\n }\n\n if (intent.kind === 'period') {\n event.preventDefault()\n emit('step', intent.delta)\n return\n }\n\n if (intent.kind === 'openDay') {\n event.preventDefault()\n emit('day-activate', iso)\n return\n }\n\n if (intent.kind === 'activate') {\n event.preventDefault()\n emit('cell-activate', { date: iso, minutes: null })\n }\n}\n\n/** Which of the flattened squares a point falls on. */\nfunction indexAt(x: number, y: number, rect: DOMRect, rtl: boolean): number {\n const cell = pointToMonthCell(\n { x, y },\n { ...gridGeometryOf(rect, columnCount.value, rtl), rows: props.weeks.length || 1 },\n rtl,\n )\n return cell.rowIndex * columnCount.value + cell.columnIndex\n}\n\nfunction onGridPointerdown(event: PointerEvent) {\n endLastGesture()\n if (gesture.value || !props.editable || event.button !== 0) return\n\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n const item = card ? eventsById.value.get(idOfCard(card)) : undefined\n const rect = gridEl.value?.getBoundingClientRect()\n if (!item || !rect) return\n const rtl = isRtl()\n\n start(\n {\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabIndex: indexAt(event.clientX, event.clientY, rect, rtl),\n },\n rtl,\n )\n}\n\n/**\n * Works out which day the chip now belongs to, from wherever the pointer last was.\n *\n * Separate from the handler because the month turning calls it too: the squares change under\n * a hand that has not moved, and the chip has to be placed against the new ones.\n */\nfunction applyPoint(state: Gesture, rect: DOMRect) {\n const index = indexAt(state.lastX, state.lastY, rect, state.rtl)\n const last = lastApplied\n if (last?.state === state && last.weeks === props.weeks && last.index === index) return\n lastApplied = { state, weeks: props.weeks, index }\n\n /*\n * The target is the event's START shifted by however many squares the pointer has crossed,\n * not the square it is over — so a bar grabbed by its third day keeps its third day under\n * the finger. The days come out of the flattened grid, which is already filtered to the\n * weekdays on show, so a hidden weekend is not counted.\n */\n const days = flat.value\n const from = days.indexOf(state.origin.start)\n const target =\n days[\n clamp((from === -1 ? state.grabIndex : from) + index - state.grabIndex, 0, days.length - 1)\n ]\n if (!target) return\n /*\n * TRAP — written only when the day changes. The gesture is a deep ref, so an equal but new\n * preview would still re-bucket and re-sort every event and re-render all 42 squares.\n */\n const next = moveEventToDay(state.origin, target)\n if (!sameTimes(state.preview, next)) state.preview = next\n}\n\n/**\n * The square `applyPoint` last placed the chip from, so the many `pointermove`s that fire\n * inside one square return before any date arithmetic. The weeks belong to what the index\n * MEANS: the month turning under a still pointer reads as a new square at the same index.\n * A plain variable, since nothing renders it; the gesture's identity says a new press began.\n */\nlet lastApplied: { state: Gesture; weeks: MonthCell[][]; index: number } | null = null\n\n/** One arrow press on a chip held by the keyboard: a day sideways, a week vertically. */\nfunction onGrabStep(state: Gesture, step: GrabStep, item: E): boolean {\n // A month has no hours to stretch, so Shift with an arrow is simply not a gesture here.\n if (step.kind !== 'grabMove') return false\n const days = flat.value\n const from = days.indexOf(state.preview.start)\n const target = days[clamp(from + dayStep(step.days, step.minutes), 0, days.length - 1)]\n // Held against the grid's edge, the press goes nowhere: nothing to announce or to drop.\n if (!target || target === state.preview.start) return false\n state.preview = moveEventToDay(state.preview, target)\n emit('announce', m.value.calendar.movedTo(item.title, longDay(state.preview.start)))\n return true\n}\n\nfunction onCardClick(item: E) {\n // `pointerup` fires before `click`, so letting go at the end of a drag would ALSO open the\n // event — the consumer's editor over every chip the reader had just moved.\n if (consumeDrag()) return\n emit('event-activate', item)\n}\n\n/**\n * A click on the empty part of a day: the pointer's side of the Enter that activates a cell,\n * so a square drawn with a pointer cursor answers a click as it answers the key.\n *\n * The chips and the two buttons a square holds answer their own clicks, so a click that came\n * from one of them is theirs alone. And a click ending a drag is swallowed like the one a chip\n * would have received: the drop has already said what the gesture meant.\n */\nfunction onGridClick(event: MouseEvent) {\n const target = event.target as HTMLElement | null\n if (target?.closest('.v-calendar-event, button')) return\n const cell = target?.closest<HTMLElement>('.v-calendar-month-cell')\n if (!cell || consumeDrag()) return\n emit('cell-activate', { date: cell.dataset.iso!, minutes: null })\n}\n\n// The same two members VCalendarTimeGrid exposes, so VCalendar drives either view without\n// asking which is on screen. A month has no hours, hence the empty `scrollToMinutes`.\ndefineExpose({\n /** Brings the focus onto the day the grid is currently pointing at. */\n focus: (options?: FocusOptions) => {\n if (tabbable.value) focusCell(cellId(tabbable.value), false, options)\n },\n /** Nothing to scroll to in a month view. */\n scrollToMinutes: () => {},\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-calendar-month\"\n :data-outside=\"gesture?.outside ? '' : undefined\"\n :style=\"{ '--calendar-columns': String(weekdayNames.length) }\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-calendar-month-head\" aria-hidden=\"true\">\n <span\n v-for=\"(name, index) in weekdayNames\"\n :key=\"index\"\n class=\"v-calendar-weekday v-calendar-month-weekday\"\n >\n {{ name }}\n </span>\n </div>\n\n <div\n ref=\"gridEl\"\n class=\"v-calendar-month-grid v-calendar-edge-cue\"\n role=\"grid\"\n :aria-label=\"label\"\n :data-edge=\"edgeCue\"\n @keydown=\"onKeydown\"\n @pointerdown=\"onGridPointerdown\"\n @pointermove=\"onPointermove\"\n @pointerup=\"onPointerup\"\n @pointercancel=\"onPointercancel\"\n @pointerleave=\"onPointerleave\"\n @click=\"onGridClick\"\n >\n <div\n v-for=\"(week, row) in weeks\"\n :key=\"week[0]?.iso ?? row\"\n role=\"row\"\n class=\"v-calendar-month-week\"\n >\n <div\n v-for=\"cell in week\"\n :id=\"cellId(cell.iso)\"\n :key=\"cell.iso\"\n role=\"gridcell\"\n class=\"v-calendar-cell v-calendar-month-cell\"\n :data-iso=\"cell.iso\"\n :data-adjacent=\"cell.adjacent ?? undefined\"\n :tabindex=\"tabbable === cell.iso ? 0 : -1\"\n :aria-label=\"longDay(cell.iso)\"\n aria-keyshortcuts=\"Shift+Enter\"\n >\n <button\n type=\"button\"\n class=\"v-calendar-button v-calendar-month-day\"\n :class=\"{ 'v-calendar-today': cell.iso === today }\"\n tabindex=\"-1\"\n :aria-label=\"m.calendar.openDay(longDay(cell.iso))\"\n @click=\"emit('day-activate', cell.iso)\"\n >\n {{ dayNumber(cell.iso) }}\n </button>\n\n <VCalendarEvent\n v-for=\"chip in dayEventsOf(cell.iso).shown\"\n :key=\"chip.event.id\"\n class=\"v-calendar-month-chip\"\n :event=\"chip.event\"\n layout=\"chip\"\n :disabled=\"disabled\"\n :time-text=\"chip.timeText\"\n v-bind=\"cardState(chip.event.id)\"\n @click=\"onCardClick(chip.event)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n\n <button\n v-if=\"dayEventsOf(cell.iso).hidden > 0\"\n type=\"button\"\n class=\"v-calendar-button v-calendar-month-more\"\n tabindex=\"-1\"\n @click=\"emit('day-activate', cell.iso)\"\n >\n {{ m.calendar.moreEvents(dayEventsOf(cell.iso).hidden) }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-calendar-month {\n display: flex;\n flex-direction: column;\n }\n\n .v-calendar-month-head,\n .v-calendar-month-week {\n display: grid;\n grid-template-columns: repeat(var(--calendar-columns), minmax(0, 1fr));\n }\n\n /* 4, the time grid's header layer: a dragged chip (3) and the edge strip (2) pass under the\n sticky names as the month scrolls, rather than over them. */\n .v-calendar-month-head {\n position: sticky;\n inset-block-start: 0;\n z-index: 4;\n background: var(--vectis-color-surface);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-month-weekday {\n padding-block: var(--vectis-space-2);\n text-align: center;\n }\n\n .v-calendar-month-grid {\n /* The edge strip is placed against this box. */\n position: relative;\n display: flex;\n /* The rows share what height there is, so a month fills the box it was given instead of\n leaving a band of nothing under the last week. The floor keeps a day readable when\n that box is short. */\n flex: 1;\n flex-direction: column;\n min-block-size: 0;\n }\n\n .v-calendar-month-week {\n flex: 1;\n min-block-size: var(--vectis-control-size-calendar-month-cell);\n }\n\n .v-calendar-month-cell {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-control-size-calendar-gap);\n overflow: hidden;\n padding: var(--vectis-space-1);\n }\n\n /* The days of the neighbouring months are kept — the grid is a fixed six rows, so the\n page never jumps — but pushed back, since they are context rather than content. */\n .v-calendar-month-cell[data-adjacent] {\n background: var(--vectis-color-surface-sunken);\n color: var(--vectis-color-text-subtle);\n }\n\n .v-calendar-month-day {\n align-self: center;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n inline-size: var(--vectis-control-height-sm);\n block-size: var(--vectis-control-height-sm);\n border-radius: var(--vectis-radius-pill);\n color: inherit;\n font-size: var(--vectis-text-body-md-size);\n }\n\n /* Disjoint from today rather than less specific: the two live in different sheets, where a\n tie would be settled by whichever one the consumer's bundler emitted last. */\n .v-calendar-month-day:hover:not(.v-calendar-today) {\n background: var(--vectis-color-surface-muted);\n }\n\n /*\n * A day in the month view is a summary, so a chip is capped at one lane however long the\n * event really is — an all-day bar and a two-hour meeting look the same here, and several\n * have to fit under the date.\n *\n * The height is SET AS A PROPERTY rather than as `block-size`. The chip rule in\n * VCalendarEvent's own sheet is (0,2,0) against this (0,1,0), so declaring the size\n * directly would lose — and matching its specificity would only hand the decision to\n * whichever sheet the consumer's bundler emitted last, which is the failure the library's\n * one-sheet-per-component rule exists to prevent.\n */\n .v-calendar-month-chip {\n --calendar-chip-height: var(--vectis-control-size-calendar-allday-lane);\n\n flex: none;\n }\n\n .v-calendar-month-more {\n flex: none;\n padding-inline: var(--vectis-space-1);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n text-align: start;\n }\n\n .v-calendar-month-more:hover {\n color: var(--vectis-color-text);\n text-decoration: underline;\n }\n\n /* The neighbouring months' days are told apart by a tint forced colors removes: grey text\n keeps them apart. */\n @media (forced-colors: active) {\n .v-calendar-month-cell[data-adjacent] {\n forced-color-adjust: none;\n background: Canvas;\n color: GrayText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VCalendarMonth.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCalendar/VCalendarMonth.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"E extends CalendarEvent\">\n// @a11y @keyboard @core\n/**\n * Month cells summarize events and overflow counts. Shared gesture logic supplies dragging and\n * keyboard grabs; grid focus remains in this view.\n */\nimport { computed, ref, useId } from 'vue'\n\nimport { formatDateDisplay } from '../../utils/date'\nimport { clamp } from '../../utils/number'\nimport { formatTimeDisplay } from '../../utils/time'\n\nimport { useMessages } from '../../i18n/state'\n\nimport VCalendarEvent from './VCalendarEvent.vue'\nimport { gridGeometryOf, useCalendarGesture, type GestureBase, type GrabStep } from './gesture'\nimport { calendarIntent } from './keyboard'\nimport {\n eventsByDay,\n isAllDayEvent,\n moveEventToDay,\n pointToMonthCell,\n sameTimes,\n timesOf,\n type MonthCell,\n} from './layout'\nimport type {\n ActivatedCell,\n CalendarEvent,\n CalendarEventId,\n CalendarEventSlotProps,\n CalendarEventTimes,\n CalendarFormat,\n} from './types'\n\nexport interface CalendarMonthProps<T> {\n /** The weeks, already padded to a fixed height and filtered to the visible weekdays. */\n weeks: MonthCell[][]\n events: T[]\n locale: string\n format: CalendarFormat\n today: string | null\n /** How many events a day shows before it starts counting the rest. */\n monthEventLimit: number\n /** Whether events can be moved from one day to another. */\n editable: boolean\n /** Whether the whole calendar is frozen, which also takes its cards out of the tab order. */\n disabled: boolean\n /** The node telling a reader how a chip can be moved, shared by every one of them. */\n hintId: string\n /** How long a drag rests against an edge before the month turns. Zero turns paging off. */\n edgeStepDelay: number\n label: string\n}\n\nconst props = defineProps<CalendarMonthProps<E>>()\n\nconst emit = defineEmits<{\n /** A day was asked to be opened on its own. */\n 'day-activate': [iso: string]\n /** An empty part of a day was activated. A month has no hours, so the time is left to the calendar. */\n 'cell-activate': [cell: ActivatedCell]\n 'event-activate': [event: E]\n /** An event was dropped on another day. A month has no hours, so it is only ever a move. */\n 'event-drop': [id: CalendarEventId, times: CalendarEventTimes, kind: 'move']\n /** Something happened that a reader who cannot see the grid needs told. */\n announce: [message: string]\n step: [delta: -1 | 1]\n}>()\n\n/** Which day holds the tab stop. Only the date matters here; the hour is the grid's affair. */\nconst focused = defineModel<string>('focused', { required: true })\n\ndefineSlots<{\n event?(props: CalendarEventSlotProps<E>): unknown\n}>()\n\nconst m = useMessages()\nconst uid = useId()\n\nconst cellId = (iso: string) => `${uid}-m-${iso}`\n\n/** The flattened list of days: what the arrows travel along, and what the events are filed by. */\nconst flat = computed(() => props.weeks.flat().map((cell) => cell.iso))\nconst columnCount = computed(() => props.weeks[0]?.length ?? 1)\n\n/** The column headings, taken from the first week so they always match the columns drawn. */\nconst weekdayNames = computed(() =>\n (props.weeks[0] ?? []).map((cell) =>\n formatDateDisplay(cell.iso, props.locale, { weekday: 'short' }),\n ),\n)\n\n/*\n * `longDay` is asked for twice in the template; the square's own `aria-label`, then the name of\n * the button that opens the day; so the 42 cells cost 126 `Intl` formats a render, and a drag\n * re-renders the whole grid each time it carries a chip onto another day. The map depends on\n * the weeks and the locale alone, so it survives every frame of a drag.\n */\nconst dayLabels = computed(() => {\n const map = new Map<string, { number: string; long: string }>()\n for (const iso of flat.value) {\n map.set(iso, {\n number: formatDateDisplay(iso, props.locale, { day: 'numeric' }),\n long: formatDateDisplay(iso, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n }),\n })\n }\n return map\n})\n\nconst dayNumber = (iso: string) => dayLabels.value.get(iso)?.number ?? ''\n\n/*\n * A day outside the grid can reach this; the announcement after a move names the day the event\n * landed on, which may be the next month; so it falls back to formatting rather than assuming\n * the map holds it.\n */\nconst longDay = (iso: string) =>\n dayLabels.value.get(iso)?.long ??\n formatDateDisplay(iso, props.locale, { weekday: 'long', day: 'numeric', month: 'long' })\n\n/** A chip being carried from one day to another, by pointer or by keyboard. */\ninterface Gesture extends GestureBase {\n /** Which square the pointer took hold in, so a long bar does not jump under it. */\n grabIndex: number\n}\n\nconst gridEl = ref<HTMLElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst {\n gesture,\n withPreview,\n edgeCue,\n isRtl,\n start,\n endLastGesture,\n consumeDrag,\n onPointermove,\n onPointerup,\n onPointercancel,\n onPointerleave,\n onFocusout,\n onCardKeydown,\n focusCell,\n idOfCard,\n cardState,\n} = useCalendarGesture<E, Gesture>({\n events: () => props.events,\n onRelease: () => {\n lastApplied = null\n },\n eventOf: (id): E | undefined => eventsById.value.get(id),\n rootEl,\n // Captured on the GRID rather than on the chip, because a chip is redrawn on another day the\n // moment the drag starts and a captured element that moves takes the pointer with it.\n captureEl: gridEl,\n measureEl: gridEl,\n columns: () => columnCount.value,\n editable: () => props.editable,\n hintId: () => props.hintId,\n // The step is irrelevant to a month, which has no hours: 1 keeps the table honest without\n // asking the calendar for a number this view never uses.\n slotMinutes: () => 1,\n edgeStepDelay: () => props.edgeStepDelay,\n layout: () => props.weeks,\n applyPoint,\n onDrop: (state) => emit('event-drop', state.id, state.preview, 'move'),\n grab: (item) =>\n props.editable\n ? {\n id: item.id,\n origin: timesOf(item),\n pointerId: null,\n originX: 0,\n originY: 0,\n grabIndex: 0,\n }\n : null,\n onGrabStep,\n step: (delta) => emit('step', delta),\n announce: (message) => emit('announce', message),\n})\n\n/**\n * The events as the month should currently DRAW them; the model, with the one being carried put\n * wherever it now is.\n */\nconst drawnEvents = computed<E[]>(() => (gesture.value ? withPreview(props.events) : props.events))\n\n/** Every day's events, worked out once rather than once per chip. */\nconst byDay = computed(() => eventsByDay(drawnEvents.value, flat.value))\n\nconst eventsById = computed(\n () => new Map<CalendarEventId, E>(drawnEvents.value.map((item) => [item.id, item])),\n)\n\n/**\n * A chip says when it happens only when that is not obvious: an all-day event has no time to\n * give, and the day it sits in already says which day it is.\n */\nconst timeTextOf = computed(() => {\n const { locale, format } = props\n const cache = new Map<string, string>()\n return (event: E): string => {\n if (isAllDayEvent(event)) return ''\n let text = cache.get(event.startTime)\n if (text === undefined) {\n text = formatTimeDisplay(event.startTime, locale, format)\n cache.set(event.startTime, text)\n }\n return text\n }\n})\n\n/** One chip as the template draws it, its time already written out. */\ninterface Chip {\n event: E\n timeText: string\n}\n\n/** What each square draws, and how many it had to leave out; one Map, built in a single pass. */\nconst dayEvents = computed(() => {\n const map = new Map<string, { shown: Chip[]; hidden: number }>()\n const textOf = timeTextOf.value\n const limit = Math.max(0, props.monthEventLimit)\n for (const [iso, all] of byDay.value) {\n const shown = all.length > limit ? all.slice(0, limit) : all\n map.set(iso, {\n shown: shown.map((event) => ({ event, timeText: textOf(event) })),\n hidden: Math.max(0, all.length - limit),\n })\n }\n return map\n})\n\n/** A square with nothing on it, shared rather than allocated per empty cell. */\nconst NO_EVENTS: { shown: Chip[]; hidden: number } = Object.freeze({ shown: [], hidden: 0 })\nconst dayEventsOf = (iso: string) => dayEvents.value.get(iso) ?? NO_EVENTS\n\nfunction moveFocusTo(iso: string) {\n focused.value = iso\n // @a11y\n focusCell(cellId(iso), true)\n}\n\nconst tabbable = computed(() =>\n flat.value.includes(focused.value) ? focused.value : (flat.value[0] ?? null),\n)\n\n/**\n * The same table serves both views, so its two axes are read differently here: a sideways step\n * is one day, and a vertical one is a whole week rather than an hour.\n */\nconst dayStep = (days: number, minutes: number) =>\n days !== 0 ? days : Math.sign(minutes) * columnCount.value\n\nfunction onKeydown(event: KeyboardEvent) {\n const target = event.target as HTMLElement | null\n\n // A chip answers its own table, delegated here on the same element as the pointer so the\n // two input paths cannot drift apart.\n const card = target?.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n onCardKeydown(event, card)\n return\n }\n\n const cell = target?.closest<HTMLElement>('.v-calendar-month-cell')\n if (!cell) return\n\n const intent = calendarIntent(event, 'cell', 1, isRtl())\n if (!intent) return\n\n const iso = cell.dataset.iso!\n const index = flat.value.indexOf(iso)\n const columns = columnCount.value\n\n if (intent.kind === 'moveFocus') {\n const next =\n flat.value[clamp(index + dayStep(intent.days, intent.minutes), 0, flat.value.length - 1)]\n if (!next) return\n event.preventDefault()\n moveFocusTo(next)\n return\n }\n\n if (intent.kind === 'rowEdge') {\n const row = Math.floor(index / columns)\n const edge = intent.edge === 'start' ? row * columns : row * columns + columns - 1\n const next = flat.value[clamp(edge, 0, flat.value.length - 1)]\n if (!next) return\n event.preventDefault()\n moveFocusTo(next)\n return\n }\n\n if (intent.kind === 'period') {\n event.preventDefault()\n emit('step', intent.delta)\n return\n }\n\n if (intent.kind === 'openDay') {\n event.preventDefault()\n emit('day-activate', iso)\n return\n }\n\n if (intent.kind === 'activate') {\n event.preventDefault()\n emit('cell-activate', { date: iso, minutes: null })\n }\n}\n\n/** Which of the flattened squares a point falls on. */\nfunction indexAt(x: number, y: number, rect: DOMRect, rtl: boolean): number {\n const cell = pointToMonthCell(\n { x, y },\n { ...gridGeometryOf(rect, columnCount.value, rtl), rows: props.weeks.length || 1 },\n rtl,\n )\n return cell.rowIndex * columnCount.value + cell.columnIndex\n}\n\nfunction onGridPointerdown(event: PointerEvent) {\n endLastGesture()\n if (gesture.value || !props.editable || event.button !== 0) return\n\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n const item = card ? eventsById.value.get(idOfCard(card)) : undefined\n const rect = gridEl.value?.getBoundingClientRect()\n if (!item || !rect) return\n const rtl = isRtl()\n\n start(\n {\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabIndex: indexAt(event.clientX, event.clientY, rect, rtl),\n },\n rtl,\n )\n}\n\n/** Works out which day the chip now belongs to, from wherever the pointer last was. */\nfunction applyPoint(state: Gesture, rect: DOMRect) {\n const index = indexAt(state.lastX, state.lastY, rect, state.rtl)\n const last = lastApplied\n if (last?.state === state && last.weeks === props.weeks && last.index === index) return\n lastApplied = { state, weeks: props.weeks, index }\n\n /*\n * The target is the event's START shifted by however many squares the pointer has crossed,\n * not the square it is over; so a bar grabbed by its third day keeps its third day under the\n * finger. The days come out of the flattened grid, which is already filtered to the weekdays\n * on show, so a hidden weekend is not counted.\n */\n const days = flat.value\n const from = days.indexOf(state.origin.start)\n const target =\n days[\n clamp((from === -1 ? state.grabIndex : from) + index - state.grabIndex, 0, days.length - 1)\n ]\n if (!target) return\n /*\n * Written only when the day changes. The gesture is a deep ref, so an equal but new preview\n * would still re-bucket and re-sort every event and re-render all 42 squares.\n */\n const next = moveEventToDay(state.origin, target)\n if (!sameTimes(state.preview, next)) state.preview = next\n}\n\n/**\n * The square `applyPoint` last placed the chip from, so the many `pointermove`s that fire\n * inside one square return before any date arithmetic.\n */\nlet lastApplied: { state: Gesture; weeks: MonthCell[][]; index: number } | null = null\n\n/** One arrow press on a chip held by the keyboard: a day sideways, a week vertically. */\nfunction onGrabStep(state: Gesture, step: GrabStep, item: E): boolean {\n if (step.kind !== 'grabMove') return false\n const days = flat.value\n const from = days.indexOf(state.preview.start)\n const target = days[clamp(from + dayStep(step.days, step.minutes), 0, days.length - 1)]\n if (!target || target === state.preview.start) return false\n state.preview = moveEventToDay(state.preview, target)\n emit('announce', m.value.calendar.movedTo(item.title, longDay(state.preview.start)))\n return true\n}\n\nfunction onCardClick(item: E) {\n // `pointerup` fires before `click`, so letting go at the end of a drag would ALSO open the\n // event; the consumer's editor over every chip the reader had just moved.\n if (consumeDrag()) return\n emit('event-activate', item)\n}\n\n/**\n * A click on the empty part of a day: the pointer's side of the Enter that activates a cell, so\n * a square drawn with a pointer cursor answers a click as it answers the key.\n */\nfunction onGridClick(event: MouseEvent) {\n const target = event.target as HTMLElement | null\n if (target?.closest('.v-calendar-event, button')) return\n const cell = target?.closest<HTMLElement>('.v-calendar-month-cell')\n if (!cell || consumeDrag()) return\n emit('cell-activate', { date: cell.dataset.iso!, minutes: null })\n}\n\n// The same two members VCalendarTimeGrid exposes, so VCalendar drives either view without\n// asking which is on screen. A month has no hours, hence the empty `scrollToMinutes`.\ndefineExpose({\n /** Brings the focus onto the day the grid is currently pointing at. */\n focus: (options?: FocusOptions) => {\n if (tabbable.value) focusCell(cellId(tabbable.value), false, options)\n },\n /** Nothing to scroll to in a month view. */\n scrollToMinutes: () => {},\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-calendar-month\"\n :data-outside=\"gesture?.outside ? '' : undefined\"\n :style=\"{ '--calendar-columns': String(weekdayNames.length) }\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-calendar-month-head\" aria-hidden=\"true\">\n <span\n v-for=\"(name, index) in weekdayNames\"\n :key=\"index\"\n class=\"v-calendar-weekday v-calendar-month-weekday\"\n >\n {{ name }}\n </span>\n </div>\n\n <div\n ref=\"gridEl\"\n class=\"v-calendar-month-grid v-calendar-edge-cue\"\n role=\"grid\"\n :aria-label=\"label\"\n :data-edge=\"edgeCue\"\n @keydown=\"onKeydown\"\n @pointerdown=\"onGridPointerdown\"\n @pointermove=\"onPointermove\"\n @pointerup=\"onPointerup\"\n @pointercancel=\"onPointercancel\"\n @pointerleave=\"onPointerleave\"\n @click=\"onGridClick\"\n >\n <div\n v-for=\"(week, row) in weeks\"\n :key=\"week[0]?.iso ?? row\"\n role=\"row\"\n class=\"v-calendar-month-week\"\n >\n <div\n v-for=\"cell in week\"\n :id=\"cellId(cell.iso)\"\n :key=\"cell.iso\"\n role=\"gridcell\"\n class=\"v-calendar-cell v-calendar-month-cell\"\n :data-iso=\"cell.iso\"\n :data-adjacent=\"cell.adjacent ?? undefined\"\n :tabindex=\"tabbable === cell.iso ? 0 : -1\"\n :aria-label=\"longDay(cell.iso)\"\n aria-keyshortcuts=\"Shift+Enter\"\n >\n <button\n type=\"button\"\n class=\"v-calendar-button v-calendar-month-day\"\n :class=\"{ 'v-calendar-today': cell.iso === today }\"\n tabindex=\"-1\"\n :aria-label=\"m.calendar.openDay(longDay(cell.iso))\"\n @click=\"emit('day-activate', cell.iso)\"\n >\n {{ dayNumber(cell.iso) }}\n </button>\n\n <VCalendarEvent\n v-for=\"chip in dayEventsOf(cell.iso).shown\"\n :key=\"chip.event.id\"\n class=\"v-calendar-month-chip\"\n :event=\"chip.event\"\n layout=\"chip\"\n :disabled=\"disabled\"\n :time-text=\"chip.timeText\"\n v-bind=\"cardState(chip.event.id)\"\n @click=\"onCardClick(chip.event)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n\n <button\n v-if=\"dayEventsOf(cell.iso).hidden > 0\"\n type=\"button\"\n class=\"v-calendar-button v-calendar-month-more\"\n tabindex=\"-1\"\n @click=\"emit('day-activate', cell.iso)\"\n >\n {{ m.calendar.moreEvents(dayEventsOf(cell.iso).hidden) }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-calendar-month {\n display: flex;\n flex-direction: column;\n }\n\n .v-calendar-month-head,\n .v-calendar-month-week {\n display: grid;\n grid-template-columns: repeat(var(--calendar-columns), minmax(0, 1fr));\n }\n\n /* 4, the time grid's header layer: a dragged chip (3) and the edge strip (2) pass under the\n sticky names as the month scrolls, rather than over them. */\n .v-calendar-month-head {\n position: sticky;\n inset-block-start: 0;\n z-index: 4;\n background: var(--vectis-color-surface);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-month-weekday {\n padding-block: var(--vectis-space-2);\n text-align: center;\n }\n\n .v-calendar-month-grid {\n position: relative;\n display: flex;\n /* The rows share what height there is, so a month fills the box it was given instead of\n leaving a band of nothing under the last week. The floor keeps a day readable when\n that box is short. */\n flex: 1;\n flex-direction: column;\n min-block-size: 0;\n }\n\n .v-calendar-month-week {\n flex: 1;\n min-block-size: var(--vectis-control-size-calendar-month-cell);\n }\n\n .v-calendar-month-cell {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-control-size-calendar-gap);\n overflow: hidden;\n padding: var(--vectis-space-1);\n }\n\n /*\n * The days of the neighbouring months are kept; the grid is a fixed six rows, so the page\n * never jumps; but pushed back, since they are context rather than content.\n */\n .v-calendar-month-cell[data-adjacent] {\n background: var(--vectis-color-surface-sunken);\n color: var(--vectis-color-text-subtle);\n }\n\n .v-calendar-month-day {\n align-self: center;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n inline-size: var(--vectis-control-height-sm);\n block-size: var(--vectis-control-height-sm);\n border-radius: var(--vectis-radius-pill);\n color: inherit;\n font-size: var(--vectis-text-body-md-size);\n }\n\n /* Disjoint from today rather than less specific: the two live in different sheets, where a\n tie would be settled by whichever one the consumer's bundler emitted last. */\n .v-calendar-month-day:hover:not(.v-calendar-today) {\n background: var(--vectis-color-surface-muted);\n }\n\n /*\n * The chip rule in VCalendarEvent's own sheet is (0,2,0) against this (0,1,0), so declaring\n * the size directly would lose; and matching its specificity would only hand the decision to\n * whichever sheet the consumer's bundler emitted last, which is the failure the library's\n * one-sheet-per-component rule exists to prevent.\n */\n .v-calendar-month-chip {\n --calendar-chip-height: var(--vectis-control-size-calendar-allday-lane);\n\n flex: none;\n }\n\n .v-calendar-month-more {\n flex: none;\n padding-inline: var(--vectis-space-1);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n text-align: start;\n }\n\n .v-calendar-month-more:hover {\n color: var(--vectis-color-text);\n text-decoration: underline;\n }\n\n /* The neighbouring months' days are told apart by a tint forced colors removes: grey text\n keeps them apart. */\n @media (forced-colors: active) {\n .v-calendar-month-cell[data-adjacent] {\n forced-color-adjust: none;\n background: Canvas;\n color: GrayText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -14,11 +14,7 @@ export interface CalendarTimeGridProps<T> {
|
|
|
14
14
|
format: CalendarFormat;
|
|
15
15
|
/** Today's date, or nothing on the server, where it cannot be known. */
|
|
16
16
|
today: string | null;
|
|
17
|
-
/**
|
|
18
|
-
* The time it is now, in minutes since midnight: again nothing on the server. It is
|
|
19
|
-
* passed in rather than read here so that one clock drives every view, and so that a
|
|
20
|
-
* calendar showing no time grid still costs no timer.
|
|
21
|
-
*/
|
|
17
|
+
/** The time it is now, in minutes since midnight: again nothing on the server. */
|
|
22
18
|
now: number | null;
|
|
23
19
|
/** Whether events can be moved and stretched. */
|
|
24
20
|
editable: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VCalendarTimeGrid.js","names":[],"sources":["../../../src/components/VCalendar/VCalendarTimeGrid.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"E extends CalendarEvent\">\n// @a11y @keyboard @core\n/**\n * The time grid: a column per day on show, an hour per row, the events drawn over it.\n * Internal to VCalendar, whose documentation covers it. It computes nothing about dates or\n * overlaps — it is handed the days and the already-placed segments and turns them into boxes.\n *\n * WHY 168 REAL CELLS rather than one repeating gradient. A background is forced to `Canvas`\n * under Windows forced-colors and the ruling would vanish, where a border is forced to\n * `CanvasText` and survives (the VSeparator argument); and the ARIA grid pattern needs real\n * cells to move focus between, which is also what makes the scrolling region Tab-reachable.\n *\n * WHY THE CARDS SIT INSIDE THE CELLS, when one overlay layer would be simpler CSS. An\n * absolutely positioned element leaves its parent's box for LAYOUT while staying inside it\n * in the document, hence in the accessibility tree. In an overlay they would sit outside the\n * grid entirely, breaking the reading order and failing `aria-required-children`.\n *\n * The day names are NOT column headers: they stay in view above the scrolling area, which\n * puts them outside the grid. Every cell names its own day in full instead (\"Wednesday 10\n * June, 09:00\"), so nothing is lost to a reader who never sees that row.\n *\n * The drag and the keyboard grab are `gesture.ts`'s. What stays here is what a moment of the\n * day means: the point under the pointer turned into a slot, and the four kinds of gesture a\n * time grid offers.\n */\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport { formatDateDisplay } from '../../utils/date'\nimport { clamp } from '../../utils/number'\nimport { formatTimeDisplay } from '../../utils/time'\n\nimport { useMessages } from '../../i18n/state'\n\nimport VCalendarEvent from './VCalendarEvent.vue'\nimport { EDGE_BAND } from './edgeStep'\nimport {\n gridGeometryOf,\n useCalendarGesture,\n type GestureBase,\n type GestureInit,\n type GrabStep,\n} from './gesture'\nimport { calendarIntent } from './keyboard'\nimport {\n MINUTES_PER_DAY,\n MINUTES_PER_HOUR,\n blockEdgeAt,\n floorToSlot,\n fractionOf,\n isAllDayEvent,\n minutesAt,\n moveEventToDay,\n packAllDay,\n packDayColumn,\n pointToCell,\n sameTimes,\n snapToSlot,\n spansMidnight,\n timeOf,\n timedSegments,\n timesOf,\n type AllDaySpan,\n type PlacedSegment,\n type SegmentPart,\n type TimeWindow,\n} from './layout'\nimport { drawnSlot, moveTimedEvent, resizeTimedEvent, roundToSlot } from './timeGrid'\nimport type {\n ActivatedCell,\n CalendarEvent,\n CalendarEventId,\n CalendarEventSlotProps,\n CalendarEventTimes,\n CalendarFormat,\n} from './types'\n\n/** Where the keyboard focus is in the grid, as a day and a moment in it. */\nexport interface FocusedCell {\n iso: string\n minutes: number\n}\n\nexport interface CalendarTimeGridProps<T> {\n days: string[]\n events: T[]\n timeWindow: TimeWindow\n slotDuration: number\n locale: string\n format: CalendarFormat\n /** Today's date, or nothing on the server, where it cannot be known. */\n today: string | null\n /**\n * The time it is now, in minutes since midnight: again nothing on the server. It is\n * passed in rather than read here so that one clock drives every view, and so that a\n * calendar showing no time grid still costs no timer.\n */\n now: number | null\n /** Whether events can be moved and stretched. */\n editable: boolean\n /** Whether the whole calendar is frozen, which also takes its cards out of the tab order. */\n disabled: boolean\n /** Whether an empty stretch of a day can be drawn out, and reported as a new event's times. */\n creatable: boolean\n /** The node telling a reader how a card can be moved, shared by every card. */\n hintId: string\n /** How long a drag rests against an edge before the view pages. Zero turns paging off. */\n edgeStepDelay: number\n /** Whether dragging near the top or bottom scrolls the grid. */\n autoScroll: boolean\n /** What the grid as a whole is called: the range it is showing. */\n label: string\n}\n\nconst props = defineProps<CalendarTimeGridProps<E>>()\n\nconst emit = defineEmits<{\n /** An empty part of the grid was activated, at this day and this moment. */\n 'cell-activate': [cell: ActivatedCell]\n /** A card was clicked or activated. */\n 'event-activate': [event: E]\n /** An event was dropped somewhere else, or stretched to a new end. */\n 'event-drop': [id: CalendarEventId, times: CalendarEventTimes, kind: 'move' | 'resize']\n /** An empty stretch of a day was drawn out, and is offered as a new event's times. */\n 'slot-create': [times: CalendarEventTimes]\n /** Something happened that a reader who cannot see the grid needs told. */\n announce: [message: string]\n /** A page key asked for the previous or next period, which only the calendar can do. */\n step: [delta: -1 | 1]\n}>()\n\n/** Which cell holds the tab stop. The calendar owns it, so the toolbar can move it too. */\nconst focused = defineModel<FocusedCell>('focused', { required: true })\n\ndefineSlots<{\n event?(props: CalendarEventSlotProps<E>): unknown\n 'day-header'?(props: { iso: string; weekday: string; dayText: string; today: boolean }): unknown\n 'all-day-label'?(): unknown\n}>()\n\nconst m = useMessages()\nconst uid = useId()\n\n/** The hour each row stands for, as minutes since midnight. */\nconst hours = computed(() => {\n const rows: number[] = []\n const { start, end } = props.timeWindow\n for (let at = start; at < end; at += MINUTES_PER_HOUR) rows.push(at)\n return rows\n})\n\n/**\n * The id a stretch of empty grid carries while it is being drawn out. It never leaves this\n * component: on release the calendar is handed the times alone, and the card is gone.\n */\nconst DRAFT_ID = '__vectis-calendar-draft__'\n\n/**\n * A gesture under way, whether it came from a pointer or from the keyboard.\n *\n * `move-days` is the whole-day one: an all-day bar dragged along the band above the grid. It\n * is a kind of its own rather than a flag on `move` because the two answer different\n * questions — one asks what MOMENT the pointer is over, the other only what day — and because\n * a bar spanning three days must keep spanning three when it lands.\n */\ninterface Gesture extends GestureBase {\n kind: 'move' | 'move-days' | 'resize' | 'create'\n /**\n * How far the arrows have taken a KEYBOARD grab from its origin: days along the days on\n * show, and minutes moved (the start) and stretched (the end). Every step is worked out from\n * the origin with these, as every pointer frame is, and never from the step before it: held\n * against the end of the day a step is written 23:59, and building on that would take a\n * minute off the event at each press.\n */\n keyDays?: number\n keyStart?: number\n keyEnd?: number\n /**\n * How far after the event's start the pointer took hold, in minutes, so the card does not\n * jump under it. Counted across midnight: an event running from 22:00 taken by its morning\n * card at 01:00 is held three hours in.\n */\n grabOffset: number\n grabColumn: number\n /**\n * The share of its column each of the dragged event's boxes had when the gesture began, filed\n * by which piece of the event it was — and nothing else.\n *\n * That much is recorded because the echo must not take part in the overlap layout: were it\n * packed alongside the card being dragged, the two would share the width of their column\n * the moment they overlapped, and the card under the pointer would halve in width and then\n * grow back as it moved away.\n *\n * WHERE the echo goes is deliberately NOT recorded, and is recomputed on every render from\n * the origin times against the days currently on show. Holding at an edge pages the view\n * under a live drag, and both of these boxes are described by INDEX into `props.days`: a\n * `dayIndex` captured in one week names a different date in the next, so a frozen box would\n * follow the column rather than the day — an event dragged out of Tuesday the 9th leaving\n * its echo on Tuesday the 2nd, instead of leaving with the week it belonged to.\n *\n * At most one of the two is ever set — a timed event is one or two segments, an all-day one a\n * bar.\n */\n ghostColumns: Partial<\n Record<SegmentPart, Pick<PlacedSegment, 'column' | 'span' | 'columns'>>\n > | null\n ghostLane: number | null\n}\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst canvasEl = ref<HTMLElement | null>(null)\nconst columnsEl = ref<HTMLElement | null>(null)\n\nconst {\n gesture,\n ghostEvent,\n withPreview,\n edgeCue,\n isRtl,\n start,\n reapply,\n endLastGesture,\n consumeDrag,\n onPointermove,\n onPointerup,\n onPointercancel,\n onPointerleave,\n onFocusout,\n onCardKeydown,\n focusCell,\n idOfCard,\n cardState,\n} = useCalendarGesture<E, Gesture>({\n events: () => props.events,\n eventOf: (id): E | undefined => eventOf(id),\n rootEl,\n // The SCROLLER, not the columns box: the all-day band sits in the sticky header, and this is\n // the only element that contains both it and the canvas.\n captureEl: rootEl,\n measureEl: columnsEl,\n columns: () => props.days.length,\n editable: () => props.editable,\n hintId: () => props.hintId,\n slotMinutes: () => props.slotDuration,\n edgeStepDelay: () => props.edgeStepDelay,\n layout: () => props.days,\n applyPoint,\n // Scrolling is measured against the SCROLLER, which is what actually moves; paging, in the\n // composable, against the columns the days occupy.\n onEdges: (state, scroller) =>\n setScrollSpeed(\n props.autoScroll && scroller ? blockEdgeAt(state.lastY, scroller, EDGE_BAND) : 0,\n ),\n onRelease: stopScrolling,\n onLetGo: (state) => {\n if (state.kind !== 'create') return false\n // A press that never travelled drew nothing: its click reports the cell instead.\n if (state.moved) emit('slot-create', state.preview)\n return true\n },\n onDrop: (state) =>\n emit('event-drop', state.id, state.preview, state.kind === 'resize' ? 'resize' : 'move'),\n // An all-day bar is taken by whole days, the pointer's own gesture on the band.\n grab: (item) =>\n props.editable\n ? initFor({\n kind: isAllDayEvent(item) ? 'move-days' : 'move',\n id: item.id,\n origin: timesOf(item),\n pointerId: null,\n originX: 0,\n originY: 0,\n grabOffset: 0,\n grabColumn: 0,\n })\n : null,\n onGrabStep,\n step: (delta) => emit('step', delta),\n announce: (message) => emit('announce', message),\n})\n\n/**\n * Where the echo goes, worked out afresh on every render rather than remembered.\n *\n * The two helpers are the SAME ones every other box goes through, run on a list of one — so\n * the echo answers to the days on show exactly as an ordinary event does, and in particular\n * is dropped when its day is not among them. That is what makes it leave with its week when\n * a drag held at an edge pages the view, rather than squatting a column of the new week.\n * It is also what keeps the two kinds mutually exclusive by construction rather than by\n * chance: `timedSegments` passes over an all-day event and `packAllDay` over a timed one.\n *\n * Only the packing captured at the start of the gesture is put back on top — see\n * `ghostColumns` above for why that one part must NOT be recomputed. It is matched by PIECE\n * rather than by position, since paging can leave the morning of an overnight echo on show\n * without its evening.\n */\nconst NO_GHOSTS: readonly PlacedSegment[] = Object.freeze([])\n\n/*\n * TRAP — the \"no echo\" answer is ONE shared array. A bar dragged along the band recomputes this\n * on every slot; a fresh `[]` each time is a new value, which would wake `placed` and re-pack\n * every column for a gesture that never touches them.\n */\nconst ghostSegments = computed<readonly PlacedSegment[]>(() => {\n const state = gesture.value\n const event = ghostEvent.value\n const packing = state?.ghostColumns\n if (!event || !packing) return NO_GHOSTS\n return timedSegments([event], props.days, props.timeWindow, props.slotDuration).flatMap(\n (segment) => {\n const columns = packing[segment.part]\n return columns ? [{ ...segment, ...columns }] : []\n },\n )\n})\n\nconst ghostSpan = computed<AllDaySpan | null>(() => {\n const state = gesture.value\n const event = ghostEvent.value\n if (!event || state?.ghostLane == null) return null\n const [span] = packAllDay([event], props.days)\n return span ? { ...span, lane: state.ghostLane } : null\n})\n\n/*\n * The model split in two, once per change to it: the timed events the columns draw, and the\n * all-day ones the band above them draws. The split is what lets a gesture rebuild only its\n * own half — see `drawnTimed`.\n */\nconst timedEvents = computed(() => props.events.filter((event) => !isAllDayEvent(event)))\nconst allDayEvents = computed(() => props.events.filter((event) => isAllDayEvent(event)))\n\n/**\n * The timed events as the columns should currently DRAW them — which is the model, with the\n * one being gestured at moved to wherever it is now.\n *\n * Running the preview through the same packing as everything else is what makes the other\n * events reflow live around the one being dragged, with no special case anywhere: the layout\n * does not know a gesture is happening, only that an event is somewhere.\n *\n * TRAP — the half a gesture belongs to is decided by its KIND and never by looking at the\n * preview. `move-days` is the only gesture that starts on the band, and the three others only\n * start on a timed card or an empty cell (both press handlers and the keyboard grab refuse an\n * all-day event), so the two lists below can each return their untouched half as the SAME\n * array. A computed whose value keeps its identity wakes nothing downstream: that is the whole\n * of why a timed drag does not re-pack the band, and a bar dragged along the band does not\n * re-pack seven columns. Reading `state.preview` in the wrong half would silently undo it.\n */\nconst drawnTimed = computed<E[]>(() => {\n const state = gesture.value\n if (!state || state.kind === 'move-days') return timedEvents.value\n\n if (state.kind === 'create') {\n // Nothing is drawn until the press travels, so a plain click on a cell flashes no card.\n if (!state.moved) return timedEvents.value\n const draft = {\n id: DRAFT_ID,\n title: m.value.calendar.untitled,\n ...state.preview,\n // A draft carries exactly the fields `CalendarEvent` declares, and a consumer's own\n // type may require more. It exists while the pointer is down and never reaches the\n // model, which is what makes the cast safe.\n } as unknown as E\n return [...timedEvents.value, draft]\n }\n\n return withPreview(timedEvents.value)\n})\n\n/** The all-day bars as the band should currently draw them — see `drawnTimed`. */\nconst drawnAllDay = computed<E[]>(() =>\n gesture.value?.kind === 'move-days' ? withPreview(allDayEvents.value) : allDayEvents.value,\n)\n\nconst timedById = computed(\n () => new Map<CalendarEventId, E>(drawnTimed.value.map((event) => [event.id, event])),\n)\nconst allDayById = computed(\n () => new Map<CalendarEventId, E>(drawnAllDay.value.map((event) => [event.id, event])),\n)\n\n/** Any event on show, as it is currently drawn. For the handlers; the template reads a half. */\nconst eventOf = (id: CalendarEventId): E | undefined =>\n timedById.value.get(id) ?? allDayById.value.get(id)\n\n/**\n * The bars above the grid, and how many rows they need between them.\n *\n * The echo is taken OUT of the packing and put back with the position it had when the drag\n * began. Left in, it would compete for lanes with the bar being dragged, and both would jump\n * a row whenever they happened to overlap.\n */\nconst allDay = computed(() => {\n const ghost = ghostSpan.value\n const spans = packAllDay(\n ghost ? drawnAllDay.value.filter((event) => event.id !== ghost.id) : drawnAllDay.value,\n props.days,\n )\n return ghost ? [...spans, ghost] : spans\n})\nconst allDayLanes = computed(() =>\n allDay.value.reduce((most, span) => Math.max(most, span.lane + 1), 0),\n)\n\n/** The row a moment of the window is drawn in, held inside the grid. */\nconst rowOf = (minutes: number) =>\n clamp(\n Math.floor((minutes - props.timeWindow.start) / MINUTES_PER_HOUR),\n 0,\n hours.value.length - 1,\n )\n\n/**\n * Where the current-time line goes, or nothing when it has no business being drawn: on the\n * server, on a range that does not include today, or at an hour the window has cropped\n * away.\n */\nconst nowMark = computed(() => {\n if (props.now === null || props.today === null) return null\n const dayIndex = props.days.indexOf(props.today)\n if (dayIndex === -1) return null\n if (props.now < props.timeWindow.start || props.now > props.timeWindow.end) return null\n return {\n day: dayIndex,\n // Clamped so the line at the very end of the window still has a cell to live in.\n row: rowOf(props.now),\n fraction: fractionOf(props.now, props.timeWindow),\n }\n})\n\n/**\n * The placed boxes, day by day. `packDayColumn` works on one column at a time, so the\n * segments are split before it sees them: events on different days never compete for width,\n * and packing them together would make a busy Monday narrow a quiet Tuesday.\n */\nconst placed = computed(() => {\n const segments = timedSegments(drawnTimed.value, props.days, props.timeWindow, props.slotDuration)\n const ghosts = ghostSegments.value\n // Read off the echo's own boxes, never off `ghostEvent`, which changes with every slot of a\n // bar's drag and would wake this for the band's gesture too.\n const ghostId = ghosts[0]?.id\n\n const byDay = new Map<number, typeof segments>()\n for (const segment of segments) {\n /*\n * The echo is held OUT of the packing and put back below with the width and column it had\n * when the drag began. Packed with the rest, it would share its column with the card\n * being dragged the moment the two overlapped — so the card under the pointer would halve\n * in width and then grow back as it moved away, which reads as the drag going wrong.\n */\n if (ghostId !== undefined && segment.id === ghostId) continue\n const list = byDay.get(segment.dayIndex)\n if (list) list.push(segment)\n else byDay.set(segment.dayIndex, [segment])\n }\n\n const real = [...byDay.values()].flatMap((list) => packDayColumn(list))\n return ghosts.length > 0 ? [...real, ...ghosts] : real\n})\n\nconst cellId = (iso: string, minutes: number) => `${uid}-c-${iso}-${minutes}`\n\n/*\n * Every label the grid writes, derived once per day and once per hour rather than once per\n * CELL — the shape `byCell` below uses, for the same reason.\n *\n * The grid renders `hours × days` cells, 24 × 7 by default, and a drag re-renders the whole\n * template every time it carries its event into another slot. Read straight from\n * `formatDateDisplay` in the template that would be 168 `cellLabel` calls per such frame, each\n * doing a `parseISO`, a `JSON.stringify` for the formatter cache key and two `Intl` formats —\n * none of which depends on the pointer. These two maps, of 7 and 24 entries, recompute when the\n * locale, the days or the window change and never because a card moved.\n *\n * Both are keyed by exactly what the template walks, `days` and `hours`, so every lookup is\n * a hit and none of them needs a formatting fallback.\n */\nconst dayLabels = computed(() => {\n const map = new Map<string, { short: string; number: string; full: string }>()\n for (const iso of props.days) {\n map.set(iso, {\n short: formatDateDisplay(iso, props.locale, { weekday: 'short' }),\n number: formatDateDisplay(iso, props.locale, { day: 'numeric' }),\n full: formatDateDisplay(iso, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n }),\n })\n }\n return map\n})\n\nconst hourLabels = computed(() => {\n const map = new Map<number, string>()\n for (const minutes of hours.value)\n map.set(minutes, formatTimeDisplay(timeOf(minutes), props.locale, props.format))\n return map\n})\n\nconst hourLabel = (minutes: number) => hourLabels.value.get(minutes) ?? ''\nconst dayName = (iso: string) => dayLabels.value.get(iso)?.short ?? ''\nconst dayNumber = (iso: string) => dayLabels.value.get(iso)?.number ?? ''\n\n/**\n * The cells, row by row, with everything about them that no gesture can change: the id the\n * focus is moved by, the name a reader hears, and the number `byCell` files cards under.\n *\n * The labels' argument carried one step further. Written in the template, each of the 168\n * cells built its id and its label on every render, and a `${day}:${row}` string twice more,\n * once to look its cards up and once to ask whether the current-time line was in it. Here that\n * is paid when the days, the window or the locale change, and a render only reads it.\n */\nconst gridRows = computed(() => {\n const columns = props.days.length\n return hours.value.map((minutes, row) => ({\n minutes,\n cells: props.days.map((iso, day) => ({\n iso,\n day,\n row,\n key: row * columns + day,\n id: cellId(iso, minutes),\n // The day in full, then the hour.\n label: `${dayLabels.value.get(iso)?.full ?? iso}, ${hourLabel(minutes)}`,\n })),\n }))\n})\n\n/**\n * An event's times, written out for the reader. The time zone is appended as an annotation\n * and never applied: the card's place in the grid comes from the local times given, which\n * is the whole of the design system's position on the matter.\n *\n * Memoized by the three fields the text is made of, because `byCell` asks for every card each\n * time a drag changes slot and all but one of them still say what they said before: two `Intl`\n * formats per card otherwise. The cache lives INSIDE the computed, so a change of locale or\n * hour format throws it away whole.\n *\n * TRAP — what a dependant reads has to be this computed, never a cache held beside it. A hit\n * reads no prop, so a computed that only ever hit would track neither the locale nor the hour\n * format, and would go on printing the old ones after either changed.\n */\nconst timeTextOf = computed(() => {\n const { locale, format } = props\n const cache = new Map<string, string>()\n return (event: CalendarEvent): string => {\n const key = `${event.startTime}|${event.endTime}|${event.timezone ?? ''}`\n let text = cache.get(key)\n if (text === undefined) {\n const start = formatTimeDisplay(event.startTime, locale, format)\n const end = formatTimeDisplay(event.endTime, locale, format)\n const range = `${start} – ${end}`\n text = event.timezone ? `${range} (${event.timezone})` : range\n cache.set(key, text)\n }\n return text\n }\n})\n\n/** One card as the template draws it, with its event and its box already worked out. */\ninterface Card {\n segment: PlacedSegment\n event: E\n timeText: string\n style: Record<string, string>\n}\n\n/**\n * The cards, filed under the cell their start falls in. Grouping once here is what keeps\n * the template from asking every cell to search the whole list — a hundred and sixty-eight\n * searches per render otherwise.\n *\n * Each card also carries what the template would otherwise work out per card per render: its\n * event, its time text and its style object. A render the layout did not cause — the edge cue\n * lighting up, the pointer leaving the calendar, the tab stop moving — rebuilds none of it.\n */\nconst byCell = computed(() => {\n const map = new Map<number, Card[]>()\n const columns = props.days.length\n const events = timedById.value\n const textOf = timeTextOf.value\n for (const segment of placed.value) {\n // A placement always has an event behind it. Skipping rather than asserting turns a\n // mismatch between the two into one missing card instead of a grid that fails to render.\n const event = events.get(segment.id)\n if (!event) continue\n const key = rowOf(segment.start) * columns + segment.dayIndex\n const card: Card = {\n segment,\n event,\n timeText: textOf(event),\n style: {\n '--calendar-day-index': String(segment.dayIndex),\n '--calendar-block-start-fraction': String(fractionOf(segment.start, props.timeWindow)),\n '--calendar-block-end-fraction': String(fractionOf(segment.end, props.timeWindow)),\n '--calendar-block-column': String(segment.column),\n '--calendar-block-span': String(segment.span),\n '--calendar-block-columns': String(segment.columns),\n },\n }\n const list = map.get(key)\n if (list) list.push(card)\n else map.set(key, [card])\n }\n return map\n})\n\n/** An hour with nothing in it, shared rather than allocated per empty cell. */\nconst NO_CARDS: readonly Card[] = Object.freeze([])\nconst cardsAt = (key: number) => byCell.value.get(key) ?? NO_CARDS\n\nconst isFocused = (iso: string, minutes: number) =>\n focused.value.iso === iso && focused.value.minutes === minutes\n\n/**\n * Which cell holds the tab stop: the focused one, rounded to its row — or, when that cell is\n * not on screen (after a view change, or before anything has been focused at all), the first\n * cell. Without the fallback no cell would carry `tabindex=\"0\"`, the grid would drop out of the\n * tab order, and the scrolling region would have nothing reachable inside it, which is an\n * accessibility failure and not merely an inconvenience.\n */\nconst tabbable = computed<FocusedCell | null>(() => {\n const current = focused.value\n const { start, end } = props.timeWindow\n if (props.days.includes(current.iso)) {\n const rounded =\n Math.floor((current.minutes - start) / MINUTES_PER_HOUR) * MINUTES_PER_HOUR + start\n if (rounded >= start && rounded < end) return { iso: current.iso, minutes: rounded }\n }\n const iso = props.days[0]\n return iso ? { iso, minutes: start } : null\n})\n\nconst isTabStop = (iso: string, minutes: number) =>\n tabbable.value?.iso === iso && tabbable.value.minutes === minutes\n\nfunction moveFocusTo(iso: string, minutes: number) {\n focused.value = { iso, minutes }\n // @a11y — the model is what decides which cell is tabbable, so the DOM focus can only be\n // moved once the render that applied it has run.\n focusCell(cellId(iso, minutes), true)\n}\n\n/** A day, then a start and an end: where a reader is told an event now is. */\nfunction timesText(times: CalendarEventTimes) {\n const start = formatTimeDisplay(times.startTime, props.locale, props.format)\n const end = formatTimeDisplay(times.endTime, props.locale, props.format)\n const day = formatDateDisplay(times.start, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n })\n return `${day}, ${start} – ${end}`\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const target = event.target as HTMLElement | null\n\n // A card answers a different table from the grid it sits in — that is what a grab mode IS.\n // Delegated here, on the same element as the pointer, so the two never drift apart.\n const card = target?.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n onCardKeydown(event, card)\n return\n }\n\n const cell = target?.closest<HTMLElement>('.v-calendar-cell')\n if (!cell) return\n\n const intent = calendarIntent(event, 'cell', props.slotDuration, isRtl())\n if (!intent) return\n\n const iso = cell.dataset.iso!\n const minutes = Number(cell.dataset.minutes)\n const index = props.days.indexOf(iso)\n\n if (intent.kind === 'moveFocus') {\n const day = props.days[clamp(index + intent.days, 0, props.days.length - 1)]!\n const at = clamp(\n minutes + intent.minutes,\n props.timeWindow.start,\n props.timeWindow.end - MINUTES_PER_HOUR,\n )\n event.preventDefault()\n moveFocusTo(day, at)\n return\n }\n\n if (intent.kind === 'rowEdge') {\n const day = intent.edge === 'start' ? props.days[0] : props.days.at(-1)\n if (!day) return\n event.preventDefault()\n moveFocusTo(day, minutes)\n return\n }\n\n if (intent.kind === 'period') {\n event.preventDefault()\n emit('step', intent.delta)\n return\n }\n\n /*\n * Enter reports the cell, `creatable` or not. That is the keyboard's route to a new event\n * (WCAG 2.1.1): the cell carries a day and an hour, and how long the event lasts is a question\n * for the consumer's own form, where the pointer answers it by how far it was drawn.\n */\n // A time grid has no day to open; Shift and Enter report the cell, as Enter does.\n if (intent.kind === 'activate' || intent.kind === 'openDay') {\n event.preventDefault()\n emit('cell-activate', { date: iso, minutes })\n }\n}\n\n/**\n * A gesture's opening state, with the echo's shape read off the layout as it stands.\n *\n * TRAP — read BEFORE the gesture is set. Both are derived from the drawn events, which the\n * gesture immediately starts rewriting — a line later and they would already describe the\n * preview rather than the place it came from. Which of the two answers is also what says\n * whether this is a timed card or an all-day bar, and therefore which of the echo's two shapes\n * the gesture has to carry.\n */\nfunction initFor(\n init: Omit<GestureInit<Gesture>, 'ghostColumns' | 'ghostLane'>,\n): GestureInit<Gesture> {\n const segments = placed.value.filter((item) => item.id === init.id)\n const span = allDay.value.find((item) => item.id === init.id)\n return {\n ...init,\n ghostColumns:\n segments.length > 0\n ? Object.fromEntries(\n segments.map((segment) => [\n segment.part,\n { column: segment.column, span: segment.span, columns: segment.columns },\n ]),\n )\n : null,\n ghostLane: span ? span.lane : null,\n }\n}\n\n/** The slot under a point, read against the measured columns box. */\nconst pointAt = (x: number, y: number, rect: DOMRect, rtl: boolean) =>\n pointToCell({ x, y }, gridGeometryOf(rect, props.days.length, rtl), props.timeWindow, rtl)\n\n/**\n * Every press on the grid, dispatched by WHAT it landed on.\n *\n * One delegated handler rather than a listener per card and per strip. That is not only\n * fewer bindings on a grid that may hold a hundred: it is what removes the question of\n * which listener runs first. Three handlers stacked on nested elements would each have to\n * guard against the others, and `stopPropagation` would make the answer depend on the order\n * they happened to be bound in. Here the target is examined once and the gesture chosen\n * once, so the strip inside a card and the card inside a cell cannot disagree.\n */\nfunction onGridPointerdown(event: PointerEvent) {\n endLastGesture()\n // Left button only: a right-click is opening a context menu, not starting a drag.\n if (gesture.value || event.button !== 0) return\n\n const target = event.target as HTMLElement | null\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!target || !rect) return\n const rtl = isRtl()\n const point = pointAt(event.clientX, event.clientY, rect, rtl)\n\n const card = target.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n if (!props.editable) return\n const item = eventOf(idOfCard(card))\n if (!item || isAllDayEvent(item)) return\n\n const resizing = target.closest('[data-calendar-handle]') !== null\n // The morning card of an overnight event is held from the evening before, so the offset\n // carries the day between the two.\n const pressedMorning = spansMidnight(item) && props.days[point.columnIndex] === item.end\n start(\n initFor({\n kind: resizing ? 'resize' : 'move',\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: resizing\n ? 0\n : (pressedMorning ? MINUTES_PER_DAY : 0) +\n point.minutes -\n minutesAt(item.startTime, props.timeWindow.start),\n grabColumn: point.columnIndex,\n }),\n rtl,\n )\n return\n }\n\n if (!props.creatable || !target.closest('.v-calendar-cell')) return\n const iso = props.days[point.columnIndex]\n if (!iso) return\n\n /*\n * The pressed slot is where the drawing is anchored — rounded DOWN, never to the nearest,\n * because a press at 09:07 means \"the nine o'clock slot\" and not the quarter past. Nothing is\n * drawn and nothing is reported until the press travels: a press that stays still is a click,\n * and reports its cell like any other.\n */\n const from = floorToSlot(point.minutes, props.slotDuration)\n start(\n initFor({\n kind: 'create',\n id: DRAFT_ID,\n origin: {\n start: iso,\n end: iso,\n startTime: timeOf(from),\n endTime: timeOf(Math.min(from + props.slotDuration, props.timeWindow.end)),\n },\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: 0,\n grabColumn: point.columnIndex,\n }),\n rtl,\n )\n}\n\n/**\n * A press on the band of all-day bars above the grid.\n *\n * It has a handler of its own rather than joining the delegated one below it, because the\n * band is not inside `.v-calendar-columns` — it lives in the sticky header, which is the one\n * part of the calendar that scroll does not carry. Everything after the press IS shared: the\n * capture, the move, the release, all of them on the scroller both boxes sit in.\n */\n/**\n * The band's bars answer the same card table as the grid's cards. They sit in the sticky\n * header, outside the grid whose keydown the cards reach, so the band delegates on its own.\n */\nfunction onBandKeydown(event: KeyboardEvent) {\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n if (card) onCardKeydown(event, card)\n}\n\nfunction onBandPointerdown(event: PointerEvent) {\n endLastGesture()\n if (gesture.value || !props.editable || event.button !== 0) return\n\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n const item = card ? eventOf(idOfCard(card)) : undefined\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!item || !rect) return\n const rtl = isRtl()\n\n start(\n initFor({\n kind: 'move-days',\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: 0,\n grabColumn: pointAt(event.clientX, event.clientY, rect, rtl).columnIndex,\n }),\n rtl,\n )\n}\n\n/**\n * One arrow press on an all-day bar held by the keyboard: a whole day sideways along the days\n * on show, nothing else, since a bar has no hours to move through or to stretch.\n */\nfunction onBarStep(state: Gesture, step: GrabStep, item: E): boolean {\n if (step.kind !== 'grabMove' || step.days === 0) return false\n const keyDays = (state.keyDays ?? 0) + step.days\n const base = props.days.indexOf(state.origin.start)\n const column = clamp(base + keyDays, Math.min(base, 0), props.days.length - 1)\n const next = column < 0 ? state.origin : moveEventToDay(state.origin, props.days[column]!)\n if (sameTimes(next, state.preview)) return false\n state.keyDays = keyDays\n state.preview = next\n emit('announce', m.value.calendar.movedTo(item.title, timesText(state.preview)))\n return true\n}\n\n/**\n * Works out where the dragged event now belongs, from wherever the pointer last was.\n *\n * It is separate from the handler because THREE things call it: the pointer moving, the view\n * paging out from under a still pointer, and the grid scrolling under one. The last two would\n * otherwise leave the card behind on a day or an hour that is no longer where the pointer is.\n */\nfunction applyPoint(state: Gesture, rect: DOMRect) {\n const point = pointAt(state.lastX, state.lastY, rect, state.rtl)\n const crossMidnight = spansMidnight(state.origin)\n\n /*\n * The slot the pointer is over, reduced to the two numbers the preview is computed from. A bar\n * reads only the column and a drawn slot only the minute; a stretch reads the column too when\n * its event runs past midnight, whose end may sit on either of two days. A move's start is\n * rounded without being held inside the day, since an overnight event's may leave it.\n */\n const column =\n state.kind === 'create' || (state.kind === 'resize' && !crossMidnight) ? 0 : point.columnIndex\n const minutes =\n state.kind === 'move-days'\n ? 0\n : state.kind === 'move'\n ? roundToSlot(point.minutes - state.grabOffset, props.slotDuration)\n : state.kind === 'create'\n ? floorToSlot(point.minutes, props.slotDuration)\n : snapToSlot(point.minutes, props.slotDuration)\n if (isLastApplied(state, column, minutes)) return\n\n if (state.kind === 'create') {\n const anchor = minutesAt(state.origin.startTime, props.timeWindow.start)\n setPreview(\n state,\n drawnSlot(state.origin.start, anchor, minutes, props.slotDuration, props.timeWindow),\n )\n return\n }\n\n if (state.kind === 'resize') {\n const endDay = props.days[column] ?? state.origin.end\n setPreview(\n state,\n resizeTimedEvent(\n state.origin,\n endDay,\n minutes,\n props.slotDuration,\n props.timeWindow,\n crossMidnight,\n ),\n )\n return\n }\n\n if (state.kind === 'move-days') {\n /*\n * The target is the bar's START shifted by however many columns the pointer has crossed,\n * not the column the pointer is over: grab a Monday-to-Wednesday bar by its Tuesday and the\n * pointer stays on its Tuesday. The day is read out of the list of days on show — never from\n * date arithmetic on how many columns were crossed, which would count the days the calendar\n * is hiding and land the event on a Saturday nobody can see. A bar keeps its length in days.\n */\n const target = clamp(\n originColumn(state) + (column - state.grabColumn),\n 0,\n props.days.length - 1,\n )\n const iso = props.days[target]\n if (iso) setPreview(state, moveEventToDay(state.origin, iso))\n return\n }\n\n /*\n * A timed card is placed from the column the pointer is over, and its start from the pointer\n * less the offset it was taken hold at. For an overnight event taken by its morning that start\n * lands on the day before the pointer's column, which `moveTimedEvent` works out.\n */\n const iso = props.days[column]\n if (!iso) return\n setPreview(state, moveTimedEvent(state.origin, iso, minutes, props.timeWindow, crossMidnight))\n}\n\n/**\n * The slot `applyPoint` last worked a preview out for.\n *\n * `pointermove` fires many times per slot, and so does the auto-scroll frame. Every one of\n * them that lands on the slot already shown returns here, before any date arithmetic. The\n * days, the window and the step are part of what the slot MEANS, so a change to any of them\n * (paging under a still pointer, most of all) reads as a new slot even at the same indices.\n * It is a plain variable rather than a field of the gesture: nothing renders it, and the\n * gesture's own identity is what says a new press has begun.\n */\nlet lastApplied: {\n state: Gesture\n days: readonly string[]\n timeWindow: TimeWindow\n step: number\n column: number\n minutes: number\n} | null = null\n\nfunction isLastApplied(state: Gesture, column: number, minutes: number): boolean {\n const last = lastApplied\n if (\n last?.state === state &&\n last.days === props.days &&\n last.timeWindow === props.timeWindow &&\n last.step === props.slotDuration &&\n last.column === column &&\n last.minutes === minutes\n ) {\n return true\n }\n lastApplied = {\n state,\n days: props.days,\n timeWindow: props.timeWindow,\n step: props.slotDuration,\n column,\n minutes,\n }\n return false\n}\n\n/**\n * Writes the preview only when it says something new.\n *\n * TRAP — the comparison is the whole point, not a nicety. The gesture is a deep ref, so any\n * assignment to `preview`, an equal object included, wakes the drawn events, the packing of\n * every column and a render of all 168 cells. Two positions inside one slot, or two slots that\n * clamp to the same times against the end of the window, must cost nothing.\n */\nfunction setPreview(state: Gesture, next: CalendarEventTimes) {\n if (!sameTimes(state.preview, next)) state.preview = next\n}\n\n/**\n * Which column the bar being moved started in.\n *\n * The fallback is what makes the formula above general rather than a special case. The two\n * differ when a bar is grabbed by its middle, which is what keeps a three-day one from jumping.\n * And when the origin day is not on screen at all — a bar that began before this range, or one\n * whose view has been paged out from under the drag — falling back to the grab column makes the\n * offset zero, so the bar lands under the pointer instead of nowhere.\n */\nfunction originColumn(state: Gesture): number {\n const index = props.days.indexOf(state.origin.start)\n return index === -1 ? state.grabColumn : index\n}\n\n/**\n * One arrow press on a card held by the keyboard.\n *\n * The arrows accumulate the way a reader expects, three presses of Down moving three slots, but\n * what accumulates is the DISTANCE (`keyDays`, `keyStart`, `keyEnd`) and the event is worked\n * out from its origin each time, as the pointer's is every frame. Applied to the preview instead,\n * a step held against the end of the day built on its 23:59 and the event came back shorter\n * and off the slot grid.\n */\nfunction onGrabStep(state: Gesture, step: GrabStep, item: E): boolean {\n const { origin } = state\n if (state.kind === 'move-days') return onBarStep(state, step, item)\n // Whether the event may cross midnight is read off where it was taken from, as the pointer's is.\n const crossMidnight = spansMidnight(origin)\n let keyDays = state.keyDays ?? 0\n let keyStart = state.keyStart ?? 0\n let keyEnd = state.keyEnd ?? 0\n if (step.kind === 'grabResize') keyEnd += step.minutes\n else {\n keyDays += step.days\n keyStart += step.minutes\n }\n\n /*\n * Sideways is a step along the days ON SHOW, exactly as the pointer's is: date arithmetic\n * would count the days the calendar is hiding. An overnight event may have been taken by its\n * morning card with its start on a day off show (index -1): it can then go right, never\n * further left than where it already is.\n */\n const base = props.days.indexOf(origin.start)\n const column = clamp(base + keyDays, Math.min(base, 0), props.days.length - 1)\n const day = column < 0 || keyDays === 0 ? origin.start : props.days[column]!\n const moved = moveTimedEvent(\n origin,\n day,\n minutesAt(origin.startTime, props.timeWindow.start) + keyStart,\n props.timeWindow,\n crossMidnight,\n )\n const next =\n keyEnd === 0\n ? moved\n : resizeTimedEvent(\n moved,\n moved.end,\n minutesAt(moved.endTime, props.timeWindow.end) + keyEnd,\n props.slotDuration,\n props.timeWindow,\n crossMidnight,\n )\n\n // Held against an edge, the press goes nowhere: nothing to announce, nothing to drop, and\n // nothing kept either, so the next press the other way moves at once.\n if (sameTimes(next, state.preview)) return false\n state.keyDays = keyDays\n state.keyStart = keyStart\n state.keyEnd = keyEnd\n state.preview = next\n emit('announce', m.value.calendar.movedTo(item.title, timesText(state.preview)))\n return true\n}\n\n/**\n * How many pixels a frame the grid scrolls at full tilt.\n *\n * At sixty frames a second that is roughly eight hundred a second, which crosses a whole\n * twenty-four-hour day in about two — fast enough to be worth doing, slow enough to stop on.\n */\nconst AUTO_SCROLL_SPEED = 14\n\n/*\n * The design system's only `requestAnimationFrame` loop, and it earns that: a pointer held\n * still fires no events at all, so nothing else can drive continuous motion. Everything else\n * that moves here is either a transition or a timer.\n */\nlet scrollFrame: number | null = null\nlet scrollSpeed = 0\n\nfunction setScrollSpeed(speed: number) {\n scrollSpeed = speed\n if (speed === 0) {\n stopScrolling()\n return\n }\n scrollFrame ??= requestAnimationFrame(scrollStep)\n}\n\nfunction scrollStep() {\n scrollFrame = null\n const root = rootEl.value\n const state = gesture.value\n if (!root || !state || scrollSpeed === 0) return\n\n root.scrollTop += scrollSpeed * AUTO_SCROLL_SPEED\n // The hours have moved under a pointer that has not, so what it is over has changed.\n reapply(state)\n scrollFrame = requestAnimationFrame(scrollStep)\n}\n\nfunction stopScrolling() {\n if (scrollFrame !== null) cancelAnimationFrame(scrollFrame)\n scrollFrame = null\n scrollSpeed = 0\n}\n\nonBeforeUnmount(stopScrolling)\n\nfunction onCardClick(id: CalendarEventId) {\n // `pointerup` fires before `click`, so letting go at the end of a drag would ALSO open the\n // event — the consumer's editor over every card the reader had just moved.\n if (consumeDrag()) return\n const item = eventOf(id)\n if (item && id !== DRAFT_ID) emit('event-activate', item)\n}\n\n/**\n * A click on an empty part of a day, reported as the cell it landed in.\n *\n * A click that ENDS a drag is not one: drawing a slot out, or dropping a card, lets go over a\n * cell, and the browser follows with a click there. Reporting it would hand the consumer a\n * cell nobody chose, on top of the slot or the move they did.\n */\nfunction onGridClick(event: MouseEvent) {\n const target = event.target as HTMLElement | null\n if (target?.closest('.v-calendar-event')) return\n if (consumeDrag()) return\n const cell = target?.closest<HTMLElement>('.v-calendar-cell')\n if (!cell) return\n emit('cell-activate', { date: cell.dataset.iso!, minutes: Number(cell.dataset.minutes) })\n}\n\n/**\n * The cursor a gesture holds for as long as it lasts, whatever the pointer passes over.\n *\n * A captured pointer's cursor is resolved against the capture target, the scroller, and a\n * dragged card takes no pointer events at all, so the resize strip's own `ns-resize` is lost the\n * instant the press begins. Naming the gesture on the scroller is what keeps it. A drawn slot\n * takes its cursor only once it travels, so a click on a cell does not flash one.\n */\nconst gestureCursor = computed(() => {\n const state = gesture.value\n if (!state || state.pointerId === null) return undefined\n if (state.kind === 'resize') return 'resize'\n if (state.kind === 'create' && state.moved) return 'create'\n return undefined\n})\n\n/**\n * Brings a moment of the day to the top of the visible area.\n *\n * The row height is measured rather than read from the token, because the token is a length\n * a consumer may override and the browser is the only thing that knows what it came to. It\n * is a handler-time read, never a render-time one, and it degrades to doing nothing where\n * nothing has been laid out — which is exactly what the test environment wants.\n */\nfunction scrollToMinutes(minutes: number) {\n const root = rootEl.value\n const canvas = canvasEl.value\n if (!root || !canvas || hours.value.length === 0) return\n const rowHeight = canvas.offsetHeight / hours.value.length\n if (rowHeight <= 0) return\n root.scrollTop = ((minutes - props.timeWindow.start) / MINUTES_PER_HOUR) * rowHeight\n}\n\n// The contract VCalendar drives every view through, so its toolbar need not know which one\n// is on screen. VCalendarMonth answers the same two, its `scrollToMinutes` being a no-op.\ndefineExpose({\n /** Brings the focus onto the cell the grid is currently pointing at. */\n focus: (options?: FocusOptions) => {\n const cell = tabbable.value\n if (cell) focusCell(cellId(cell.iso, cell.minutes), false, options)\n },\n /** Scrolls the grid so a given moment of the day sits at the top of the visible area. */\n scrollToMinutes,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-calendar-time-grid\"\n :data-outside=\"gesture?.outside ? '' : undefined\"\n :data-gesture=\"gestureCursor\"\n :style=\"{ '--calendar-columns': String(days.length) }\"\n @pointermove=\"onPointermove\"\n @pointerup=\"onPointerup\"\n @pointercancel=\"onPointercancel\"\n @pointerleave=\"onPointerleave\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-calendar-head\">\n <div class=\"v-calendar-head-gutter\" />\n <div v-for=\"iso in days\" :key=\"iso\" class=\"v-calendar-head-day\">\n <slot\n name=\"day-header\"\n :iso=\"iso\"\n :weekday=\"dayName(iso)\"\n :day-text=\"dayNumber(iso)\"\n :today=\"iso === today\"\n >\n <span class=\"v-calendar-weekday\">{{ dayName(iso) }}</span>\n <span class=\"v-calendar-head-number\" :class=\"{ 'v-calendar-today': iso === today }\">\n {{ dayNumber(iso) }}\n </span>\n </slot>\n </div>\n\n <!-- The band of all-day bars, kept INSIDE the header so that the whole of it is one\n sticky box: two boxes would each need to know how tall the other was. -->\n <template v-if=\"allDay.length > 0\">\n <div class=\"v-calendar-allday-label\">\n <slot name=\"all-day-label\">{{ m.calendar.allDay }}</slot>\n </div>\n <div\n class=\"v-calendar-allday\"\n role=\"group\"\n :aria-label=\"m.calendar.allDay\"\n :style=\"{ '--calendar-lanes': String(allDayLanes) }\"\n @pointerdown=\"onBandPointerdown\"\n @keydown=\"onBandKeydown\"\n >\n <VCalendarEvent\n v-for=\"span in allDay\"\n :key=\"span.id\"\n class=\"v-calendar-bar\"\n :event=\"allDayById.get(span.id)!\"\n layout=\"chip\"\n :disabled=\"disabled\"\n :continues-before=\"span.continuesBefore\"\n :continues-after=\"span.continuesAfter\"\n v-bind=\"cardState(span.id)\"\n :style=\"{\n '--calendar-day-index': String(span.startIndex),\n '--calendar-bar-span': String(span.span),\n '--calendar-bar-lane': String(span.lane),\n }\"\n @click=\"onCardClick(span.id)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n </div>\n </template>\n </div>\n\n <div ref=\"canvasEl\" class=\"v-calendar-canvas\">\n <div class=\"v-calendar-hours\" aria-hidden=\"true\">\n <span v-for=\"minutes in hours\" :key=\"minutes\" class=\"v-calendar-hour\">\n {{ hourLabel(minutes) }}\n </span>\n </div>\n\n <div\n ref=\"columnsEl\"\n class=\"v-calendar-columns v-calendar-edge-cue\"\n role=\"grid\"\n :aria-label=\"label\"\n :data-edge=\"edgeCue\"\n @keydown=\"onKeydown\"\n @click=\"onGridClick\"\n @pointerdown=\"onGridPointerdown\"\n >\n <div v-for=\"gridRow in gridRows\" :key=\"gridRow.minutes\" role=\"row\" class=\"v-calendar-row\">\n <div\n v-for=\"cell in gridRow.cells\"\n :id=\"cell.id\"\n :key=\"cell.iso\"\n role=\"gridcell\"\n class=\"v-calendar-cell\"\n :data-iso=\"cell.iso\"\n :data-minutes=\"gridRow.minutes\"\n :data-today=\"cell.iso === today ? '' : undefined\"\n :tabindex=\"isTabStop(cell.iso, gridRow.minutes) ? 0 : -1\"\n :aria-label=\"cell.label\"\n :aria-selected=\"isFocused(cell.iso, gridRow.minutes) ? true : undefined\"\n >\n <VCalendarEvent\n v-for=\"card in cardsAt(cell.key)\"\n :key=\"card.segment.id\"\n class=\"v-calendar-block\"\n :event=\"card.event\"\n layout=\"block\"\n :disabled=\"disabled\"\n :time-text=\"card.timeText\"\n :continues-before=\"card.segment.clippedStart\"\n :continues-after=\"card.segment.clippedEnd\"\n :resizable=\"editable && card.segment.id !== DRAFT_ID && card.segment.part !== 'head'\"\n v-bind=\"cardState(card.segment.id)\"\n :style=\"card.style\"\n @click=\"onCardClick(card.segment.id)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n\n <!-- The line is drawn INSIDE the cell holding the current hour, never as a\n direct child of the grid or of a row: a `grid` owns only rows and a row only\n cells, so an extra box at either level fails `aria-required-children`. -->\n <div\n v-if=\"nowMark && nowMark.day === cell.day && nowMark.row === cell.row\"\n class=\"v-calendar-now\"\n aria-hidden=\"true\"\n :style=\"{\n '--calendar-day-index': String(nowMark.day),\n '--calendar-now-fraction': String(nowMark.fraction),\n }\"\n />\n </div>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-calendar-time-grid {\n /*\n * The one scrolling box — its scrolling itself is `.v-calendar-view`'s, in VCalendar's\n * sheet. The day names are sticky INSIDE it rather than in a container of their own, which\n * is what keeps them aligned with the columns for free: a separate header would have to be\n * told how far the body had scrolled sideways, and told again every time the column widths\n * changed.\n */\n --calendar-hour: var(--vectis-control-size-calendar-hour);\n --calendar-gutter: var(--vectis-control-size-calendar-gutter);\n /*\n * The day columns, written once for the three boxes that lay them out: the header and the\n * canvas behind the hour gutter, the all-day band and the columns box without it. The three\n * must resolve to identical tracks — a bar is dropped by reading the pointer against the\n * COLUMNS' geometry — so one definition is what keeps them from drifting apart.\n *\n * It is declared on the element that also carries `--calendar-columns` inline, where the\n * inner `var()` is resolved before the value is inherited: declared any higher, it would\n * be frozen at whatever column count that ancestor saw, which is none.\n */\n --calendar-tracks: repeat(\n var(--calendar-columns),\n minmax(var(--vectis-control-size-calendar-day-min), 1fr)\n );\n /*\n * Half a line of an hour label. It is used TWICE and in opposite directions — the label\n * is pulled up by it so it straddles its rule, and the tick is pushed back down by it so\n * it lands on that same rule. One number, so the two cannot drift apart; two literals\n * would misalign the moment the caption's leading changed, with nothing to point at it.\n */\n --calendar-hour-lift: calc(\n var(--vectis-text-caption-size) * var(--vectis-text-caption-leading) / 2\n );\n\n position: relative;\n }\n\n /*\n * The cursor of a gesture under way — see `gestureCursor`. It is set on every descendant as\n * well as on the scroller, because an engine that resolves the cursor from what is under the\n * pointer rather than from the capture target would otherwise show the `pointer` of whichever\n * card the gesture passes over.\n *\n * `:not([data-outside])` keeps it off a gesture held outside the calendar, whose `not-allowed`\n * lives in VCalendar's sheet at (0,2,0): at equal weight the two would be arbitrated by the\n * order the consumer's bundler put the sheets in.\n */\n .v-calendar-time-grid[data-gesture='resize']:not([data-outside]),\n .v-calendar-time-grid[data-gesture='resize']:not([data-outside]) * {\n cursor: ns-resize;\n }\n\n .v-calendar-time-grid[data-gesture='create']:not([data-outside]),\n .v-calendar-time-grid[data-gesture='create']:not([data-outside]) * {\n cursor: move;\n }\n\n .v-calendar-head,\n .v-calendar-canvas {\n display: grid;\n grid-template-columns: var(--calendar-gutter) var(--calendar-tracks);\n /*\n * The tracks have a floor, so on a narrow screen they add up to more than the box and\n * the calendar scrolls sideways. Without this the BOX would stay the width of the\n * scroller while the tracks spilled out of it: the sticky day names would paint their\n * background only as far as the original width, and the columns would run out from\n * under them halfway across. `min-content` on a grid is the sum of its track minimums,\n * which is exactly the width wanted here.\n */\n min-inline-size: min-content;\n }\n\n /*\n * THE STACKING ORDER OF THE WHOLE GRID, in one place because it is decided in four:\n *\n * 1 .v-calendar-block an event card\n * 2 .v-calendar-now the current-time line, over every card\n * .v-calendar-edge-cue the edge strip, in VCalendar's sheet\n * 3 [data-dragging] / [data-grabbed] a card being moved, over the line\n * 4 .v-calendar-head the sticky day names, over everything\n *\n * Neither `.v-calendar-time-grid` nor `.v-calendar-columns` establishes a stacking context —\n * both are `relative` with no `z-index` — so all four compete at the root, and any TIE is\n * broken by document order. That is what put the current-time line over the header while\n * both sat at 2: the line is further down the template. The header has to sit above the\n * dragged card as well, which is why it is 4 and not 3. The month view's header takes the\n * same 4, for the same reason.\n */\n .v-calendar-head {\n position: sticky;\n inset-block-start: 0;\n z-index: 4;\n background: var(--vectis-color-surface);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-head-gutter {\n /* The corner above the hour labels. It is empty, and it is here so the day names line\n up with their columns rather than starting at the edge of the box. */\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-head-day {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--vectis-space-1);\n padding-block: var(--vectis-space-2);\n padding-inline: var(--vectis-space-1);\n }\n\n /* Today is marked on the number alone, the way a calendar marks a date rather than a\n column — the `.v-calendar-today` paint, in VCalendar's sheet. */\n .v-calendar-head-number {\n display: flex;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-height-sm);\n block-size: var(--vectis-control-height-sm);\n border-radius: var(--vectis-radius-pill);\n font-size: var(--vectis-text-body-lg-size);\n }\n\n .v-calendar-allday-label {\n display: flex;\n align-items: start;\n justify-content: end;\n padding-block-start: var(--vectis-space-1);\n padding-inline-end: var(--vectis-space-2);\n border-block-start: 1px solid var(--vectis-color-border);\n border-inline-end: 1px solid var(--vectis-color-border);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n .v-calendar-allday {\n /*\n * The band's two axes are DISCRETE — a day, and a row — where the time grid's vertical\n * axis is continuous. That is why the bars are placed with real grid lines here and\n * with fractions there: forcing one mechanism onto both would cost a translation in\n * each direction and buy nothing.\n */\n display: grid;\n grid-column: 2 / -1;\n grid-template-columns: var(--calendar-tracks);\n grid-template-rows: repeat(\n var(--calendar-lanes),\n var(--vectis-control-size-calendar-allday-lane)\n );\n gap: 1px;\n overflow-y: auto;\n max-block-size: var(--vectis-control-size-calendar-allday-max);\n padding-block: var(--vectis-space-1);\n /*\n * No inline padding, deliberately. The band's tracks have to line up with the canvas's\n * exactly, because dragging a bar reads the pointer against the CANVAS geometry — the two\n * boxes span the same grid columns, so one measurement serves both. A few pixels of\n * padding here would put the last column out by that much, and a bar dropped on the right\n * edge would land a day early.\n */\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-bar {\n grid-column: calc(var(--calendar-day-index) + 1) / span var(--calendar-bar-span);\n grid-row: calc(var(--calendar-bar-lane) + 1);\n min-inline-size: 0;\n }\n\n .v-calendar-hours {\n display: grid;\n grid-auto-rows: var(--calendar-hour);\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n /*\n * The label names the hour its row BEGINS at, and it is drawn ON that rule rather than\n * under it: `align-self: start` shrinks the span from its 4rem row down to its one line,\n * and the negative margin then straddles it across the rule above.\n */\n .v-calendar-hour {\n position: relative;\n align-self: start;\n margin-block-start: calc(-1 * var(--calendar-hour-lift));\n padding-inline-end: var(--vectis-space-2);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n text-align: end;\n }\n\n /* The rule carried a little way back into the gutter, so the eye joins the label to the\n line it names. A border and not a background: Windows forced-colors flattens a\n background to Canvas and the tick would vanish, where a border keeps a colour — the\n argument VSeparator and the icon registry both make. */\n .v-calendar-hour::before {\n content: '';\n position: absolute;\n inset-block-start: var(--calendar-hour-lift);\n inset-inline-end: 0;\n inline-size: var(--vectis-control-size-calendar-tick);\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * The first hour has no rule of its own: the sticky header already draws a border along\n * that edge, and a second one under it reads as a thick smudge. Its label goes with it,\n * since half of it would sit above the top of the canvas anyway — the cost you accepted,\n * paid in one label a reader can work out from the one below.\n *\n * `visibility` carries the tick away too, being inherited, so the pseudo-element needs no\n * rule of its own here.\n *\n * TRAP — HIDDEN and not removed. The span has to go on occupying its grid row: `display:\n * none` would take the row with it and shift every hour below up by one.\n */\n .v-calendar-hour:first-child {\n visibility: hidden;\n }\n\n .v-calendar-row:first-child .v-calendar-cell {\n border-block-start: none;\n }\n\n .v-calendar-columns {\n /* The containing block every card and the edge strip are placed against. The rows below\n are `display: contents`, so the cells themselves are this grid's items. */\n position: relative;\n display: grid;\n grid-column: 2 / -1;\n /*\n * The canvas's own tracks, deliberately restated rather than borrowed with `subgrid`. This\n * box spans exactly the canvas columns it repeats, and both sides divide the same width by\n * the same rule, so the two resolve identically — which makes subgrid a second mechanism\n * buying nothing, the conclusion the library's own audit of it reached.\n */\n grid-template-columns: var(--calendar-tracks);\n grid-auto-rows: var(--calendar-hour);\n }\n\n .v-calendar-row {\n /* The row exists for the accessibility tree and must not exist for the layout: a real\n box here would make the cells its children instead of the grid's, and every column\n would collapse. */\n display: contents;\n }\n\n .v-calendar-cell[data-today] {\n background: var(--vectis-color-accent-surface);\n }\n\n /*\n * A card is positioned against the whole columns box, not against its cell — which is why\n * the horizontal formula carries the day index. Being a child of the cell is what keeps\n * it in the accessibility tree in the right place; being placed against the grid is what\n * lets it be any length at all.\n *\n * TRAP — the height is the event's own length and NOTHING ELSE. No `max()` against a floor\n * token here: the minimum is already applied where it belongs, `timedSegments` stretching\n * every segment to at least `slotDuration` BEFORE the overlap packing sees it, so what is\n * drawn and what the packing believes are the same box. A second floor in CSS can only\n * disagree with the first — the packing cannot read a stylesheet — and the symptom is two\n * cards packed side by side and then drawn on top of each other.\n *\n * A card is therefore never taller than its slot. Fitting the text into a short one is the\n * stylesheet's problem, and VCalendarEvent solves it by asking how tall the card came out.\n *\n * TRAP — compounded with `.v-calendar-event`, which sets `position: relative` on the same\n * element from its own sheet: at an equal (0,1,0) the winner was whichever of the two sheets\n * the consumer's bundler put last, and every card of the grid fell into normal flow.\n */\n .v-calendar-event.v-calendar-block {\n position: absolute;\n inset-block-start: calc(var(--calendar-block-start-fraction) * 100%);\n block-size: calc(\n (var(--calendar-block-end-fraction) - var(--calendar-block-start-fraction)) * 100%\n );\n inset-inline-start: calc(\n (var(--calendar-day-index) + var(--calendar-block-column) / var(--calendar-block-columns)) /\n var(--calendar-columns) * 100%\n );\n inline-size: calc(\n var(--calendar-block-span) / var(--calendar-block-columns) / var(--calendar-columns) * 100% -\n var(--vectis-control-size-calendar-gap)\n );\n z-index: 1;\n }\n\n /*\n * The current-time line, placed the same way a card is — against the whole columns box,\n * so its formula carries the day index too. It is `aria-hidden`: it is a second rendering\n * of something the cell labels and the day marked as today already say, and announcing\n * \"current time\" from the middle of a grid would interrupt without informing.\n */\n .v-calendar-now {\n position: absolute;\n inset-block-start: calc(var(--calendar-now-fraction) * 100%);\n inset-inline-start: calc(var(--calendar-day-index) / var(--calendar-columns) * 100%);\n inline-size: calc(100% / var(--calendar-columns));\n border-block-start: 1px solid var(--vectis-color-danger);\n /* Above the cards: it is the one thing that must stay readable over a full day. */\n z-index: 2;\n pointer-events: none;\n }\n\n .v-calendar-now::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n inline-size: var(--vectis-control-size-calendar-now-dot);\n block-size: var(--vectis-control-size-calendar-now-dot);\n /* Half its own size upwards and backwards, so the dot is centred ON the line's start\n rather than hanging below it. */\n margin-block-start: calc(var(--vectis-control-size-calendar-now-dot) / -2);\n margin-inline-start: calc(var(--vectis-control-size-calendar-now-dot) / -2);\n border-radius: var(--vectis-radius-pill);\n background: var(--vectis-color-danger);\n }\n\n /* The line is a border, which forced colors keeps; the dot at its start is a background,\n which it would flatten to Canvas. */\n @media (forced-colors: active) {\n .v-calendar-now::before {\n forced-color-adjust: none;\n background: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VCalendarTimeGrid.js","names":[],"sources":["../../../src/components/VCalendar/VCalendarTimeGrid.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"E extends CalendarEvent\">\n// @a11y @keyboard @core\n/**\n * Render event buttons inside their owning grid cells for ARIA and keyboard navigation. Borders\n * preserve time rulings in forced colours; shared gestures manage editing.\n */\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport { formatDateDisplay } from '../../utils/date'\nimport { clamp } from '../../utils/number'\nimport { formatTimeDisplay } from '../../utils/time'\n\nimport { useMessages } from '../../i18n/state'\n\nimport VCalendarEvent from './VCalendarEvent.vue'\nimport { EDGE_BAND } from './edgeStep'\nimport {\n gridGeometryOf,\n useCalendarGesture,\n type GestureBase,\n type GestureInit,\n type GrabStep,\n} from './gesture'\nimport { calendarIntent } from './keyboard'\nimport {\n MINUTES_PER_DAY,\n MINUTES_PER_HOUR,\n blockEdgeAt,\n floorToSlot,\n fractionOf,\n isAllDayEvent,\n minutesAt,\n moveEventToDay,\n packAllDay,\n packDayColumn,\n pointToCell,\n sameTimes,\n snapToSlot,\n spansMidnight,\n timeOf,\n timedSegments,\n timesOf,\n type AllDaySpan,\n type PlacedSegment,\n type SegmentPart,\n type TimeWindow,\n} from './layout'\nimport { drawnSlot, moveTimedEvent, resizeTimedEvent, roundToSlot } from './timeGrid'\nimport type {\n ActivatedCell,\n CalendarEvent,\n CalendarEventId,\n CalendarEventSlotProps,\n CalendarEventTimes,\n CalendarFormat,\n} from './types'\n\n/** Where the keyboard focus is in the grid, as a day and a moment in it. */\nexport interface FocusedCell {\n iso: string\n minutes: number\n}\n\nexport interface CalendarTimeGridProps<T> {\n days: string[]\n events: T[]\n timeWindow: TimeWindow\n slotDuration: number\n locale: string\n format: CalendarFormat\n /** Today's date, or nothing on the server, where it cannot be known. */\n today: string | null\n /** The time it is now, in minutes since midnight: again nothing on the server. */\n now: number | null\n /** Whether events can be moved and stretched. */\n editable: boolean\n /** Whether the whole calendar is frozen, which also takes its cards out of the tab order. */\n disabled: boolean\n /** Whether an empty stretch of a day can be drawn out, and reported as a new event's times. */\n creatable: boolean\n /** The node telling a reader how a card can be moved, shared by every card. */\n hintId: string\n /** How long a drag rests against an edge before the view pages. Zero turns paging off. */\n edgeStepDelay: number\n /** Whether dragging near the top or bottom scrolls the grid. */\n autoScroll: boolean\n /** What the grid as a whole is called: the range it is showing. */\n label: string\n}\n\nconst props = defineProps<CalendarTimeGridProps<E>>()\n\nconst emit = defineEmits<{\n /** An empty part of the grid was activated, at this day and this moment. */\n 'cell-activate': [cell: ActivatedCell]\n /** A card was clicked or activated. */\n 'event-activate': [event: E]\n /** An event was dropped somewhere else, or stretched to a new end. */\n 'event-drop': [id: CalendarEventId, times: CalendarEventTimes, kind: 'move' | 'resize']\n /** An empty stretch of a day was drawn out, and is offered as a new event's times. */\n 'slot-create': [times: CalendarEventTimes]\n /** Something happened that a reader who cannot see the grid needs told. */\n announce: [message: string]\n /** A page key asked for the previous or next period, which only the calendar can do. */\n step: [delta: -1 | 1]\n}>()\n\n/** Which cell holds the tab stop. The calendar owns it, so the toolbar can move it too. */\nconst focused = defineModel<FocusedCell>('focused', { required: true })\n\ndefineSlots<{\n event?(props: CalendarEventSlotProps<E>): unknown\n 'day-header'?(props: { iso: string; weekday: string; dayText: string; today: boolean }): unknown\n 'all-day-label'?(): unknown\n}>()\n\nconst m = useMessages()\nconst uid = useId()\n\n/** The hour each row stands for, as minutes since midnight. */\nconst hours = computed(() => {\n const rows: number[] = []\n const { start, end } = props.timeWindow\n for (let at = start; at < end; at += MINUTES_PER_HOUR) rows.push(at)\n return rows\n})\n\n/**\n * The id a stretch of empty grid carries while it is being drawn out. It never leaves this\n * component: on release the calendar is handed the times alone, and the card is gone.\n */\nconst DRAFT_ID = '__vectis-calendar-draft__'\n\n/** A gesture under way, whether it came from a pointer or from the keyboard. */\ninterface Gesture extends GestureBase {\n kind: 'move' | 'move-days' | 'resize' | 'create'\n /**\n * How far the arrows have taken a KEYBOARD grab from its origin: days along the days on show,\n * and minutes moved (the start) and stretched (the end).\n */\n keyDays?: number\n keyStart?: number\n keyEnd?: number\n /**\n * How far after the event's start the pointer took hold, in minutes, so the card does not\n * jump under it.\n */\n grabOffset: number\n grabColumn: number\n /**\n * Store the dragged event's column shares by event piece, not day index: paging changes which\n * dates those indices represent.\n */\n ghostColumns: Partial<\n Record<SegmentPart, Pick<PlacedSegment, 'column' | 'span' | 'columns'>>\n > | null\n ghostLane: number | null\n}\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst canvasEl = ref<HTMLElement | null>(null)\nconst columnsEl = ref<HTMLElement | null>(null)\n\nconst {\n gesture,\n ghostEvent,\n withPreview,\n edgeCue,\n isRtl,\n start,\n reapply,\n endLastGesture,\n consumeDrag,\n onPointermove,\n onPointerup,\n onPointercancel,\n onPointerleave,\n onFocusout,\n onCardKeydown,\n focusCell,\n idOfCard,\n cardState,\n} = useCalendarGesture<E, Gesture>({\n events: () => props.events,\n eventOf: (id): E | undefined => eventOf(id),\n rootEl,\n // The SCROLLER, not the columns box: the all-day band sits in the sticky header, and this is\n // the only element that contains both it and the canvas.\n captureEl: rootEl,\n measureEl: columnsEl,\n columns: () => props.days.length,\n editable: () => props.editable,\n hintId: () => props.hintId,\n slotMinutes: () => props.slotDuration,\n edgeStepDelay: () => props.edgeStepDelay,\n layout: () => props.days,\n applyPoint,\n onEdges: (state, scroller) =>\n setScrollSpeed(\n props.autoScroll && scroller ? blockEdgeAt(state.lastY, scroller, EDGE_BAND) : 0,\n ),\n onRelease: stopScrolling,\n onLetGo: (state) => {\n if (state.kind !== 'create') return false\n // A press that never travelled drew nothing: its click reports the cell instead.\n if (state.moved) emit('slot-create', state.preview)\n return true\n },\n onDrop: (state) =>\n emit('event-drop', state.id, state.preview, state.kind === 'resize' ? 'resize' : 'move'),\n grab: (item) =>\n props.editable\n ? initFor({\n kind: isAllDayEvent(item) ? 'move-days' : 'move',\n id: item.id,\n origin: timesOf(item),\n pointerId: null,\n originX: 0,\n originY: 0,\n grabOffset: 0,\n grabColumn: 0,\n })\n : null,\n onGrabStep,\n step: (delta) => emit('step', delta),\n announce: (message) => emit('announce', message),\n})\n\n/** Where the echo goes, worked out afresh on every render rather than remembered. */\nconst NO_GHOSTS: readonly PlacedSegment[] = Object.freeze([])\n\n/*\n * The \"no echo\" answer is ONE shared array. A bar dragged along the band recomputes this on\n * every slot; a fresh `[]` each time is a new value, which would wake `placed` and re-pack\n * every column for a gesture that never touches them.\n */\nconst ghostSegments = computed<readonly PlacedSegment[]>(() => {\n const state = gesture.value\n const event = ghostEvent.value\n const packing = state?.ghostColumns\n if (!event || !packing) return NO_GHOSTS\n return timedSegments([event], props.days, props.timeWindow, props.slotDuration).flatMap(\n (segment) => {\n const columns = packing[segment.part]\n return columns ? [{ ...segment, ...columns }] : []\n },\n )\n})\n\nconst ghostSpan = computed<AllDaySpan | null>(() => {\n const state = gesture.value\n const event = ghostEvent.value\n if (!event || state?.ghostLane == null) return null\n const [span] = packAllDay([event], props.days)\n return span ? { ...span, lane: state.ghostLane } : null\n})\n\n/*\n * The model split in two, once per change to it: the timed events the columns draw, and the\n * all-day ones the band above them draws. The split is what lets a gesture rebuild only its own\n * half; see `drawnTimed`.\n */\nconst timedEvents = computed(() => props.events.filter((event) => !isAllDayEvent(event)))\nconst allDayEvents = computed(() => props.events.filter((event) => isAllDayEvent(event)))\n\n/**\n * The half a gesture belongs to is decided by its KIND and never by looking at the preview.\n * `move-days` is the only gesture that starts on the band, and the three others only start on a\n * timed card or an empty cell (both press handlers and the keyboard grab refuse an all-day\n * event), so the two lists below can each return their untouched half as the same array.\n */\nconst drawnTimed = computed<E[]>(() => {\n const state = gesture.value\n if (!state || state.kind === 'move-days') return timedEvents.value\n\n if (state.kind === 'create') {\n if (!state.moved) return timedEvents.value\n const draft = {\n id: DRAFT_ID,\n title: m.value.calendar.untitled,\n ...state.preview,\n // It exists while the pointer is down and never reaches the model, which makes the cast\n // safe.\n } as unknown as E\n return [...timedEvents.value, draft]\n }\n\n return withPreview(timedEvents.value)\n})\n\n/** The all-day bars as the band should currently draw them; see `drawnTimed`. */\nconst drawnAllDay = computed<E[]>(() =>\n gesture.value?.kind === 'move-days' ? withPreview(allDayEvents.value) : allDayEvents.value,\n)\n\nconst timedById = computed(\n () => new Map<CalendarEventId, E>(drawnTimed.value.map((event) => [event.id, event])),\n)\nconst allDayById = computed(\n () => new Map<CalendarEventId, E>(drawnAllDay.value.map((event) => [event.id, event])),\n)\n\n/** Any event on show, as it is currently drawn. For the handlers; the template reads a half. */\nconst eventOf = (id: CalendarEventId): E | undefined =>\n timedById.value.get(id) ?? allDayById.value.get(id)\n\n/** The bars above the grid, and how many rows they need between them. */\nconst allDay = computed(() => {\n const ghost = ghostSpan.value\n const spans = packAllDay(\n ghost ? drawnAllDay.value.filter((event) => event.id !== ghost.id) : drawnAllDay.value,\n props.days,\n )\n return ghost ? [...spans, ghost] : spans\n})\nconst allDayLanes = computed(() =>\n allDay.value.reduce((most, span) => Math.max(most, span.lane + 1), 0),\n)\n\n/** The row a moment of the window is drawn in, held inside the grid. */\nconst rowOf = (minutes: number) =>\n clamp(\n Math.floor((minutes - props.timeWindow.start) / MINUTES_PER_HOUR),\n 0,\n hours.value.length - 1,\n )\n\n/**\n * Where the current-time line goes, or nothing when it has no business being drawn: on the\n * server, on a range that does not include today, or at an hour the window has cropped\n * away.\n */\nconst nowMark = computed(() => {\n if (props.now === null || props.today === null) return null\n const dayIndex = props.days.indexOf(props.today)\n if (dayIndex === -1) return null\n if (props.now < props.timeWindow.start || props.now > props.timeWindow.end) return null\n return {\n day: dayIndex,\n row: rowOf(props.now),\n fraction: fractionOf(props.now, props.timeWindow),\n }\n})\n\n/** The placed boxes, day by day. */\nconst placed = computed(() => {\n const segments = timedSegments(drawnTimed.value, props.days, props.timeWindow, props.slotDuration)\n const ghosts = ghostSegments.value\n // Read off the echo's own boxes, never off `ghostEvent`, which changes with every slot of a\n // bar's drag and would wake this for the band's gesture too.\n const ghostId = ghosts[0]?.id\n\n const byDay = new Map<number, typeof segments>()\n for (const segment of segments) {\n /*\n * The echo is held OUT of the packing and put back below with the width and column it had\n * when the drag began. Packed with the rest, it would share its column with the card being\n * dragged the moment the two overlapped; so the card under the pointer would halve in width\n * and then grow back as it moved away, which reads as the drag going wrong.\n */\n if (ghostId !== undefined && segment.id === ghostId) continue\n const list = byDay.get(segment.dayIndex)\n if (list) list.push(segment)\n else byDay.set(segment.dayIndex, [segment])\n }\n\n const real = [...byDay.values()].flatMap((list) => packDayColumn(list))\n return ghosts.length > 0 ? [...real, ...ghosts] : real\n})\n\nconst cellId = (iso: string, minutes: number) => `${uid}-c-${iso}-${minutes}`\n\n/*\n * These two maps, of 7 and 24 entries, recompute when the locale, the days or the window change\n * and never because a card moved. Both are keyed by exactly what the template walks, `days` and\n * `hours`, so every lookup is a hit and none of them needs a formatting fallback.\n */\nconst dayLabels = computed(() => {\n const map = new Map<string, { short: string; number: string; full: string }>()\n for (const iso of props.days) {\n map.set(iso, {\n short: formatDateDisplay(iso, props.locale, { weekday: 'short' }),\n number: formatDateDisplay(iso, props.locale, { day: 'numeric' }),\n full: formatDateDisplay(iso, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n }),\n })\n }\n return map\n})\n\nconst hourLabels = computed(() => {\n const map = new Map<number, string>()\n for (const minutes of hours.value)\n map.set(minutes, formatTimeDisplay(timeOf(minutes), props.locale, props.format))\n return map\n})\n\nconst hourLabel = (minutes: number) => hourLabels.value.get(minutes) ?? ''\nconst dayName = (iso: string) => dayLabels.value.get(iso)?.short ?? ''\nconst dayNumber = (iso: string) => dayLabels.value.get(iso)?.number ?? ''\n\n/**\n * The cells, row by row, with everything about them that no gesture can change: the id the\n * focus is moved by, the name a reader hears, and the number `byCell` files cards under.\n */\nconst gridRows = computed(() => {\n const columns = props.days.length\n return hours.value.map((minutes, row) => ({\n minutes,\n cells: props.days.map((iso, day) => ({\n iso,\n day,\n row,\n key: row * columns + day,\n id: cellId(iso, minutes),\n label: `${dayLabels.value.get(iso)?.full ?? iso}, ${hourLabel(minutes)}`,\n })),\n }))\n})\n\n/**\n * What a dependant reads has to be this computed, never a cache held beside it. A hit reads no\n * prop, so a computed that only ever hit would track neither the locale nor the hour format,\n * and would go on printing the old ones after either changed.\n */\nconst timeTextOf = computed(() => {\n const { locale, format } = props\n const cache = new Map<string, string>()\n return (event: CalendarEvent): string => {\n const key = `${event.startTime}|${event.endTime}|${event.timezone ?? ''}`\n let text = cache.get(key)\n if (text === undefined) {\n const start = formatTimeDisplay(event.startTime, locale, format)\n const end = formatTimeDisplay(event.endTime, locale, format)\n const range = `${start} – ${end}`\n text = event.timezone ? `${range} (${event.timezone})` : range\n cache.set(key, text)\n }\n return text\n }\n})\n\n/** One card as the template draws it, with its event and its box already worked out. */\ninterface Card {\n segment: PlacedSegment\n event: E\n timeText: string\n style: Record<string, string>\n}\n\n/** The cards, filed under the cell their start falls in. */\nconst byCell = computed(() => {\n const map = new Map<number, Card[]>()\n const columns = props.days.length\n const events = timedById.value\n const textOf = timeTextOf.value\n for (const segment of placed.value) {\n // A placement always has an event behind it. Skipping rather than asserting turns a\n // mismatch between the two into one missing card instead of a grid that fails to render.\n const event = events.get(segment.id)\n if (!event) continue\n const key = rowOf(segment.start) * columns + segment.dayIndex\n const card: Card = {\n segment,\n event,\n timeText: textOf(event),\n style: {\n '--calendar-day-index': String(segment.dayIndex),\n '--calendar-block-start-fraction': String(fractionOf(segment.start, props.timeWindow)),\n '--calendar-block-end-fraction': String(fractionOf(segment.end, props.timeWindow)),\n '--calendar-block-column': String(segment.column),\n '--calendar-block-span': String(segment.span),\n '--calendar-block-columns': String(segment.columns),\n },\n }\n const list = map.get(key)\n if (list) list.push(card)\n else map.set(key, [card])\n }\n return map\n})\n\n/** An hour with nothing in it, shared rather than allocated per empty cell. */\nconst NO_CARDS: readonly Card[] = Object.freeze([])\nconst cardsAt = (key: number) => byCell.value.get(key) ?? NO_CARDS\n\nconst isFocused = (iso: string, minutes: number) =>\n focused.value.iso === iso && focused.value.minutes === minutes\n\n/**\n * Which cell holds the tab stop: the focused one, rounded to its row; or, when that cell is not\n * on screen (after a view change, or before anything has been focused at all), the first cell.\n */\nconst tabbable = computed<FocusedCell | null>(() => {\n const current = focused.value\n const { start, end } = props.timeWindow\n if (props.days.includes(current.iso)) {\n const rounded =\n Math.floor((current.minutes - start) / MINUTES_PER_HOUR) * MINUTES_PER_HOUR + start\n if (rounded >= start && rounded < end) return { iso: current.iso, minutes: rounded }\n }\n const iso = props.days[0]\n return iso ? { iso, minutes: start } : null\n})\n\nconst isTabStop = (iso: string, minutes: number) =>\n tabbable.value?.iso === iso && tabbable.value.minutes === minutes\n\nfunction moveFocusTo(iso: string, minutes: number) {\n focused.value = { iso, minutes }\n // @a11y\n focusCell(cellId(iso, minutes), true)\n}\n\n/** A day, then a start and an end: where a reader is told an event now is. */\nfunction timesText(times: CalendarEventTimes) {\n const start = formatTimeDisplay(times.startTime, props.locale, props.format)\n const end = formatTimeDisplay(times.endTime, props.locale, props.format)\n const day = formatDateDisplay(times.start, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n })\n return `${day}, ${start} – ${end}`\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const target = event.target as HTMLElement | null\n\n // A card answers a different table from the grid it sits in; that is what a grab mode IS.\n // Delegated here, on the same element as the pointer, so the two never drift apart.\n const card = target?.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n onCardKeydown(event, card)\n return\n }\n\n const cell = target?.closest<HTMLElement>('.v-calendar-cell')\n if (!cell) return\n\n const intent = calendarIntent(event, 'cell', props.slotDuration, isRtl())\n if (!intent) return\n\n const iso = cell.dataset.iso!\n const minutes = Number(cell.dataset.minutes)\n const index = props.days.indexOf(iso)\n\n if (intent.kind === 'moveFocus') {\n const day = props.days[clamp(index + intent.days, 0, props.days.length - 1)]!\n const at = clamp(\n minutes + intent.minutes,\n props.timeWindow.start,\n props.timeWindow.end - MINUTES_PER_HOUR,\n )\n event.preventDefault()\n moveFocusTo(day, at)\n return\n }\n\n if (intent.kind === 'rowEdge') {\n const day = intent.edge === 'start' ? props.days[0] : props.days.at(-1)\n if (!day) return\n event.preventDefault()\n moveFocusTo(day, minutes)\n return\n }\n\n if (intent.kind === 'period') {\n event.preventDefault()\n emit('step', intent.delta)\n return\n }\n\n /*\n * That is the keyboard's route to a new event (WCAG 2.1.1): the cell carries a day and an\n * hour, and how long the event lasts is a question for the consumer's own form, where the\n * pointer answers it by how far it was drawn.\n */\n if (intent.kind === 'activate' || intent.kind === 'openDay') {\n event.preventDefault()\n emit('cell-activate', { date: iso, minutes })\n }\n}\n\n/**\n * Read before the gesture is set. Both are derived from the drawn events, which the gesture\n * immediately starts rewriting; a line later and they would already describe the preview rather\n * than the place it came from.\n */\nfunction initFor(\n init: Omit<GestureInit<Gesture>, 'ghostColumns' | 'ghostLane'>,\n): GestureInit<Gesture> {\n const segments = placed.value.filter((item) => item.id === init.id)\n const span = allDay.value.find((item) => item.id === init.id)\n return {\n ...init,\n ghostColumns:\n segments.length > 0\n ? Object.fromEntries(\n segments.map((segment) => [\n segment.part,\n { column: segment.column, span: segment.span, columns: segment.columns },\n ]),\n )\n : null,\n ghostLane: span ? span.lane : null,\n }\n}\n\nconst pointAt = (x: number, y: number, rect: DOMRect, rtl: boolean) =>\n pointToCell({ x, y }, gridGeometryOf(rect, props.days.length, rtl), props.timeWindow, rtl)\n\n/** Every press on the grid, dispatched by what it landed on. */\nfunction onGridPointerdown(event: PointerEvent) {\n endLastGesture()\n if (gesture.value || event.button !== 0) return\n\n const target = event.target as HTMLElement | null\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!target || !rect) return\n const rtl = isRtl()\n const point = pointAt(event.clientX, event.clientY, rect, rtl)\n\n const card = target.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n if (!props.editable) return\n const item = eventOf(idOfCard(card))\n if (!item || isAllDayEvent(item)) return\n\n const resizing = target.closest('[data-calendar-handle]') !== null\n // The morning card of an overnight event is held from the evening before, so the offset\n // carries the day between the two.\n const pressedMorning = spansMidnight(item) && props.days[point.columnIndex] === item.end\n start(\n initFor({\n kind: resizing ? 'resize' : 'move',\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: resizing\n ? 0\n : (pressedMorning ? MINUTES_PER_DAY : 0) +\n point.minutes -\n minutesAt(item.startTime, props.timeWindow.start),\n grabColumn: point.columnIndex,\n }),\n rtl,\n )\n return\n }\n\n if (!props.creatable || !target.closest('.v-calendar-cell')) return\n const iso = props.days[point.columnIndex]\n if (!iso) return\n\n /*\n * The pressed slot is where the drawing is anchored; rounded DOWN, never to the nearest,\n * because a press at 09:07 means \"the nine o'clock slot\" and not the quarter past. Nothing is\n * drawn and nothing is reported until the press travels: a press that stays still is a click,\n * and reports its cell like any other.\n */\n const from = floorToSlot(point.minutes, props.slotDuration)\n start(\n initFor({\n kind: 'create',\n id: DRAFT_ID,\n origin: {\n start: iso,\n end: iso,\n startTime: timeOf(from),\n endTime: timeOf(Math.min(from + props.slotDuration, props.timeWindow.end)),\n },\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: 0,\n grabColumn: point.columnIndex,\n }),\n rtl,\n )\n}\n\n/** A press on the band of all-day bars above the grid. */\n/**\n * The band's bars answer the same card table as the grid's cards. They sit in the sticky\n * header, outside the grid whose keydown the cards reach, so the band delegates on its own.\n */\nfunction onBandKeydown(event: KeyboardEvent) {\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n if (card) onCardKeydown(event, card)\n}\n\nfunction onBandPointerdown(event: PointerEvent) {\n endLastGesture()\n if (gesture.value || !props.editable || event.button !== 0) return\n\n const card = (event.target as HTMLElement | null)?.closest<HTMLElement>('.v-calendar-event')\n const item = card ? eventOf(idOfCard(card)) : undefined\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!item || !rect) return\n const rtl = isRtl()\n\n start(\n initFor({\n kind: 'move-days',\n id: item.id,\n origin: timesOf(item),\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: 0,\n grabColumn: pointAt(event.clientX, event.clientY, rect, rtl).columnIndex,\n }),\n rtl,\n )\n}\n\n/**\n * One arrow press on an all-day bar held by the keyboard: a whole day sideways along the days\n * on show, nothing else, since a bar has no hours to move through or to stretch.\n */\nfunction onBarStep(state: Gesture, step: GrabStep, item: E): boolean {\n if (step.kind !== 'grabMove' || step.days === 0) return false\n const keyDays = (state.keyDays ?? 0) + step.days\n const base = props.days.indexOf(state.origin.start)\n const column = clamp(base + keyDays, Math.min(base, 0), props.days.length - 1)\n const next = column < 0 ? state.origin : moveEventToDay(state.origin, props.days[column]!)\n if (sameTimes(next, state.preview)) return false\n state.keyDays = keyDays\n state.preview = next\n emit('announce', m.value.calendar.movedTo(item.title, timesText(state.preview)))\n return true\n}\n\n/** Works out where the dragged event now belongs, from wherever the pointer last was. */\nfunction applyPoint(state: Gesture, rect: DOMRect) {\n const point = pointAt(state.lastX, state.lastY, rect, state.rtl)\n const crossMidnight = spansMidnight(state.origin)\n\n /*\n * A move's start is rounded without being held inside the day, since an overnight event's may\n * leave it.\n */\n const column =\n state.kind === 'create' || (state.kind === 'resize' && !crossMidnight) ? 0 : point.columnIndex\n const minutes =\n state.kind === 'move-days'\n ? 0\n : state.kind === 'move'\n ? roundToSlot(point.minutes - state.grabOffset, props.slotDuration)\n : state.kind === 'create'\n ? floorToSlot(point.minutes, props.slotDuration)\n : snapToSlot(point.minutes, props.slotDuration)\n if (isLastApplied(state, column, minutes)) return\n\n if (state.kind === 'create') {\n const anchor = minutesAt(state.origin.startTime, props.timeWindow.start)\n setPreview(\n state,\n drawnSlot(state.origin.start, anchor, minutes, props.slotDuration, props.timeWindow),\n )\n return\n }\n\n if (state.kind === 'resize') {\n const endDay = props.days[column] ?? state.origin.end\n setPreview(\n state,\n resizeTimedEvent(\n state.origin,\n endDay,\n minutes,\n props.slotDuration,\n props.timeWindow,\n crossMidnight,\n ),\n )\n return\n }\n\n if (state.kind === 'move-days') {\n /*\n * The day is read out of the list of days on show; never from date arithmetic on how many\n * columns were crossed, which would count the days the calendar is hiding and land the\n * event on a Saturday nobody can see. A bar keeps its length in days.\n */\n const target = clamp(\n originColumn(state) + (column - state.grabColumn),\n 0,\n props.days.length - 1,\n )\n const iso = props.days[target]\n if (iso) setPreview(state, moveEventToDay(state.origin, iso))\n return\n }\n\n /*\n * A timed card is placed from the column the pointer is over, and its start from the pointer\n * less the offset it was taken hold at. For an overnight event taken by its morning that start\n * lands on the day before the pointer's column, which `moveTimedEvent` works out.\n */\n const iso = props.days[column]\n if (!iso) return\n setPreview(state, moveTimedEvent(state.origin, iso, minutes, props.timeWindow, crossMidnight))\n}\n\n/** The slot `applyPoint` last worked a preview out for. */\nlet lastApplied: {\n state: Gesture\n days: readonly string[]\n timeWindow: TimeWindow\n step: number\n column: number\n minutes: number\n} | null = null\n\nfunction isLastApplied(state: Gesture, column: number, minutes: number): boolean {\n const last = lastApplied\n if (\n last?.state === state &&\n last.days === props.days &&\n last.timeWindow === props.timeWindow &&\n last.step === props.slotDuration &&\n last.column === column &&\n last.minutes === minutes\n ) {\n return true\n }\n lastApplied = {\n state,\n days: props.days,\n timeWindow: props.timeWindow,\n step: props.slotDuration,\n column,\n minutes,\n }\n return false\n}\n\n/**\n * The comparison is the whole point, not a nicety. The gesture is a deep ref, so any assignment\n * to `preview`, an equal object included, wakes the drawn events, the packing of every column\n * and a render of all 168 cells.\n */\nfunction setPreview(state: Gesture, next: CalendarEventTimes) {\n if (!sameTimes(state.preview, next)) state.preview = next\n}\n\n/**\n * Which column the bar being moved started in. And when the origin day is not on screen at all;\n * a bar that began before this range, or one whose view has been paged out from under the drag;\n * falling back to the grab column makes the offset zero, so the bar lands under the pointer\n * instead of nowhere.\n */\nfunction originColumn(state: Gesture): number {\n const index = props.days.indexOf(state.origin.start)\n return index === -1 ? state.grabColumn : index\n}\n\n/**\n * One arrow press on a card held by the keyboard. Applied to the preview instead, a step held\n * against the end of the day built on its 23:59 and the event came back shorter and off the\n * slot grid.\n */\nfunction onGrabStep(state: Gesture, step: GrabStep, item: E): boolean {\n const { origin } = state\n if (state.kind === 'move-days') return onBarStep(state, step, item)\n const crossMidnight = spansMidnight(origin)\n let keyDays = state.keyDays ?? 0\n let keyStart = state.keyStart ?? 0\n let keyEnd = state.keyEnd ?? 0\n if (step.kind === 'grabResize') keyEnd += step.minutes\n else {\n keyDays += step.days\n keyStart += step.minutes\n }\n\n /*\n * An overnight event may have been taken by its morning card with its start on a day off show\n * (index -1): it can then go right, never further left than where it already is.\n */\n const base = props.days.indexOf(origin.start)\n const column = clamp(base + keyDays, Math.min(base, 0), props.days.length - 1)\n const day = column < 0 || keyDays === 0 ? origin.start : props.days[column]!\n const moved = moveTimedEvent(\n origin,\n day,\n minutesAt(origin.startTime, props.timeWindow.start) + keyStart,\n props.timeWindow,\n crossMidnight,\n )\n const next =\n keyEnd === 0\n ? moved\n : resizeTimedEvent(\n moved,\n moved.end,\n minutesAt(moved.endTime, props.timeWindow.end) + keyEnd,\n props.slotDuration,\n props.timeWindow,\n crossMidnight,\n )\n\n // Held against an edge, the press goes nowhere: nothing to announce, nothing to drop, and\n // nothing kept either, so the next press the other way moves at once.\n if (sameTimes(next, state.preview)) return false\n state.keyDays = keyDays\n state.keyStart = keyStart\n state.keyEnd = keyEnd\n state.preview = next\n emit('announce', m.value.calendar.movedTo(item.title, timesText(state.preview)))\n return true\n}\n\n/** How many pixels a frame the grid scrolls at full tilt. */\nconst AUTO_SCROLL_SPEED = 14\n\n/*\n * The design system's only `requestAnimationFrame` loop, and it earns that: a pointer held\n * still fires no events at all, so nothing else can drive continuous motion. Everything else\n * that moves here is either a transition or a timer.\n */\nlet scrollFrame: number | null = null\nlet scrollSpeed = 0\n\nfunction setScrollSpeed(speed: number) {\n scrollSpeed = speed\n if (speed === 0) {\n stopScrolling()\n return\n }\n scrollFrame ??= requestAnimationFrame(scrollStep)\n}\n\nfunction scrollStep() {\n scrollFrame = null\n const root = rootEl.value\n const state = gesture.value\n if (!root || !state || scrollSpeed === 0) return\n\n root.scrollTop += scrollSpeed * AUTO_SCROLL_SPEED\n reapply(state)\n scrollFrame = requestAnimationFrame(scrollStep)\n}\n\nfunction stopScrolling() {\n if (scrollFrame !== null) cancelAnimationFrame(scrollFrame)\n scrollFrame = null\n scrollSpeed = 0\n}\n\nonBeforeUnmount(stopScrolling)\n\nfunction onCardClick(id: CalendarEventId) {\n // `pointerup` fires before `click`, so letting go at the end of a drag would ALSO open the\n // event; the consumer's editor over every card the reader had just moved.\n if (consumeDrag()) return\n const item = eventOf(id)\n if (item && id !== DRAFT_ID) emit('event-activate', item)\n}\n\n/** A click on an empty part of a day, reported as the cell it landed in. */\nfunction onGridClick(event: MouseEvent) {\n const target = event.target as HTMLElement | null\n if (target?.closest('.v-calendar-event')) return\n if (consumeDrag()) return\n const cell = target?.closest<HTMLElement>('.v-calendar-cell')\n if (!cell) return\n emit('cell-activate', { date: cell.dataset.iso!, minutes: Number(cell.dataset.minutes) })\n}\n\n/** The cursor a gesture holds for as long as it lasts, whatever the pointer passes over. */\nconst gestureCursor = computed(() => {\n const state = gesture.value\n if (!state || state.pointerId === null) return undefined\n if (state.kind === 'resize') return 'resize'\n if (state.kind === 'create' && state.moved) return 'create'\n return undefined\n})\n\n/** Brings a moment of the day to the top of the visible area. */\nfunction scrollToMinutes(minutes: number) {\n const root = rootEl.value\n const canvas = canvasEl.value\n if (!root || !canvas || hours.value.length === 0) return\n const rowHeight = canvas.offsetHeight / hours.value.length\n if (rowHeight <= 0) return\n root.scrollTop = ((minutes - props.timeWindow.start) / MINUTES_PER_HOUR) * rowHeight\n}\n\n// The contract VCalendar drives every view through, so its toolbar need not know which one\n// is on screen. VCalendarMonth answers the same two, its `scrollToMinutes` being a no-op.\ndefineExpose({\n /** Brings the focus onto the cell the grid is currently pointing at. */\n focus: (options?: FocusOptions) => {\n const cell = tabbable.value\n if (cell) focusCell(cellId(cell.iso, cell.minutes), false, options)\n },\n /** Scrolls the grid so a given moment of the day sits at the top of the visible area. */\n scrollToMinutes,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-calendar-time-grid\"\n :data-outside=\"gesture?.outside ? '' : undefined\"\n :data-gesture=\"gestureCursor\"\n :style=\"{ '--calendar-columns': String(days.length) }\"\n @pointermove=\"onPointermove\"\n @pointerup=\"onPointerup\"\n @pointercancel=\"onPointercancel\"\n @pointerleave=\"onPointerleave\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-calendar-head\">\n <div class=\"v-calendar-head-gutter\" />\n <div v-for=\"iso in days\" :key=\"iso\" class=\"v-calendar-head-day\">\n <slot\n name=\"day-header\"\n :iso=\"iso\"\n :weekday=\"dayName(iso)\"\n :day-text=\"dayNumber(iso)\"\n :today=\"iso === today\"\n >\n <span class=\"v-calendar-weekday\">{{ dayName(iso) }}</span>\n <span class=\"v-calendar-head-number\" :class=\"{ 'v-calendar-today': iso === today }\">\n {{ dayNumber(iso) }}\n </span>\n </slot>\n </div>\n\n <!-- The band of all-day bars, kept INSIDE the header so that the whole of it is one\n sticky box: two boxes would each need to know how tall the other was. -->\n <template v-if=\"allDay.length > 0\">\n <div class=\"v-calendar-allday-label\">\n <slot name=\"all-day-label\">{{ m.calendar.allDay }}</slot>\n </div>\n <div\n class=\"v-calendar-allday\"\n role=\"group\"\n :aria-label=\"m.calendar.allDay\"\n :style=\"{ '--calendar-lanes': String(allDayLanes) }\"\n @pointerdown=\"onBandPointerdown\"\n @keydown=\"onBandKeydown\"\n >\n <VCalendarEvent\n v-for=\"span in allDay\"\n :key=\"span.id\"\n class=\"v-calendar-bar\"\n :event=\"allDayById.get(span.id)!\"\n layout=\"chip\"\n :disabled=\"disabled\"\n :continues-before=\"span.continuesBefore\"\n :continues-after=\"span.continuesAfter\"\n v-bind=\"cardState(span.id)\"\n :style=\"{\n '--calendar-day-index': String(span.startIndex),\n '--calendar-bar-span': String(span.span),\n '--calendar-bar-lane': String(span.lane),\n }\"\n @click=\"onCardClick(span.id)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n </div>\n </template>\n </div>\n\n <div ref=\"canvasEl\" class=\"v-calendar-canvas\">\n <div class=\"v-calendar-hours\" aria-hidden=\"true\">\n <span v-for=\"minutes in hours\" :key=\"minutes\" class=\"v-calendar-hour\">\n {{ hourLabel(minutes) }}\n </span>\n </div>\n\n <div\n ref=\"columnsEl\"\n class=\"v-calendar-columns v-calendar-edge-cue\"\n role=\"grid\"\n :aria-label=\"label\"\n :data-edge=\"edgeCue\"\n @keydown=\"onKeydown\"\n @click=\"onGridClick\"\n @pointerdown=\"onGridPointerdown\"\n >\n <div v-for=\"gridRow in gridRows\" :key=\"gridRow.minutes\" role=\"row\" class=\"v-calendar-row\">\n <div\n v-for=\"cell in gridRow.cells\"\n :id=\"cell.id\"\n :key=\"cell.iso\"\n role=\"gridcell\"\n class=\"v-calendar-cell\"\n :data-iso=\"cell.iso\"\n :data-minutes=\"gridRow.minutes\"\n :data-today=\"cell.iso === today ? '' : undefined\"\n :tabindex=\"isTabStop(cell.iso, gridRow.minutes) ? 0 : -1\"\n :aria-label=\"cell.label\"\n :aria-selected=\"isFocused(cell.iso, gridRow.minutes) ? true : undefined\"\n >\n <VCalendarEvent\n v-for=\"card in cardsAt(cell.key)\"\n :key=\"card.segment.id\"\n class=\"v-calendar-block\"\n :event=\"card.event\"\n layout=\"block\"\n :disabled=\"disabled\"\n :time-text=\"card.timeText\"\n :continues-before=\"card.segment.clippedStart\"\n :continues-after=\"card.segment.clippedEnd\"\n :resizable=\"editable && card.segment.id !== DRAFT_ID && card.segment.part !== 'head'\"\n v-bind=\"cardState(card.segment.id)\"\n :style=\"card.style\"\n @click=\"onCardClick(card.segment.id)\"\n >\n <template v-if=\"$slots.event\" #default=\"slotProps\">\n <slot name=\"event\" v-bind=\"slotProps\" />\n </template>\n </VCalendarEvent>\n\n <!-- The line is drawn INSIDE the cell holding the current hour, never as a\n direct child of the grid or of a row: a `grid` owns only rows and a row only\n cells, so an extra box at either level fails `aria-required-children`. -->\n <div\n v-if=\"nowMark && nowMark.day === cell.day && nowMark.row === cell.row\"\n class=\"v-calendar-now\"\n aria-hidden=\"true\"\n :style=\"{\n '--calendar-day-index': String(nowMark.day),\n '--calendar-now-fraction': String(nowMark.fraction),\n }\"\n />\n </div>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-calendar-time-grid {\n /*\n * The one scrolling box; its scrolling itself is `.v-calendar-view`'s, in VCalendar's\n * sheet. The day names are sticky INSIDE it rather than in a container of their own, which\n * keeps them aligned with the columns for free: a separate header would have to be told how\n * far the body had scrolled sideways, and told again every time the column widths changed.\n */\n --calendar-hour: var(--vectis-control-size-calendar-hour);\n --calendar-gutter: var(--vectis-control-size-calendar-gutter);\n /*\n * The day columns, written once for the three boxes that lay them out: the header and the\n * canvas behind the hour gutter, the all-day band and the columns box without it. The three\n * must resolve to identical tracks; a bar is dropped by reading the pointer against the\n * COLUMNS' geometry; so one definition is what keeps them from drifting apart.\n */\n --calendar-tracks: repeat(\n var(--calendar-columns),\n minmax(var(--vectis-control-size-calendar-day-min), 1fr)\n );\n /*\n * One number, so the two cannot drift apart; two literals would misalign the moment the\n * caption's leading changed, with nothing to point at it.\n */\n --calendar-hour-lift: calc(\n var(--vectis-text-caption-size) * var(--vectis-text-caption-leading) / 2\n );\n\n position: relative;\n }\n\n /*\n * It is set on every descendant as well as on the scroller, because an engine that resolves\n * the cursor from what is under the pointer rather than from the capture target would\n * otherwise show the `pointer` of whichever card the gesture passes over.\n * `:not([data-outside])` keeps it off a gesture held outside the calendar, whose\n * `not-allowed` lives in VCalendar's sheet at (0,2,0): at equal weight the two would be\n * arbitrated by the order the consumer's bundler put the sheets in.\n */\n .v-calendar-time-grid[data-gesture='resize']:not([data-outside]),\n .v-calendar-time-grid[data-gesture='resize']:not([data-outside]) * {\n cursor: ns-resize;\n }\n\n .v-calendar-time-grid[data-gesture='create']:not([data-outside]),\n .v-calendar-time-grid[data-gesture='create']:not([data-outside]) * {\n cursor: move;\n }\n\n .v-calendar-head,\n .v-calendar-canvas {\n display: grid;\n grid-template-columns: var(--calendar-gutter) var(--calendar-tracks);\n /*\n * Without this the BOX would stay the width of the scroller while the tracks spilled out of\n * it: the sticky day names would paint their background only as far as the original width,\n * and the columns would run out from under them halfway across. `min-content` on a grid is\n * the sum of its track minimums, which is exactly the width wanted here.\n */\n min-inline-size: min-content;\n }\n\n /*\n * Shared grid stacking order: cards 1, current-time line and edge cues 2, dragged cards 3,\n * sticky headings 4. Intermediate containers must not create stacking contexts.\n */\n .v-calendar-head {\n position: sticky;\n inset-block-start: 0;\n z-index: 4;\n background: var(--vectis-color-surface);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-head-gutter {\n /* The corner above the hour labels. It is empty, and it is here so the day names line\n up with their columns rather than starting at the edge of the box. */\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-head-day {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--vectis-space-1);\n padding-block: var(--vectis-space-2);\n padding-inline: var(--vectis-space-1);\n }\n\n /*\n * Today is marked on the number alone, the way a calendar marks a date rather than a column;\n * the `.v-calendar-today` paint, in VCalendar's sheet.\n */\n .v-calendar-head-number {\n display: flex;\n align-items: center;\n justify-content: center;\n inline-size: var(--vectis-control-height-sm);\n block-size: var(--vectis-control-height-sm);\n border-radius: var(--vectis-radius-pill);\n font-size: var(--vectis-text-body-lg-size);\n }\n\n .v-calendar-allday-label {\n display: flex;\n align-items: start;\n justify-content: end;\n padding-block-start: var(--vectis-space-1);\n padding-inline-end: var(--vectis-space-2);\n border-block-start: 1px solid var(--vectis-color-border);\n border-inline-end: 1px solid var(--vectis-color-border);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n .v-calendar-allday {\n /*\n * The band's two axes are DISCRETE; a day, and a row; where the time grid's vertical axis\n * is continuous. That is why the bars are placed with real grid lines here and with\n * fractions there: forcing one mechanism onto both would cost a translation in each\n * direction and buy nothing.\n */\n display: grid;\n grid-column: 2 / -1;\n grid-template-columns: var(--calendar-tracks);\n grid-template-rows: repeat(\n var(--calendar-lanes),\n var(--vectis-control-size-calendar-allday-lane)\n );\n gap: 1px;\n overflow-y: auto;\n max-block-size: var(--vectis-control-size-calendar-allday-max);\n padding-block: var(--vectis-space-1);\n /*\n * The band's tracks have to line up with the canvas's exactly, because dragging a bar reads\n * the pointer against the CANVAS geometry; the two boxes span the same grid columns, so one\n * measurement serves both. A few pixels of padding here would put the last column out by\n * that much, and a bar dropped on the right edge would land a day early.\n */\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n .v-calendar-bar {\n grid-column: calc(var(--calendar-day-index) + 1) / span var(--calendar-bar-span);\n grid-row: calc(var(--calendar-bar-lane) + 1);\n min-inline-size: 0;\n }\n\n .v-calendar-hours {\n display: grid;\n grid-auto-rows: var(--calendar-hour);\n border-inline-end: 1px solid var(--vectis-color-border);\n }\n\n /*\n * The label names the hour its row BEGINS at, and it is drawn on that rule rather than under\n * it: `align-self: start` shrinks the span from its 4rem row down to its one line, and the\n * negative margin then straddles it across the rule above.\n */\n .v-calendar-hour {\n position: relative;\n align-self: start;\n margin-block-start: calc(-1 * var(--calendar-hour-lift));\n padding-inline-end: var(--vectis-space-2);\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n text-align: end;\n }\n\n /*\n * The rule carried a little way back into the gutter, so the eye joins the label to the line\n * it names. A border and not a background: Windows forced-colors flattens a background to\n * Canvas and the tick would vanish, where a border keeps a colour; the argument VSeparator\n * and the icon registry both make.\n */\n .v-calendar-hour::before {\n content: '';\n position: absolute;\n inset-block-start: var(--calendar-hour-lift);\n inset-inline-end: 0;\n inline-size: var(--vectis-control-size-calendar-tick);\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * Hidden and not removed. The span has to go on occupying its grid row: `display: none` would\n * take the row with it and shift every hour below up by one.\n */\n .v-calendar-hour:first-child {\n visibility: hidden;\n }\n\n .v-calendar-row:first-child .v-calendar-cell {\n border-block-start: none;\n }\n\n .v-calendar-columns {\n /* The containing block every card and the edge strip are placed against. The rows below\n are `display: contents`, so the cells themselves are this grid's items. */\n position: relative;\n display: grid;\n grid-column: 2 / -1;\n /*\n * Repeat the canvas track formula over the same width so grid and overlay columns align\n * without another layout mechanism.\n */\n grid-template-columns: var(--calendar-tracks);\n grid-auto-rows: var(--calendar-hour);\n }\n\n .v-calendar-row {\n /* The row exists for the accessibility tree and must not exist for the layout: a real\n box here would make the cells its children instead of the grid's, and every column\n would collapse. */\n display: contents;\n }\n\n .v-calendar-cell[data-today] {\n background: var(--vectis-color-accent-surface);\n }\n\n /*\n * The height is the event's own length and NOTHING ELSE. No `max()` against a floor token\n * here: the minimum is already applied where it belongs, `timedSegments` stretching every\n * segment to at least `slotDuration` before the overlap packing sees it, so what is drawn and\n * what the packing believes are the same box.\n */\n .v-calendar-event.v-calendar-block {\n position: absolute;\n inset-block-start: calc(var(--calendar-block-start-fraction) * 100%);\n block-size: calc(\n (var(--calendar-block-end-fraction) - var(--calendar-block-start-fraction)) * 100%\n );\n inset-inline-start: calc(\n (var(--calendar-day-index) + var(--calendar-block-column) / var(--calendar-block-columns)) /\n var(--calendar-columns) * 100%\n );\n inline-size: calc(\n var(--calendar-block-span) / var(--calendar-block-columns) / var(--calendar-columns) * 100% -\n var(--vectis-control-size-calendar-gap)\n );\n z-index: 1;\n }\n\n /*\n * It is `aria-hidden`: it is a second rendering of something the cell labels and the day\n * marked as today already say, and announcing \"current time\" from the middle of a grid would\n * interrupt without informing.\n */\n .v-calendar-now {\n position: absolute;\n inset-block-start: calc(var(--calendar-now-fraction) * 100%);\n inset-inline-start: calc(var(--calendar-day-index) / var(--calendar-columns) * 100%);\n inline-size: calc(100% / var(--calendar-columns));\n border-block-start: 1px solid var(--vectis-color-danger);\n /* Above the cards: it is the one thing that must stay readable over a full day. */\n z-index: 2;\n pointer-events: none;\n }\n\n .v-calendar-now::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n inline-size: var(--vectis-control-size-calendar-now-dot);\n block-size: var(--vectis-control-size-calendar-now-dot);\n /*\n * Half its own size upwards and backwards, so the dot is centred on the line's start rather\n * than hanging below it.\n */\n margin-block-start: calc(var(--vectis-control-size-calendar-now-dot) / -2);\n margin-inline-start: calc(var(--vectis-control-size-calendar-now-dot) / -2);\n border-radius: var(--vectis-radius-pill);\n background: var(--vectis-color-danger);\n }\n\n /* The line is a border, which forced colors keeps; the dot at its start is a background,\n which it would flatten to Canvas. */\n @media (forced-colors: active) {\n .v-calendar-now::before {\n forced-color-adjust: none;\n background: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|