vectis-ui 0.1.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/dist/components/VAccordion/VAccordion.css +1 -1
- package/dist/components/VAccordion/VAccordion.d.ts +9 -16
- package/dist/components/VAccordion/VAccordion.js.map +1 -1
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.css +1 -1
- package/dist/components/VAccordion/VAccordionItem.d.ts +33 -19
- package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js +50 -38
- package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/context.d.ts +1 -8
- package/dist/components/VAccordion/context.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.css +1 -1
- package/dist/components/VAvatar/VAvatar.d.ts +44 -29
- package/dist/components/VAvatar/VAvatar.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js +38 -35
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.d.ts +5 -16
- package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/context.d.ts +6 -10
- package/dist/components/VAvatar/context.js.map +1 -1
- package/dist/components/VBadge/VBadge.css +1 -1
- package/dist/components/VBadge/VBadge.d.ts +31 -10
- package/dist/components/VBadge/VBadge.js.map +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js +39 -24
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.css +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +4 -17
- package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js +23 -25
- package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButton.css +1 -1
- package/dist/components/VButton/VButton.d.ts +30 -10
- package/dist/components/VButton/VButton.js.map +1 -1
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js +39 -30
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.css +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +76 -13
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js +74 -8
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/context.d.ts +30 -0
- package/dist/components/VButton/context.js +6 -0
- package/dist/components/VButton/context.js.map +1 -0
- package/dist/components/VCalendar/VCalendar.css +1 -1
- package/dist/components/VCalendar/VCalendar.d.ts +50 -36
- package/dist/components/VCalendar/VCalendar.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js +99 -85
- package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.css +1 -1
- package/dist/components/VCalendar/VCalendarEvent.d.ts +5 -11
- package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.css +1 -1
- package/dist/components/VCalendar/VCalendarMonth.d.ts +10 -13
- package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js +183 -244
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.css +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +15 -20
- package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js +385 -448
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.css +1 -1
- package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js +53 -49
- package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/gesture.d.ts +158 -0
- package/dist/components/VCalendar/gesture.js +170 -0
- package/dist/components/VCalendar/gesture.js.map +1 -0
- package/dist/components/VCalendar/keyboard.d.ts +9 -3
- package/dist/components/VCalendar/keyboard.js +2 -6
- package/dist/components/VCalendar/keyboard.js.map +1 -1
- package/dist/components/VCalendar/layout.d.ts +67 -54
- package/dist/components/VCalendar/layout.js +77 -69
- package/dist/components/VCalendar/layout.js.map +1 -1
- package/dist/components/VCalendar/timeGrid.d.ts +55 -0
- package/dist/components/VCalendar/timeGrid.js +71 -0
- package/dist/components/VCalendar/timeGrid.js.map +1 -0
- package/dist/components/VCalendar/types.d.ts +41 -16
- package/dist/components/VCarousel/VCarousel.css +1 -1
- package/dist/components/VCarousel/VCarousel.d.ts +21 -41
- package/dist/components/VCarousel/VCarousel.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +157 -129
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.d.ts +0 -17
- 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.d.ts +0 -12
- package/dist/components/VCarousel/context.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.css +1 -1
- package/dist/components/VCheckbox/VCheckbox.d.ts +31 -16
- package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js +62 -29
- package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VChip/VChip.css +1 -1
- package/dist/components/VChip/VChip.d.ts +36 -26
- package/dist/components/VChip/VChip.js.map +1 -1
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js +57 -48
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.css +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +127 -31
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +336 -247
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.css +1 -1
- package/dist/components/VCombobox/VComboboxGroup.d.ts +0 -9
- package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.css +1 -1
- package/dist/components/VCombobox/VComboboxOption.d.ts +0 -15
- package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js +4 -4
- 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.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.css +1 -1
- package/dist/components/VDataTable/VDataTable.d.ts +50 -28
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +183 -194
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.css +1 -1
- package/dist/components/VDateInput/VDateInput.d.ts +90 -42
- package/dist/components/VDateInput/VDateInput.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js +159 -191
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.css +1 -1
- package/dist/components/VDatePicker/VDatePicker.d.ts +61 -30
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js +243 -221
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialog.css +1 -1
- package/dist/components/VDialog/VDialog.d.ts +45 -33
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +65 -64
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.d.ts +24 -20
- package/dist/components/VDialog/VDialogAlert.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js +36 -25
- package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.css +1 -1
- package/dist/components/VFileInput/VFileInput.d.ts +78 -26
- package/dist/components/VFileInput/VFileInput.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js +144 -132
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.css +1 -1
- package/dist/components/VFilePicker/VFilePicker.d.ts +47 -30
- package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js +166 -163
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFilePicker/fileKind.d.ts +2 -27
- package/dist/components/VFilePicker/fileKind.js +9 -8
- package/dist/components/VFilePicker/fileKind.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.css +1 -1
- package/dist/components/VHotkeys/VHotkeys.d.ts +8 -30
- package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VHotkeys/platform.js.map +1 -1
- package/dist/components/VIcon/VIcon.css +1 -1
- package/dist/components/VIcon/VIcon.d.ts +15 -6
- package/dist/components/VIcon/VIcon.js.map +1 -1
- package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js +45 -37
- package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/iconProps.js.map +1 -1
- package/dist/components/VIcon/icons/index.d.ts +1 -1
- package/dist/components/VIcon/icons/index.js.map +1 -1
- package/dist/components/VIcon/icons/names.d.ts +1 -1
- package/dist/components/VIcon/icons/names.js.map +1 -1
- package/dist/components/VIcon/resolver.d.ts +3 -3
- package/dist/components/VIcon/resolver.js.map +1 -1
- package/dist/components/VIcon/types.d.ts +12 -12
- package/dist/components/VIconButton/VIconButton.css +1 -1
- package/dist/components/VIconButton/VIconButton.d.ts +28 -11
- package/dist/components/VIconButton/VIconButton.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js +26 -20
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInput.css +1 -1
- package/dist/components/VInput/VInput.d.ts +26 -7
- package/dist/components/VInput/VInput.js.map +1 -1
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js +86 -75
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInputGroup.css +2 -0
- package/dist/components/VInput/VInputGroup.d.ts +47 -0
- package/dist/components/VInput/VInputGroup.js +9 -0
- package/dist/components/VInput/VInputGroup.js.map +1 -0
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +80 -0
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/VInput/context.d.ts +11 -0
- package/dist/components/VInput/context.js +6 -0
- package/dist/components/VInput/context.js.map +1 -0
- package/dist/components/VInputOTP/VInputOTP.css +1 -1
- package/dist/components/VInputOTP/VInputOTP.d.ts +37 -22
- package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +98 -61
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenu.d.ts +23 -30
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js +31 -27
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.css +1 -1
- package/dist/components/VMenu/VMenuGroup.d.ts +12 -8
- package/dist/components/VMenu/VMenuGroup.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js +11 -11
- package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.css +1 -1
- package/dist/components/VMenu/VMenuItem.d.ts +11 -19
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +65 -57
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +14 -30
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +43 -44
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.d.ts +2 -6
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.css +1 -1
- package/dist/components/VPagination/VPagination.d.ts +60 -22
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +113 -161
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VPopover/VPopover.css +1 -1
- package/dist/components/VPopover/VPopover.d.ts +26 -7
- package/dist/components/VPopover/VPopover.js.map +1 -1
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +43 -37
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.css +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.d.ts +15 -4
- package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js +32 -30
- package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.css +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.d.ts +23 -8
- package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js +34 -32
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VRadio/VRadio.css +1 -1
- package/dist/components/VRadio/VRadio.d.ts +36 -18
- package/dist/components/VRadio/VRadio.js.map +1 -1
- package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js +55 -26
- package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.d.ts +4 -2
- 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.css +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.d.ts +5 -19
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js +5 -5
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.css +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +9 -17
- package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.css +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +22 -30
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +77 -87
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/context.d.ts +0 -14
- package/dist/components/VSideNavigation/context.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.css +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +9 -9
- 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.css +1 -1
- package/dist/components/VSlider/VSlider.d.ts +52 -27
- package/dist/components/VSlider/VSlider.js.map +1 -1
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js +202 -118
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.css +1 -1
- package/dist/components/VSnackbar/VSnackbar.d.ts +0 -23
- package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js +38 -38
- package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/state.d.ts +0 -27
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.css +1 -1
- package/dist/components/VSpinner/VSpinner.d.ts +6 -6
- package/dist/components/VSpinner/VSpinner.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js +10 -9
- package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.css +1 -1
- package/dist/components/VSwitch/VSwitch.d.ts +35 -13
- package/dist/components/VSwitch/VSwitch.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js +62 -21
- package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTab.css +1 -1
- package/dist/components/VTabs/VTab.d.ts +12 -12
- package/dist/components/VTabs/VTab.js.map +1 -1
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js +36 -24
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.d.ts +2 -10
- package/dist/components/VTabs/VTabPanel.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabs.css +1 -1
- package/dist/components/VTabs/VTabs.d.ts +27 -18
- package/dist/components/VTabs/VTabs.js.map +1 -1
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js +92 -81
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/context.d.ts +9 -19
- package/dist/components/VTabs/context.js +1 -1
- package/dist/components/VTabs/context.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.css +1 -1
- package/dist/components/VTextarea/VTextarea.d.ts +34 -6
- package/dist/components/VTextarea/VTextarea.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js +91 -74
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.css +1 -1
- package/dist/components/VTimeInput/VTimeInput.d.ts +123 -33
- package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js +294 -291
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/search.d.ts +24 -0
- package/dist/components/VTimeInput/search.js +22 -0
- package/dist/components/VTimeInput/search.js.map +1 -0
- package/dist/components/VTimePicker/VTimePicker.css +1 -1
- package/dist/components/VTimePicker/VTimePicker.d.ts +51 -30
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js +216 -151
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/limits.d.ts +88 -0
- package/dist/components/VTimePicker/limits.js +70 -0
- package/dist/components/VTimePicker/limits.js.map +1 -0
- package/dist/components/VToast/VToast.css +1 -1
- package/dist/components/VToast/VToast.d.ts +0 -10
- package/dist/components/VToast/VToast.js.map +1 -1
- package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js +10 -10
- package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/VToaster.css +1 -1
- package/dist/components/VToast/VToaster.d.ts +0 -17
- package/dist/components/VToast/VToaster.js.map +1 -1
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js +29 -28
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/state.d.ts +10 -19
- package/dist/components/VToast/state.js +1 -1
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/components/VToggle/VToggle.css +1 -1
- package/dist/components/VToggle/VToggle.d.ts +56 -11
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +80 -46
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.d.ts +16 -2
- package/dist/components/VToggle/VToggleItem.js +0 -2
- package/dist/components/VToggle/VToggleItem.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js +23 -15
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/context.d.ts +4 -24
- package/dist/components/VToggle/context.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.css +1 -1
- package/dist/components/VTooltip/VTooltip.d.ts +9 -33
- package/dist/components/VTooltip/VTooltip.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +28 -24
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTypography/VTypography.js.map +1 -1
- package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useAriaLabel.d.ts +0 -17
- package/dist/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useClearable.d.ts +42 -0
- package/dist/composables/useClearable.js +19 -0
- package/dist/composables/useClearable.js.map +1 -0
- package/dist/composables/useControlShape.d.ts +22 -0
- package/dist/composables/useControlShape.js +13 -0
- package/dist/composables/useControlShape.js.map +1 -0
- package/dist/composables/useDetailsOpen.d.ts +22 -0
- package/dist/composables/useDetailsOpen.js +21 -0
- package/dist/composables/useDetailsOpen.js.map +1 -0
- package/dist/composables/useFieldIds.d.ts +0 -15
- package/dist/composables/useFieldIds.js +8 -7
- package/dist/composables/useFieldIds.js.map +1 -1
- package/dist/composables/useFieldPanel.d.ts +21 -18
- package/dist/composables/useFieldPanel.js +43 -23
- package/dist/composables/useFieldPanel.js.map +1 -1
- package/dist/composables/useFileDrop.d.ts +0 -22
- package/dist/composables/useFileDrop.js.map +1 -1
- package/dist/composables/useFileField.d.ts +45 -17
- package/dist/composables/useFileField.js +40 -20
- package/dist/composables/useFileField.js.map +1 -1
- package/dist/composables/useFocusoutDismiss.d.ts +0 -15
- package/dist/composables/useFocusoutDismiss.js.map +1 -1
- package/dist/composables/useIconClickHandlers.d.ts +29 -14
- package/dist/composables/useIconClickHandlers.js +15 -5
- package/dist/composables/useIconClickHandlers.js.map +1 -1
- package/dist/composables/useInertLink.d.ts +20 -0
- package/dist/composables/useInertLink.js +23 -0
- package/dist/composables/useInertLink.js.map +1 -0
- package/dist/composables/useMaskedField.d.ts +33 -10
- package/dist/composables/useMaskedField.js +45 -1
- package/dist/composables/useMaskedField.js.map +1 -1
- package/dist/composables/usePopover.d.ts +15 -1
- package/dist/composables/usePopover.js +23 -8
- package/dist/composables/usePopover.js.map +1 -1
- package/dist/composables/useProgressValue.d.ts +5 -8
- package/dist/composables/useProgressValue.js +6 -3
- package/dist/composables/useProgressValue.js.map +1 -1
- package/dist/composables/useRootAttrs.d.ts +12 -19
- package/dist/composables/useRootAttrs.js.map +1 -1
- package/dist/composables/useTimer.d.ts +8 -10
- package/dist/composables/useTimer.js.map +1 -1
- package/dist/i18n/en.d.ts +2 -10
- package/dist/i18n/en.js +18 -19
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.d.ts +2 -18
- package/dist/i18n/fr.js +18 -19
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/state.d.ts +44 -47
- package/dist/i18n/state.js +20 -17
- package/dist/i18n/state.js.map +1 -1
- package/dist/i18n/types.d.ts +53 -44
- package/dist/icons.d.ts +3 -3
- package/dist/index.d.ts +43 -17
- package/dist/index.js +45 -44
- package/dist/styles.css +1 -1
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.d.ts +5 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +5 -1
- package/dist/tokens/semantic.js +6 -2
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens/themes/dark.js.map +1 -1
- package/dist/tokens/types.d.ts +7 -15
- package/dist/tokens/types.js.map +1 -1
- package/dist/tokens.json +25 -5
- package/dist/types.d.ts +24 -0
- package/dist/utils/array.d.ts +3 -6
- package/dist/utils/array.js.map +1 -1
- package/dist/utils/arrowNav.d.ts +17 -17
- package/dist/utils/arrowNav.js +5 -3
- package/dist/utils/arrowNav.js.map +1 -1
- package/dist/utils/chip.d.ts +11 -15
- package/dist/utils/chip.js.map +1 -1
- package/dist/utils/css.d.ts +22 -11
- package/dist/utils/css.js +5 -2
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/date.d.ts +42 -19
- package/dist/utils/date.js +97 -98
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/direction.d.ts +6 -12
- package/dist/utils/direction.js.map +1 -1
- package/dist/utils/env.d.ts +5 -6
- package/dist/utils/file.d.ts +46 -58
- package/dist/utils/file.js +43 -36
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/focus.d.ts +8 -17
- package/dist/utils/focus.js.map +1 -1
- package/dist/utils/hostWarns.d.ts +12 -0
- package/dist/utils/hostWarns.js +6 -0
- package/dist/utils/hostWarns.js.map +1 -0
- package/dist/utils/ids.d.ts +10 -0
- package/dist/utils/ids.js +8 -0
- package/dist/utils/ids.js.map +1 -0
- package/dist/utils/matcher.d.ts +5 -9
- package/dist/utils/matcher.js.map +1 -1
- package/dist/utils/memo.d.ts +12 -0
- package/dist/utils/memo.js +9 -0
- package/dist/utils/memo.js.map +1 -0
- package/dist/utils/number.d.ts +3 -4
- package/dist/utils/number.js.map +1 -1
- package/dist/utils/text.d.ts +20 -9
- package/dist/utils/text.js +17 -3
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/time.d.ts +53 -61
- package/dist/utils/time.js +49 -45
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/vnode.d.ts +8 -15
- package/dist/utils/vnode.js.map +1 -1
- package/package.json +6 -6
- package/dist/components/VToggle/VToggleItem.css +0 -2
|
@@ -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, and the events drawn over it.\n *\n * It is internal to VCalendar and has no story of its own — its documentation lives with\n * the component that renders it. It computes nothing about dates or overlaps: it is handed\n * the days and the already-placed segments, and its whole job is to turn them into boxes.\n *\n * WHY EVERY CELL EXISTS. Twenty-four rows times seven days is a hundred and sixty-eight\n * elements, where the rules between the hours could have been one repeating gradient. Two\n * things pay for them. A background is forced to `Canvas` under Windows forced-colors and\n * the ruling would vanish, where a border is forced to `CanvasText` and survives — the\n * argument VSeparator and the VIcon registry both make. And the ARIA \"grid\" pattern needs\n * real cells to move a focus between, which is what gives the keyboard somewhere to be and\n * what satisfies the rule that a scrolling region must hold something reachable by Tab.\n *\n * WHY THE CARDS SIT INSIDE THE CELLS. An event is placed absolutely against the whole\n * columns box, so putting the cards in one overlay layer would have been simpler CSS. They\n * are instead children of the cell their start falls in, because an absolutely positioned\n * element leaves its parent's box for LAYOUT while staying inside it in the document — and\n * therefore in the accessibility tree. In an overlay they would sit outside the grid\n * entirely, which breaks the reading order and fails `aria-required-children` the moment\n * anything tries to put them back.\n *\n * The day names are NOT marked up as column headers. They live above the scrolling area so\n * that they can stay in view, which puts them outside the grid; instead every cell names\n * its own day in full (\"Wednesday 10 June, 09:00\"), so nothing is lost to a reader who\n * never sees the row at all.\n */\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport { formatDisplay as formatDate } from '../../utils/date'\nimport { isRtl as isElementRtl } from '../../utils/direction'\nimport { clamp } from '../../utils/number'\nimport { formatDisplay as formatTimeDisplay, type HourFormat } from '../../utils/time'\n\nimport { useMessages } from '../../i18n/state'\n\nimport VCalendarEvent from './VCalendarEvent.vue'\nimport {\n DRAG_THRESHOLD,\n blockEdgeAt,\n floorToSlot,\n fractionOf,\n inlineEdgeAt,\n isAllDayEvent,\n minutesAt,\n moveEvent,\n moveEventToDay,\n packAllDay,\n packDayColumn,\n pointToCell,\n pointWithin,\n resizeEvent,\n snapToSlot,\n timeOf,\n timedSegments,\n timesOf,\n type AllDaySpan,\n type PlacedSegment,\n type TimeWindow,\n} from './layout'\nimport { calendarIntent } from './keyboard'\nimport { EDGE_BAND, useEdgeStep } from './edgeStep'\nimport type { CalendarEvent, CalendarEventId, CalendarEventTimes } 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 TimeGridProps<T> {\n days: string[]\n events: T[]\n window: TimeWindow\n slotDuration: number\n locale: string\n hourFormat: HourFormat\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 an empty part of the grid makes an event when it is taken up. */\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<TimeGridProps<E>>()\n\nconst emit = defineEmits<{\n /** An empty part of the grid was activated, at this day and this moment. */\n 'cell-activate': [iso: string, minutes: number]\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 asking to become an event. */\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: {\n event: E\n layout: 'block' | 'chip'\n timeText: string\n continuesBefore: boolean\n continuesAfter: boolean\n dragging: boolean\n grabbed: boolean\n }): unknown\n 'day-header'?(props: { iso: string; weekday: string; day: string; today: boolean }): unknown\n 'all-day-label'?(): unknown\n}>()\n\nconst m = useMessages()\nconst uid = useId()\n\nconst MINUTES_PER_HOUR = 60\n\n/** The hour each row stands for, as minutes since midnight. */\nconst hours = computed(() => {\n const rows: number[] = []\n for (let at = props.window.start; at < props.window.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 mints an id of its own.\n */\nconst DRAFT_ID = '__vectis-calendar-draft__'\n\n/**\n * The id the faded echo of a dragged event carries.\n *\n * It has to DIFFER from the event's own, or the two would collide in every map the layout\n * keys by id and only one of them would ever be drawn. The original is recovered from it —\n * and handed to the card as `ghostOf` — because the colour of an event is derived from its\n * id, so an echo under a fresh id would come out a different colour from the card it belongs\n * to. `hueOf` reads a number and its digits as the same event, so the string form is enough.\n */\nconst GHOST_PREFIX = '__vectis-calendar-ghost__'\nconst ghostIdOf = (id: CalendarEventId) => `${GHOST_PREFIX}${id}`\nconst originalIdOf = (id: CalendarEventId) => String(id).slice(GHOST_PREFIX.length)\nconst isGhostId = (id: CalendarEventId) => String(id).startsWith(GHOST_PREFIX)\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 {\n kind: 'move' | 'move-days' | 'resize' | 'create'\n id: CalendarEventId\n /**\n * Where the event was when the gesture began. Every frame is computed from THIS and never\n * from the frame before it, so a long drag cannot accumulate rounding drift.\n */\n origin: CalendarEventTimes\n /** What the card shows right now. The model is not touched until the gesture ends. */\n preview: CalendarEventTimes\n pointerId: number | null\n originX: number\n originY: number\n /** How far into the card the pointer took hold, so the card does not jump under it. */\n grabOffset: number\n grabColumn: number\n /**\n * Where the pointer last was.\n *\n * Kept because two things move the calendar UNDER a still pointer — paging at an edge, and\n * auto-scrolling — and each has to work out afresh what the pointer is now over. Without it\n * a hand held motionless at an edge would page the view and leave the card behind on a day\n * that is no longer on screen.\n */\n lastX: number\n lastY: number\n /**\n * The share of its column the dragged card had when the gesture began — 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 * Exactly one of the two is ever set — a timed event is a segment, an all-day one a bar.\n */\n ghostColumn: Pick<PlacedSegment, 'column' | 'span' | 'columns'> | null\n ghostLane: number | null\n /** Whether the pointer has travelled far enough for this to be a drag and not a click. */\n moved: boolean\n /**\n * Whether the pointer is off the calendar altogether — over the toolbar, or over the page\n * beside it. Letting go here abandons the gesture whole and writes nothing.\n *\n * A KEYBOARD grab can never set it, and structurally rather than by a guard: `onPointermove`\n * is the only thing that writes it, and it returns on the id mismatch before reaching that\n * line — a grab's `pointerId` is null, which no real pointer's id ever equals.\n */\n outside: boolean\n}\n\nconst gesture = ref<Gesture | null>(null)\n\n/** True while a card is being held by the keyboard rather than dragged by a pointer. */\nconst grabbing = computed(() => gesture.value !== null && gesture.value.pointerId === null)\n\n/**\n * The event the echo stands for: the one being dragged, back where the drag began.\n *\n * ONE predicate, read by the three places that have to agree — the event list, the packing\n * and the band. They were briefly allowed to disagree, and the failure was immediate and\n * total: a placement with no event behind it, and every card in the grid failing to render.\n * A press that has not travelled yet produces none, or a click would flash a second card\n * over the first.\n */\nconst ghostEvent = computed<E | null>(() => {\n const state = gesture.value\n if (!state?.moved) return null\n const original = props.events.find((event) => event.id === state.id)\n return original ? ({ ...original, ...state.origin, id: ghostIdOf(state.id) } as E) : null\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, instead of staying on the column it used to occupy.\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 * `ghostColumn` above for why that one part must NOT be recomputed.\n */\nconst ghostSegment = computed<PlacedSegment | null>(() => {\n const state = gesture.value\n const event = ghostEvent.value\n if (!event || !state?.ghostColumn) return null\n const [segment] = timedSegments([event], props.days, props.window, props.slotDuration)\n return segment ? { ...segment, ...state.ghostColumn } : null\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 events as the grid should currently DRAW them — which is the model, with the one being\n * 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 */\nconst drawnEvents = computed<E[]>(() => {\n const state = gesture.value\n if (!state) return props.events\n\n if (state.kind === 'create') {\n const draft = {\n id: DRAFT_ID,\n title: m.value.calendar.newEvent(props.events.length + 1),\n ...state.preview,\n // A draft carries exactly the fields `CalendarEvent` declares, and a consumer's own\n // type may require more. It exists for one render and never reaches the model, which\n // is what makes the cast safe — the calendar mints the real event on release.\n } as unknown as E\n return [...props.events, draft]\n }\n\n const moving = props.events.map((event) =>\n event.id === state.id ? ({ ...event, ...state.preview } as E) : event,\n )\n\n /*\n * The echo of where it started. It is listed here whether or not it has a box to draw on,\n * because this is what `eventsById` is built from and the template reads a card's event\n * back out of that map. An echo whose day the view has paged past simply produces no\n * placement below — which is what happens to any ordinary event on a day that is off show.\n */\n return ghostEvent.value ? [...moving, ghostEvent.value] : moving\n})\n\nconst eventsById = computed(() => new Map(drawnEvents.value.map((event) => [event.id, event])))\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 ? drawnEvents.value.filter((event) => event.id !== ghost.id) : drawnEvents.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/**\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.window.start || props.now > props.window.end) return null\n const row = Math.floor((props.now - props.window.start) / MINUTES_PER_HOUR)\n return {\n dayIndex,\n // Clamped so the line at the very end of the window still has a cell to live in.\n cell: `${dayIndex}:${clamp(row, 0, hours.value.length - 1)}`,\n fraction: fractionOf(props.now, props.window),\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(drawnEvents.value, props.days, props.window, props.slotDuration)\n const ghost = ghostSegment.value\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 (ghost && segment.id === ghost.id) 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 ghost ? [...real, ghost] : real\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 */\nconst byCell = computed(() => {\n const map = new Map<string, PlacedSegment[]>()\n for (const segment of placed.value) {\n const row = Math.floor((segment.start - props.window.start) / MINUTES_PER_HOUR)\n const key = `${segment.dayIndex}:${clamp(row, 0, hours.value.length - 1)}`\n const list = map.get(key)\n if (list) list.push(segment)\n else map.set(key, [segment])\n }\n return map\n})\n\nconst cellId = (iso: string, minutes: number) => `${uid}-c-${iso}-${minutes}`\n\n/*\n * Every label the grid writes, worked out once per day and once per hour instead of once\n * per CELL — the shape `byCell` above already uses, and for the same reason.\n *\n * The grid renders `hours × days` cells, 24 × 7 by default. Reading these straight from\n * `formatDate` in the template meant 168 `cellLabel` calls per render, each doing a\n * `parseISO`, a `JSON.stringify` to build the formatter's cache key and two `Intl` formats,\n * plus `dayName`/`dayNumber` twice per column (slot props, then the fallback content). None\n * of it depends on the pointer — yet `applyPoint` assigns a new `state.preview` on every\n * `pointermove`, so the whole template re-renders at pointer rate and all of it was\n * recomputed on every frame of a drag.\n *\n * Two maps of 7 and 24 entries replace that: they recompute when the locale, the days or\n * the window change, and never because a card moved.\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: formatDate(iso, props.locale, { weekday: 'short' }),\n number: formatDate(iso, props.locale, { day: 'numeric' }),\n full: formatDate(iso, props.locale, { weekday: 'long', day: 'numeric', month: 'long' }),\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.hourFormat))\n return map\n})\n\n/*\n * The hour gutter and the drag readout both ask for an hour that is not always one of the\n * rows — a preview lands between them — so this falls back to formatting rather than\n * assuming the map holds every minute.\n */\nconst hourLabel = (minutes: number) =>\n hourLabels.value.get(minutes) ??\n formatTimeDisplay(timeOf(minutes), props.locale, props.hourFormat)\n\nconst dayName = (iso: string, weekday: 'short' | 'long') =>\n weekday === 'short'\n ? (dayLabels.value.get(iso)?.short ?? formatDate(iso, props.locale, { weekday }))\n : formatDate(iso, props.locale, { weekday })\n\nconst dayNumber = (iso: string) =>\n dayLabels.value.get(iso)?.number ?? formatDate(iso, props.locale, { day: 'numeric' })\n\n/** What one cell is called: the day in full, then the hour. */\nconst cellLabel = (iso: string, minutes: number) =>\n `${dayLabels.value.get(iso)?.full ?? iso}, ${hourLabel(minutes)}`\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 */\nfunction timeTextOf(event: E): string {\n const start = formatTimeDisplay(event.startTime, props.locale, props.hourFormat)\n const end = formatTimeDisplay(event.endTime, props.locale, props.hourFormat)\n const range = `${start} – ${end}`\n return event.timezone ? `${range} (${event.timezone})` : range\n}\n\nfunction styleFor(segment: PlacedSegment) {\n return {\n '--event-start': String(fractionOf(segment.start, props.window)),\n '--event-end': String(fractionOf(segment.end, props.window)),\n '--event-day': String(segment.dayIndex),\n '--event-column': String(segment.column),\n '--event-span': String(segment.span),\n '--event-columns': String(segment.columns),\n }\n}\n\nconst isFocused = (iso: string, minutes: number) =>\n focused.value.iso === iso && focused.value.minutes === minutes\n\n/**\n * Where the tab stop goes when the focused cell is not one of the cells on screen — after\n * a view change, or before anything has been focused at all. Without it no cell would carry\n * `tabindex=\"0\"`, the grid would drop out of the tab order, and the scrolling region would\n * have nothing reachable inside it, which is an accessibility failure and not merely an\n * inconvenience.\n */\nconst fallbackCell = computed(() => {\n const iso = props.days[0]\n if (!iso) return null\n return { iso, minutes: props.window.start }\n})\n\nconst tabbable = computed<FocusedCell | null>(() => {\n const current = focused.value\n if (props.days.includes(current.iso)) {\n const rounded =\n Math.floor((current.minutes - props.window.start) / MINUTES_PER_HOUR) * MINUTES_PER_HOUR +\n props.window.start\n if (rounded >= props.window.start && rounded < props.window.end) {\n return { iso: current.iso, minutes: rounded }\n }\n }\n return fallbackCell.value\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 void nextTick(() => document.getElementById(cellId(iso, minutes))?.focus())\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.key, event.shiftKey, '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.window.start,\n props.window.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 if (intent.kind === 'activate') {\n event.preventDefault()\n emit('cell-activate', iso, minutes)\n }\n}\n\n/* ------------------------------------------------------------------ the gestures */\n\n/**\n * Starts a gesture, from whichever of the three ways one can begin.\n *\n * Every entry point funnels through here, so the rules that make a gesture safe — one at a\n * time, the origin captured once, the pointer taken hold of on the GRID — are written once\n * rather than three times.\n */\nfunction begin(\n state: Omit<\n Gesture,\n 'preview' | 'moved' | 'outside' | 'lastX' | 'lastY' | 'ghostColumn' | 'ghostLane'\n >,\n event?: PointerEvent,\n) {\n /*\n * Read BEFORE the gesture is set. Both are derived from `drawnEvents`, which the gesture\n * immediately starts rewriting — a line later and they would already describe the preview\n * rather than the place it came from. Which of the two answers is also what says whether\n * this is a timed card or an all-day bar, and therefore which of the echo's two shapes the\n * gesture has to carry.\n */\n const segment = placed.value.find((item) => item.id === state.id)\n const span = allDay.value.find((item) => item.id === state.id)\n\n gesture.value = {\n ...state,\n preview: { ...state.origin },\n lastX: state.originX,\n lastY: state.originY,\n ghostColumn: segment\n ? { column: segment.column, span: segment.span, columns: segment.columns }\n : null,\n ghostLane: span ? span.lane : null,\n moved: false,\n outside: false,\n }\n\n if (!event) return\n /*\n * @fallback — capturing the pointer on the COLUMNS box, and not on the card, is what makes\n * one set of move and up handlers serve all three gestures and what keeps a drag alive\n * when the pointer wanders off the grid: capture retargets every later pointer event back\n * to the captured element, so the bindings in the template are the whole of it — no\n * document listener, no release, nothing to tear down on unmount.\n *\n * It is wrapped because a pointer event fired by a TEST refers to no real pointer, and the\n * call then throws. Failing to capture merely means the drag stops at the edge, which no\n * test is checking.\n */\n try {\n // The SCROLLER, not the columns box: the all-day band sits in the sticky header, and this\n // is the only element that contains both it and the canvas. Capture retargets every later\n // pointer event here, which is why one set of move and up bindings serves all four kinds.\n rootEl.value?.setPointerCapture(event.pointerId)\n } catch {\n /* a synthetic pointer: nothing to capture */\n }\n}\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 if (!target) return\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!rect) return\n\n const point = pointToCell(\n { x: event.clientX, y: event.clientY },\n geometryOf(rect),\n props.window,\n isRtl(),\n )\n\n const card = target.closest<HTMLElement>('.v-calendar-event')\n if (card) {\n if (!props.editable) return\n const item = eventsById.value.get(cardIdOf(card))\n if (!item || isAllDayEvent(item)) return\n\n const resizing = target.closest('[data-calendar-handle]') !== null\n begin(\n {\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 // Where inside the card the pointer took hold, so it does not jump under the finger.\n grabOffset: resizing ? 0 : point.minutes - minutesAt(item.startTime, props.window.start),\n grabColumn: point.columnIndex,\n },\n event,\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 * A new event begins at the top of the slot the pointer went down in — rounded DOWN, never\n * to the nearest — because a press at 09:07 means \"the nine o'clock slot\" and not the\n * quarter past. It is one slot long to begin with, so a press that never moves creates\n * exactly the short event a click is meant to create, and a press that travels stretches\n * its end instead. One path serves both, and a click is simply the drag that stayed still.\n */\n const start = floorToSlot(point.minutes, props.slotDuration)\n begin(\n {\n kind: 'create',\n id: DRAFT_ID,\n origin: {\n start: iso,\n end: iso,\n startTime: timeOf(start),\n endTime: timeOf(Math.min(start + props.slotDuration, props.window.end)),\n },\n pointerId: event.pointerId,\n originX: event.clientX,\n originY: event.clientY,\n grabOffset: 0,\n grabColumn: point.columnIndex,\n },\n event,\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 */\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 if (!card) return\n const item = eventsById.value.get(cardIdOf(card))\n if (!item) return\n\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!rect) return\n const point = pointToCell(\n { x: event.clientX, y: event.clientY },\n geometryOf(rect),\n props.window,\n isRtl(),\n )\n\n begin(\n {\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: point.columnIndex,\n },\n event,\n )\n}\n\n/** The event a card stands for, read back off the attribute the card publishes. */\nfunction cardIdOf(card: HTMLElement): CalendarEventId {\n const raw = card.dataset.eventId ?? ''\n /*\n * An id may be a number, and an attribute is always text. The map is keyed by the ORIGINAL\n * value, so a numeric id has to be turned back into one — otherwise every card belonging\n * to a numerically-keyed calendar would look up as a miss and nothing would drag.\n */\n return eventsById.value.has(raw) ? raw : Number(raw)\n}\n\nfunction onPointermove(event: PointerEvent) {\n const state = gesture.value\n if (!state || state.pointerId !== event.pointerId) return\n\n state.lastX = event.clientX\n state.lastY = event.clientY\n\n // Below the threshold nothing has happened yet. That is what keeps a click a click: the\n // hand's tremor during a press would otherwise register as a one-pixel drag.\n if (\n !state.moved &&\n Math.abs(event.clientX - state.originX) < DRAG_THRESHOLD &&\n Math.abs(event.clientY - state.originY) < DRAG_THRESHOLD\n ) {\n return\n }\n\n state.moved = true\n state.outside = isPointerOutside(state)\n applyPoint(state)\n watchEdges(state)\n}\n\n/**\n * Whether the pointer has left the calendar's own box.\n *\n * The box is the WHOLE view — the scroller, the element carrying `.v-calendar-view` — and\n * deliberately not `.v-calendar-columns`, whose rect excludes the sticky day names, the all-day\n * band and the hour gutter: a bar dropped on the band is an ordinary thing to do, and measured\n * against the columns it would read as dropped off the calendar and silently revert.\n *\n * The rect goes in RAW and never through `geometryOf`, whose `inlineStart` is the box's RIGHT\n * edge in a right-to-left page: containment has no reading direction, and flipping the axis here\n * would answer a question nobody asked — every right-to-left drop would read as outside.\n *\n * It is called from `onPointermove` ALONE, and not from `applyPoint`. That function has three\n * callers, and the other two — the view paging, and the auto-scroll frame — move the days and\n * the scroll position under a pointer that has not moved at all: they cannot change this answer,\n * and asking them to recompute it would force a layout on every animation frame.\n *\n * TRAP — neither `applyPoint` nor `watchEdges` is gated on the result, and neither may become\n * so. The card must keep following the pointer while it is out (that is what makes coming back\n * in seamless), and paging must keep running (pushing past the edge is how one crosses into the\n * next week). Being outside decides what happens on RELEASE, and nothing else.\n */\nfunction isPointerOutside(state: Gesture): boolean {\n const rect = rootEl.value?.getBoundingClientRect()\n return rect ? !pointWithin({ x: state.lastX, y: state.lastY }, rect) : false\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) {\n const rect = columnsEl.value?.getBoundingClientRect()\n if (!rect) return\n // Measuring here is safe: this runs from a handler or a frame, hence in a browser, never\n // in a render.\n const point = pointToCell(\n { x: state.lastX, y: state.lastY },\n geometryOf(rect),\n props.window,\n isRtl(),\n )\n\n if (state.kind === 'move-days') {\n /*\n * A bar keeps its length in days, so the target is its START shifted by however many\n * columns the pointer has crossed — not the column the pointer is over. Grab a\n * Monday-to-Wednesday bar by its Tuesday and the pointer stays on its Tuesday.\n */\n const column = clamp(\n originColumn(state) + (point.columnIndex - state.grabColumn),\n 0,\n props.days.length - 1,\n )\n const iso = props.days[column]\n if (iso) state.preview = moveEventToDay(state.origin, iso)\n return\n }\n\n if (state.kind === 'move') {\n /*\n * The day comes from the COLUMN the pointer is over, read out of the list of days on\n * show — never from date arithmetic on how many columns were crossed, which would count\n * the days the calendar is hiding and land the event on a Saturday nobody can see.\n */\n const column = clamp(\n originColumn(state) + (point.columnIndex - state.grabColumn),\n 0,\n props.days.length - 1,\n )\n const iso = props.days[column]!\n const start = snapToSlot(point.minutes - state.grabOffset, props.slotDuration)\n const delta = start - minutesAt(state.origin.startTime, props.window.start)\n state.preview = { ...moveEvent(state.origin, 0, delta, props.window), start: iso, end: iso }\n return\n }\n\n state.preview = resizeEvent(\n state.origin,\n snapToSlot(point.minutes, props.slotDuration),\n props.slotDuration,\n props.window,\n )\n}\n\n/* ------------------------------------------------------- crossing the boundaries */\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\nconst edge = useEdgeStep(\n (direction) => emit('step', direction),\n () => props.edgeStepDelay,\n)\n\n/** Which edge is counting down, in words, for the stylesheet to light up. */\nconst edgeCue = computed(() =>\n edge.pending.value === -1 ? 'start' : edge.pending.value === 1 ? 'end' : undefined,\n)\n\n/** Tells the two boundary mechanisms where the pointer now is. */\nfunction watchEdges(state: Gesture) {\n const columns = columnsEl.value?.getBoundingClientRect()\n const scroller = rootEl.value?.getBoundingClientRect()\n if (!columns || !scroller) return\n\n // Paging is measured against the COLUMNS, which is what the days occupy; scrolling against\n // the SCROLLER, which is what actually moves.\n edge.watchEdge(inlineEdgeAt(state.lastX, geometryOf(columns), EDGE_BAND, isRtl()))\n setScrollSpeed(props.autoScroll ? blockEdgeAt(state.lastY, scroller, EDGE_BAND) : 0)\n}\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 applyPoint(state)\n scrollFrame = requestAnimationFrame(scrollStep)\n}\n\nfunction stopScrolling() {\n if (scrollFrame !== null) cancelAnimationFrame(scrollFrame)\n scrollFrame = null\n scrollSpeed = 0\n}\n\n/** Everything a gesture leaves running, dropped on every path out of one. */\nfunction releaseBoundaries() {\n edge.cancel()\n stopScrolling()\n}\n\nonBeforeUnmount(stopScrolling)\n\n/*\n * Paging swaps the days out from under a live drag, and the dragged event's day is then off\n * screen — the card would vanish until the next pointer move, which never comes if the hand\n * is holding still against the edge. Re-applying the last position as soon as the new days\n * are rendered is what keeps the card under the pointer across the boundary.\n */\nwatch(\n () => props.days,\n () => {\n const state = gesture.value\n if (state?.moved) applyPoint(state)\n },\n { flush: 'post' },\n)\n\n/**\n * Which column the event being moved started in.\n *\n * The fallback is what makes the formula above general rather than a special case. For a\n * TIMED card these two are necessarily equal — you grab a card, and a card lives inside its\n * own column — so the offset is zero and the event simply follows the pointer. For a BAR they\n * differ, which is what keeps a three-day one from jumping when grabbed by its middle. And\n * when the origin day is not on screen at all — a bar that began before this range, or an\n * event whose view has been paged out from under the drag — falling back to the grab column\n * makes the offset zero again, so the event 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 * Whether the press that is about to become a click was a drag.\n *\n * A plain `let`, since nothing renders it: `pointerup` always fires before `click`, so this\n * is written on the way out of the gesture and read by the click that immediately follows.\n * Without it, letting go of a card at the end of a drag would ALSO open it — the consumer's\n * editor would appear over every event the reader had just moved.\n */\nlet justDragged = false\n\n/**\n * Closes the books on whatever gesture came before, at the start of a new press.\n *\n * TRAP — without this the flag can be left STANDING, and what it then breaks surfaces nowhere\n * near its cause. It is lowered by the click that reads it, and that click only reaches\n * `onCardClick` when it lands on a card: draw a new event and the press began on a CELL, so the\n * click goes there and the flag stays raised.\n *\n * What normally hides that is `onPointerup` overwriting the flag with its own `moved` — so the\n * next press heals it, and only a press that starts NO gesture does not. Which is a real\n * configuration and not a curiosity: a calendar that lets events be drawn but not moved\n * (`creatable` without `editable`) reaches it every time. The reader clicks an event to open it,\n * nothing happens, they click again and it works.\n *\n * Clearing it HERE makes the flag incapable of outliving its own interaction, and the ordering\n * is the whole argument: the click this exists to swallow follows its own `pointerup` with no\n * press in between, so it is still protected — while any LATER click on a card is necessarily\n * preceded by a press on that card, which arrives here first.\n *\n * It runs before every guard in the three handlers that call it, deliberately: a press that\n * starts no gesture at all is still the start of a new interaction, and those are precisely the\n * cases this is for.\n */\nfunction endLastGesture() {\n justDragged = false\n}\n\nfunction onPointerup(event: PointerEvent) {\n const state = gesture.value\n if (!state || state.pointerId !== event.pointerId) return\n gesture.value = null\n justDragged = state.moved\n releaseBoundaries()\n\n /*\n * Let go with the pointer off the calendar and the gesture is abandoned whole: no move, no new\n * length, and no new event either. The card goes back to where the model still says it is,\n * which is where the echo has been showing all along. It is the hand's version of the Escape\n * the keyboard grab already has — a gesture that ends somewhere it cannot mean anything must\n * mean nothing.\n *\n * TRAP — `justDragged` above is what still has to happen, and it is the ONE thing the cancel\n * path deliberately does not do: `pointerup` fires before `click`, so without it an abandoned\n * drag would end by OPENING the very event it has just refused to move. Which is also why\n * this branch and `onPointercancel` must NOT be factored into one helper. They differ by\n * exactly that line and it is correct in both directions — no click follows a cancel, so a\n * `true` left behind there would swallow the next genuine one instead.\n *\n * Nothing is announced. The pointer says nothing on a SUCCESSFUL drop either: `grabbed`,\n * `dropped` and `reverted` belong to the keyboard grab, which has no pointer to show where the\n * event went. A revert that spoke would be the only thing the pointer ever said.\n */\n if (state.outside) return\n\n if (state.kind === 'create') {\n emit('slot-create', state.preview)\n return\n }\n\n // A press that never travelled is an ordinary click, which the button's own click event is\n // about to report. Writing anything here would move an event nobody dragged.\n if (!state.moved) return\n\n emit('event-drop', state.id, state.preview, state.kind === 'resize' ? 'resize' : 'move')\n}\n\nfunction onCardClick(id: CalendarEventId) {\n if (justDragged) {\n justDragged = false\n return\n }\n const item = eventsById.value.get(id)\n if (item && id !== DRAFT_ID) emit('event-activate', item)\n}\n\n/**\n * A click on an empty part of a day, for a calendar that does NOT make events itself.\n *\n * When it does, the press has already become a `create` gesture and reported itself on\n * release; saying it again here would report the same click twice. So this path exists only\n * for a consumer who turned creation off to run their own form — they still get told where\n * the reader pointed, which is the whole point of turning it off rather than ignoring it.\n */\nfunction onGridClick(event: MouseEvent) {\n if (props.creatable) return\n const target = event.target as HTMLElement | null\n if (target?.closest('.v-calendar-event')) return\n const cell = target?.closest<HTMLElement>('.v-calendar-cell')\n if (!cell) return\n emit('cell-activate', cell.dataset.iso!, Number(cell.dataset.minutes))\n}\n\nfunction onPointercancel() {\n /*\n * The gesture was taken away — a system gesture, a context menu, the page starting to\n * scroll. Nothing is written and nothing is announced: the card simply goes back to where\n * the model still says it is.\n */\n gesture.value = null\n releaseBoundaries()\n}\n\n/* ------------------------------------------------------------ the keyboard's version */\n\nfunction announceTimes(title: string, times: CalendarEventTimes) {\n const start = formatTimeDisplay(times.startTime, props.locale, props.hourFormat)\n const end = formatTimeDisplay(times.endTime, props.locale, props.hourFormat)\n const day = formatDate(times.start, props.locale, {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n })\n emit('announce', m.value.calendar.movedTo(title, `${day}, ${start} – ${end}`))\n}\n\n/** Puts the focus back on a card that has just been redrawn somewhere else. */\nfunction refocusCard(id: CalendarEventId) {\n void nextTick(() => {\n /*\n * The card is found by WALKING the cards rather than by building an attribute selector.\n * An id belongs to the consumer, so it may hold a quote or a backslash that a selector\n * would need escaped — and `CSS.escape` cannot do that escaping here: jsdom defines no\n * `CSS` object at all, so the call threw on every keyboard grab, from inside a `nextTick`\n * where nothing could catch it (an unhandled rejection, which fails the run while every\n * assertion still passes). `menuInvoker` in VMenu/context.ts escapes by hand for the same\n * reason; comparing the attribute's own text sidesteps the question entirely, since\n * `data-event-id` is written as exactly `String(event.id)`.\n */\n const cards = columnsEl.value?.querySelectorAll<HTMLElement>('.v-calendar-event')\n Array.from(cards ?? [])\n .find((card) => card.dataset.eventId === String(id))\n ?.focus()\n })\n}\n\nfunction onCardKeydown(event: KeyboardEvent, card: HTMLElement) {\n const item = eventsById.value.get(cardIdOf(card))\n if (!item) return\n\n const held = grabbing.value && gesture.value?.id === item.id\n const intent = calendarIntent(\n event.key,\n event.shiftKey,\n held ? 'grabbed' : 'event',\n props.slotDuration,\n isRtl(),\n )\n if (!intent) return\n\n if (!held) {\n if (intent.kind !== 'activate') return\n // An event that cannot be moved keeps a button's ordinary behaviour: Enter opens it,\n // which the click handler below already reports.\n if (!props.editable || isAllDayEvent(item)) return\n /*\n * Enter and Space are ALSO how a button is pressed. Without this the same keystroke\n * would take hold of the event and open it at once, and every attempt to move something\n * would fire the consumer's editor over the top of it.\n */\n event.preventDefault()\n begin({\n kind: '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 emit('announce', m.value.calendar.grabbed)\n return\n }\n\n const state = gesture.value!\n event.preventDefault()\n\n if (intent.kind === 'grabResize') {\n /*\n * Each step is applied to the PREVIEW rather than to the origin, so the arrows accumulate\n * the way a reader expects — three presses of Down move three slots, not one. That is the\n * opposite of the pointer, which recomputes from the origin every frame because it always\n * knows where it is; the keyboard only knows how far it has just asked to go.\n */\n state.preview = resizeEvent(\n state.preview,\n minutesAt(state.preview.endTime, props.window.end) + intent.minutes,\n props.slotDuration,\n props.window,\n )\n state.moved = true\n announceTimes(item.title, state.preview)\n refocusCard(state.id)\n return\n }\n\n if (intent.kind === 'grabMove') {\n // Sideways is a step along the days ON SHOW, exactly as the pointer's is: date\n // arithmetic would count the days the calendar is hiding.\n const column = clamp(\n props.days.indexOf(state.preview.start) + intent.days,\n 0,\n props.days.length - 1,\n )\n const iso = props.days[column] ?? state.preview.start\n state.preview = {\n ...moveEvent(state.preview, 0, intent.minutes, props.window),\n start: iso,\n end: iso,\n }\n state.moved = true\n announceTimes(item.title, state.preview)\n refocusCard(state.id)\n return\n }\n\n if (intent.kind === 'activate') {\n gesture.value = null\n if (state.moved) {\n emit('event-drop', state.id, state.preview, state.kind === 'resize' ? 'resize' : 'move')\n }\n emit('announce', m.value.calendar.dropped)\n refocusCard(state.id)\n return\n }\n\n if (intent.kind === 'cancel') {\n gesture.value = null\n emit('announce', m.value.calendar.reverted)\n refocusCard(state.id)\n }\n}\n\n/* ---------------------------------------------------------------------- measurement */\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst canvasEl = ref<HTMLElement | null>(null)\nconst columnsEl = ref<HTMLElement | null>(null)\n\n/** The columns box as the pure geometry wants it: plain numbers, no element. */\nfunction geometryOf(rect: DOMRect) {\n const rtl = isRtl()\n return {\n top: rect.top,\n height: rect.height,\n // The edge the first column starts at, which is the other one in a right-to-left page.\n inlineStart: rtl ? rect.right : rect.left,\n inlineSize: rect.width,\n columns: props.days.length,\n }\n}\n\nconst isRtl = () => isElementRtl(columnsEl.value)\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.window.start) / MINUTES_PER_HOUR) * rowHeight\n}\n\n/** Puts the focus on whichever cell currently holds the tab stop. */\nfunction focus() {\n const cell = tabbable.value\n if (cell) document.getElementById(cellId(cell.iso, cell.minutes))?.focus()\n}\n\ndefineExpose({ focus, scrollToMinutes })\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-calendar-grid\"\n :data-outside=\"gesture?.outside ? '' : undefined\"\n :style=\"{ '--calendar-columns': String(days.length) }\"\n @pointermove=\"onPointermove\"\n @pointerup=\"onPointerup\"\n @pointercancel=\"onPointercancel\"\n >\n <div class=\"v-calendar-head\">\n <div class=\"v-calendar-head-gutter\" />\n <div\n v-for=\"iso in days\"\n :key=\"iso\"\n class=\"v-calendar-head-day\"\n :data-today=\"iso === today ? '' : undefined\"\n >\n <slot\n name=\"day-header\"\n :iso=\"iso\"\n :weekday=\"dayName(iso, 'short')\"\n :day=\"dayNumber(iso)\"\n :today=\"iso === today\"\n >\n <span class=\"v-calendar-head-weekday\">{{ dayName(iso, 'short') }}</span>\n <span class=\"v-calendar-head-number\">{{ dayNumber(iso) }}</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 >\n <VCalendarEvent\n v-for=\"span in allDay\"\n :key=\"span.id\"\n class=\"v-calendar-bar\"\n :event=\"eventsById.get(span.id)!\"\n layout=\"chip\"\n :continues-before=\"span.continuesBefore\"\n :continues-after=\"span.continuesAfter\"\n :dragging=\"gesture?.id === span.id && gesture.pointerId !== null\"\n :rejected=\"gesture?.id === span.id && gesture.outside\"\n :grabbed=\"grabbing && gesture?.id === span.id\"\n :hint-id=\"editable && !isGhostId(span.id) ? hintId : undefined\"\n :ghost-of=\"isGhostId(span.id) ? originalIdOf(span.id) : undefined\"\n :style=\"{\n '--event-day': String(span.startIndex),\n '--event-span': String(span.span),\n '--event-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\"\n role=\"grid\"\n :aria-label=\"label\"\n :data-gesture=\"gesture?.kind\"\n :data-edge=\"edgeCue\"\n @keydown=\"onKeydown\"\n @click=\"onGridClick\"\n @pointerdown=\"onGridPointerdown\"\n >\n <div v-for=\"(minutes, row) in hours\" :key=\"minutes\" role=\"row\" class=\"v-calendar-row\">\n <div\n v-for=\"(iso, day) in days\"\n :id=\"cellId(iso, minutes)\"\n :key=\"iso\"\n role=\"gridcell\"\n class=\"v-calendar-cell\"\n :data-iso=\"iso\"\n :data-minutes=\"minutes\"\n :data-today=\"iso === today ? '' : undefined\"\n :tabindex=\"isTabStop(iso, minutes) ? 0 : -1\"\n :aria-label=\"cellLabel(iso, minutes)\"\n :aria-selected=\"isFocused(iso, minutes) ? true : undefined\"\n >\n <VCalendarEvent\n v-for=\"segment in byCell.get(`${day}:${row}`) ?? []\"\n :key=\"segment.id\"\n class=\"v-calendar-block\"\n :event=\"eventsById.get(segment.id)!\"\n layout=\"block\"\n :time-text=\"timeTextOf(eventsById.get(segment.id)!)\"\n :continues-before=\"segment.clippedStart\"\n :continues-after=\"segment.clippedEnd\"\n :resizable=\"editable && segment.id !== DRAFT_ID\"\n :dragging=\"gesture?.id === segment.id && gesture.pointerId !== null\"\n :rejected=\"gesture?.id === segment.id && gesture.outside\"\n :grabbed=\"grabbing && gesture?.id === segment.id\"\n :hint-id=\"editable && !isGhostId(segment.id) ? hintId : undefined\"\n :ghost-of=\"isGhostId(segment.id) ? originalIdOf(segment.id) : undefined\"\n :style=\"styleFor(segment)\"\n @click=\"onCardClick(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.cell === `${day}:${row}`\"\n class=\"v-calendar-now\"\n aria-hidden=\"true\"\n :style=\"{\n '--event-day': String(nowMark.dayIndex),\n '--calendar-now': 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-grid {\n /*\n * The one scrolling box. The day names are sticky INSIDE it rather than in a container\n * of their own, which is what keeps them aligned with the columns for free: a separate\n * header would have to be told how far the body had scrolled sideways, and told again\n * 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 * 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 overflow: auto;\n block-size: 100%;\n min-block-size: 0;\n font-family: var(--vectis-text-family);\n color: var(--vectis-color-text);\n }\n\n /*\n * A drag currently held off the calendar. `not-allowed` is the word the library already uses\n * for \"you cannot do this\", on every disabled control.\n *\n * BEST-EFFORT, and never the signal: while a pointer is captured the cursor is resolved\n * against the capture target rather than against whatever sits under the pointer, and engines\n * differ on it. What carries the message is the card's own paint, which is why this is a\n * second cue and not the first.\n */\n .v-calendar-grid[data-outside] {\n cursor: not-allowed;\n }\n\n .v-calendar-head,\n .v-calendar-canvas {\n display: grid;\n grid-template-columns: var(--calendar-gutter) repeat(\n var(--calendar-columns),\n minmax(var(--vectis-control-size-calendar-day-min), 1fr)\n );\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 * 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-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.\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 .v-calendar-head-weekday {\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n text-transform: uppercase;\n }\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 /* Today is marked on the number alone, the way a calendar marks a date rather than a\n column. Semibold here is state emphasis, not a type role. */\n .v-calendar-head-day[data-today] .v-calendar-head-number {\n background: var(--vectis-color-accent);\n color: var(--vectis-color-text-on-accent);\n font-weight: var(--vectis-font-weight-semibold);\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: repeat(\n var(--calendar-columns),\n minmax(var(--vectis-control-size-calendar-day-min), 1fr)\n );\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(--event-day) + 1) / span var(--event-span);\n grid-row: calc(var(--event-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 is placed against. The rows below are `display:\n contents`, so the cells themselves are this grid's items. */\n position: relative;\n display: grid;\n grid-column: 2 / -1;\n /*\n * The same track recipe as the canvas above, deliberately restated rather than borrowed\n * with `subgrid`. This box spans exactly the canvas columns it repeats, and both sides\n * divide the same width by the same rule, so the two resolve identically — which makes\n * subgrid a second mechanism buying nothing, the conclusion the library's own audit of\n * it reached.\n */\n grid-template-columns: repeat(\n var(--calendar-columns),\n minmax(var(--vectis-control-size-calendar-day-min), 1fr)\n );\n grid-auto-rows: var(--calendar-hour);\n }\n\n /*\n * The strip that lights up when a drag is resting against an edge, counting down to turn\n * the page. It is what stops the paging being a surprise.\n *\n * Drawn INSIDE the columns box rather than on the scroller, because a scroller's absolutely\n * positioned child is placed against its content and scrolls away with it — here the box is\n * as tall as the whole day, so the strip is visible wherever the reader happens to be. An\n * absolutely positioned pseudo-element is not a grid item, so it disturbs no column.\n *\n * Its width is `--vectis-control-size-calendar-edge`, whose twin is `EDGE_BAND` in\n * `edgeStep.ts` — the JavaScript that decides where the countdown actually starts. Neither\n * may move without the other.\n */\n .v-calendar-columns[data-edge]::after {\n content: '';\n position: absolute;\n inset-block: 0;\n inline-size: var(--vectis-control-size-calendar-edge);\n background: var(--vectis-color-accent-surface);\n z-index: 2;\n pointer-events: none;\n }\n\n .v-calendar-columns[data-edge='start']::after {\n inset-inline-start: 0;\n }\n\n .v-calendar-columns[data-edge='end']::after {\n inset-inline-end: 0;\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 {\n border-block-start: 1px solid var(--vectis-color-border);\n border-inline-start: 1px solid var(--vectis-color-border);\n cursor: pointer;\n }\n\n .v-calendar-cell:first-child {\n border-inline-start: none;\n }\n\n .v-calendar-cell:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n /* Drawn inwards: the grid is a scrolling box, and an outward ring on a cell at the edge\n would be cropped by it. */\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\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 /*\n * TRAP — the height is the event's own length and NOTHING ELSE. There used to be a `max()`\n * against a floor token here, and it is what made a quarter of an hour spill over the slot\n * below it.\n *\n * The floor it was providing is already applied where it belongs: `timedSegments` stretches\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 could only\n * ever disagree with the first — the packing cannot see a stylesheet — which is exactly how\n * two cards came to be laid out side by side and then drawn on top of each other.\n *\n * So a card is never taller than its slot, whatever `slotDuration` is set to. Making the\n * text fit inside a slot that short is the stylesheet's problem, and VCalendarEvent solves\n * it by asking how tall the card actually came out.\n */\n .v-calendar-block {\n position: absolute;\n inset-block-start: calc(var(--event-start) * 100%);\n block-size: calc((var(--event-end) - var(--event-start)) * 100%);\n inset-inline-start: calc(\n (var(--event-day) + var(--event-column) / var(--event-columns)) / var(--calendar-columns) *\n 100%\n );\n inline-size: calc(\n var(--event-span) / var(--event-columns) / var(--calendar-columns) * 100% - 2px\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) * 100%);\n inset-inline-start: calc(var(--event-day) / 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</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 * 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 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 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 grab: (item) =>\n props.editable && !isAllDayEvent(item)\n ? initFor({\n kind: '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.key, event.shiftKey, '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 if (intent.kind === 'activate') {\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 */\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 * 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 * Each step is applied to the PREVIEW rather than to the origin, so the arrows accumulate the\n * way a reader expects — three presses of Down move three slots, not one. That is the opposite\n * of the pointer, which recomputes from the origin every frame because it always knows where it\n * is; the keyboard only knows how far it has just asked to go.\n */\nfunction onGrabStep(state: Gesture, step: GrabStep, item: E): boolean {\n // Whether the event may cross midnight is read off where it was taken from, as the pointer's is.\n const crossMidnight = spansMidnight(state.origin)\n if (step.kind === 'grabResize') {\n state.preview = resizeTimedEvent(\n state.preview,\n state.preview.end,\n minutesAt(state.preview.endTime, props.timeWindow.end) + step.minutes,\n props.slotDuration,\n props.timeWindow,\n crossMidnight,\n )\n } else {\n // Sideways is a step along the days ON SHOW, exactly as the pointer's is: date\n // arithmetic would count the days the calendar is hiding. A vertical step keeps the day,\n // which an overnight event's start may already have left the days on show for.\n const iso =\n step.days === 0\n ? state.preview.start\n : (props.days[\n clamp(props.days.indexOf(state.preview.start) + step.days, 0, props.days.length - 1)\n ] ?? state.preview.start)\n state.preview = moveTimedEvent(\n state.preview,\n iso,\n minutesAt(state.preview.startTime, props.timeWindow.start) + step.minutes,\n props.timeWindow,\n crossMidnight,\n )\n }\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: () => {\n const cell = tabbable.value\n if (cell) focusCell(cellId(cell.iso, cell.minutes))\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 >\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 >\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 .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 2px\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</style>\n"],"mappings":""}
|