@allbluecn/web-app 0.18.2 → 0.19.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/package.json +66 -51
- package/src/__tests__/components/auth/auth-layout.test.tsx +13 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/canonical-id.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/favicon.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/github-sync.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/share-merge.test.ts +7 -7
- package/src/__tests__/security/xss-sanitize.test.ts +9 -8
- package/src/components/ai/__tests__/chat-thread-id-stability.test.tsx +125 -0
- package/src/components/ai/ai-chat-prompt.tsx +8 -2
- package/src/components/ai/ai-prompt-input/index.tsx +2 -1
- package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +1 -2
- package/src/components/ai/global-harness-sidebar/index.tsx +22 -10
- package/src/components/bug/__tests__/board-subgroups.test.tsx +512 -0
- package/src/components/bug/__tests__/board-view.test.tsx +350 -0
- package/src/components/bug/__tests__/bug-list-loading-defer.test.tsx +85 -0
- package/src/components/bug/__tests__/bug-toolbar.test.tsx +7 -1
- package/src/components/bug/__tests__/comment-reactions.test.tsx +182 -0
- package/src/components/bug/__tests__/date-and-lightbox.test.tsx +96 -0
- package/src/components/bug/__tests__/export-entry.test.tsx +217 -0
- package/src/components/bug/__tests__/filters-compat.test.tsx +157 -0
- package/src/components/bug/__tests__/fix-request-race.test.tsx +396 -0
- package/src/components/bug/__tests__/peek-modes.test.tsx +365 -0
- package/src/components/bug/__tests__/saved-views.test.tsx +309 -0
- package/src/components/bug/__tests__/story-bugs-panel.test.tsx +146 -0
- package/src/components/bug/__tests__/subgroup-header.test.tsx +150 -0
- package/src/components/bug/__tests__/toolbar-shell.test.tsx +369 -0
- package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +58 -0
- package/src/components/bug/__tests__/view-type.test.tsx +247 -0
- package/src/components/bug/bug-list-view.tsx +310 -13
- package/src/components/bug/bug-panel-header.tsx +84 -8
- package/src/components/bug/bug-toolbar.tsx +268 -116
- package/src/components/bug/create-bug-dialog.tsx +1 -1
- package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -0
- package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +236 -0
- package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +2 -1
- package/src/components/bug/detail/activity-timeline.tsx +1 -1
- package/src/components/bug/detail/bug-detail-body.tsx +316 -18
- package/src/components/bug/detail/comment-item.tsx +7 -2
- package/src/components/bug/detail/comment-reactions.tsx +138 -0
- package/src/components/bug/detail/error-context.tsx +29 -9
- package/src/components/bug/detail/props-section.tsx +31 -3
- package/src/components/bug/detail/repro-report-card.tsx +1 -1
- package/src/components/bug/display/constants.ts +4 -2
- package/src/components/bug/display/display-dropdown.tsx +76 -280
- package/src/components/bug/display/types.ts +24 -8
- package/src/components/bug/display/use-paged-bug-groups.ts +183 -32
- package/src/components/bug/filters/{filter-value-input.tsx → field-configs.tsx} +218 -106
- package/src/components/bug/filters/index.tsx +14 -5
- package/src/components/bug/inline-editors/assignee-editor.tsx +2 -2
- package/src/components/bug/inline-editors/labels-editor.tsx +1 -1
- package/src/components/bug/inline-editors/severity-editor.tsx +37 -18
- package/src/components/bug/inline-editors/state-editor.tsx +1 -1
- package/src/components/bug/peek-panel.tsx +153 -35
- package/src/components/bug/select-bug-dialog.tsx +500 -0
- package/src/components/bug/story-bugs-panel.tsx +1090 -0
- package/src/components/bug/types.ts +9 -2
- package/src/components/bug/views/board-view.tsx +661 -0
- package/src/components/bug/views/bug-list/batch-toolbar.tsx +25 -34
- package/src/components/bug/views/bug-list/group.tsx +28 -13
- package/src/components/bug/views/bug-list/index.tsx +223 -59
- package/src/components/bug/views/bug-list/row.tsx +7 -1
- package/src/components/bug/views/create-view-dialog.tsx +160 -0
- package/src/components/bug/views/view-list-section.tsx +140 -0
- package/src/components/bug/views/view-selector-button.tsx +145 -0
- package/src/components/bug/views/view-switcher.tsx +8 -6
- package/src/components/bug/views/view-types.ts +162 -0
- package/src/components/collab/__tests__/activity-attribution.test.tsx +211 -0
- package/src/components/collab/__tests__/canvas-collab-panel.test.tsx +118 -0
- package/src/components/collab/__tests__/collab-presence-header.test.tsx +335 -0
- package/src/components/collab/__tests__/entity-presence-badge.test.tsx +226 -0
- package/src/components/collab/activity-attribution.tsx +221 -0
- package/src/components/collab/canvas-collab-panel.tsx +104 -0
- package/src/components/collab/collab-presence-header.tsx +220 -0
- package/src/components/collab/entity-presence-badge.tsx +130 -0
- package/src/components/collab/field-editing-pill.tsx +137 -0
- package/src/components/command/command-palette.tsx +6 -6
- package/src/components/design/canvas/document/__tests__/canvas-tree-fixture-snapshot.ts +589 -0
- package/src/components/design/canvas/document/__tests__/canvas-tree-reconcile.test.ts +159 -0
- package/src/components/design/canvas/panels/toolbar.tsx +3 -2
- package/src/components/design/canvas/plugins/presence/__tests__/canvas-collab-wiring.test.tsx +660 -0
- package/src/components/design/canvas/plugins/presence/__tests__/layout.test.ts +292 -0
- package/src/components/design/canvas/plugins/presence/__tests__/presence-overlay.test.tsx +139 -0
- package/src/components/design/canvas/plugins/presence/layout.ts +222 -0
- package/src/components/design/canvas/plugins/presence/presence-layer.tsx +353 -0
- package/src/components/design/canvas/plugins/presence/presence-overlay.tsx +155 -0
- package/src/components/design/canvas/plugins/presence/viewer-badge.tsx +80 -0
- package/src/components/design/canvas/react/__tests__/hooks.test.tsx +4 -4
- package/src/components/design/editor/collaboration/__tests__/design-agent-panel-regenerate.test.tsx +3 -1
- package/src/components/design/editor/collaboration/__tests__/design-agent-panel-stream.test.tsx +3 -1
- package/src/components/design/editor/collaboration/design-agent-panel.tsx +3 -0
- package/src/components/design/editor/collaboration/design-comment-review-panel.tsx +5 -5
- package/src/components/design/editor/dashboard/create-design-dialog.tsx +1 -1
- package/src/components/design/editor/dashboard/design-dashboard.tsx +1 -10
- package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +153 -0
- package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +8 -0
- package/src/components/design/editor/layout/design-editor-layout.tsx +28 -1
- package/src/components/design/editor/layout/design-navigation.tsx +0 -4
- package/src/components/design/editor/layout/hooks/__tests__/use-design-auto-save-collab.test.tsx +130 -0
- package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +18 -1
- package/src/components/design/editor/share/share-comment-panel.tsx +37 -20
- package/src/components/design/editor/share/share-layout.tsx +3 -0
- package/src/components/design/editor/share/share-nav-bar.tsx +7 -1
- package/src/components/design/editor/share/share-theme-toggle.tsx +10 -15
- package/src/components/design/editor/tiptap/node/paragraph-node/paragraph-node.scss +1 -1
- package/src/components/harness/__tests__/composer-draft.test.ts +126 -0
- package/src/components/harness/composer/composer-editor.tsx +64 -6
- package/src/components/harness/composer/harness-composer.tsx +1 -1
- package/src/components/harness/harness-layout.tsx +1 -1
- package/src/components/harness/harness-workbench-page.tsx +6 -1
- package/src/components/harness/turn-group.tsx +1 -1
- package/src/components/hotkeys/shortcut-badge.tsx +4 -4
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +1 -1
- package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +542 -0
- package/src/components/knowledge/editor/doc-header.tsx +7 -49
- package/src/components/knowledge/editor/knowledge-doc-editor.tsx +264 -51
- package/src/components/knowledge/editor/mindmap-doc-editor.tsx +3 -19
- package/src/components/knowledge/knowledge-homepage.tsx +3 -3
- package/src/components/knowledge/knowledge-toolbar.tsx +8 -8
- package/src/components/{knowledge-public → knowledge/public}/bookmark-form-dialog.tsx +4 -4
- package/src/components/{knowledge-public → knowledge/public}/knowledge-card.tsx +1 -15
- package/src/components/{knowledge-public → knowledge/public}/knowledge-form-dialog.tsx +9 -9
- package/src/components/{knowledge-public → knowledge/public}/legal-compliance-editor.tsx +35 -28
- package/src/components/{knowledge-public → knowledge/public}/sharers-avatar-group.tsx +3 -2
- package/src/components/layout/layout-responsive-provider.tsx +79 -0
- package/src/components/layout/sidebar-03/__tests__/presence-avatar-stack.test.tsx +148 -0
- package/src/components/layout/sidebar-03/app-sidebar.tsx +9 -3
- package/src/components/layout/sidebar-03/index.tsx +19 -4
- package/src/components/layout/sidebar-03/nav-main.tsx +28 -10
- package/src/components/layout/sidebar-03/presence-avatar-stack.tsx +112 -0
- package/src/components/layout/sidebar-03/user-menu.tsx +3 -3
- package/src/components/project/create-project-dialog.tsx +1 -1
- package/src/components/project/invite-member-dialog.tsx +1 -1
- package/src/components/project/project-activity-list.tsx +2 -2
- package/src/components/project/project-member-manager.tsx +2 -2
- package/src/components/reui/__tests__/kanban-drop-indicator.test.ts +88 -0
- package/src/components/reui/__tests__/kanban-keyboard.test.ts +262 -0
- package/src/components/reui/event-calendar/event-calendar-agenda-view.tsx +236 -0
- package/src/components/reui/event-calendar/event-calendar-content.tsx +80 -0
- package/src/components/reui/event-calendar/event-calendar-dnd.tsx +1062 -0
- package/src/components/reui/event-calendar/event-calendar-event.tsx +555 -0
- package/src/components/reui/event-calendar/event-calendar-i18n.tsx +291 -0
- package/src/components/reui/event-calendar/event-calendar-lib.tsx +677 -0
- package/src/components/reui/event-calendar/event-calendar-month-view.tsx +1287 -0
- package/src/components/reui/event-calendar/event-calendar-nav.tsx +615 -0
- package/src/components/reui/event-calendar/event-calendar-recurrence.tsx +592 -0
- package/src/components/reui/event-calendar/event-calendar-resource-view.tsx +810 -0
- package/src/components/reui/event-calendar/event-calendar-time-grid.tsx +1366 -0
- package/src/components/reui/event-calendar/event-calendar-types.tsx +240 -0
- package/src/components/reui/event-calendar/event-calendar.tsx +1916 -0
- package/src/components/reui/gantt/__tests__/gantt-row-window.test.ts +243 -0
- package/src/components/reui/gantt/__tests__/gantt-view-virtualize.test.tsx +243 -0
- package/src/components/reui/gantt/gantt-bar.tsx +439 -0
- package/src/components/reui/gantt/gantt-dnd.tsx +1000 -0
- package/src/components/reui/gantt/gantt-i18n.tsx +287 -0
- package/src/components/reui/gantt/gantt-lib.tsx +695 -0
- package/src/components/reui/gantt/gantt-nav.tsx +446 -0
- package/src/components/reui/gantt/gantt-recurrence.tsx +402 -0
- package/src/components/reui/gantt/gantt-row-window.ts +119 -0
- package/src/components/reui/gantt/gantt-types.tsx +283 -0
- package/src/components/reui/gantt/gantt-view.tsx +4466 -0
- package/src/components/reui/gantt/gantt.tsx +1822 -0
- package/src/components/reui/gantt/use-gantt-row-window.ts +159 -0
- package/src/components/reui/icon-stack.tsx +90 -0
- package/src/components/reui/kanban-keyboard.ts +268 -0
- package/src/components/reui/kanban.tsx +1227 -0
- package/src/components/settings/personal-team/account/account-client.tsx +3 -2
- package/src/components/settings/personal-team/team/invitation-cards/invitation-history-list.tsx +5 -5
- package/src/components/settings/personal-team/team/team-client.tsx +2 -1
- package/src/components/settings/personal-team/team/team-member-list.tsx +4 -4
- package/src/components/share/share-presence-bar.tsx +182 -0
- package/src/components/shared-kit/auth/auth-client.ts +5 -0
- package/src/components/shared-kit/avatars/__tests__/avatar-shape.test.ts +44 -0
- package/src/components/shared-kit/avatars/__tests__/user-avatar.test.tsx +46 -0
- package/src/components/{tags/tags-collection.ts → shared-kit/avatars/avatar-shape.ts} +14 -20
- package/src/components/shared-kit/avatars/notion-avatar.tsx +62 -11
- package/src/components/shared-kit/avatars/user-avatar.tsx +7 -4
- package/src/components/shared-kit/list-kit/__tests__/batch-toolbar-shell.test.tsx +133 -0
- package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +89 -0
- package/src/components/shared-kit/list-kit/__tests__/level-badge.test.tsx +85 -0
- package/src/components/shared-kit/list-kit/__tests__/toolbar-shell.test.tsx +178 -0
- package/src/components/shared-kit/list-kit/batch-toolbar-shell.tsx +111 -0
- package/src/components/shared-kit/list-kit/csv-download.ts +46 -0
- package/src/components/shared-kit/list-kit/display/__tests__/dropdown.test.tsx +215 -0
- package/src/components/shared-kit/list-kit/display/dropdown.tsx +582 -0
- package/src/components/shared-kit/list-kit/display/types.ts +64 -0
- package/src/components/shared-kit/list-kit/display/values.ts +60 -0
- package/src/components/shared-kit/list-kit/filters/__tests__/create-filters-module.test.tsx +181 -0
- package/src/components/shared-kit/list-kit/filters/__tests__/types.test.ts +133 -0
- package/src/components/shared-kit/list-kit/filters/index.tsx +757 -0
- package/src/components/shared-kit/list-kit/filters/types.ts +68 -0
- package/src/components/shared-kit/list-kit/frozen-header.ts +45 -0
- package/src/components/shared-kit/list-kit/icon-semantics.ts +50 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/drop-layout.test.ts +139 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/frozen-header.test.ts +111 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +130 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/kanban-board-view.test.tsx +901 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/manual-order.test.ts +35 -0
- package/src/components/shared-kit/list-kit/kanban/drop-commit.ts +212 -0
- package/src/components/shared-kit/list-kit/kanban/drop-layout.ts +98 -0
- package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +88 -0
- package/src/components/shared-kit/list-kit/kanban/instance-key.ts +83 -0
- package/src/components/shared-kit/list-kit/kanban/kanban-board-view.tsx +1414 -0
- package/src/components/shared-kit/list-kit/kanban/manual-order.ts +39 -0
- package/src/components/shared-kit/list-kit/level-badge.tsx +71 -0
- package/src/components/shared-kit/list-kit/toolbar-shell.tsx +103 -0
- package/src/components/shared-kit/loading/__tests__/use-deferred-flag.test.tsx +115 -0
- package/src/components/shared-kit/loading/use-deferred-flag.ts +35 -0
- package/src/components/shared-kit/peek/dock-slot.ts +62 -0
- package/src/components/shared-kit/peek/peek-mode.ts +34 -0
- package/src/components/shared-kit/theme/animated-theme-toggler.tsx +13 -7
- package/src/components/shared-kit/theme/theme-toggle.tsx +7 -18
- package/src/components/shared-kit/views/view-icon.tsx +49 -0
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +215 -0
- package/src/components/story/__tests__/story-list-loading-defer.test.tsx +88 -0
- package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +242 -0
- package/src/components/story/__tests__/story-list-view-restore.test.tsx +45 -3
- package/src/components/story/__tests__/story-list-view-ssr.test.tsx +0 -1
- package/src/components/story/ai/__tests__/action-registry.test.ts +11 -11
- package/src/components/story/ai/action-registry.ts +5 -7
- package/src/components/story/ai/handlers/stream-text.ts +1 -3
- package/src/components/story/ai/story-ai-toolbar.tsx +58 -16
- package/src/components/story/ai/use-toolbar-overflow.ts +4 -7
- package/src/components/story/columns.tsx +2 -2
- package/src/components/story/constants.ts +17 -1
- package/src/components/story/create-story-dialog.tsx +8 -7
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +38 -4
- package/src/components/story/detail/__tests__/categories.test.tsx +52 -26
- package/src/components/story/detail/__tests__/story-collab.test.tsx +897 -0
- package/src/components/story/detail/activity-tabs.tsx +5 -4
- package/src/components/story/detail/activity-timeline.tsx +15 -3
- package/src/components/story/detail/comment-input.tsx +2 -2
- package/src/components/story/detail/comment-item.tsx +6 -3
- package/src/components/story/detail/comment-thread.tsx +1 -0
- package/src/components/story/detail/reaction-bar.tsx +1 -1
- package/src/components/story/detail/story-detail-body.tsx +501 -76
- package/src/components/story/detail/story-detail-toolbar/__tests__/related-test-cases.test.tsx +173 -10
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +144 -8
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +82 -38
- package/src/components/story/detail/story-detail-toolbar/children.tsx +254 -174
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +140 -58
- package/src/components/story/detail/story-detail-toolbar/index.tsx +613 -0
- package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +1010 -87
- package/src/components/story/detail/story-detail-toolbar/scroll-hint.tsx +57 -0
- package/src/components/story/detail/story-detail-toolbar/use-panel-frozen-groups.ts +97 -0
- package/src/components/story/detail/story-properties.tsx +9 -5
- package/src/components/story/display/__tests__/display-dropdown-helpers.test.ts +17 -1
- package/src/components/story/display/__tests__/display-dropdown.test.tsx +9 -0
- package/src/components/story/display/__tests__/flatten-story-groups.test.ts +42 -0
- package/src/components/story/display/__tests__/group-fields.test.ts +81 -1
- package/src/components/story/display/__tests__/group-resolver.test.ts +153 -1
- package/src/components/story/display/__tests__/use-collapsed-subgroups.test.ts +158 -0
- package/src/components/story/display/__tests__/use-display-options.test.ts +32 -3
- package/src/components/story/display/__tests__/use-fit-count.test.ts +198 -0
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +77 -3
- package/src/components/story/display/constants.ts +23 -2
- package/src/components/story/display/display-dropdown.tsx +95 -397
- package/src/components/story/display/flatten-story-groups.ts +39 -0
- package/src/components/story/display/group-fields.ts +32 -1
- package/src/components/story/display/group-resolver.tsx +128 -5
- package/src/components/story/display/types.ts +23 -42
- package/src/components/story/display/use-collapsed-subgroups.ts +109 -0
- package/src/components/story/display/use-display-options.ts +21 -5
- package/src/components/story/display/use-fit-count.ts +124 -0
- package/src/components/story/display/use-paged-story-groups.ts +174 -31
- package/src/components/story/filters/field-configs.tsx +263 -0
- package/src/components/story/filters/index.tsx +14 -5
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +2 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +15 -0
- package/src/components/story/inline-editors/assignee-editor.tsx +7 -5
- package/src/components/story/inline-editors/date-editor.tsx +7 -2
- package/src/components/story/inline-editors/inline-trigger.tsx +17 -4
- package/src/components/story/inline-editors/labels-editor.tsx +243 -35
- package/src/components/story/inline-editors/parent-story-editor.tsx +1 -1
- package/src/components/story/inline-editors/priority-editor.tsx +6 -4
- package/src/components/story/inline-editors/state-editor.tsx +6 -4
- package/src/components/story/inline-editors/story-select-dialog.tsx +56 -19
- package/src/components/story/peek-panel.tsx +234 -51
- package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +131 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +1 -1
- package/src/components/story/relations/design-select-dialog.tsx +285 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +3 -3
- package/src/components/story/relations/link-manage-dialog.tsx +2 -2
- package/src/components/story/story-list-view.tsx +175 -17
- package/src/components/story/story-panel-header.tsx +18 -11
- package/src/components/story/story-toolbar.tsx +177 -126
- package/src/components/story/transfer-story-popover.tsx +1 -1
- package/src/components/story/types.ts +11 -2
- package/src/components/story/views/__tests__/calendar-view.test.tsx +262 -0
- package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +68 -0
- package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +184 -0
- package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +179 -0
- package/src/components/story/views/__tests__/timeline-view.test.tsx +453 -0
- package/src/components/story/views/__tests__/view-icon.test.tsx +45 -0
- package/src/components/story/views/__tests__/view-types.test.ts +0 -8
- package/src/components/story/views/calendar/__tests__/mappers.test.ts +201 -0
- package/src/components/story/views/calendar/__tests__/slot-draft.test.ts +78 -0
- package/src/components/story/views/calendar/drag-blocked.ts +41 -0
- package/src/components/story/views/calendar/i18n.ts +91 -0
- package/src/components/story/views/calendar/mappers.ts +119 -0
- package/src/components/story/views/calendar/slot-draft.ts +48 -0
- package/src/components/story/views/calendar-view.tsx +490 -223
- package/src/components/story/views/create-view-dialog.tsx +6 -6
- package/src/components/story/views/kanban-view.tsx +618 -0
- package/src/components/story/views/{__tests__ → story-list/__tests__}/use-flat-rows.test.ts +1 -1
- package/src/components/story/views/story-list/batch-preview-dialog.tsx +3 -3
- package/src/components/story/views/story-list/batch-toolbar.tsx +17 -34
- package/src/components/story/views/story-list/group.tsx +4 -2
- package/src/components/story/views/story-list/index.tsx +142 -34
- package/src/components/story/views/story-list/row.tsx +4 -0
- package/src/components/story/views/{use-flat-rows.ts → story-list/use-flat-rows.ts} +2 -2
- package/src/components/story/views/timeline/__tests__/mappers.test.ts +742 -0
- package/src/components/story/views/timeline/__tests__/sentinel-geometry.test.ts +76 -0
- package/src/components/story/views/timeline/__tests__/use-date-commit.test.tsx +372 -0
- package/src/components/story/views/timeline/i18n.ts +96 -0
- package/src/components/story/views/timeline/mappers.ts +506 -0
- package/src/components/story/views/timeline/sentinel-geometry.ts +43 -0
- package/src/components/story/views/timeline/use-date-commit.ts +169 -0
- package/src/components/story/views/timeline-view.tsx +593 -222
- package/src/components/story/views/view-list-section.tsx +4 -4
- package/src/components/story/views/view-selector-button.tsx +9 -10
- package/src/components/story/views/view-switcher.tsx +7 -1
- package/src/components/story/views/view-types.ts +1 -5
- package/src/components/testcase/__tests__/batch-toolbar.test.tsx +245 -0
- package/src/components/testcase/__tests__/board-view.test.tsx +241 -0
- package/src/components/testcase/__tests__/case-row.test.tsx +24 -10
- package/src/components/testcase/__tests__/constants.test.ts +151 -0
- package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +173 -0
- package/src/components/testcase/__tests__/detail-properties.test.tsx +19 -0
- package/src/components/testcase/__tests__/display-options.test.tsx +154 -0
- package/src/components/testcase/__tests__/export-entry.test.tsx +279 -0
- package/src/components/testcase/__tests__/filters.test.tsx +280 -0
- package/src/components/testcase/__tests__/list-view-scope.test.tsx +290 -0
- package/src/components/testcase/__tests__/list-view.test.tsx +552 -7
- package/src/components/testcase/__tests__/manual-move.test.ts +69 -0
- package/src/components/testcase/__tests__/peek-panel.test.tsx +89 -8
- package/src/components/testcase/__tests__/toolbar.test.tsx +302 -0
- package/src/components/testcase/__tests__/view-fallback.test.tsx +272 -0
- package/src/components/testcase/__tests__/view-management.test.tsx +179 -0
- package/src/components/testcase/ai/generate-dialog.tsx +1 -1
- package/src/components/testcase/constants.ts +83 -0
- package/src/components/testcase/create-testcase-dialog.tsx +76 -8
- package/src/components/testcase/detail/__tests__/select-cases-dialog.test.tsx +163 -0
- package/src/components/testcase/detail/case-detail-body.tsx +311 -30
- package/src/components/testcase/detail/select-cases-dialog.tsx +465 -0
- package/src/components/testcase/detail/story-link-editor.tsx +1 -1
- package/src/components/testcase/display/display-dropdown.tsx +147 -0
- package/src/components/testcase/display/field-configs.ts +80 -0
- package/src/components/testcase/display/use-testcase-display-options.ts +59 -0
- package/src/components/testcase/filters/field-configs.tsx +748 -0
- package/src/components/testcase/filters/index.tsx +35 -127
- package/src/components/testcase/inline-editors/assignee-editor.tsx +6 -6
- package/src/components/testcase/inline-editors/folder-editor.tsx +9 -9
- package/src/components/testcase/inline-editors/priority-editor.tsx +47 -31
- package/src/components/testcase/inline-editors/state-editor.tsx +14 -12
- package/src/components/testcase/inline-editors/type-editor.tsx +31 -20
- package/src/components/testcase/list/case-row.tsx +60 -2
- package/src/components/testcase/list/grouped-list.tsx +3 -2
- package/src/components/testcase/list-view.tsx +633 -137
- package/src/components/testcase/peek-panel.tsx +69 -11
- package/src/components/testcase/plans/plan-list.tsx +6 -1
- package/src/components/testcase/step-editor.tsx +34 -3
- package/src/components/testcase/testcase-toolbar.tsx +349 -94
- package/src/components/testcase/views/batch-toolbar.tsx +203 -0
- package/src/components/testcase/views/board-view.tsx +546 -0
- package/src/components/testcase/views/create-view-dialog.tsx +26 -14
- package/src/components/testcase/views/move.ts +56 -0
- package/src/components/testcase/views/view-selector-button.tsx +82 -30
- package/src/components/testcase/views/view-switcher.tsx +12 -5
- package/src/components/testcase/views/view-types.ts +50 -27
- package/src/components/tiptap/mindmap/lib/__tests__/shortcuts.test.ts +5 -4
- package/src/components/tiptap/mindmap/lib/shortcuts.ts +4 -3
- package/src/components/tiptap/node/doc-card-node-view.tsx +2 -1
- package/src/components/tiptap/notion-like/editor-extensions.ts +231 -0
- package/src/components/tiptap/notion-like/notion-like-editor.tsx +29 -156
- package/src/components/tiptap/ui/ai-menu/ai-menu.tsx +6 -1
- package/src/components/tiptap/ui/doc-reference-interaction/doc-reference-interaction.tsx +2 -1
- package/src/components/ui/checkbox.tsx +4 -4
- package/src/components/ui/popover.tsx +1 -1
- package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +1 -1
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +1 -1
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +1 -1
- package/src/components/workspace/members/workspace-member-item.tsx +2 -2
- package/src/components/workspace/members/workspace-members-manager.tsx +2 -2
- package/src/config/sidebar-routes.tsx +11 -13
- package/src/design-engine/document/serialize-leafer-json.ts +2 -1
- package/src/design-engine/engine/__tests__/engine-split.test.ts +1 -1
- package/src/design-engine/engine/__tests__/engine.test.ts +20 -0
- package/src/design-engine/engine/app.ts +5 -0
- package/src/design-engine/engine/engine-base.ts +17 -0
- package/src/design-engine/engine/engine-doc.ts +32 -0
- package/src/hooks/__tests__/use-commands.test.tsx +17 -5
- package/src/hooks/use-commands.ts +4 -4
- package/src/hooks/use-toolbar-search.ts +7 -2
- package/src/lib/__tests__/datetime-local.test.ts +46 -0
- package/src/lib/__tests__/datetime.test.ts +7 -0
- package/src/lib/__tests__/layout-responsive.test.ts +123 -0
- package/src/lib/__tests__/sidebar-utils.test.ts +4 -4
- package/src/lib/ai/story-ai-stream.ts +1 -1
- package/src/lib/ai/use-selected-model.ts +15 -5
- package/src/lib/changelog/sdk-versions.ts +22 -22
- package/src/lib/collab/__tests__/bindings-fields.test.ts +496 -0
- package/src/lib/collab/__tests__/bindings-steps.test.ts +332 -0
- package/src/lib/collab/__tests__/presence-store.test.ts +414 -0
- package/src/lib/collab/__tests__/provider-handle-contract.test.ts +89 -0
- package/src/lib/collab/__tests__/provider.test.ts +504 -0
- package/src/lib/collab/__tests__/use-presence-list-refresh.test.tsx +309 -0
- package/src/lib/collab/__tests__/use-presence-report.test.ts +356 -0
- package/src/lib/collab/__tests__/use-share-presence.test.ts +555 -0
- package/src/lib/collab/__tests__/user-color.test.ts +53 -0
- package/src/lib/collab/bindings/__tests__/canvas-local.test.ts +583 -0
- package/src/lib/collab/bindings/__tests__/canvas-presence.test.ts +378 -0
- package/src/lib/collab/bindings/__tests__/canvas-remote.test.ts +454 -0
- package/src/lib/collab/bindings/__tests__/canvas-tree-perf.test.ts +134 -0
- package/src/lib/collab/bindings/canvas-local.ts +415 -0
- package/src/lib/collab/bindings/canvas-presence.ts +420 -0
- package/src/lib/collab/bindings/canvas-remote.ts +226 -0
- package/src/lib/collab/bindings/fields.ts +463 -0
- package/src/lib/collab/bindings/steps.ts +332 -0
- package/src/lib/collab/env.ts +33 -0
- package/src/lib/collab/module-map.ts +131 -0
- package/src/lib/collab/presence-store.ts +350 -0
- package/src/lib/collab/provider.ts +378 -0
- package/src/lib/collab/use-collab-doc.ts +100 -0
- package/src/lib/collab/use-presence-list-refresh.ts +142 -0
- package/src/lib/collab/use-presence-report.ts +176 -0
- package/src/lib/collab/use-self-profile-broadcast.ts +113 -0
- package/src/lib/collab/use-share-presence.ts +561 -0
- package/src/lib/collab/user-color.ts +40 -0
- package/src/lib/collections/index.ts +0 -1
- package/src/lib/collections/reference/bookmark-categories-collection.ts +1 -1
- package/src/lib/commands/__tests__/registry.test.ts +2 -5
- package/src/lib/commands/registry.ts +0 -3
- package/src/lib/commands/types.ts +1 -1
- package/src/lib/datetime.ts +20 -3
- package/src/lib/hooks/__tests__/use-synced-state.test.tsx +54 -0
- package/src/lib/hooks/use-story-panel-open.ts +134 -0
- package/src/lib/hooks/use-synced-state.ts +17 -0
- package/src/lib/hotkeys/__tests__/registry.test.ts +1 -1
- package/src/lib/hotkeys/registry.ts +0 -3
- package/src/lib/layout-responsive.ts +140 -0
- package/src/lib/pacer.ts +10 -1
- package/src/lib/peek-dock.ts +32 -0
- package/src/lib/plugin-runtime/__tests__/plugin-tool-schema-parity.test.ts +140 -0
- package/src/lib/plugin-runtime/plugin-client-tools.ts +28 -46
- package/src/lib/plugin-runtime/plugin-tool-defs.ts +152 -0
- package/src/lib/project-module-meta.ts +1 -8
- package/src/lib/rate-limit.ts +15 -6
- package/src/lib/sanitize.ts +122 -10
- package/src/lib/share-events.ts +20 -11
- package/src/lib/sidebar-utils.ts +1 -1
- package/src/lib/theme/__tests__/theme-mode.test.ts +96 -0
- package/src/lib/theme/theme-mode.ts +77 -0
- package/src/paraglide/messages/_index.js +5312 -1591
- package/src/paraglide/messages/en.js +1880 -685
- package/src/paraglide/messages/zh.js +1877 -682
- package/src/providers/index.tsx +4 -1
- package/src/providers/theme-mode-provider.tsx +135 -0
- package/src/routeTree.gen.ts +78 -101
- package/src/routes/$lang/docs/$/route.lazy.tsx +7 -2
- package/src/routes/-tests/chat-body-validation.test.ts +61 -3
- package/src/routes/__tests__/-edition-routing.test.tsx +12 -8
- package/src/routes/_nav/-components/nav-not-found.tsx +2 -2
- package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +16 -2
- package/src/routes/_nav/bugs/$bugId/route.tsx +11 -2
- package/src/routes/_nav/dashboard/-components/avatar-group.tsx +2 -2
- package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.lazy.tsx +31 -17
- package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.tsx +12 -1
- package/src/routes/_nav/knowledge/$kbId/index.lazy.tsx +4 -2
- package/src/routes/_nav/knowledge/$kbId/route.lazy.tsx +7 -3
- package/src/routes/_nav/knowledge/bookmarks/$id.lazy.tsx +6 -6
- package/src/routes/_nav/knowledge/bookmarks/index.lazy.tsx +10 -10
- package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -9
- package/src/routes/_nav/knowledge/compliance/edit/$id.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +8 -8
- package/src/routes/_nav/knowledge/dictionaries/$id.lazy.tsx +4 -4
- package/src/routes/_nav/knowledge/dictionaries/index.lazy.tsx +6 -6
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +9 -9
- package/src/routes/_nav/knowledge/laws/edit/$id.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +11 -11
- package/src/routes/_nav/knowledge/summary/index.lazy.tsx +1 -1
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +14 -6
- package/src/routes/_nav/route.lazy.tsx +11 -6
- package/src/routes/_nav/settings/account/route.tsx +3 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +52 -8
- package/src/routes/_nav/stories/$storyId/route.tsx +11 -0
- package/src/routes/_nav/stories/-components/story-detail-skeleton.tsx +1 -1
- package/src/{components/tags/pages/tag-detail-page.tsx → routes/_nav/tasks/$taskId/route.lazy.tsx} +20 -10
- package/src/routes/_nav/tasks/$taskId/route.tsx +43 -0
- package/src/routes/_nav/tasks/-components/ai-task-detail.tsx +307 -0
- package/src/routes/_nav/tasks/-components/ai-task-list.tsx +217 -0
- package/src/routes/_nav/tasks/-components/ai-task-status-badge.tsx +46 -0
- package/src/routes/_nav/tasks/-components/assigned-list.tsx +157 -0
- package/src/routes/_nav/tasks/-components/create-ai-task-dialog.tsx +228 -0
- package/src/routes/_nav/tasks/-components/source-ai-exec-dialog.tsx +175 -0
- package/src/routes/_nav/tasks/-components/task-constants.ts +115 -0
- package/src/routes/_nav/tasks/-components/use-task-filter.ts +64 -0
- package/src/routes/_nav/tasks/-tests/create-ai-task-dialog.test.tsx +175 -0
- package/src/routes/_nav/tasks/-tests/task-filter.test.ts +134 -0
- package/src/routes/_nav/tasks/-tests/task-styles.test.tsx +119 -0
- package/src/routes/_nav/tasks/index.lazy.tsx +114 -0
- package/src/routes/_nav/tasks/index.tsx +47 -0
- package/src/routes/_nav/{tags → tasks}/route.tsx +3 -3
- package/src/routes/_nav/test-cases/$caseId/route.lazy.tsx +3 -1
- package/src/routes/_nav/test-cases/$caseId/route.tsx +11 -2
- package/src/routes/api/chat.ts +23 -19
- package/src/routes/api/design/prototype/route.ts +2 -4
- package/src/routes/{_nav/acceptance-criteria/route.tsx → api/internal/collab-reindex.ts} +11 -8
- package/src/routes/api/knowledge/sync-github.ts +1 -1
- package/src/routes/design-files/$projectId/$artboardId/$filename.ts +9 -0
- package/src/routes/invite/$token/route.lazy.tsx +2 -2
- package/src/routes/share/$versionId/route.lazy.tsx +8 -0
- package/src/routes/share/compliance/$id/route.lazy.tsx +2 -2
- package/src/routes/share/compliance/$id/route.tsx +1 -1
- package/src/routes/share/law/$id/route.lazy.tsx +2 -2
- package/src/routes/share/law/$id/route.tsx +1 -1
- package/src/server/__tests__/bug-groups-two-level.test.ts +263 -0
- package/src/server/__tests__/builtin-project.test.ts +45 -1
- package/src/server/__tests__/csv-export.test.ts +319 -0
- package/src/server/__tests__/share-version.test.ts +7 -2
- package/src/server/__tests__/task-assigned-query.test.ts +232 -0
- package/src/server/__tests__/task-context-builder.test.ts +175 -0
- package/src/server/__tests__/task-executor.test.ts +349 -0
- package/src/server/__tests__/task-priority-map.test.ts +41 -0
- package/src/server/__tests__/task-serverfn-crud.test.ts +639 -0
- package/src/server/__tests__/task-serverfn-queries.test.ts +403 -0
- package/src/server/__tests__/testcase-board.test.ts +301 -0
- package/src/server/__tests__/testcase-bulk.test.ts +155 -0
- package/src/server/__tests__/testcase-list-scope.test.ts +215 -0
- package/src/server/ai-gateway/__tests__/chat-handler-prompt-key.test.ts +106 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +9 -3
- package/src/server/ai-gateway/__tests__/conformance.test.ts +4 -3
- package/src/server/ai-gateway/adapter.ts +4 -2
- package/src/server/ai-gateway/chat-handler.ts +13 -5
- package/src/server/ai-gateway/chat-stream-builder.ts +12 -7
- package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +88 -0
- package/src/server/ai-gateway/sandbox/harness-sandbox.ts +1 -1
- package/src/server/ai-gateway/story-handler.ts +7 -2
- package/src/server/ai-gateway/tools/plugin-lifecycle-defs.ts +8 -99
- package/src/server/ai-gateway/tools/plugin-tools.ts +6 -10
- package/src/server/ai-task/assigned-query.ts +167 -0
- package/src/server/ai-task/context-builder.ts +182 -0
- package/src/server/ai-task/executor.ts +296 -0
- package/src/server/ai-task/priority-map.ts +37 -0
- package/src/server/ai-task/types.ts +15 -0
- package/src/server/bug-report/__tests__/report-handler.test.ts +55 -4
- package/src/server/bug-report/report-handler.ts +54 -8
- package/src/server/builtin-project.ts +24 -0
- package/src/server/cloudflare/prisma-wasm-base64-stub.ts +5 -0
- package/src/server/collab/__tests__/collab-permissions.test.ts +412 -0
- package/src/server/collab/__tests__/knowledge-additions.test.ts +121 -0
- package/src/server/collab/__tests__/reindex-endpoint.test.ts +168 -0
- package/src/server/collab/__tests__/resolve.test.ts +249 -0
- package/src/server/collab/__tests__/touch-snapshot.test.ts +58 -0
- package/src/server/collab/__tests__/write-guard-sdk.test.ts +501 -0
- package/src/server/collab/__tests__/write-guard.test.ts +373 -0
- package/src/server/collab/knowledge-additions.ts +37 -0
- package/src/server/collab/reindex-endpoint.ts +75 -0
- package/src/server/collab/resolve.ts +122 -0
- package/src/server/collab/seed.ts +408 -0
- package/src/server/collab/ticket-utils.ts +107 -0
- package/src/server/collab/touch-snapshot.ts +55 -0
- package/src/server/collab/write-guard.ts +258 -0
- package/src/server/collab-permissions.ts +248 -0
- package/src/server/csv.ts +61 -0
- package/src/server/{knowledge-public → knowledge/public}/github-sync.ts +12 -2
- package/src/server/{knowledge-public → knowledge/public}/share-merge.ts +31 -8
- package/src/server/lib/__tests__/sequence-number-retry.test.ts +160 -0
- package/src/server/lib/rate-limit.ts +13 -2
- package/src/server/lib/sequence-number-retry.ts +85 -0
- package/src/server/mcp/__tests__/tools.test.ts +10 -0
- package/src/server/mcp/tools.ts +26 -2
- package/src/server/middlewares/auth-rate-limit.ts +18 -9
- package/src/server/notifications/event-bus.ts +3 -0
- package/src/server/serverFns/__tests__/collab-snapshot.test.ts +665 -0
- package/src/server/serverFns/__tests__/collab.test.ts +459 -0
- package/src/server/serverFns/__tests__/knowledge-delete.test.ts +167 -0
- package/src/server/serverFns/__tests__/ownership-merge.test.ts +34 -2
- package/src/server/serverFns/__tests__/read-write-merge.test.ts +2 -2
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +12 -2
- package/src/server/serverFns/__tests__/story-crud.test.ts +51 -11
- package/src/server/serverFns/__tests__/story-tree.test.ts +66 -34
- package/src/server/serverFns/__tests__/story-types.test.ts +14 -9
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +325 -0
- package/src/server/serverFns/__tests__/update-document-guard.test.ts +285 -0
- package/src/server/serverFns/__tests__/update-page-guard.test.ts +276 -0
- package/src/server/serverFns/__tests__/update-story-guard.test.ts +436 -0
- package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +293 -0
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +25 -0
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +315 -15
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +36 -18
- package/src/server/serverFns/bug/__tests__/bug-list-query.test.ts +21 -0
- package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +335 -0
- package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +165 -0
- package/src/server/serverFns/bug/__tests__/bug-story.test.ts +4 -1
- package/src/server/serverFns/bug/bug-comments.ts +5 -0
- package/src/server/serverFns/bug/bug-crud.ts +321 -72
- package/src/server/serverFns/bug/bug-display-options.ts +11 -0
- package/src/server/serverFns/bug/bug-export.ts +137 -0
- package/src/server/serverFns/bug/bug-group-query.ts +16 -5
- package/src/server/serverFns/bug/bug-groups.ts +39 -15
- package/src/server/serverFns/bug/bug-list-query.ts +8 -1
- package/src/server/serverFns/bug/bug-reactions.ts +123 -0
- package/src/server/serverFns/bug/bug-reorder.ts +64 -0
- package/src/server/serverFns/bug/bug-story-bulk.ts +280 -0
- package/src/server/serverFns/bug/bug-story.ts +47 -39
- package/src/server/serverFns/bug/index.ts +6 -0
- package/src/server/serverFns/collab-presence.ts +119 -0
- package/src/server/serverFns/collab-snapshot.ts +201 -0
- package/src/server/serverFns/collab.ts +153 -0
- package/src/server/serverFns/design/__tests__/designs.test.ts +0 -2
- package/src/server/serverFns/design/designs.ts +0 -64
- package/src/server/serverFns/design/detail.ts +36 -2
- package/src/server/serverFns/design/pages.ts +73 -10
- package/src/server/serverFns/iteration/iteration-crud.ts +25 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/create.ts +3 -3
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/refresh-favicon.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/sync-github.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/update.ts +5 -4
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/update.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/update.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/update.ts +3 -2
- package/src/server/serverFns/knowledge.ts +135 -19
- package/src/server/serverFns/project/module-overview.ts +1 -2
- package/src/server/serverFns/share/version.ts +7 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +6 -0
- package/src/server/serverFns/story/__tests__/create-story-internal.test.ts +112 -3
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +77 -6
- package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +187 -0
- package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +163 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -0
- package/src/server/serverFns/story/__tests__/story-list-query.test.ts +20 -0
- package/src/server/serverFns/story/__tests__/story-search.test.ts +5 -0
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +64 -1
- package/src/server/serverFns/story/index.ts +2 -2
- package/src/server/serverFns/story/story-assoc.ts +44 -14
- package/src/server/serverFns/story/story-crud/archive.ts +52 -28
- package/src/server/serverFns/story/story-crud/bulk.ts +14 -4
- package/src/server/serverFns/story/story-crud/create-story-internal.ts +39 -21
- package/src/server/serverFns/story/story-crud.ts +283 -116
- package/src/server/serverFns/story/story-dependencies.ts +31 -0
- package/src/server/serverFns/story/story-groups.ts +15 -11
- package/src/server/serverFns/story/story-list-query.ts +16 -4
- package/src/server/serverFns/story/story-search.ts +9 -6
- package/src/server/serverFns/story/story-transfer.ts +99 -42
- package/src/server/serverFns/story/story-tree.ts +70 -48
- package/src/server/serverFns/story-display-options.ts +21 -10
- package/src/server/serverFns/story-view.ts +2 -2
- package/src/server/serverFns/tasks/task-crud.ts +264 -0
- package/src/server/serverFns/tasks/task-queries.ts +172 -0
- package/src/server/serverFns/testcase/__tests__/generation.test.ts +3 -0
- package/src/server/serverFns/testcase/__tests__/stats.test.ts +18 -4
- package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +113 -0
- package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +62 -14
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +32 -10
- package/src/server/serverFns/testcase/bulk.ts +195 -0
- package/src/server/serverFns/testcase/generation.ts +10 -0
- package/src/server/serverFns/testcase/stats.ts +22 -8
- package/src/server/serverFns/testcase/testcase-board.ts +198 -0
- package/src/server/serverFns/testcase/testcase-crud.ts +165 -33
- package/src/server/serverFns/testcase/testcase-display-options.ts +166 -0
- package/src/server/serverFns/testcase/testcase-export.ts +125 -0
- package/src/server/serverFns/testcase/testcase-filter-views.ts +36 -9
- package/src/server/serverFns/testcase/testcase-groups.ts +27 -23
- package/src/server/serverFns/testcase/testcase-reorder.ts +66 -0
- package/src/server/story/__tests__/date-key.test.ts +57 -0
- package/src/server/story/date-key.ts +36 -0
- package/src/server/story/types.ts +3 -7
- package/src/server/testcase/__tests__/list-query.test.ts +269 -0
- package/src/server/testcase/list-query.ts +145 -0
- package/src/server-entry.ts +32 -0
- package/src/shared/bug/__tests__/constants.test.ts +44 -0
- package/src/shared/bug/constants.ts +37 -1
- package/src/start.ts +7 -2
- package/src/styles/globals.css +24 -3
- package/src/types/project.ts +2 -3
- package/vite.shared.ts +110 -3
- package/src/__tests__/tag-badge.test.tsx +0 -41
- package/src/components/bug/__tests__/bug-list-table.test.tsx +0 -289
- package/src/components/bug/bug-list-table.tsx +0 -187
- package/src/components/bug/columns.tsx +0 -193
- package/src/components/bug/filters/add-filter-dropdown.tsx +0 -70
- package/src/components/bug/filters/applied-filters.tsx +0 -214
- package/src/components/bug/filters/filter-condition.tsx +0 -151
- package/src/components/bug/filters/filter-row.tsx +0 -104
- package/src/components/bug/filters/filter-toggle.tsx +0 -61
- package/src/components/bug/filters/filter-types.ts +0 -146
- package/src/components/story/detail/story-detail-toolbar.tsx +0 -382
- package/src/components/story/filters/add-filter-dropdown.tsx +0 -70
- package/src/components/story/filters/applied-filters.tsx +0 -225
- package/src/components/story/filters/filter-condition.tsx +0 -152
- package/src/components/story/filters/filter-row.tsx +0 -116
- package/src/components/story/filters/filter-toggle.tsx +0 -60
- package/src/components/story/filters/filter-types.ts +0 -120
- package/src/components/story/filters/filter-value-input.tsx +0 -182
- package/src/components/story/views/kanban-board.tsx +0 -464
- package/src/components/story/views/kanban-card.tsx +0 -100
- package/src/components/tags/__tests__/tags.test.ts +0 -388
- package/src/components/tags/nav-tag.tsx +0 -121
- package/src/components/tags/pages/tag-settings-page.tsx +0 -32
- package/src/components/tags/pages/tags-index-page.tsx +0 -32
- package/src/components/tags/tag-badge.tsx +0 -56
- package/src/components/tags/tag-color-classes.ts +0 -61
- package/src/components/tags/tag-color-picker.tsx +0 -54
- package/src/components/tags/tag-components-index.ts +0 -23
- package/src/components/tags/tag-manager.tsx +0 -208
- package/src/components/tags/tag-selector.tsx +0 -164
- package/src/components/testcase/__tests__/table-view.test.tsx +0 -328
- package/src/components/testcase/plans/add-to-plan-bar.tsx +0 -64
- package/src/components/testcase/table/columns.tsx +0 -170
- package/src/components/testcase/table/table-view.tsx +0 -152
- package/src/lib/tag-colors.ts +0 -35
- package/src/routes/_nav/tags/$id/route.lazy.tsx +0 -23
- package/src/routes/_nav/tags/$id/route.tsx +0 -20
- package/src/routes/_nav/tags/index.lazy.tsx +0 -23
- package/src/routes/_nav/tags/index.tsx +0 -20
- package/src/routes/_nav/tags/settings/route.lazy.tsx +0 -23
- package/src/routes/_nav/tags/settings/route.tsx +0 -23
- package/src/server/serverFns/tags.ts +0 -138
- /package/src/components/{knowledge-public → knowledge/public}/bookmark-card.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/category-icon.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/chip-utils.ts +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-card-grid.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-filters.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-search-bar.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/markdown-editor.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/markdown-view.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/tag-filter.tsx +0 -0
- /package/src/components/{story/detail → shared-kit/comments}/emoji-picker.tsx +0 -0
- /package/src/components/story/{schemas/story-create.ts → story-create-schema.ts} +0 -0
- /package/src/server/{knowledge-public → knowledge/public}/canonical-id.ts +0 -0
- /package/src/server/{knowledge-public → knowledge/public}/favicon.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/categories.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/favorite.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/public.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/entity-tags.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/favorite.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/public.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/unshare.ts +0 -0
|
@@ -0,0 +1,1916 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
type ComponentType,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type RefObject,
|
|
13
|
+
} from "react"
|
|
14
|
+
import {
|
|
15
|
+
mergeEventCalendarI18n,
|
|
16
|
+
type EventCalendarI18nConfig,
|
|
17
|
+
type EventCalendarI18nOverrides,
|
|
18
|
+
} from "@/components/reui/event-calendar/event-calendar-i18n"
|
|
19
|
+
import {
|
|
20
|
+
buildEventIndex,
|
|
21
|
+
defaultEventOrder,
|
|
22
|
+
eventsOverlap,
|
|
23
|
+
getDayKey,
|
|
24
|
+
getRangeKey,
|
|
25
|
+
getViewDateRange,
|
|
26
|
+
stepDate,
|
|
27
|
+
toZoned,
|
|
28
|
+
zonedStartOfDay,
|
|
29
|
+
type EventCalendarDayBucket,
|
|
30
|
+
type EventCalendarIndex,
|
|
31
|
+
type WeekStartsOn,
|
|
32
|
+
} from "@/components/reui/event-calendar/event-calendar-lib"
|
|
33
|
+
import type {
|
|
34
|
+
CalendarEvent,
|
|
35
|
+
CalendarView,
|
|
36
|
+
EventCalendarDateRange,
|
|
37
|
+
EventCalendarDragState,
|
|
38
|
+
EventCalendarEventId,
|
|
39
|
+
EventCalendarInteractions,
|
|
40
|
+
EventCalendarOccurrence,
|
|
41
|
+
EventCalendarOffDaysConfig,
|
|
42
|
+
EventCalendarProposedUpdate,
|
|
43
|
+
EventCalendarRangeInfo,
|
|
44
|
+
EventCalendarResource,
|
|
45
|
+
EventCalendarSegment,
|
|
46
|
+
EventCalendarSelection,
|
|
47
|
+
EventCalendarSlotDraft,
|
|
48
|
+
EventCalendarSlotInfo,
|
|
49
|
+
EventCalendarState,
|
|
50
|
+
EventCalendarUpdateResult,
|
|
51
|
+
EventCalendarViewSettings,
|
|
52
|
+
} from "@/components/reui/event-calendar/event-calendar-types"
|
|
53
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
54
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
55
|
+
import { addDays, type Locale } from "date-fns"
|
|
56
|
+
|
|
57
|
+
import { cn } from "cn"
|
|
58
|
+
|
|
59
|
+
const BASE_VIEWS: CalendarView[] = ["month", "week", "day", "days", "agenda"]
|
|
60
|
+
const ALL_VIEWS: CalendarView[] = [...BASE_VIEWS, "resource"]
|
|
61
|
+
|
|
62
|
+
const DEFAULT_INTERACTIONS: EventCalendarInteractions = {
|
|
63
|
+
drag: true,
|
|
64
|
+
resize: true,
|
|
65
|
+
selectSlot: true,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const EMPTY_SELECTION: EventCalendarSelection = { eventKeys: [], slot: null }
|
|
69
|
+
|
|
70
|
+
// resolveSettings runs on every render, so a freshly allocated default would
|
|
71
|
+
// bust the view memos keyed on it (the month grid rebuilds its 42 zoned day
|
|
72
|
+
// starts whenever weekendDays changes identity). Shared, never mutated.
|
|
73
|
+
const DEFAULT_WEEKEND_DAYS: number[] = [0, 6]
|
|
74
|
+
const EMPTY_RESOURCES: EventCalendarResource[] = []
|
|
75
|
+
const DEFAULT_EVENT_PRIORITY = (event: CalendarEvent<never>) =>
|
|
76
|
+
event.priority ?? 0
|
|
77
|
+
|
|
78
|
+
// Priority-aware default order, cached on the resolver identity: eventOrder is
|
|
79
|
+
// part of the index cache key, so a fresh closure per render would rebuild the
|
|
80
|
+
// whole index on every render.
|
|
81
|
+
const priorityOrderCache = new WeakMap<object, unknown>()
|
|
82
|
+
type EventOrder<TData> = (
|
|
83
|
+
a: EventCalendarOccurrence<TData>,
|
|
84
|
+
b: EventCalendarOccurrence<TData>
|
|
85
|
+
) => number
|
|
86
|
+
function priorityEventOrder<TData>(
|
|
87
|
+
getEventPriority: (event: CalendarEvent<TData>) => number
|
|
88
|
+
): EventOrder<TData> {
|
|
89
|
+
const cached = priorityOrderCache.get(getEventPriority)
|
|
90
|
+
if (cached) return cached as EventOrder<TData>
|
|
91
|
+
// higher priority packs and orders first; ties fall through to the
|
|
92
|
+
// start/duration/key default
|
|
93
|
+
const order: EventOrder<TData> = (a, b) =>
|
|
94
|
+
getEventPriority(b.event) - getEventPriority(a.event) ||
|
|
95
|
+
defaultEventOrder(a, b)
|
|
96
|
+
priorityOrderCache.set(getEventPriority, order)
|
|
97
|
+
return order
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface EventCalendarCallbacks<TData = unknown> {
|
|
101
|
+
onEventClick?: (
|
|
102
|
+
occurrence: EventCalendarOccurrence<TData>,
|
|
103
|
+
e: React.MouseEvent
|
|
104
|
+
) => void
|
|
105
|
+
onEventDoubleClick?: (
|
|
106
|
+
occurrence: EventCalendarOccurrence<TData>,
|
|
107
|
+
e: React.MouseEvent
|
|
108
|
+
) => void
|
|
109
|
+
onEventUpdate?: (
|
|
110
|
+
update: EventCalendarProposedUpdate<TData>
|
|
111
|
+
) => EventCalendarUpdateResult
|
|
112
|
+
canDropEvent?: (update: EventCalendarProposedUpdate<TData>) => boolean
|
|
113
|
+
/**
|
|
114
|
+
* A move/resize was attempted on an event that cannot be dragged (readOnly,
|
|
115
|
+
* per-event draggable/resizable false, or the interaction is off). Fires once
|
|
116
|
+
* per gesture when the pointer crosses the activation threshold; the calendar
|
|
117
|
+
* shows a not-allowed cursor for the duration but never picks the message -
|
|
118
|
+
* broadcast it here so the consumer can surface a custom one.
|
|
119
|
+
*/
|
|
120
|
+
onDragBlocked?: (
|
|
121
|
+
occurrence: EventCalendarOccurrence<TData>,
|
|
122
|
+
info: {
|
|
123
|
+
gesture: "move" | "resize"
|
|
124
|
+
reason: "readOnly" | "disabled" | "interactions-off"
|
|
125
|
+
}
|
|
126
|
+
) => void
|
|
127
|
+
onSlotClick?: (slot: EventCalendarSlotInfo, e: React.MouseEvent) => void
|
|
128
|
+
onSelectSlot?: (slot: EventCalendarSlotDraft) => void
|
|
129
|
+
canSelectSlot?: (slot: EventCalendarSlotDraft) => boolean
|
|
130
|
+
onRangeChange?: (info: EventCalendarRangeInfo) => void
|
|
131
|
+
onViewChange?: (view: CalendarView) => void
|
|
132
|
+
onDateChange?: (date: Date) => void
|
|
133
|
+
onDayCountChange?: (count: number) => void
|
|
134
|
+
onSelectionChange?: (selection: EventCalendarSelection) => void
|
|
135
|
+
onInteractionsChange?: (interactions: EventCalendarInteractions) => void
|
|
136
|
+
onViewSettingsChange?: (viewSettings: EventCalendarViewSettings) => void
|
|
137
|
+
onEventsChange?: (events: CalendarEvent<TData>[]) => void
|
|
138
|
+
onMoreClick?: (
|
|
139
|
+
day: Date,
|
|
140
|
+
segments: EventCalendarOccurrence<TData>[],
|
|
141
|
+
e: React.MouseEvent
|
|
142
|
+
) => void | false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface UseEventCalendarStateOptions<
|
|
146
|
+
TData = unknown,
|
|
147
|
+
> extends EventCalendarCallbacks<TData> {
|
|
148
|
+
events?: CalendarEvent<TData>[]
|
|
149
|
+
defaultEvents?: CalendarEvent<TData>[]
|
|
150
|
+
view?: CalendarView
|
|
151
|
+
defaultView?: CalendarView
|
|
152
|
+
date?: Date
|
|
153
|
+
defaultDate?: Date
|
|
154
|
+
dayCount?: number
|
|
155
|
+
defaultDayCount?: number
|
|
156
|
+
selection?: EventCalendarSelection
|
|
157
|
+
defaultSelection?: EventCalendarSelection
|
|
158
|
+
interactions?: Partial<EventCalendarInteractions>
|
|
159
|
+
defaultInteractions?: Partial<EventCalendarInteractions>
|
|
160
|
+
viewSettings?: EventCalendarViewSettings
|
|
161
|
+
defaultViewSettings?: EventCalendarViewSettings
|
|
162
|
+
loading?: boolean
|
|
163
|
+
views?: CalendarView[]
|
|
164
|
+
timeZone?: string
|
|
165
|
+
locale?: Locale
|
|
166
|
+
weekStartsOn?: WeekStartsOn
|
|
167
|
+
dayStartHour?: number
|
|
168
|
+
dayEndHour?: number
|
|
169
|
+
slotDuration?: number
|
|
170
|
+
snapDuration?: number
|
|
171
|
+
agendaDayCount?: number
|
|
172
|
+
fixedWeeks?: boolean
|
|
173
|
+
showOutsideDays?: boolean
|
|
174
|
+
i18n?: EventCalendarI18nOverrides
|
|
175
|
+
/** Bookable resources for the resource view. */
|
|
176
|
+
resources?: EventCalendarResource[]
|
|
177
|
+
getEventPriority?: (event: CalendarEvent<TData>) => number
|
|
178
|
+
eventOrder?: (
|
|
179
|
+
a: EventCalendarOccurrence<TData>,
|
|
180
|
+
b: EventCalendarOccurrence<TData>
|
|
181
|
+
) => number
|
|
182
|
+
getOccurrences?: (
|
|
183
|
+
event: CalendarEvent<TData>,
|
|
184
|
+
range: EventCalendarDateRange,
|
|
185
|
+
ctx: { timeZone: string }
|
|
186
|
+
) => Array<{ start: Date; end: Date }> | null
|
|
187
|
+
/**
|
|
188
|
+
* Weekday numbers (0 = Sunday) treated as the weekend by the "weekends"
|
|
189
|
+
* view toggle. @default [0, 6]
|
|
190
|
+
*/
|
|
191
|
+
weekendDays?: number[]
|
|
192
|
+
/** Pointer-gesture tuning (activation distances, touch delay, autoscroll). */
|
|
193
|
+
activation?: Partial<EventCalendarActivationConfig>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface EventCalendarActivationConfig {
|
|
197
|
+
moveDistancePx: number
|
|
198
|
+
createDistancePx: number
|
|
199
|
+
touchDelayMs: number
|
|
200
|
+
touchTolerancePx: number
|
|
201
|
+
autoScrollEdgePx: number
|
|
202
|
+
autoScrollMaxStepPx: number
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolved configuration: every UseEventCalendarStateOptions field except the
|
|
207
|
+
* controlled/uncontrolled state pairs, with defaults applied and i18n merged.
|
|
208
|
+
* Read via ref semantics - callback identity changes never re-render the grid.
|
|
209
|
+
*/
|
|
210
|
+
interface EventCalendarSettings<
|
|
211
|
+
TData = unknown,
|
|
212
|
+
> extends EventCalendarCallbacks<TData> {
|
|
213
|
+
timeZone: string
|
|
214
|
+
locale?: Locale
|
|
215
|
+
weekStartsOn: WeekStartsOn
|
|
216
|
+
views: CalendarView[]
|
|
217
|
+
dayStartHour: number
|
|
218
|
+
dayEndHour: number
|
|
219
|
+
slotDuration: number
|
|
220
|
+
snapDuration: number
|
|
221
|
+
agendaDayCount: number
|
|
222
|
+
fixedWeeks: boolean
|
|
223
|
+
showOutsideDays: boolean
|
|
224
|
+
i18n: EventCalendarI18nConfig
|
|
225
|
+
resources: EventCalendarResource[]
|
|
226
|
+
weekendDays: number[]
|
|
227
|
+
activation?: Partial<EventCalendarActivationConfig>
|
|
228
|
+
getEventPriority: (event: CalendarEvent<TData>) => number
|
|
229
|
+
eventOrder: (
|
|
230
|
+
a: EventCalendarOccurrence<TData>,
|
|
231
|
+
b: EventCalendarOccurrence<TData>
|
|
232
|
+
) => number
|
|
233
|
+
getOccurrences?: (
|
|
234
|
+
event: CalendarEvent<TData>,
|
|
235
|
+
range: EventCalendarDateRange,
|
|
236
|
+
ctx: { timeZone: string }
|
|
237
|
+
) => Array<{ start: Date; end: Date }> | null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface EventCalendarApi<TData = unknown> {
|
|
241
|
+
next(): void
|
|
242
|
+
prev(): void
|
|
243
|
+
today(): void
|
|
244
|
+
goTo(date: Date): void
|
|
245
|
+
setView(view: CalendarView, opts?: { dayCount?: number }): void
|
|
246
|
+
setDayCount(count: number): void
|
|
247
|
+
getEvents(): CalendarEvent<TData>[]
|
|
248
|
+
getEvent(id: EventCalendarEventId): CalendarEvent<TData> | undefined
|
|
249
|
+
setEvents(events: CalendarEvent<TData>[]): void
|
|
250
|
+
addEvent(event: CalendarEvent<TData>): void
|
|
251
|
+
updateEvent(
|
|
252
|
+
id: EventCalendarEventId,
|
|
253
|
+
patch: Partial<CalendarEvent<TData>>
|
|
254
|
+
): void
|
|
255
|
+
removeEvent(id: EventCalendarEventId): void
|
|
256
|
+
getOccurrences(
|
|
257
|
+
range?: EventCalendarDateRange
|
|
258
|
+
): EventCalendarOccurrence<TData>[]
|
|
259
|
+
getOccurrencesForDay(day: Date): EventCalendarOccurrence<TData>[]
|
|
260
|
+
findOverlapping(candidate: {
|
|
261
|
+
start: Date
|
|
262
|
+
end: Date
|
|
263
|
+
excludeEventId?: string
|
|
264
|
+
}): EventCalendarOccurrence<TData>[]
|
|
265
|
+
select(selection: Partial<EventCalendarSelection>): void
|
|
266
|
+
selectEvent(key: string, opts?: { additive?: boolean }): void
|
|
267
|
+
clearSelection(): void
|
|
268
|
+
setInteractions(patch: Partial<EventCalendarInteractions>): void
|
|
269
|
+
setViewSettings(patch: EventCalendarViewSettings): void
|
|
270
|
+
getVisibleRange(): EventCalendarDateRange
|
|
271
|
+
getActiveRange(): EventCalendarDateRange
|
|
272
|
+
/** TZDate in the calendar's display time zone. */
|
|
273
|
+
toZoned(date: Date): Date
|
|
274
|
+
/** number = minutes from the zoned day start; no-op outside time-grid views. */
|
|
275
|
+
scrollToTime(time: Date | number): void
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Cross-file plumbing for sibling view/interaction modules; not public API. */
|
|
279
|
+
interface EventCalendarInternals<TData = unknown> {
|
|
280
|
+
getIndex(): EventCalendarIndex<TData>
|
|
281
|
+
setDrag(drag: EventCalendarDragState<TData> | null): void
|
|
282
|
+
setSlotDraft(draft: EventCalendarSlotDraft | null): void
|
|
283
|
+
registerScrollHandler(handler: ((time: Date | number) => void) | null): void
|
|
284
|
+
applyProposedUpdate(
|
|
285
|
+
update: EventCalendarProposedUpdate<TData>,
|
|
286
|
+
extraPatch?: Partial<CalendarEvent<TData>>
|
|
287
|
+
): boolean
|
|
288
|
+
getSettingsVersion(): number
|
|
289
|
+
/** The rendered calendar root element, or null before mount. */
|
|
290
|
+
getRootEl(): HTMLElement | null
|
|
291
|
+
setRootEl(el: HTMLElement | null): void
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface EventCalendarInstance<TData = unknown> {
|
|
295
|
+
getState(): EventCalendarState<TData>
|
|
296
|
+
subscribe(listener: () => void): () => void
|
|
297
|
+
api: EventCalendarApi<TData>
|
|
298
|
+
settings: EventCalendarSettings<TData>
|
|
299
|
+
internals: EventCalendarInternals<TData>
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveSettings<TData>(
|
|
303
|
+
options: UseEventCalendarStateOptions<TData>
|
|
304
|
+
): EventCalendarSettings<TData> {
|
|
305
|
+
const {
|
|
306
|
+
// strip state pairs; the rest flows into settings
|
|
307
|
+
events: _e,
|
|
308
|
+
defaultEvents: _de,
|
|
309
|
+
view: _v,
|
|
310
|
+
defaultView: _dv,
|
|
311
|
+
date: _d,
|
|
312
|
+
defaultDate: _dd,
|
|
313
|
+
dayCount: _dc,
|
|
314
|
+
defaultDayCount: _ddc,
|
|
315
|
+
selection: _s,
|
|
316
|
+
defaultSelection: _ds,
|
|
317
|
+
interactions: _i,
|
|
318
|
+
defaultInteractions: _di,
|
|
319
|
+
viewSettings: _vs,
|
|
320
|
+
defaultViewSettings: _dvs,
|
|
321
|
+
loading: _l,
|
|
322
|
+
...rest
|
|
323
|
+
} = options
|
|
324
|
+
const getEventPriority =
|
|
325
|
+
options.getEventPriority ??
|
|
326
|
+
(DEFAULT_EVENT_PRIORITY as (event: CalendarEvent<TData>) => number)
|
|
327
|
+
return {
|
|
328
|
+
...rest,
|
|
329
|
+
timeZone:
|
|
330
|
+
options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
331
|
+
locale: options.locale,
|
|
332
|
+
// locale-first default: a de/fr locale gets Monday weeks without also
|
|
333
|
+
// having to set weekStartsOn; an explicit weekStartsOn always wins
|
|
334
|
+
weekStartsOn:
|
|
335
|
+
options.weekStartsOn ?? options.locale?.options?.weekStartsOn ?? 0,
|
|
336
|
+
// the resource view only makes sense with resources configured
|
|
337
|
+
views:
|
|
338
|
+
options.views ?? (options.resources?.length ? ALL_VIEWS : BASE_VIEWS),
|
|
339
|
+
dayStartHour: options.dayStartHour ?? 0,
|
|
340
|
+
dayEndHour: options.dayEndHour ?? 24,
|
|
341
|
+
slotDuration: options.slotDuration ?? 30,
|
|
342
|
+
snapDuration: options.snapDuration ?? 15,
|
|
343
|
+
agendaDayCount: options.agendaDayCount ?? 30,
|
|
344
|
+
fixedWeeks: options.fixedWeeks ?? true,
|
|
345
|
+
showOutsideDays: options.showOutsideDays ?? true,
|
|
346
|
+
i18n: mergeEventCalendarI18n(options.i18n),
|
|
347
|
+
resources: options.resources ?? EMPTY_RESOURCES,
|
|
348
|
+
getEventPriority,
|
|
349
|
+
eventOrder: options.eventOrder ?? priorityEventOrder(getEventPriority),
|
|
350
|
+
getOccurrences: options.getOccurrences,
|
|
351
|
+
weekendDays: options.weekendDays ?? DEFAULT_WEEKEND_DAYS,
|
|
352
|
+
activation: options.activation,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const warned = new Set<string>()
|
|
357
|
+
function warnOnce(key: string, message: string) {
|
|
358
|
+
if (process.env.NODE_ENV !== "production" && !warned.has(key)) {
|
|
359
|
+
warned.add(key)
|
|
360
|
+
console.warn(`[event-calendar] ${message}`)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interface EventCalendarStore<TData> {
|
|
365
|
+
instance: EventCalendarInstance<TData>
|
|
366
|
+
setOptions(next: UseEventCalendarStateOptions<TData>): boolean
|
|
367
|
+
notify(): void
|
|
368
|
+
emitRangeIfChanged(): void
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function createEventCalendarStore<TData>(
|
|
372
|
+
initial: UseEventCalendarStateOptions<TData>
|
|
373
|
+
): EventCalendarStore<TData> {
|
|
374
|
+
let options = initial
|
|
375
|
+
let settings = resolveSettings(initial)
|
|
376
|
+
let settingsVersion = 0
|
|
377
|
+
|
|
378
|
+
const listeners = new Set<() => void>()
|
|
379
|
+
|
|
380
|
+
const resolveView = (view: CalendarView): CalendarView => {
|
|
381
|
+
if (settings.views.includes(view)) return view
|
|
382
|
+
const fallback = settings.views[0] ?? "month"
|
|
383
|
+
warnOnce(
|
|
384
|
+
`view-${view}`,
|
|
385
|
+
`view "${view}" is not in views [${settings.views.join(", ")}]; falling back to "${fallback}".`
|
|
386
|
+
)
|
|
387
|
+
return fallback
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const internal = {
|
|
391
|
+
view: resolveView(initial.defaultView ?? "month"),
|
|
392
|
+
date: initial.defaultDate ?? new Date(),
|
|
393
|
+
dayCount: initial.defaultDayCount ?? 3,
|
|
394
|
+
events: initial.defaultEvents ?? [],
|
|
395
|
+
selection: initial.defaultSelection ?? EMPTY_SELECTION,
|
|
396
|
+
interactions: { ...DEFAULT_INTERACTIONS, ...initial.defaultInteractions },
|
|
397
|
+
viewSettings: initial.defaultViewSettings ?? {},
|
|
398
|
+
drag: null as EventCalendarDragState<TData> | null,
|
|
399
|
+
slotDraft: null as EventCalendarSlotDraft | null,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let snapshot: EventCalendarState<TData> | null = null
|
|
403
|
+
let indexCache: {
|
|
404
|
+
events: CalendarEvent<TData>[]
|
|
405
|
+
rangeKey: string
|
|
406
|
+
timeZone: string
|
|
407
|
+
weekStartsOn: WeekStartsOn
|
|
408
|
+
eventOrder: EventCalendarSettings<TData>["eventOrder"]
|
|
409
|
+
getOccurrences: EventCalendarSettings<TData>["getOccurrences"]
|
|
410
|
+
index: EventCalendarIndex<TData>
|
|
411
|
+
} | null = null
|
|
412
|
+
// Second single-entry cache, for api.getOccurrences(range) outside the
|
|
413
|
+
// visible range: that branch builds a throwaway index, so without it every
|
|
414
|
+
// call hands back brand new occurrence OBJECTS and the identity-based
|
|
415
|
+
// isEqual in useEventCalendarOccurrences can never settle - an unstable
|
|
416
|
+
// getSnapshot is a hard render loop under useSyncExternalStore, not a slow
|
|
417
|
+
// render.
|
|
418
|
+
let rangeCache: {
|
|
419
|
+
events: CalendarEvent<TData>[]
|
|
420
|
+
rangeKey: string
|
|
421
|
+
timeZone: string
|
|
422
|
+
weekStartsOn: WeekStartsOn
|
|
423
|
+
eventOrder: EventCalendarSettings<TData>["eventOrder"]
|
|
424
|
+
getOccurrences: EventCalendarSettings<TData>["getOccurrences"]
|
|
425
|
+
occurrences: EventCalendarOccurrence<TData>[]
|
|
426
|
+
} | null = null
|
|
427
|
+
let scrollHandler: ((time: Date | number) => void) | null = null
|
|
428
|
+
// The rendered calendar root element, registered by the <EventCalendar> host.
|
|
429
|
+
// The drag engine falls back to it to find day cells when a gesture starts
|
|
430
|
+
// from a portaled surface (e.g. a chip inside the "+N more" popover), whose
|
|
431
|
+
// DOM ancestors do not include the calendar.
|
|
432
|
+
let rootEl: HTMLElement | null = null
|
|
433
|
+
let lastEmittedRangeKey: string | null = null
|
|
434
|
+
|
|
435
|
+
// Controlled interactions merge, cached on input identity: rebuilding the
|
|
436
|
+
// merged object per snapshot would break Object.is for selector hooks.
|
|
437
|
+
let interactionsCache: {
|
|
438
|
+
input: Partial<EventCalendarInteractions>
|
|
439
|
+
merged: EventCalendarInteractions
|
|
440
|
+
} | null = null
|
|
441
|
+
const mergedInteractions = (
|
|
442
|
+
input: Partial<EventCalendarInteractions>
|
|
443
|
+
): EventCalendarInteractions => {
|
|
444
|
+
if (interactionsCache?.input !== input) {
|
|
445
|
+
interactionsCache = {
|
|
446
|
+
input,
|
|
447
|
+
merged: { ...DEFAULT_INTERACTIONS, ...input },
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return interactionsCache.merged
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const invalidate = () => {
|
|
454
|
+
snapshot = null
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const notify = () => {
|
|
458
|
+
listeners.forEach((listener) => listener())
|
|
459
|
+
emitRangeIfChanged()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const getState = (): EventCalendarState<TData> => {
|
|
463
|
+
if (snapshot) return snapshot
|
|
464
|
+
const view = resolveView(options.view ?? internal.view)
|
|
465
|
+
const date = options.date ?? internal.date
|
|
466
|
+
const dayCount = Math.max(1, options.dayCount ?? internal.dayCount)
|
|
467
|
+
const { visibleRange, activeRange } = getViewDateRange(view, date, {
|
|
468
|
+
timeZone: settings.timeZone,
|
|
469
|
+
weekStartsOn: settings.weekStartsOn,
|
|
470
|
+
dayCount,
|
|
471
|
+
agendaDayCount: settings.agendaDayCount,
|
|
472
|
+
fixedWeeks: settings.fixedWeeks,
|
|
473
|
+
})
|
|
474
|
+
snapshot = {
|
|
475
|
+
view,
|
|
476
|
+
date,
|
|
477
|
+
dayCount,
|
|
478
|
+
visibleRange,
|
|
479
|
+
activeRange,
|
|
480
|
+
events: options.events ?? internal.events,
|
|
481
|
+
selection: options.selection ?? internal.selection,
|
|
482
|
+
interactions: options.interactions
|
|
483
|
+
? mergedInteractions(options.interactions)
|
|
484
|
+
: internal.interactions,
|
|
485
|
+
loading: options.loading ?? false,
|
|
486
|
+
drag: internal.drag,
|
|
487
|
+
slotDraft: internal.slotDraft,
|
|
488
|
+
viewSettings: options.viewSettings ?? internal.viewSettings,
|
|
489
|
+
}
|
|
490
|
+
return snapshot
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const emitRangeIfChanged = () => {
|
|
494
|
+
if (!settings.onRangeChange) return
|
|
495
|
+
const state = getState()
|
|
496
|
+
const key = `${state.view}:${getRangeKey(state.visibleRange)}:${settings.timeZone}`
|
|
497
|
+
if (key === lastEmittedRangeKey) return
|
|
498
|
+
lastEmittedRangeKey = key
|
|
499
|
+
settings.onRangeChange({
|
|
500
|
+
range: state.visibleRange,
|
|
501
|
+
activeRange: state.activeRange,
|
|
502
|
+
view: state.view,
|
|
503
|
+
date: state.date,
|
|
504
|
+
timeZone: settings.timeZone,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
type ControlledKey =
|
|
509
|
+
| "view"
|
|
510
|
+
| "date"
|
|
511
|
+
| "dayCount"
|
|
512
|
+
| "events"
|
|
513
|
+
| "selection"
|
|
514
|
+
| "interactions"
|
|
515
|
+
| "viewSettings"
|
|
516
|
+
|
|
517
|
+
const setField = <K extends ControlledKey>(
|
|
518
|
+
key: K,
|
|
519
|
+
value: EventCalendarState<TData>[K extends "events" ? "events" : K]
|
|
520
|
+
) => {
|
|
521
|
+
const controlled = options[key] !== undefined
|
|
522
|
+
if (!controlled) {
|
|
523
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
524
|
+
;(internal as any)[key] = value
|
|
525
|
+
invalidate()
|
|
526
|
+
}
|
|
527
|
+
const callbacks: Record<ControlledKey, ((v: never) => void) | undefined> = {
|
|
528
|
+
view: settings.onViewChange as never,
|
|
529
|
+
date: settings.onDateChange as never,
|
|
530
|
+
dayCount: settings.onDayCountChange as never,
|
|
531
|
+
events: settings.onEventsChange as never,
|
|
532
|
+
selection: settings.onSelectionChange as never,
|
|
533
|
+
interactions: settings.onInteractionsChange as never,
|
|
534
|
+
viewSettings: settings.onViewSettingsChange as never,
|
|
535
|
+
}
|
|
536
|
+
callbacks[key]?.(value as never)
|
|
537
|
+
if (!controlled) notify()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// An occurrence key encodes the start instant (id::startISO), so committing a
|
|
541
|
+
// move re-keys the occurrence and a selection holding the old key would point
|
|
542
|
+
// at nothing. Remapped in the same commit and emitted BEFORE the events write
|
|
543
|
+
// so a controlled consumer applies the two in a consistent order.
|
|
544
|
+
const remapSelectionKey = (
|
|
545
|
+
id: EventCalendarEventId,
|
|
546
|
+
oldKey: string,
|
|
547
|
+
nextStart: Date
|
|
548
|
+
) => {
|
|
549
|
+
const newKey = `${id}::${nextStart.toISOString()}`
|
|
550
|
+
if (newKey === oldKey) return
|
|
551
|
+
const selection = getState().selection
|
|
552
|
+
if (!selection.eventKeys.includes(oldKey)) return
|
|
553
|
+
setField("selection", {
|
|
554
|
+
...selection,
|
|
555
|
+
eventKeys: selection.eventKeys.map((key) =>
|
|
556
|
+
key === oldKey ? newKey : key
|
|
557
|
+
),
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// extraPatch: non-timing fields committed in the SAME write. Two sequential
|
|
562
|
+
// setField("events") calls break controlled mode - the second one re-reads
|
|
563
|
+
// the still-stale controlled array and its onEventsChange payload silently
|
|
564
|
+
// reverts the timing change the first one emitted.
|
|
565
|
+
const applyProposedUpdate = (
|
|
566
|
+
update: EventCalendarProposedUpdate<TData>,
|
|
567
|
+
extraPatch?: Partial<CalendarEvent<TData>>
|
|
568
|
+
): boolean => {
|
|
569
|
+
const result = settings.onEventUpdate?.(update)
|
|
570
|
+
if (result === false) return false
|
|
571
|
+
const adjusted: Partial<CalendarEvent<TData>> =
|
|
572
|
+
result && typeof result === "object"
|
|
573
|
+
? {
|
|
574
|
+
start: result.start ?? update.start,
|
|
575
|
+
end: result.end ?? update.end,
|
|
576
|
+
allDay: result.allDay ?? update.allDay,
|
|
577
|
+
}
|
|
578
|
+
: { start: update.start, end: update.end, allDay: update.allDay }
|
|
579
|
+
if (update.resourceId !== undefined) adjusted.resourceId = update.resourceId
|
|
580
|
+
// the STORED event holds the pre-commit start, which is what the live
|
|
581
|
+
// occurrence key was built from (update.event already carries the proposal
|
|
582
|
+
// when the call comes from api.updateEvent)
|
|
583
|
+
const stored = getState().events.find(
|
|
584
|
+
(event) => event.id === update.event.id
|
|
585
|
+
)
|
|
586
|
+
const oldKey =
|
|
587
|
+
update.occurrence?.key ??
|
|
588
|
+
(stored ? `${stored.id}::${stored.start.toISOString()}` : null)
|
|
589
|
+
if (oldKey) {
|
|
590
|
+
remapSelectionKey(update.event.id, oldKey, adjusted.start ?? update.start)
|
|
591
|
+
}
|
|
592
|
+
const events = getState().events
|
|
593
|
+
const next = events.map((event) =>
|
|
594
|
+
event.id === update.event.id
|
|
595
|
+
? { ...event, ...extraPatch, ...adjusted }
|
|
596
|
+
: event
|
|
597
|
+
)
|
|
598
|
+
setField("events", next)
|
|
599
|
+
return true
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const getIndex = (): EventCalendarIndex<TData> => {
|
|
603
|
+
const state = getState()
|
|
604
|
+
const rangeKey = getRangeKey(state.visibleRange)
|
|
605
|
+
if (
|
|
606
|
+
indexCache &&
|
|
607
|
+
indexCache.events === state.events &&
|
|
608
|
+
indexCache.rangeKey === rangeKey &&
|
|
609
|
+
indexCache.timeZone === settings.timeZone &&
|
|
610
|
+
indexCache.weekStartsOn === settings.weekStartsOn &&
|
|
611
|
+
indexCache.eventOrder === settings.eventOrder &&
|
|
612
|
+
indexCache.getOccurrences === settings.getOccurrences
|
|
613
|
+
) {
|
|
614
|
+
return indexCache.index
|
|
615
|
+
}
|
|
616
|
+
const index = buildEventIndex(state.events, state.visibleRange, {
|
|
617
|
+
timeZone: settings.timeZone,
|
|
618
|
+
weekStartsOn: settings.weekStartsOn,
|
|
619
|
+
eventOrder: settings.eventOrder,
|
|
620
|
+
getOccurrences: settings.getOccurrences,
|
|
621
|
+
})
|
|
622
|
+
indexCache = {
|
|
623
|
+
events: state.events,
|
|
624
|
+
rangeKey,
|
|
625
|
+
timeZone: settings.timeZone,
|
|
626
|
+
weekStartsOn: settings.weekStartsOn,
|
|
627
|
+
eventOrder: settings.eventOrder,
|
|
628
|
+
getOccurrences: settings.getOccurrences,
|
|
629
|
+
index,
|
|
630
|
+
}
|
|
631
|
+
return index
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const api: EventCalendarApi<TData> = {
|
|
635
|
+
next() {
|
|
636
|
+
const state = getState()
|
|
637
|
+
setField(
|
|
638
|
+
"date",
|
|
639
|
+
stepDate(state.view, state.date, 1, {
|
|
640
|
+
timeZone: settings.timeZone,
|
|
641
|
+
dayCount: state.dayCount,
|
|
642
|
+
agendaDayCount: settings.agendaDayCount,
|
|
643
|
+
})
|
|
644
|
+
)
|
|
645
|
+
},
|
|
646
|
+
prev() {
|
|
647
|
+
const state = getState()
|
|
648
|
+
setField(
|
|
649
|
+
"date",
|
|
650
|
+
stepDate(state.view, state.date, -1, {
|
|
651
|
+
timeZone: settings.timeZone,
|
|
652
|
+
dayCount: state.dayCount,
|
|
653
|
+
agendaDayCount: settings.agendaDayCount,
|
|
654
|
+
})
|
|
655
|
+
)
|
|
656
|
+
},
|
|
657
|
+
today() {
|
|
658
|
+
setField("date", new Date())
|
|
659
|
+
},
|
|
660
|
+
goTo(date) {
|
|
661
|
+
setField("date", date)
|
|
662
|
+
},
|
|
663
|
+
setView(view, opts) {
|
|
664
|
+
if (opts?.dayCount !== undefined) {
|
|
665
|
+
setField("dayCount", Math.max(1, opts.dayCount))
|
|
666
|
+
}
|
|
667
|
+
setField("view", resolveView(view))
|
|
668
|
+
},
|
|
669
|
+
setDayCount(count) {
|
|
670
|
+
setField("dayCount", Math.max(1, count))
|
|
671
|
+
},
|
|
672
|
+
getEvents() {
|
|
673
|
+
return getState().events
|
|
674
|
+
},
|
|
675
|
+
getEvent(id) {
|
|
676
|
+
return getState().events.find((event) => event.id === id)
|
|
677
|
+
},
|
|
678
|
+
setEvents(events) {
|
|
679
|
+
setField("events", events)
|
|
680
|
+
},
|
|
681
|
+
addEvent(event) {
|
|
682
|
+
setField("events", [...getState().events, event])
|
|
683
|
+
},
|
|
684
|
+
updateEvent(id, patch) {
|
|
685
|
+
const event = api.getEvent(id)
|
|
686
|
+
if (!event) return
|
|
687
|
+
const merged = { ...event, ...patch }
|
|
688
|
+
const timingChanged =
|
|
689
|
+
patch.start !== undefined ||
|
|
690
|
+
patch.end !== undefined ||
|
|
691
|
+
patch.allDay !== undefined
|
|
692
|
+
if (timingChanged && settings.onEventUpdate) {
|
|
693
|
+
// single write: the non-timing rest rides along as extraPatch so
|
|
694
|
+
// controlled mode sees one consistent onEventsChange payload
|
|
695
|
+
const rest = { ...patch }
|
|
696
|
+
delete rest.start
|
|
697
|
+
delete rest.end
|
|
698
|
+
delete rest.allDay
|
|
699
|
+
applyProposedUpdate(
|
|
700
|
+
{
|
|
701
|
+
event: merged,
|
|
702
|
+
occurrence: null,
|
|
703
|
+
start: merged.start,
|
|
704
|
+
end: merged.end,
|
|
705
|
+
allDay: merged.allDay ?? false,
|
|
706
|
+
source: "api",
|
|
707
|
+
},
|
|
708
|
+
rest
|
|
709
|
+
)
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
if (timingChanged) {
|
|
713
|
+
remapSelectionKey(
|
|
714
|
+
id,
|
|
715
|
+
`${id}::${event.start.toISOString()}`,
|
|
716
|
+
merged.start
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
setField(
|
|
720
|
+
"events",
|
|
721
|
+
getState().events.map((e) => (e.id === id ? merged : e))
|
|
722
|
+
)
|
|
723
|
+
},
|
|
724
|
+
removeEvent(id) {
|
|
725
|
+
setField(
|
|
726
|
+
"events",
|
|
727
|
+
getState().events.filter((event) => event.id !== id)
|
|
728
|
+
)
|
|
729
|
+
},
|
|
730
|
+
getOccurrences(range) {
|
|
731
|
+
if (!range) return getIndex().occurrences
|
|
732
|
+
const state = getState()
|
|
733
|
+
const within =
|
|
734
|
+
range.start >= state.visibleRange.start &&
|
|
735
|
+
range.end <= state.visibleRange.end
|
|
736
|
+
if (within) {
|
|
737
|
+
return getIndex().occurrences.filter((occ) => eventsOverlap(occ, range))
|
|
738
|
+
}
|
|
739
|
+
const rangeKey = getRangeKey(range)
|
|
740
|
+
if (
|
|
741
|
+
rangeCache &&
|
|
742
|
+
rangeCache.events === state.events &&
|
|
743
|
+
rangeCache.rangeKey === rangeKey &&
|
|
744
|
+
rangeCache.timeZone === settings.timeZone &&
|
|
745
|
+
rangeCache.weekStartsOn === settings.weekStartsOn &&
|
|
746
|
+
rangeCache.eventOrder === settings.eventOrder &&
|
|
747
|
+
rangeCache.getOccurrences === settings.getOccurrences
|
|
748
|
+
) {
|
|
749
|
+
return rangeCache.occurrences
|
|
750
|
+
}
|
|
751
|
+
const { occurrences } = buildEventIndex(state.events, range, {
|
|
752
|
+
timeZone: settings.timeZone,
|
|
753
|
+
weekStartsOn: settings.weekStartsOn,
|
|
754
|
+
eventOrder: settings.eventOrder,
|
|
755
|
+
getOccurrences: settings.getOccurrences,
|
|
756
|
+
})
|
|
757
|
+
rangeCache = {
|
|
758
|
+
events: state.events,
|
|
759
|
+
rangeKey,
|
|
760
|
+
timeZone: settings.timeZone,
|
|
761
|
+
weekStartsOn: settings.weekStartsOn,
|
|
762
|
+
eventOrder: settings.eventOrder,
|
|
763
|
+
getOccurrences: settings.getOccurrences,
|
|
764
|
+
occurrences,
|
|
765
|
+
}
|
|
766
|
+
return occurrences
|
|
767
|
+
},
|
|
768
|
+
getOccurrencesForDay(day) {
|
|
769
|
+
const bucket = getIndex().byDay.get(getDayKey(day, settings.timeZone))
|
|
770
|
+
if (!bucket) return []
|
|
771
|
+
const seen = new Set<string>()
|
|
772
|
+
const result: EventCalendarOccurrence<TData>[] = []
|
|
773
|
+
for (const seg of [...bucket.allDay, ...bucket.timed]) {
|
|
774
|
+
if (seen.has(seg.occurrence.key)) continue
|
|
775
|
+
seen.add(seg.occurrence.key)
|
|
776
|
+
result.push(seg.occurrence)
|
|
777
|
+
}
|
|
778
|
+
return result
|
|
779
|
+
},
|
|
780
|
+
findOverlapping({ start, end, excludeEventId }) {
|
|
781
|
+
return api
|
|
782
|
+
.getOccurrences({ start, end })
|
|
783
|
+
.filter((occ) => occ.eventId !== excludeEventId)
|
|
784
|
+
},
|
|
785
|
+
select(partial) {
|
|
786
|
+
const current = getState().selection
|
|
787
|
+
setField("selection", {
|
|
788
|
+
eventKeys: partial.eventKeys ?? current.eventKeys,
|
|
789
|
+
slot: partial.slot !== undefined ? partial.slot : current.slot,
|
|
790
|
+
})
|
|
791
|
+
},
|
|
792
|
+
selectEvent(key, opts) {
|
|
793
|
+
const current = getState().selection
|
|
794
|
+
const eventKeys = opts?.additive
|
|
795
|
+
? current.eventKeys.includes(key)
|
|
796
|
+
? current.eventKeys.filter((k) => k !== key)
|
|
797
|
+
: [...current.eventKeys, key]
|
|
798
|
+
: [key]
|
|
799
|
+
setField("selection", { ...current, eventKeys })
|
|
800
|
+
},
|
|
801
|
+
clearSelection() {
|
|
802
|
+
setField("selection", EMPTY_SELECTION)
|
|
803
|
+
},
|
|
804
|
+
setInteractions(patch) {
|
|
805
|
+
setField("interactions", { ...getState().interactions, ...patch })
|
|
806
|
+
},
|
|
807
|
+
setViewSettings(patch) {
|
|
808
|
+
setField("viewSettings", { ...getState().viewSettings, ...patch })
|
|
809
|
+
},
|
|
810
|
+
getVisibleRange() {
|
|
811
|
+
return getState().visibleRange
|
|
812
|
+
},
|
|
813
|
+
getActiveRange() {
|
|
814
|
+
return getState().activeRange
|
|
815
|
+
},
|
|
816
|
+
toZoned(date) {
|
|
817
|
+
return toZoned(date, settings.timeZone)
|
|
818
|
+
},
|
|
819
|
+
scrollToTime(time) {
|
|
820
|
+
scrollHandler?.(time)
|
|
821
|
+
},
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const internals: EventCalendarInternals<TData> = {
|
|
825
|
+
getIndex,
|
|
826
|
+
setDrag(drag) {
|
|
827
|
+
internal.drag = drag
|
|
828
|
+
invalidate()
|
|
829
|
+
notify()
|
|
830
|
+
},
|
|
831
|
+
setSlotDraft(draft) {
|
|
832
|
+
internal.slotDraft = draft
|
|
833
|
+
invalidate()
|
|
834
|
+
notify()
|
|
835
|
+
},
|
|
836
|
+
registerScrollHandler(handler) {
|
|
837
|
+
scrollHandler = handler
|
|
838
|
+
},
|
|
839
|
+
applyProposedUpdate,
|
|
840
|
+
getSettingsVersion() {
|
|
841
|
+
return settingsVersion
|
|
842
|
+
},
|
|
843
|
+
getRootEl() {
|
|
844
|
+
return rootEl
|
|
845
|
+
},
|
|
846
|
+
setRootEl(el) {
|
|
847
|
+
rootEl = el
|
|
848
|
+
},
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const instance: EventCalendarInstance<TData> = {
|
|
852
|
+
getState,
|
|
853
|
+
subscribe(listener) {
|
|
854
|
+
listeners.add(listener)
|
|
855
|
+
return () => listeners.delete(listener)
|
|
856
|
+
},
|
|
857
|
+
api,
|
|
858
|
+
get settings() {
|
|
859
|
+
return settings
|
|
860
|
+
},
|
|
861
|
+
internals,
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const STATE_KEYS = [
|
|
865
|
+
"events",
|
|
866
|
+
"view",
|
|
867
|
+
"date",
|
|
868
|
+
"dayCount",
|
|
869
|
+
"selection",
|
|
870
|
+
"interactions",
|
|
871
|
+
"viewSettings",
|
|
872
|
+
"loading",
|
|
873
|
+
] as const
|
|
874
|
+
const SETTINGS_KEYS = [
|
|
875
|
+
"timeZone",
|
|
876
|
+
"locale",
|
|
877
|
+
"weekStartsOn",
|
|
878
|
+
"views",
|
|
879
|
+
"dayStartHour",
|
|
880
|
+
"dayEndHour",
|
|
881
|
+
"slotDuration",
|
|
882
|
+
"snapDuration",
|
|
883
|
+
"agendaDayCount",
|
|
884
|
+
"fixedWeeks",
|
|
885
|
+
"showOutsideDays",
|
|
886
|
+
"i18n",
|
|
887
|
+
"resources",
|
|
888
|
+
"getEventPriority",
|
|
889
|
+
"eventOrder",
|
|
890
|
+
"getOccurrences",
|
|
891
|
+
"weekendDays",
|
|
892
|
+
"activation",
|
|
893
|
+
] as const
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
instance,
|
|
897
|
+
setOptions(next) {
|
|
898
|
+
const prev = options
|
|
899
|
+
options = next
|
|
900
|
+
let changed = false
|
|
901
|
+
for (const key of STATE_KEYS) {
|
|
902
|
+
if (prev[key] !== next[key]) {
|
|
903
|
+
changed = true
|
|
904
|
+
break
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
let settingsChanged = false
|
|
908
|
+
for (const key of SETTINGS_KEYS) {
|
|
909
|
+
if (prev[key] !== next[key]) {
|
|
910
|
+
settingsChanged = true
|
|
911
|
+
break
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
settings = resolveSettings(next)
|
|
915
|
+
if (settingsChanged) {
|
|
916
|
+
settingsVersion++
|
|
917
|
+
changed = true
|
|
918
|
+
}
|
|
919
|
+
if (changed) invalidate()
|
|
920
|
+
return changed
|
|
921
|
+
},
|
|
922
|
+
notify,
|
|
923
|
+
emitRangeIfChanged,
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Headless root hook - the full calendar engine without any markup.
|
|
929
|
+
* Pass the returned instance to <EventCalendar calendar={instance}> or drive
|
|
930
|
+
* fully custom UI from instance.getState()/subscribe/api.
|
|
931
|
+
*/
|
|
932
|
+
function useEventCalendarState<TData = unknown>(
|
|
933
|
+
options: UseEventCalendarStateOptions<TData> = {}
|
|
934
|
+
): EventCalendarInstance<TData> {
|
|
935
|
+
const [store] = useState(() => createEventCalendarStore<TData>(options))
|
|
936
|
+
const changed = store.setOptions(options)
|
|
937
|
+
const changedRef = useRef(false)
|
|
938
|
+
if (changed) changedRef.current = true
|
|
939
|
+
useLayoutEffect(() => {
|
|
940
|
+
if (changedRef.current) {
|
|
941
|
+
changedRef.current = false
|
|
942
|
+
store.notify()
|
|
943
|
+
}
|
|
944
|
+
})
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
store.emitRangeIfChanged()
|
|
947
|
+
// mount-only: onRangeChange fires once for the initial range
|
|
948
|
+
}, [])
|
|
949
|
+
return store.instance
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const EventCalendarContext =
|
|
953
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
954
|
+
createContext<EventCalendarInstance<any> | null>(null)
|
|
955
|
+
|
|
956
|
+
/** The stable calendar instance; throws outside <EventCalendar>. */
|
|
957
|
+
function useEventCalendar<TData = unknown>(): EventCalendarInstance<TData> {
|
|
958
|
+
const instance = useContext(EventCalendarContext)
|
|
959
|
+
if (!instance) {
|
|
960
|
+
throw new Error("useEventCalendar must be used within <EventCalendar>")
|
|
961
|
+
}
|
|
962
|
+
return instance as EventCalendarInstance<TData>
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
interface UseEventCalendarSelectorOptions<TData, TSelected> {
|
|
966
|
+
calendar?: EventCalendarInstance<TData>
|
|
967
|
+
isEqual?: (a: TSelected, b: TSelected) => boolean
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Fine-grained subscription with equality memoization (Object.is default). */
|
|
971
|
+
function useEventCalendarSelector<TData = unknown, TSelected = unknown>(
|
|
972
|
+
selector: (state: EventCalendarState<TData>) => TSelected,
|
|
973
|
+
options?: UseEventCalendarSelectorOptions<TData, TSelected>
|
|
974
|
+
): TSelected {
|
|
975
|
+
const contextInstance = useContext(EventCalendarContext)
|
|
976
|
+
const instance = options?.calendar ?? contextInstance
|
|
977
|
+
if (!instance) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
"useEventCalendarSelector needs an <EventCalendar> ancestor or an explicit `calendar` option"
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
const isEqual = options?.isEqual ?? Object.is
|
|
983
|
+
const lastRef = useRef<{ value: TSelected } | null>(null)
|
|
984
|
+
const selectorRef = useRef(selector)
|
|
985
|
+
selectorRef.current = selector
|
|
986
|
+
|
|
987
|
+
const getSnapshot = () => {
|
|
988
|
+
const next = selectorRef.current(
|
|
989
|
+
instance.getState() as EventCalendarState<TData>
|
|
990
|
+
)
|
|
991
|
+
if (lastRef.current && isEqual(lastRef.current.value, next)) {
|
|
992
|
+
return lastRef.current.value
|
|
993
|
+
}
|
|
994
|
+
lastRef.current = { value: next }
|
|
995
|
+
return next
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return useSyncExternalStore(instance.subscribe, getSnapshot, getSnapshot)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function useEventCalendarView(): {
|
|
1002
|
+
view: CalendarView
|
|
1003
|
+
dayCount: number
|
|
1004
|
+
/** The resolved `views` option from settings. */
|
|
1005
|
+
availableViews: CalendarView[]
|
|
1006
|
+
setView: (view: CalendarView, opts?: { dayCount?: number }) => void
|
|
1007
|
+
} {
|
|
1008
|
+
const instance = useEventCalendar()
|
|
1009
|
+
const view = useEventCalendarSelector((state) => state.view)
|
|
1010
|
+
const dayCount = useEventCalendarSelector((state) => state.dayCount)
|
|
1011
|
+
useEventCalendarSettingsVersion(instance)
|
|
1012
|
+
return {
|
|
1013
|
+
view,
|
|
1014
|
+
dayCount,
|
|
1015
|
+
availableViews: instance.settings.views,
|
|
1016
|
+
setView: instance.api.setView,
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* isToday is a wall-clock read, so no store write ever invalidates it and a
|
|
1022
|
+
* calendar left open overnight keeps highlighting yesterday. One timer per
|
|
1023
|
+
* display zone, armed for the next zoned midnight and re-armed on fire, wakes
|
|
1024
|
+
* every day-scoped hook exactly when the answer changes; an interval would
|
|
1025
|
+
* tick thousands of times a day to catch one transition.
|
|
1026
|
+
*/
|
|
1027
|
+
const midnightTicker = (() => {
|
|
1028
|
+
const zones = new Map<
|
|
1029
|
+
string,
|
|
1030
|
+
{ listeners: Set<() => void>; timer: ReturnType<typeof setTimeout> | null }
|
|
1031
|
+
>()
|
|
1032
|
+
|
|
1033
|
+
const arm = (timeZone: string) => {
|
|
1034
|
+
const entry = zones.get(timeZone)
|
|
1035
|
+
if (!entry) return
|
|
1036
|
+
const now = new Date()
|
|
1037
|
+
const next = zonedStartOfDay(
|
|
1038
|
+
addDays(toZoned(now, timeZone), 1),
|
|
1039
|
+
timeZone
|
|
1040
|
+
).getTime()
|
|
1041
|
+
entry.timer = setTimeout(
|
|
1042
|
+
() => {
|
|
1043
|
+
entry.listeners.forEach((listener) => listener())
|
|
1044
|
+
arm(timeZone)
|
|
1045
|
+
},
|
|
1046
|
+
Math.max(1000, next - now.getTime())
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return {
|
|
1051
|
+
subscribe(timeZone: string, listener: () => void) {
|
|
1052
|
+
let entry = zones.get(timeZone)
|
|
1053
|
+
if (!entry) {
|
|
1054
|
+
entry = { listeners: new Set(), timer: null }
|
|
1055
|
+
zones.set(timeZone, entry)
|
|
1056
|
+
entry.listeners.add(listener)
|
|
1057
|
+
arm(timeZone)
|
|
1058
|
+
} else {
|
|
1059
|
+
entry.listeners.add(listener)
|
|
1060
|
+
}
|
|
1061
|
+
const current = entry
|
|
1062
|
+
return () => {
|
|
1063
|
+
current.listeners.delete(listener)
|
|
1064
|
+
if (current.listeners.size > 0) return
|
|
1065
|
+
if (current.timer) clearTimeout(current.timer)
|
|
1066
|
+
zones.delete(timeZone)
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
}
|
|
1070
|
+
})()
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Today's day key in `timeZone`, re-rendering the caller at the next midnight.
|
|
1074
|
+
* The clock is read in the snapshot, never in render: React Compiler caches a
|
|
1075
|
+
* render-time `new Date()`, so the wake-up re-rendered yesterday's answer.
|
|
1076
|
+
*/
|
|
1077
|
+
function useTodayKey(timeZone: string): string {
|
|
1078
|
+
const subscribe = useCallback(
|
|
1079
|
+
(listener: () => void) => midnightTicker.subscribe(timeZone, listener),
|
|
1080
|
+
[timeZone]
|
|
1081
|
+
)
|
|
1082
|
+
const getSnapshot = useCallback(
|
|
1083
|
+
() => getDayKey(new Date(), timeZone),
|
|
1084
|
+
[timeZone]
|
|
1085
|
+
)
|
|
1086
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function useEventCalendarNavigation(): {
|
|
1090
|
+
date: Date
|
|
1091
|
+
/** i18n.functions.formatTitle output for the current view. */
|
|
1092
|
+
title: string
|
|
1093
|
+
visibleRange: EventCalendarDateRange
|
|
1094
|
+
activeRange: EventCalendarDateRange
|
|
1095
|
+
next: () => void
|
|
1096
|
+
prev: () => void
|
|
1097
|
+
today: () => void
|
|
1098
|
+
goTo: (date: Date) => void
|
|
1099
|
+
/** True when the anchor period contains now in the display time zone. */
|
|
1100
|
+
isToday: boolean
|
|
1101
|
+
} {
|
|
1102
|
+
const instance = useEventCalendar()
|
|
1103
|
+
const { settings } = instance
|
|
1104
|
+
const slice = useEventCalendarSelector(
|
|
1105
|
+
(state) => ({
|
|
1106
|
+
date: state.date,
|
|
1107
|
+
view: state.view,
|
|
1108
|
+
visibleRange: state.visibleRange,
|
|
1109
|
+
activeRange: state.activeRange,
|
|
1110
|
+
}),
|
|
1111
|
+
{
|
|
1112
|
+
isEqual: (a, b) =>
|
|
1113
|
+
a.date.getTime() === b.date.getTime() &&
|
|
1114
|
+
a.view === b.view &&
|
|
1115
|
+
getRangeKey(a.visibleRange) === getRangeKey(b.visibleRange),
|
|
1116
|
+
}
|
|
1117
|
+
)
|
|
1118
|
+
useEventCalendarSettingsVersion(instance)
|
|
1119
|
+
// The key only wakes this hook at midnight. The range test reads the clock
|
|
1120
|
+
// in a selector, which runs on every render whether compiled or not.
|
|
1121
|
+
useTodayKey(settings.timeZone)
|
|
1122
|
+
const isToday = useEventCalendarSelector((state) => {
|
|
1123
|
+
const now = new Date()
|
|
1124
|
+
return now >= state.activeRange.start && now < state.activeRange.end
|
|
1125
|
+
})
|
|
1126
|
+
return {
|
|
1127
|
+
date: slice.date,
|
|
1128
|
+
title: settings.i18n.functions.formatTitle(slice.view, {
|
|
1129
|
+
date: toZoned(slice.date, settings.timeZone),
|
|
1130
|
+
activeRange: slice.activeRange,
|
|
1131
|
+
visibleRange: slice.visibleRange,
|
|
1132
|
+
locale: settings.locale,
|
|
1133
|
+
}),
|
|
1134
|
+
visibleRange: slice.visibleRange,
|
|
1135
|
+
activeRange: slice.activeRange,
|
|
1136
|
+
next: instance.api.next,
|
|
1137
|
+
prev: instance.api.prev,
|
|
1138
|
+
today: instance.api.today,
|
|
1139
|
+
goTo: instance.api.goTo,
|
|
1140
|
+
isToday,
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function useEventCalendarSelection(): {
|
|
1145
|
+
selection: EventCalendarSelection
|
|
1146
|
+
select: (selection: Partial<EventCalendarSelection>) => void
|
|
1147
|
+
selectEvent: (key: string, opts?: { additive?: boolean }) => void
|
|
1148
|
+
clearSelection: () => void
|
|
1149
|
+
} {
|
|
1150
|
+
const instance = useEventCalendar()
|
|
1151
|
+
const selection = useEventCalendarSelector((state) => state.selection)
|
|
1152
|
+
return {
|
|
1153
|
+
selection,
|
|
1154
|
+
select: instance.api.select,
|
|
1155
|
+
selectEvent: instance.api.selectEvent,
|
|
1156
|
+
clearSelection: instance.api.clearSelection,
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function useEventCalendarInteractions(): {
|
|
1161
|
+
interactions: EventCalendarInteractions
|
|
1162
|
+
setInteractions: (patch: Partial<EventCalendarInteractions>) => void
|
|
1163
|
+
} {
|
|
1164
|
+
const instance = useEventCalendar()
|
|
1165
|
+
const interactions = useEventCalendarSelector((state) => state.interactions)
|
|
1166
|
+
return { interactions, setInteractions: instance.api.setInteractions }
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Expanded, sorted occurrences; defaults to the visible range. */
|
|
1170
|
+
function useEventCalendarOccurrences<TData = unknown>(
|
|
1171
|
+
range?: EventCalendarDateRange
|
|
1172
|
+
): EventCalendarOccurrence<TData>[] {
|
|
1173
|
+
const instance = useEventCalendar<TData>()
|
|
1174
|
+
return useEventCalendarSelector<TData, EventCalendarOccurrence<TData>[]>(
|
|
1175
|
+
() => instance.api.getOccurrences(range),
|
|
1176
|
+
{
|
|
1177
|
+
calendar: instance,
|
|
1178
|
+
// element identity, not keys: keys encode id+start only, so an end-only
|
|
1179
|
+
// resize or a title/color/data edit would never invalidate a key-based
|
|
1180
|
+
// memo. The index rebuilds occurrence objects precisely when events
|
|
1181
|
+
// change, so identity is the correct (and cheapest) change signal.
|
|
1182
|
+
isEqual: (a, b) =>
|
|
1183
|
+
a === b || (a.length === b.length && a.every((occ, i) => occ === b[i])),
|
|
1184
|
+
}
|
|
1185
|
+
)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const EMPTY_BUCKET: EventCalendarDayBucket = { allDay: [], timed: [] }
|
|
1189
|
+
|
|
1190
|
+
/** Per-cell subscription: only cells whose segments changed re-render. */
|
|
1191
|
+
function useEventCalendarDay<TData = unknown>(
|
|
1192
|
+
day: Date
|
|
1193
|
+
): {
|
|
1194
|
+
segments: EventCalendarDayBucket<TData>
|
|
1195
|
+
isToday: boolean
|
|
1196
|
+
isOutside: boolean
|
|
1197
|
+
} {
|
|
1198
|
+
const instance = useEventCalendar<TData>()
|
|
1199
|
+
const { timeZone } = instance.settings
|
|
1200
|
+
const dayKey = getDayKey(day, timeZone)
|
|
1201
|
+
|
|
1202
|
+
const bucket = useEventCalendarSelector<TData, EventCalendarDayBucket<TData>>(
|
|
1203
|
+
() =>
|
|
1204
|
+
instance.internals.getIndex().byDay.get(dayKey) ??
|
|
1205
|
+
(EMPTY_BUCKET as EventCalendarDayBucket<TData>),
|
|
1206
|
+
{
|
|
1207
|
+
calendar: instance,
|
|
1208
|
+
// Element identity, not a content key: an occurrence key encodes
|
|
1209
|
+
// id+start only, so a title/color/data edit produced an identical key
|
|
1210
|
+
// and this cell kept serving its STALE segments. Buckets come straight
|
|
1211
|
+
// out of the memoized index, which rebuilds precisely when events
|
|
1212
|
+
// change, so identity is both correct and the cheapest signal.
|
|
1213
|
+
isEqual: (a, b) =>
|
|
1214
|
+
a === b ||
|
|
1215
|
+
(a.allDay.length === b.allDay.length &&
|
|
1216
|
+
a.timed.length === b.timed.length &&
|
|
1217
|
+
a.allDay.every((segment, i) => segment === b.allDay[i]) &&
|
|
1218
|
+
a.timed.every((segment, i) => segment === b.timed[i])),
|
|
1219
|
+
}
|
|
1220
|
+
)
|
|
1221
|
+
const activeRange = useEventCalendarSelector<TData, EventCalendarDateRange>(
|
|
1222
|
+
(state) => state.activeRange,
|
|
1223
|
+
{
|
|
1224
|
+
calendar: instance,
|
|
1225
|
+
isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),
|
|
1226
|
+
}
|
|
1227
|
+
)
|
|
1228
|
+
const todayKey = useTodayKey(timeZone)
|
|
1229
|
+
const dayStart = zonedStartOfDay(day, timeZone)
|
|
1230
|
+
return {
|
|
1231
|
+
segments: bucket,
|
|
1232
|
+
isToday: todayKey === dayKey,
|
|
1233
|
+
isOutside: dayStart < activeRange.start || dayStart >= activeRange.end,
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const EMPTY_BARS: EventCalendarSegment[] = []
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Per-week-row subscription for the month view: the laned multi-day/all-day
|
|
1241
|
+
* bar segments (one per occurrence per row, colStart/colSpan/lane set) that
|
|
1242
|
+
* render as continuous cross-day bars. `laneCount` is the row's bar height.
|
|
1243
|
+
* Matched by CONTAINMENT - any day inside the row resolves it - so a
|
|
1244
|
+
* weekends-hidden month (first visible day Monday) still finds its row;
|
|
1245
|
+
* `rowStart` returns the row's TRUE start for colStart/colSpan day math.
|
|
1246
|
+
*/
|
|
1247
|
+
function useEventCalendarWeek<TData = unknown>(
|
|
1248
|
+
day: Date
|
|
1249
|
+
): {
|
|
1250
|
+
bars: EventCalendarSegment<TData>[]
|
|
1251
|
+
laneCount: number
|
|
1252
|
+
rowStart: Date | null
|
|
1253
|
+
} {
|
|
1254
|
+
const instance = useEventCalendar<TData>()
|
|
1255
|
+
const { timeZone } = instance.settings
|
|
1256
|
+
const dayStartMs = zonedStartOfDay(day, timeZone).getTime()
|
|
1257
|
+
|
|
1258
|
+
const row = useEventCalendarSelector<
|
|
1259
|
+
TData,
|
|
1260
|
+
{ bars: EventCalendarSegment<TData>[]; rowStart: Date | null }
|
|
1261
|
+
>(
|
|
1262
|
+
() => {
|
|
1263
|
+
const index = instance.internals.getIndex()
|
|
1264
|
+
const match = index.weekRows.find((r) => {
|
|
1265
|
+
const startMs = zonedStartOfDay(r.rowStart, timeZone).getTime()
|
|
1266
|
+
// calendar-aware row end: a fixed 168h window would let the first
|
|
1267
|
+
// day AFTER a spring-forward week (167h long) match the wrong row
|
|
1268
|
+
const endMs = zonedStartOfDay(
|
|
1269
|
+
addDays(toZoned(r.rowStart, timeZone), 7),
|
|
1270
|
+
timeZone
|
|
1271
|
+
).getTime()
|
|
1272
|
+
return dayStartMs >= startMs && dayStartMs < endMs
|
|
1273
|
+
})
|
|
1274
|
+
return {
|
|
1275
|
+
bars: match?.bars ?? (EMPTY_BARS as EventCalendarSegment<TData>[]),
|
|
1276
|
+
rowStart: match?.rowStart ?? null,
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
calendar: instance,
|
|
1281
|
+
// Same reasoning as the day bucket above: `bars` is the array held by
|
|
1282
|
+
// the memoized index, so element identity catches content edits that a
|
|
1283
|
+
// key built from id+start could never see.
|
|
1284
|
+
isEqual: (a, b) =>
|
|
1285
|
+
(a.rowStart?.getTime() ?? 0) === (b.rowStart?.getTime() ?? 0) &&
|
|
1286
|
+
(a.bars === b.bars ||
|
|
1287
|
+
(a.bars.length === b.bars.length &&
|
|
1288
|
+
a.bars.every((segment, i) => segment === b.bars[i]))),
|
|
1289
|
+
}
|
|
1290
|
+
)
|
|
1291
|
+
const laneCount = row.bars.reduce((m, s) => Math.max(m, (s.lane ?? 0) + 1), 0)
|
|
1292
|
+
return { bars: row.bars, laneCount, rowStart: row.rowStart }
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* User view settings (weekends, week numbers, now line, off days, schedule
|
|
1297
|
+
* hint) + the effective values after falling back to the root view-config
|
|
1298
|
+
* props. Drives the nav submenu; fully controllable from outside via
|
|
1299
|
+
* `viewSettings`/`onViewSettingsChange` or api.setViewSettings.
|
|
1300
|
+
*/
|
|
1301
|
+
function useEventCalendarViewSettings(): {
|
|
1302
|
+
viewSettings: EventCalendarViewSettings
|
|
1303
|
+
setViewSettings: (patch: EventCalendarViewSettings) => void
|
|
1304
|
+
effective: Required<EventCalendarViewSettings>
|
|
1305
|
+
} {
|
|
1306
|
+
const instance = useEventCalendar()
|
|
1307
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
1308
|
+
const viewSettings = useEventCalendarSelector((state) => state.viewSettings)
|
|
1309
|
+
return {
|
|
1310
|
+
viewSettings,
|
|
1311
|
+
setViewSettings: instance.api.setViewSettings,
|
|
1312
|
+
effective: {
|
|
1313
|
+
weekends: viewSettings.weekends ?? true,
|
|
1314
|
+
weekNumbers: viewSettings.weekNumbers ?? viewConfig.showWeekNumbers,
|
|
1315
|
+
nowIndicator: viewSettings.nowIndicator ?? viewConfig.nowIndicator,
|
|
1316
|
+
offDays:
|
|
1317
|
+
viewSettings.offDays ??
|
|
1318
|
+
(viewConfig.offDays !== undefined && viewConfig.offDays !== false),
|
|
1319
|
+
},
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** Subscribes to settings changes only (version counter, not state). */
|
|
1324
|
+
function useEventCalendarSettingsVersion<TData>(
|
|
1325
|
+
instance: EventCalendarInstance<TData>
|
|
1326
|
+
): number {
|
|
1327
|
+
return useSyncExternalStore(
|
|
1328
|
+
instance.subscribe,
|
|
1329
|
+
instance.internals.getSettingsVersion,
|
|
1330
|
+
instance.internals.getSettingsVersion
|
|
1331
|
+
)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** Resolved settings incl. merged i18n; re-renders only when settings change. */
|
|
1335
|
+
function useEventCalendarSettings<
|
|
1336
|
+
TData = unknown,
|
|
1337
|
+
>(): EventCalendarSettings<TData> {
|
|
1338
|
+
const instance = useEventCalendar<TData>()
|
|
1339
|
+
useEventCalendarSettingsVersion(instance)
|
|
1340
|
+
return instance.settings
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const EventCalendarViewContext = createContext<{ view: CalendarView } | null>(
|
|
1344
|
+
null
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Per-element class hooks, cn()-merged AFTER the built-in classes (so tailwind
|
|
1349
|
+
* variants and ! overrides win). Metric CSS variables can ride on any parent
|
|
1350
|
+
* key, e.g. classNames.timeGrid: "[--ec-gutter-width:4.5rem]".
|
|
1351
|
+
*/
|
|
1352
|
+
interface EventCalendarClassNames {
|
|
1353
|
+
nav?: string
|
|
1354
|
+
toolbar?: string
|
|
1355
|
+
content?: string
|
|
1356
|
+
monthView?: string
|
|
1357
|
+
monthCell?: string
|
|
1358
|
+
timeGrid?: string
|
|
1359
|
+
timeGutter?: string
|
|
1360
|
+
dayColumn?: string
|
|
1361
|
+
allDaySection?: string
|
|
1362
|
+
agendaView?: string
|
|
1363
|
+
event?: string
|
|
1364
|
+
/** The styled hover tooltip popup (viewConfig.eventTooltip). */
|
|
1365
|
+
eventTooltip?: string
|
|
1366
|
+
moreIndicator?: string
|
|
1367
|
+
/** The "+N more" popover panel. Control the on-demand scroll cap through
|
|
1368
|
+
* the CSS variable, e.g. "[--ec-more-max-height:20rem]". */
|
|
1369
|
+
morePopover?: string
|
|
1370
|
+
/** "+N more" popover day header row. */
|
|
1371
|
+
morePopoverHeader?: string
|
|
1372
|
+
// nav family (reachable without recomposing the default nav)
|
|
1373
|
+
navButton?: string
|
|
1374
|
+
title?: string
|
|
1375
|
+
navTooltip?: string
|
|
1376
|
+
viewSwitcherContent?: string
|
|
1377
|
+
viewSwitcherLabel?: string
|
|
1378
|
+
viewShortcut?: string
|
|
1379
|
+
datePickerContent?: string
|
|
1380
|
+
// month view
|
|
1381
|
+
monthHeader?: string
|
|
1382
|
+
monthDayHeader?: string
|
|
1383
|
+
monthBody?: string
|
|
1384
|
+
monthRow?: string
|
|
1385
|
+
weekNumber?: string
|
|
1386
|
+
monthBarOverlay?: string
|
|
1387
|
+
monthBar?: string
|
|
1388
|
+
monthCellContent?: string
|
|
1389
|
+
monthCellFooter?: string
|
|
1390
|
+
monthDayNumber?: string
|
|
1391
|
+
dayAddButton?: string
|
|
1392
|
+
// time grid / resource
|
|
1393
|
+
timeGridHeader?: string
|
|
1394
|
+
timeGutterLabel?: string
|
|
1395
|
+
allDayLabel?: string
|
|
1396
|
+
allDayCell?: string
|
|
1397
|
+
timedChip?: string
|
|
1398
|
+
resourceHeader?: string
|
|
1399
|
+
// interaction surfaces (shared by every view)
|
|
1400
|
+
dragGhost?: string
|
|
1401
|
+
dragCarry?: string
|
|
1402
|
+
dragCarryInvalid?: string
|
|
1403
|
+
dropHint?: string
|
|
1404
|
+
dropIndicator?: string
|
|
1405
|
+
slotDraft?: string
|
|
1406
|
+
resizeHandle?: string
|
|
1407
|
+
resizeGrip?: string
|
|
1408
|
+
// agenda
|
|
1409
|
+
noEvents?: string
|
|
1410
|
+
agendaDay?: string
|
|
1411
|
+
agendaDayHeader?: string
|
|
1412
|
+
agendaDayGutter?: string
|
|
1413
|
+
agendaDate?: string
|
|
1414
|
+
agendaDayToggle?: string
|
|
1415
|
+
agendaDayContent?: string
|
|
1416
|
+
agendaItem?: string
|
|
1417
|
+
agendaItemSurface?: string
|
|
1418
|
+
agendaItemToggle?: string
|
|
1419
|
+
agendaDaySummary?: string
|
|
1420
|
+
agendaSummaryDot?: string
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
interface EventCalendarRenderEventProps<TData = unknown> {
|
|
1424
|
+
occurrence: EventCalendarOccurrence<TData>
|
|
1425
|
+
segment: EventCalendarSegment<TData>
|
|
1426
|
+
view: CalendarView
|
|
1427
|
+
isDragging: boolean
|
|
1428
|
+
isSelected: boolean
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* View-layer configuration: display props and render overrides. These live on
|
|
1433
|
+
* <EventCalendar> (and per-view components), never in the headless options.
|
|
1434
|
+
*/
|
|
1435
|
+
interface EventCalendarViewConfig<TData = unknown> {
|
|
1436
|
+
scrollToHour: number
|
|
1437
|
+
nowIndicator: boolean
|
|
1438
|
+
/**
|
|
1439
|
+
* Grid interval in minutes for the time-based views (day, week, N-days,
|
|
1440
|
+
* time grid): gutter slots and gridlines follow it. Also accepted as a
|
|
1441
|
+
* prop on each view component.
|
|
1442
|
+
*/
|
|
1443
|
+
interval: number
|
|
1444
|
+
maxEventsPerCell: number | "auto"
|
|
1445
|
+
showWeekNumbers: boolean
|
|
1446
|
+
enableShortcuts: boolean
|
|
1447
|
+
shortcutsScope: "focus-within" | "global"
|
|
1448
|
+
/**
|
|
1449
|
+
* "contained" (default): the calendar fills its container and views scroll
|
|
1450
|
+
* internally. "page": content flows with the document, the page scrolls,
|
|
1451
|
+
* and day headers stick below `--ec-sticky-offset` (default 0px).
|
|
1452
|
+
*/
|
|
1453
|
+
scrollMode: "contained" | "page"
|
|
1454
|
+
/** Stick the default nav to the top while the page scrolls. */
|
|
1455
|
+
stickyNav: boolean
|
|
1456
|
+
/**
|
|
1457
|
+
* Custom per-day indication (light background classes work in both themes,
|
|
1458
|
+
* e.g. "bg-amber-500/10"). Applied to month cells, time-grid day columns,
|
|
1459
|
+
* and all-day cells; content stays readable on top of it.
|
|
1460
|
+
*/
|
|
1461
|
+
dayClassName?: (day: Date) => string | undefined
|
|
1462
|
+
/**
|
|
1463
|
+
* Extra classes for the CURRENT day, appended after the built-in highlight
|
|
1464
|
+
* (primary-tinted background + accent top border) on month cells, time-grid
|
|
1465
|
+
* day columns, and day headers.
|
|
1466
|
+
*/
|
|
1467
|
+
todayClassName?: string
|
|
1468
|
+
/**
|
|
1469
|
+
* Show a hover "+" add affordance on month cells next to the day number.
|
|
1470
|
+
* It fires the same onSlotClick as clicking the day. Calendar-level config
|
|
1471
|
+
* (consistent affordance, wired to the create flow); use renderMonthCell
|
|
1472
|
+
* when a fully custom cell is needed instead.
|
|
1473
|
+
*/
|
|
1474
|
+
showDayAddButton: boolean
|
|
1475
|
+
/**
|
|
1476
|
+
* Scroll implementation for every internally scrolling surface (time grid,
|
|
1477
|
+
* agenda, time-grid resources, month "+N more" popover):
|
|
1478
|
+
* "custom" (default, shadcn ScrollArea) or "native" (browser scrollbars
|
|
1479
|
+
* via overflow auto).
|
|
1480
|
+
*/
|
|
1481
|
+
scrollbars: "custom" | "native"
|
|
1482
|
+
/** Nav button variant; all nav buttons follow it. Default "ghost". */
|
|
1483
|
+
navButtonVariant: "ghost" | "outline" | "secondary" | "default"
|
|
1484
|
+
/** Nav button size; icon buttons use the icon twin. Default "sm". */
|
|
1485
|
+
navButtonSize: "sm" | "default"
|
|
1486
|
+
/**
|
|
1487
|
+
* Off-day (non-working day) marking. true = weekends with a muted
|
|
1488
|
+
* background; a config object customizes weekdays, explicit dates, a
|
|
1489
|
+
* predicate, and the marker class. Marked cells carry data-off.
|
|
1490
|
+
*/
|
|
1491
|
+
offDays?: boolean | EventCalendarOffDaysConfig
|
|
1492
|
+
classNames?: EventCalendarClassNames
|
|
1493
|
+
components?: Partial<Record<CalendarView, ComponentType>>
|
|
1494
|
+
renderEvent?: (props: EventCalendarRenderEventProps<TData>) => ReactNode
|
|
1495
|
+
renderAgendaEvent?: (props: EventCalendarRenderEventProps<TData>) => ReactNode
|
|
1496
|
+
/**
|
|
1497
|
+
* Content for the styled hover tooltip (viewConfig.eventTooltip). Return a
|
|
1498
|
+
* falsy value (null / undefined, or the `false` a `cond && <node>` yields)
|
|
1499
|
+
* to fall back to the default label (title + time); `label` itself is
|
|
1500
|
+
* undefined when a consumer i18n.formatEventLabel opts out.
|
|
1501
|
+
*/
|
|
1502
|
+
renderEventTooltip?: (props: {
|
|
1503
|
+
occurrence: EventCalendarOccurrence<TData>
|
|
1504
|
+
segment: EventCalendarSegment<TData>
|
|
1505
|
+
view: CalendarView
|
|
1506
|
+
label: string | undefined
|
|
1507
|
+
}) => ReactNode
|
|
1508
|
+
renderDragPreview?: (props: {
|
|
1509
|
+
drag: EventCalendarDragState<TData>
|
|
1510
|
+
}) => ReactNode
|
|
1511
|
+
renderMonthCell?: (props: {
|
|
1512
|
+
day: Date
|
|
1513
|
+
segments: EventCalendarDayBucket<TData>
|
|
1514
|
+
isToday: boolean
|
|
1515
|
+
isOutside: boolean
|
|
1516
|
+
overflowCount: number
|
|
1517
|
+
defaultContent: ReactNode
|
|
1518
|
+
}) => ReactNode
|
|
1519
|
+
/**
|
|
1520
|
+
* Time-grid business-logic layer, rendered pointer-events-none BEHIND event
|
|
1521
|
+
* segments in each day column. Position overlays with
|
|
1522
|
+
* top/height: calc(var(--ec-hour-height) * minutes / 60).
|
|
1523
|
+
*/
|
|
1524
|
+
renderDayColumnBackground?: (props: {
|
|
1525
|
+
day: Date
|
|
1526
|
+
boundsStartMin: number
|
|
1527
|
+
boundsEndMin: number
|
|
1528
|
+
totalMinutes: number
|
|
1529
|
+
}) => ReactNode
|
|
1530
|
+
renderDayHeader?: (props: {
|
|
1531
|
+
day: Date
|
|
1532
|
+
view: CalendarView
|
|
1533
|
+
isToday: boolean
|
|
1534
|
+
}) => ReactNode
|
|
1535
|
+
renderTimeGutterSlot?: (props: {
|
|
1536
|
+
time: Date
|
|
1537
|
+
hour: number
|
|
1538
|
+
minute: number
|
|
1539
|
+
}) => ReactNode
|
|
1540
|
+
renderAllDaySection?: (props: {
|
|
1541
|
+
days: Date[]
|
|
1542
|
+
segments: EventCalendarSegment<TData>[]
|
|
1543
|
+
}) => ReactNode
|
|
1544
|
+
renderMoreIndicator?: (props: {
|
|
1545
|
+
day: Date
|
|
1546
|
+
count: number
|
|
1547
|
+
segments: EventCalendarSegment<TData>[]
|
|
1548
|
+
}) => ReactNode
|
|
1549
|
+
/**
|
|
1550
|
+
* Replaces the ENTIRE body of the built-in "+N more" popover (header +
|
|
1551
|
+
* chip list) while keeping its trigger and positioning; `close` dismisses
|
|
1552
|
+
* it. For a fully custom surface, return false from onMoreClick instead
|
|
1553
|
+
* and open your own UI.
|
|
1554
|
+
*/
|
|
1555
|
+
renderMoreContent?: (props: {
|
|
1556
|
+
day: Date
|
|
1557
|
+
segments: EventCalendarSegment<TData>[]
|
|
1558
|
+
close: () => void
|
|
1559
|
+
}) => ReactNode
|
|
1560
|
+
/**
|
|
1561
|
+
* Agenda-only expandable details. When it returns a node for an occurrence,
|
|
1562
|
+
* the agenda row gains an expand/collapse toggle revealing the details
|
|
1563
|
+
* below the chip (the calendar itself never knows the consumer's fields).
|
|
1564
|
+
*/
|
|
1565
|
+
renderAgendaEventDetails?: (
|
|
1566
|
+
occurrence: EventCalendarOccurrence<TData>
|
|
1567
|
+
) => ReactNode
|
|
1568
|
+
renderNowIndicator?: (props: { time: Date }) => ReactNode
|
|
1569
|
+
renderNoEvents?: () => ReactNode
|
|
1570
|
+
/** Resource column header cell content; default is resource.title. */
|
|
1571
|
+
renderResourceHeader?: (props: {
|
|
1572
|
+
resource: EventCalendarResource
|
|
1573
|
+
}) => ReactNode
|
|
1574
|
+
/** Agenda date gutter (day badge + weekday + collapse toggle). */
|
|
1575
|
+
renderAgendaDayHeader?: (props: {
|
|
1576
|
+
day: Date
|
|
1577
|
+
collapsed: boolean
|
|
1578
|
+
count: number
|
|
1579
|
+
toggle: () => void
|
|
1580
|
+
defaultContent: ReactNode
|
|
1581
|
+
}) => ReactNode
|
|
1582
|
+
/** Collapsed agenda day summary row content. */
|
|
1583
|
+
renderAgendaDaySummary?: (props: {
|
|
1584
|
+
day: Date
|
|
1585
|
+
occurrences: EventCalendarOccurrence<TData>[]
|
|
1586
|
+
count: number
|
|
1587
|
+
expand: () => void
|
|
1588
|
+
defaultContent: ReactNode
|
|
1589
|
+
}) => ReactNode
|
|
1590
|
+
/**
|
|
1591
|
+
* N-day presets offered by the view switcher when the "days" view is
|
|
1592
|
+
* enabled. @default [5]
|
|
1593
|
+
*/
|
|
1594
|
+
dayCountPresets: number[]
|
|
1595
|
+
/**
|
|
1596
|
+
* Nav tooltips: false disables them all; an object tunes placement and
|
|
1597
|
+
* timings. @default { side: "bottom", delay: 600, closeDelay: 0, timeout: 300 }
|
|
1598
|
+
*/
|
|
1599
|
+
navTooltips?:
|
|
1600
|
+
| false
|
|
1601
|
+
| {
|
|
1602
|
+
side?: "top" | "bottom" | "left" | "right"
|
|
1603
|
+
delay?: number
|
|
1604
|
+
closeDelay?: number
|
|
1605
|
+
timeout?: number
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Styled tooltip on event hover / keyboard focus. `false` (default) keeps
|
|
1609
|
+
* only the native title attribute; `true` shows the standard Tooltip with
|
|
1610
|
+
* the event label; an object also tunes the side and open delay. Content is
|
|
1611
|
+
* overridable with renderEventTooltip. @default false
|
|
1612
|
+
*/
|
|
1613
|
+
eventTooltip?:
|
|
1614
|
+
| boolean
|
|
1615
|
+
| {
|
|
1616
|
+
side?: "top" | "bottom" | "left" | "right"
|
|
1617
|
+
delay?: number
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Timed events shorter than this render the compact single-row chip layout.
|
|
1621
|
+
* @default 45
|
|
1622
|
+
*/
|
|
1623
|
+
compactEventMinutes: number
|
|
1624
|
+
/** "+N more" popover alignment against its trigger. @default "start" */
|
|
1625
|
+
morePopoverAlign: "start" | "center" | "end"
|
|
1626
|
+
/** Now-indicator refresh cadence in milliseconds. @default 30000 */
|
|
1627
|
+
nowIndicatorInterval: number
|
|
1628
|
+
/** Max color dots in a collapsed agenda day summary. @default 6 */
|
|
1629
|
+
agendaSummaryMaxDots: number
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const DEFAULT_VIEW_CONFIG: EventCalendarViewConfig = {
|
|
1633
|
+
scrollToHour: 7,
|
|
1634
|
+
nowIndicator: true,
|
|
1635
|
+
interval: 60,
|
|
1636
|
+
maxEventsPerCell: "auto",
|
|
1637
|
+
showWeekNumbers: false,
|
|
1638
|
+
enableShortcuts: true,
|
|
1639
|
+
shortcutsScope: "focus-within",
|
|
1640
|
+
scrollMode: "contained",
|
|
1641
|
+
stickyNav: false,
|
|
1642
|
+
showDayAddButton: false,
|
|
1643
|
+
scrollbars: "custom",
|
|
1644
|
+
navButtonVariant: "ghost",
|
|
1645
|
+
navButtonSize: "sm",
|
|
1646
|
+
dayCountPresets: [5],
|
|
1647
|
+
eventTooltip: false,
|
|
1648
|
+
compactEventMinutes: 45,
|
|
1649
|
+
morePopoverAlign: "start",
|
|
1650
|
+
nowIndicatorInterval: 30_000,
|
|
1651
|
+
agendaSummaryMaxDots: 6,
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const EventCalendarViewConfigContext = createContext<
|
|
1655
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1656
|
+
EventCalendarViewConfig<any>
|
|
1657
|
+
>(DEFAULT_VIEW_CONFIG)
|
|
1658
|
+
|
|
1659
|
+
/** Root-level display props + render overrides, for view components. */
|
|
1660
|
+
function useEventCalendarViewConfig<
|
|
1661
|
+
TData = unknown,
|
|
1662
|
+
>(): EventCalendarViewConfig<TData> {
|
|
1663
|
+
return useContext(EventCalendarViewConfigContext)
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const VIEW_CONFIG_KEYS: Array<keyof EventCalendarViewConfig> = [
|
|
1667
|
+
"scrollToHour",
|
|
1668
|
+
"nowIndicator",
|
|
1669
|
+
"interval",
|
|
1670
|
+
"maxEventsPerCell",
|
|
1671
|
+
"showWeekNumbers",
|
|
1672
|
+
"enableShortcuts",
|
|
1673
|
+
"shortcutsScope",
|
|
1674
|
+
"scrollMode",
|
|
1675
|
+
"stickyNav",
|
|
1676
|
+
"dayClassName",
|
|
1677
|
+
"todayClassName",
|
|
1678
|
+
"showDayAddButton",
|
|
1679
|
+
"scrollbars",
|
|
1680
|
+
"navButtonVariant",
|
|
1681
|
+
"navButtonSize",
|
|
1682
|
+
"offDays",
|
|
1683
|
+
"classNames",
|
|
1684
|
+
"components",
|
|
1685
|
+
"renderEvent",
|
|
1686
|
+
"renderAgendaEvent",
|
|
1687
|
+
"renderEventTooltip",
|
|
1688
|
+
"renderDragPreview",
|
|
1689
|
+
"renderMonthCell",
|
|
1690
|
+
"renderDayColumnBackground",
|
|
1691
|
+
"renderDayHeader",
|
|
1692
|
+
"renderTimeGutterSlot",
|
|
1693
|
+
"renderAllDaySection",
|
|
1694
|
+
"renderMoreIndicator",
|
|
1695
|
+
"renderMoreContent",
|
|
1696
|
+
"renderNowIndicator",
|
|
1697
|
+
"renderNoEvents",
|
|
1698
|
+
"renderAgendaEventDetails",
|
|
1699
|
+
"renderResourceHeader",
|
|
1700
|
+
"renderAgendaDayHeader",
|
|
1701
|
+
"renderAgendaDaySummary",
|
|
1702
|
+
"dayCountPresets",
|
|
1703
|
+
"navTooltips",
|
|
1704
|
+
"eventTooltip",
|
|
1705
|
+
"compactEventMinutes",
|
|
1706
|
+
"morePopoverAlign",
|
|
1707
|
+
"nowIndicatorInterval",
|
|
1708
|
+
"agendaSummaryMaxDots",
|
|
1709
|
+
]
|
|
1710
|
+
|
|
1711
|
+
/** The rendering view of the nearest view component ("month", "week", ...). */
|
|
1712
|
+
function useEventCalendarViewContext(): { view: CalendarView } {
|
|
1713
|
+
const ctx = useContext(EventCalendarViewContext)
|
|
1714
|
+
if (!ctx) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
"useEventCalendarViewContext must be used inside a calendar view"
|
|
1717
|
+
)
|
|
1718
|
+
}
|
|
1719
|
+
return ctx
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
interface EventCalendarProps<TData = unknown>
|
|
1723
|
+
extends
|
|
1724
|
+
UseEventCalendarStateOptions<TData>,
|
|
1725
|
+
Partial<EventCalendarViewConfig<TData>>,
|
|
1726
|
+
Omit<useRender.ComponentProps<"div">, "children" | "defaultValue"> {
|
|
1727
|
+
/** Adopt a hoisted useEventCalendarState instance; option props are then ignored. */
|
|
1728
|
+
calendar?: EventCalendarInstance<TData>
|
|
1729
|
+
/** Imperative escape hatch usable from outside the tree. */
|
|
1730
|
+
apiRef?: RefObject<EventCalendarApi<TData> | null>
|
|
1731
|
+
children?: ReactNode
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const OPTION_KEYS: Array<keyof UseEventCalendarStateOptions> = [
|
|
1735
|
+
"events",
|
|
1736
|
+
"defaultEvents",
|
|
1737
|
+
"view",
|
|
1738
|
+
"defaultView",
|
|
1739
|
+
"date",
|
|
1740
|
+
"defaultDate",
|
|
1741
|
+
"dayCount",
|
|
1742
|
+
"defaultDayCount",
|
|
1743
|
+
"selection",
|
|
1744
|
+
"defaultSelection",
|
|
1745
|
+
"interactions",
|
|
1746
|
+
"defaultInteractions",
|
|
1747
|
+
"viewSettings",
|
|
1748
|
+
"defaultViewSettings",
|
|
1749
|
+
"loading",
|
|
1750
|
+
"views",
|
|
1751
|
+
"timeZone",
|
|
1752
|
+
"locale",
|
|
1753
|
+
"weekStartsOn",
|
|
1754
|
+
"dayStartHour",
|
|
1755
|
+
"dayEndHour",
|
|
1756
|
+
"slotDuration",
|
|
1757
|
+
"snapDuration",
|
|
1758
|
+
"agendaDayCount",
|
|
1759
|
+
"fixedWeeks",
|
|
1760
|
+
"showOutsideDays",
|
|
1761
|
+
"i18n",
|
|
1762
|
+
"resources",
|
|
1763
|
+
"getEventPriority",
|
|
1764
|
+
"eventOrder",
|
|
1765
|
+
"getOccurrences",
|
|
1766
|
+
"weekendDays",
|
|
1767
|
+
"activation",
|
|
1768
|
+
"onEventClick",
|
|
1769
|
+
"onEventDoubleClick",
|
|
1770
|
+
"onEventUpdate",
|
|
1771
|
+
"canDropEvent",
|
|
1772
|
+
"onDragBlocked",
|
|
1773
|
+
"onSlotClick",
|
|
1774
|
+
"onSelectSlot",
|
|
1775
|
+
"canSelectSlot",
|
|
1776
|
+
"onRangeChange",
|
|
1777
|
+
"onViewChange",
|
|
1778
|
+
"onDateChange",
|
|
1779
|
+
"onDayCountChange",
|
|
1780
|
+
"onSelectionChange",
|
|
1781
|
+
"onInteractionsChange",
|
|
1782
|
+
"onViewSettingsChange",
|
|
1783
|
+
"onEventsChange",
|
|
1784
|
+
"onMoreClick",
|
|
1785
|
+
]
|
|
1786
|
+
|
|
1787
|
+
function splitOptions<TData>(props: Record<string, unknown>): {
|
|
1788
|
+
options: UseEventCalendarStateOptions<TData>
|
|
1789
|
+
viewConfig: EventCalendarViewConfig<TData>
|
|
1790
|
+
rest: Record<string, unknown>
|
|
1791
|
+
} {
|
|
1792
|
+
const options: Record<string, unknown> = {}
|
|
1793
|
+
const viewConfig: Record<string, unknown> = { ...DEFAULT_VIEW_CONFIG }
|
|
1794
|
+
const rest: Record<string, unknown> = {}
|
|
1795
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1796
|
+
if ((OPTION_KEYS as string[]).includes(key)) options[key] = value
|
|
1797
|
+
else if ((VIEW_CONFIG_KEYS as string[]).includes(key)) {
|
|
1798
|
+
if (value !== undefined) viewConfig[key] = value
|
|
1799
|
+
} else rest[key] = value
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
options: options as UseEventCalendarStateOptions<TData>,
|
|
1803
|
+
viewConfig: viewConfig as unknown as EventCalendarViewConfig<TData>,
|
|
1804
|
+
rest,
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Root provider + container. Composition contract:
|
|
1810
|
+
* <EventCalendar><EventCalendarNav/><EventCalendarToolbar/><EventCalendarContent/></EventCalendar>
|
|
1811
|
+
*/
|
|
1812
|
+
function EventCalendar<TData = unknown>({
|
|
1813
|
+
calendar,
|
|
1814
|
+
apiRef,
|
|
1815
|
+
className,
|
|
1816
|
+
render,
|
|
1817
|
+
children,
|
|
1818
|
+
...props
|
|
1819
|
+
}: EventCalendarProps<TData>) {
|
|
1820
|
+
const { options, viewConfig, rest } = splitOptions<TData>(
|
|
1821
|
+
props as Record<string, unknown>
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
if (calendar && Object.keys(options).length > 0) {
|
|
1825
|
+
warnOnce(
|
|
1826
|
+
"calendar-and-options",
|
|
1827
|
+
"both `calendar` and option props were passed; option props are ignored when adopting an instance."
|
|
1828
|
+
)
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const own = useEventCalendarState<TData>(calendar ? {} : options)
|
|
1832
|
+
const instance = calendar ?? own
|
|
1833
|
+
|
|
1834
|
+
useEffect(() => {
|
|
1835
|
+
if (apiRef) apiRef.current = instance.api
|
|
1836
|
+
}, [apiRef, instance])
|
|
1837
|
+
|
|
1838
|
+
// Register the root element so the drag engine can find day cells even when a
|
|
1839
|
+
// gesture starts from a portaled surface (the "+N more" popover).
|
|
1840
|
+
const registerRoot = useCallback(
|
|
1841
|
+
(el: HTMLElement | null) => instance.internals.setRootEl(el),
|
|
1842
|
+
[instance]
|
|
1843
|
+
)
|
|
1844
|
+
|
|
1845
|
+
const defaultProps = {
|
|
1846
|
+
"data-slot": "event-calendar",
|
|
1847
|
+
ref: registerRoot,
|
|
1848
|
+
// text-xs is the calendar-wide default type size; because it sits before
|
|
1849
|
+
// `className`, a consumer can override the whole scale with e.g.
|
|
1850
|
+
// <EventCalendar className="text-sm"> and every inheriting element follows.
|
|
1851
|
+
// Inner elements omit their own text-size so they cascade from here (the
|
|
1852
|
+
// few portaled surfaces - "+N more" popover, drag carry clone - pin the
|
|
1853
|
+
// size explicitly since DOM inheritance does not cross a portal).
|
|
1854
|
+
className: cn("flex min-h-0 min-w-0 flex-col text-xs", className),
|
|
1855
|
+
children: (
|
|
1856
|
+
<>
|
|
1857
|
+
{children}
|
|
1858
|
+
<div
|
|
1859
|
+
data-slot="event-calendar-announcer"
|
|
1860
|
+
aria-live="polite"
|
|
1861
|
+
className="sr-only"
|
|
1862
|
+
/>
|
|
1863
|
+
</>
|
|
1864
|
+
),
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
return (
|
|
1868
|
+
<EventCalendarContext.Provider value={instance}>
|
|
1869
|
+
<EventCalendarViewConfigContext.Provider value={viewConfig}>
|
|
1870
|
+
{useRender({
|
|
1871
|
+
defaultTagName: "div",
|
|
1872
|
+
render,
|
|
1873
|
+
props: mergeProps<"div">(defaultProps, rest),
|
|
1874
|
+
})}
|
|
1875
|
+
</EventCalendarViewConfigContext.Provider>
|
|
1876
|
+
</EventCalendarContext.Provider>
|
|
1877
|
+
)
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
export {
|
|
1881
|
+
ALL_VIEWS,
|
|
1882
|
+
BASE_VIEWS,
|
|
1883
|
+
DEFAULT_VIEW_CONFIG,
|
|
1884
|
+
EventCalendar,
|
|
1885
|
+
EventCalendarContext,
|
|
1886
|
+
EventCalendarViewConfigContext,
|
|
1887
|
+
EventCalendarViewContext,
|
|
1888
|
+
useEventCalendar,
|
|
1889
|
+
useEventCalendarDay,
|
|
1890
|
+
useEventCalendarWeek,
|
|
1891
|
+
useEventCalendarInteractions,
|
|
1892
|
+
useEventCalendarNavigation,
|
|
1893
|
+
useEventCalendarOccurrences,
|
|
1894
|
+
useEventCalendarSelection,
|
|
1895
|
+
useEventCalendarSelector,
|
|
1896
|
+
useEventCalendarSettings,
|
|
1897
|
+
useEventCalendarSettingsVersion,
|
|
1898
|
+
useEventCalendarState,
|
|
1899
|
+
useEventCalendarView,
|
|
1900
|
+
useEventCalendarViewConfig,
|
|
1901
|
+
useEventCalendarViewContext,
|
|
1902
|
+
useEventCalendarViewSettings,
|
|
1903
|
+
}
|
|
1904
|
+
export type {
|
|
1905
|
+
EventCalendarActivationConfig,
|
|
1906
|
+
EventCalendarApi,
|
|
1907
|
+
EventCalendarCallbacks,
|
|
1908
|
+
EventCalendarClassNames,
|
|
1909
|
+
EventCalendarInstance,
|
|
1910
|
+
EventCalendarInternals,
|
|
1911
|
+
EventCalendarProps,
|
|
1912
|
+
EventCalendarRenderEventProps,
|
|
1913
|
+
EventCalendarSettings,
|
|
1914
|
+
EventCalendarViewConfig,
|
|
1915
|
+
UseEventCalendarStateOptions,
|
|
1916
|
+
}
|