@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,1287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useId,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
type Ref,
|
|
11
|
+
} from "react"
|
|
12
|
+
import {
|
|
13
|
+
EventCalendarViewContext,
|
|
14
|
+
useEventCalendar,
|
|
15
|
+
useEventCalendarDay,
|
|
16
|
+
useEventCalendarSelector,
|
|
17
|
+
useEventCalendarSettings,
|
|
18
|
+
useEventCalendarViewConfig,
|
|
19
|
+
useEventCalendarViewSettings,
|
|
20
|
+
useEventCalendarWeek,
|
|
21
|
+
} from "@/components/reui/event-calendar/event-calendar"
|
|
22
|
+
import {
|
|
23
|
+
useEventCalendarGestures,
|
|
24
|
+
wasRecentChipPress,
|
|
25
|
+
wasRecentDrag,
|
|
26
|
+
} from "@/components/reui/event-calendar/event-calendar-dnd"
|
|
27
|
+
import {
|
|
28
|
+
EVENT_CALENDAR_GHOST,
|
|
29
|
+
EVENT_CALENDAR_SLOT_DRAFT,
|
|
30
|
+
EventCalendarEvent,
|
|
31
|
+
} from "@/components/reui/event-calendar/event-calendar-event"
|
|
32
|
+
import {
|
|
33
|
+
getDayKey,
|
|
34
|
+
getRangeKey,
|
|
35
|
+
resolveOffDay,
|
|
36
|
+
toZoned,
|
|
37
|
+
zonedStartOfDay,
|
|
38
|
+
} from "@/components/reui/event-calendar/event-calendar-lib"
|
|
39
|
+
import type {
|
|
40
|
+
EventCalendarDateRange,
|
|
41
|
+
EventCalendarDragState,
|
|
42
|
+
EventCalendarEventId,
|
|
43
|
+
EventCalendarSegment,
|
|
44
|
+
} from "@/components/reui/event-calendar/event-calendar-types"
|
|
45
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
46
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
47
|
+
import { addDays, format, getWeek } from "date-fns"
|
|
48
|
+
|
|
49
|
+
import { cn } from "cn"
|
|
50
|
+
import {
|
|
51
|
+
Popover,
|
|
52
|
+
PopoverContent,
|
|
53
|
+
PopoverTrigger,
|
|
54
|
+
} from "@/components/ui/popover"
|
|
55
|
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
56
|
+
import { PlusIcon } from "lucide-react"
|
|
57
|
+
|
|
58
|
+
// Layout-effect on the client (measure before paint, no flash), plain effect on
|
|
59
|
+
// the server (never runs there) to avoid the SSR useLayoutEffect warning.
|
|
60
|
+
const useIsoLayoutEffect =
|
|
61
|
+
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
62
|
+
|
|
63
|
+
// An occurrence key encodes the start instant and is also the chip's React key,
|
|
64
|
+
// so committing a move re-keys the chip: React remounts it and the browser
|
|
65
|
+
// drops focus to <body>. The chip that owns focus is recorded here so the cell
|
|
66
|
+
// rendering its replacement can hand focus back. Module scope because the drop
|
|
67
|
+
// can land in a different cell than the one the chip left, and only one element
|
|
68
|
+
// holds focus at a time anyway.
|
|
69
|
+
let focusedChip: {
|
|
70
|
+
node: HTMLElement
|
|
71
|
+
eventId: EventCalendarEventId
|
|
72
|
+
recurrenceIndex?: number
|
|
73
|
+
} | null = null
|
|
74
|
+
|
|
75
|
+
/** Give focus back to the recorded chip's replacement, if `root` renders it. */
|
|
76
|
+
function restoreChipFocus(
|
|
77
|
+
root: HTMLElement | null,
|
|
78
|
+
segments: EventCalendarSegment[]
|
|
79
|
+
) {
|
|
80
|
+
const pending = focusedChip
|
|
81
|
+
// Only a chip removed WHILE focused needs help: a node still in the tree, or
|
|
82
|
+
// a focus that has already moved on by itself, is left alone.
|
|
83
|
+
if (!root || !pending || pending.node.isConnected) return
|
|
84
|
+
const active = document.activeElement
|
|
85
|
+
if (active && active !== document.body) return
|
|
86
|
+
const index = segments.findIndex(
|
|
87
|
+
(segment) =>
|
|
88
|
+
segment.occurrence.eventId === pending.eventId &&
|
|
89
|
+
segment.occurrence.recurrenceIndex === pending.recurrenceIndex
|
|
90
|
+
)
|
|
91
|
+
if (index < 0) return
|
|
92
|
+
const chip = root.querySelectorAll<HTMLElement>(
|
|
93
|
+
"[data-slot=event-calendar-event]"
|
|
94
|
+
)[index]
|
|
95
|
+
if (!chip) return
|
|
96
|
+
// cleared first: focus() re-records through the new chip's own onFocus
|
|
97
|
+
focusedChip = null
|
|
98
|
+
chip.focus()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface EventCalendarMonthViewProps extends useRender.ComponentProps<"div"> {
|
|
102
|
+
maxEventsPerCell?: number | "auto"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function EventCalendarMonthView({
|
|
106
|
+
className,
|
|
107
|
+
render,
|
|
108
|
+
maxEventsPerCell,
|
|
109
|
+
...props
|
|
110
|
+
}: EventCalendarMonthViewProps) {
|
|
111
|
+
const instance = useEventCalendar()
|
|
112
|
+
const settings = useEventCalendarSettings()
|
|
113
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
114
|
+
const visibleRange = useEventCalendarSelector<
|
|
115
|
+
unknown,
|
|
116
|
+
EventCalendarDateRange
|
|
117
|
+
>((state) => state.visibleRange, {
|
|
118
|
+
isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),
|
|
119
|
+
})
|
|
120
|
+
const anchorDate = useEventCalendarSelector((state) => state.date)
|
|
121
|
+
|
|
122
|
+
const { effective } = useEventCalendarViewSettings()
|
|
123
|
+
const weeks = useMemo(() => {
|
|
124
|
+
const days: Date[] = []
|
|
125
|
+
let cursor = zonedStartOfDay(visibleRange.start, settings.timeZone)
|
|
126
|
+
while (cursor < visibleRange.end) {
|
|
127
|
+
days.push(cursor)
|
|
128
|
+
cursor = zonedStartOfDay(
|
|
129
|
+
addDays(toZoned(cursor, settings.timeZone), 1),
|
|
130
|
+
settings.timeZone
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const rows: Date[][] = []
|
|
134
|
+
for (let i = 0; i < days.length; i += 7) rows.push(days.slice(i, i + 7))
|
|
135
|
+
if (effective.weekends) return rows
|
|
136
|
+
return rows.map((row) =>
|
|
137
|
+
row.filter(
|
|
138
|
+
(day) =>
|
|
139
|
+
!settings.weekendDays.includes(
|
|
140
|
+
toZoned(day, settings.timeZone).getDay()
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
}, [
|
|
145
|
+
visibleRange,
|
|
146
|
+
settings.timeZone,
|
|
147
|
+
settings.weekendDays,
|
|
148
|
+
effective.weekends,
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
const headerDays = weeks[0] ?? []
|
|
152
|
+
const title = settings.i18n.functions.formatTitle("month", {
|
|
153
|
+
date: toZoned(anchorDate, settings.timeZone),
|
|
154
|
+
activeRange: instance.api.getActiveRange(),
|
|
155
|
+
visibleRange,
|
|
156
|
+
locale: settings.locale,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const gridTemplateColumns = `${effective.weekNumbers ? "var(--ec-week-number-w, 2.75rem) " : ""}repeat(${headerDays.length}, minmax(0, 1fr))`
|
|
160
|
+
const cap = maxEventsPerCell ?? viewConfig.maxEventsPerCell
|
|
161
|
+
const contained = viewConfig.scrollMode !== "page"
|
|
162
|
+
|
|
163
|
+
// "auto" fits as many event rows as the cell height allows and rolls the rest
|
|
164
|
+
// into "+N more". Only the contained mode gives a cell a bounded height to
|
|
165
|
+
// measure; page mode grows to fit, so "auto" there keeps the fixed fallback.
|
|
166
|
+
const autoFit = cap === "auto" && contained
|
|
167
|
+
// slotProbe resolves the event-row height (--ec-month-bar-h) to px, honoring
|
|
168
|
+
// the current font size and any consumer override; contentProbe is the first
|
|
169
|
+
// cell's flex-1 content area, whose height is the event space per cell.
|
|
170
|
+
const slotProbeRef = useRef<HTMLDivElement | null>(null)
|
|
171
|
+
const contentProbeRef = useRef<HTMLDivElement | null>(null)
|
|
172
|
+
const [autoCap, setAutoCap] = useState<number | null>(null)
|
|
173
|
+
const measureCap = useCallback(() => {
|
|
174
|
+
const content = contentProbeRef.current
|
|
175
|
+
const slot = slotProbeRef.current
|
|
176
|
+
if (!content || !slot) return
|
|
177
|
+
const laneH = slot.getBoundingClientRect().height
|
|
178
|
+
if (laneH <= 0) return
|
|
179
|
+
const cs = getComputedStyle(content)
|
|
180
|
+
const inner = content.clientHeight - (parseFloat(cs.paddingTop) || 0)
|
|
181
|
+
const gap = parseFloat(cs.rowGap) || 0
|
|
182
|
+
// N rows occupy N*laneH - gap (the last row has no trailing gap)
|
|
183
|
+
setAutoCap(Math.max(1, Math.floor((inner + gap) / laneH)))
|
|
184
|
+
}, [])
|
|
185
|
+
useIsoLayoutEffect(() => {
|
|
186
|
+
if (!autoFit) {
|
|
187
|
+
setAutoCap(null)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const content = contentProbeRef.current
|
|
191
|
+
if (!content || typeof ResizeObserver === "undefined") return
|
|
192
|
+
measureCap()
|
|
193
|
+
const observer = new ResizeObserver(measureCap)
|
|
194
|
+
observer.observe(content)
|
|
195
|
+
return () => observer.disconnect()
|
|
196
|
+
// re-observe the first cell after a re-layout (row count or month change)
|
|
197
|
+
}, [autoFit, measureCap, weeks.length, anchorDate])
|
|
198
|
+
const resolvedCap = cap === "auto" ? (autoFit ? (autoCap ?? 3) : 3) : cap
|
|
199
|
+
|
|
200
|
+
const defaultProps = {
|
|
201
|
+
"data-slot": "event-calendar-month-view",
|
|
202
|
+
"data-view": "month",
|
|
203
|
+
role: "grid",
|
|
204
|
+
"aria-label": title,
|
|
205
|
+
className: cn(
|
|
206
|
+
"flex flex-col border-t",
|
|
207
|
+
contained && "min-h-0 flex-1 overflow-hidden",
|
|
208
|
+
viewConfig.classNames?.monthView,
|
|
209
|
+
className
|
|
210
|
+
),
|
|
211
|
+
children: (
|
|
212
|
+
<>
|
|
213
|
+
<div
|
|
214
|
+
role="row"
|
|
215
|
+
data-slot="event-calendar-month-header"
|
|
216
|
+
// @container scopes the narrow-label breakpoint to the header row
|
|
217
|
+
className={cn(
|
|
218
|
+
"@container grid border-b",
|
|
219
|
+
viewConfig.classNames?.monthHeader
|
|
220
|
+
)}
|
|
221
|
+
style={{ gridTemplateColumns }}
|
|
222
|
+
>
|
|
223
|
+
{effective.weekNumbers && (
|
|
224
|
+
<div
|
|
225
|
+
role="columnheader"
|
|
226
|
+
aria-hidden
|
|
227
|
+
className={cn(
|
|
228
|
+
"border-e px-2 py-1.5",
|
|
229
|
+
viewConfig.classNames?.weekNumber
|
|
230
|
+
)}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
{headerDays.map((day) => (
|
|
234
|
+
<div
|
|
235
|
+
key={day.getTime()}
|
|
236
|
+
role="columnheader"
|
|
237
|
+
className={cn(
|
|
238
|
+
"text-muted-foreground truncate px-2 py-1.5 font-medium",
|
|
239
|
+
viewConfig.classNames?.monthDayHeader
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{viewConfig.renderDayHeader?.({
|
|
243
|
+
day,
|
|
244
|
+
view: "month",
|
|
245
|
+
isToday:
|
|
246
|
+
getDayKey(day, settings.timeZone) ===
|
|
247
|
+
getDayKey(new Date(), settings.timeZone),
|
|
248
|
+
}) ?? (
|
|
249
|
+
<>
|
|
250
|
+
<span className="@max-[36rem]:hidden">
|
|
251
|
+
{format(
|
|
252
|
+
toZoned(day, settings.timeZone),
|
|
253
|
+
settings.i18n.formats.monthDayHeader,
|
|
254
|
+
{ locale: settings.locale }
|
|
255
|
+
)}
|
|
256
|
+
</span>
|
|
257
|
+
<span className="hidden @max-[36rem]:inline">
|
|
258
|
+
{format(
|
|
259
|
+
toZoned(day, settings.timeZone),
|
|
260
|
+
settings.i18n.formats.monthDayHeaderNarrow,
|
|
261
|
+
{ locale: settings.locale }
|
|
262
|
+
)}
|
|
263
|
+
</span>
|
|
264
|
+
</>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
<div
|
|
270
|
+
data-slot="event-calendar-month-body"
|
|
271
|
+
className={cn(
|
|
272
|
+
"grid",
|
|
273
|
+
contained && "min-h-0 flex-1",
|
|
274
|
+
viewConfig.classNames?.monthBody
|
|
275
|
+
)}
|
|
276
|
+
style={{
|
|
277
|
+
gridTemplateRows: contained
|
|
278
|
+
? `repeat(${weeks.length}, minmax(0, 1fr))`
|
|
279
|
+
: `repeat(${weeks.length}, minmax(var(--ec-month-row-min-h, 8rem), auto))`,
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{weeks.map((week, rowIndex) => (
|
|
283
|
+
<EventCalendarMonthWeek
|
|
284
|
+
key={rowIndex}
|
|
285
|
+
week={week}
|
|
286
|
+
gridTemplateColumns={gridTemplateColumns}
|
|
287
|
+
showWeekNumber={effective.weekNumbers}
|
|
288
|
+
cap={resolvedCap}
|
|
289
|
+
autoFit={autoFit}
|
|
290
|
+
contentRef={rowIndex === 0 ? contentProbeRef : undefined}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
{autoFit && (
|
|
295
|
+
<div
|
|
296
|
+
ref={slotProbeRef}
|
|
297
|
+
aria-hidden
|
|
298
|
+
className="pointer-events-none invisible absolute h-[var(--ec-month-bar-h,1.75rem)] w-0"
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
</>
|
|
302
|
+
),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<EventCalendarViewContext.Provider value={{ view: "month" }}>
|
|
307
|
+
{useRender({
|
|
308
|
+
defaultTagName: "div",
|
|
309
|
+
render,
|
|
310
|
+
props: mergeProps<"div">(defaultProps, props),
|
|
311
|
+
})}
|
|
312
|
+
</EventCalendarViewContext.Provider>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One month week row. Multi-day / all-day events render as CONTINUOUS bars in
|
|
318
|
+
* an overlay grid that spans day columns (colStart -> colSpan) and stacks by
|
|
319
|
+
* lane; single-day timed events render inside each cell below the reserved bar
|
|
320
|
+
* lanes. This is what makes a cross-day event read as one whole block instead
|
|
321
|
+
* of a chip repeated per cell.
|
|
322
|
+
*/
|
|
323
|
+
function EventCalendarMonthWeek({
|
|
324
|
+
week,
|
|
325
|
+
gridTemplateColumns,
|
|
326
|
+
showWeekNumber,
|
|
327
|
+
cap,
|
|
328
|
+
autoFit,
|
|
329
|
+
contentRef,
|
|
330
|
+
}: {
|
|
331
|
+
week: Date[]
|
|
332
|
+
gridTemplateColumns: string
|
|
333
|
+
showWeekNumber: boolean
|
|
334
|
+
cap: number
|
|
335
|
+
autoFit: boolean
|
|
336
|
+
/** Set on the first week only: forwarded to its first cell's content area so
|
|
337
|
+
* the view can measure the per-cell event height for "auto". */
|
|
338
|
+
contentRef?: Ref<HTMLDivElement>
|
|
339
|
+
}) {
|
|
340
|
+
const settings = useEventCalendarSettings()
|
|
341
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
342
|
+
const { bars, rowStart } = useEventCalendarWeek(week[0])
|
|
343
|
+
const colOffset = showWeekNumber ? 1 : 0
|
|
344
|
+
const dayMs = 86400000
|
|
345
|
+
const rowStartMs = zonedStartOfDay(
|
|
346
|
+
rowStart ?? week[0],
|
|
347
|
+
settings.timeZone
|
|
348
|
+
).getTime()
|
|
349
|
+
// Day offsets from the TRUE row start (0-6) for each visible column, so a
|
|
350
|
+
// weekends-hidden month still places bars on the right days.
|
|
351
|
+
const offsets = week.map((d) =>
|
|
352
|
+
Math.round(
|
|
353
|
+
(zonedStartOfDay(d, settings.timeZone).getTime() - rowStartMs) / dayMs
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
/** Clamp a day-offset span onto the visible columns; null = fully hidden. */
|
|
357
|
+
const gridPos = (colStart: number, colSpan: number) => {
|
|
358
|
+
let start = -1
|
|
359
|
+
let end = -1
|
|
360
|
+
for (let o = colStart; o < colStart + colSpan; o++) {
|
|
361
|
+
const col = offsets.indexOf(o)
|
|
362
|
+
if (col === -1) continue
|
|
363
|
+
if (start === -1) start = col
|
|
364
|
+
end = col
|
|
365
|
+
}
|
|
366
|
+
return start === -1 ? null : { col: start, span: end - start + 1 }
|
|
367
|
+
}
|
|
368
|
+
// bars fit within the cap; deeper lanes fall into each day's "+N more"
|
|
369
|
+
const visibleBars = bars.filter((b) => (b.lane ?? 0) < cap)
|
|
370
|
+
const covers = (b: EventCalendarSegment, dayOffset: number) =>
|
|
371
|
+
(b.colStart ?? 0) <= dayOffset &&
|
|
372
|
+
dayOffset < (b.colStart ?? 0) + (b.colSpan ?? 1)
|
|
373
|
+
// Occurrence keys of the bars hidden in each column (lane >= cap). Threaded to
|
|
374
|
+
// the cell so its "+N more" popover can list the hidden bars WITHOUT re-listing
|
|
375
|
+
// the visible ones (day buckets carry no lane, so the week row - which owns bar
|
|
376
|
+
// laning - is the only place that knows which bars are hidden).
|
|
377
|
+
const hiddenBarKeysByCol = week.map(
|
|
378
|
+
(_, col) =>
|
|
379
|
+
new Set(
|
|
380
|
+
bars
|
|
381
|
+
.filter((b) => (b.lane ?? 0) >= cap && covers(b, offsets[col]))
|
|
382
|
+
.map((b) => b.occurrence.key)
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// Live move/resize ghost at the PROPOSED day span. Standardized treatment
|
|
387
|
+
// (EVENT_CALENDAR_GHOST): move = the event carried as a full clone, resize =
|
|
388
|
+
// the same clone with a dashed boundary; invalid adds destructive marking
|
|
389
|
+
// while the engine shows the not-allowed cursor + validation hint.
|
|
390
|
+
const dragGhost = useEventCalendarSelector<
|
|
391
|
+
unknown,
|
|
392
|
+
| (Pick<
|
|
393
|
+
EventCalendarDragState,
|
|
394
|
+
| "kind"
|
|
395
|
+
| "valid"
|
|
396
|
+
| "occurrence"
|
|
397
|
+
| "proposedStart"
|
|
398
|
+
| "proposedEnd"
|
|
399
|
+
| "proposedAllDay"
|
|
400
|
+
> & {
|
|
401
|
+
colStart: number
|
|
402
|
+
colSpan: number
|
|
403
|
+
isStart: boolean
|
|
404
|
+
isEnd: boolean
|
|
405
|
+
color?: string
|
|
406
|
+
})
|
|
407
|
+
| null
|
|
408
|
+
>(
|
|
409
|
+
(state) => {
|
|
410
|
+
const drag = state.drag
|
|
411
|
+
if (!drag) return null
|
|
412
|
+
const rowEndMs = rowStartMs + 7 * dayMs
|
|
413
|
+
const startDayMs = zonedStartOfDay(
|
|
414
|
+
drag.proposedStart,
|
|
415
|
+
settings.timeZone
|
|
416
|
+
).getTime()
|
|
417
|
+
// exclusive end -> the last covered day
|
|
418
|
+
const lastDayMs = zonedStartOfDay(
|
|
419
|
+
new Date(drag.proposedEnd.getTime() - 1),
|
|
420
|
+
settings.timeZone
|
|
421
|
+
).getTime()
|
|
422
|
+
if (startDayMs >= rowEndMs || lastDayMs < rowStartMs) return null
|
|
423
|
+
const startCol = Math.max(
|
|
424
|
+
0,
|
|
425
|
+
Math.round((startDayMs - rowStartMs) / dayMs)
|
|
426
|
+
)
|
|
427
|
+
const endCol = Math.min(6, Math.round((lastDayMs - rowStartMs) / dayMs))
|
|
428
|
+
if (endCol < startCol) return null
|
|
429
|
+
return {
|
|
430
|
+
kind: drag.kind,
|
|
431
|
+
valid: drag.valid,
|
|
432
|
+
occurrence: drag.occurrence,
|
|
433
|
+
proposedStart: drag.proposedStart,
|
|
434
|
+
proposedEnd: drag.proposedEnd,
|
|
435
|
+
proposedAllDay: drag.proposedAllDay,
|
|
436
|
+
colStart: startCol,
|
|
437
|
+
colSpan: endCol - startCol + 1,
|
|
438
|
+
isStart: startDayMs >= rowStartMs,
|
|
439
|
+
isEnd: lastDayMs < rowEndMs,
|
|
440
|
+
color: drag.occurrence.event.color,
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
isEqual: (a, b) =>
|
|
445
|
+
a === b ||
|
|
446
|
+
(a !== null &&
|
|
447
|
+
b !== null &&
|
|
448
|
+
a.colStart === b.colStart &&
|
|
449
|
+
a.colSpan === b.colSpan &&
|
|
450
|
+
a.valid === b.valid &&
|
|
451
|
+
a.kind === b.kind &&
|
|
452
|
+
a.proposedAllDay === b.proposedAllDay &&
|
|
453
|
+
a.proposedStart.getTime() === b.proposedStart.getTime() &&
|
|
454
|
+
a.proposedEnd.getTime() === b.proposedEnd.getTime()),
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
const ghostPos = dragGhost
|
|
458
|
+
? gridPos(dragGhost.colStart, dragGhost.colSpan)
|
|
459
|
+
: null
|
|
460
|
+
const ghostLane = dragGhost
|
|
461
|
+
? (bars.find((b) => b.occurrence.key === dragGhost.occurrence.key)?.lane ??
|
|
462
|
+
0)
|
|
463
|
+
: 0
|
|
464
|
+
// A single-day timed MOVE is indicated INLINE in the target cell (a
|
|
465
|
+
// placeholder at the time-sorted position, rendered by EventCalendarMonthCell)
|
|
466
|
+
// rather than as a bar in this overlay, so only bar drags and every resize
|
|
467
|
+
// render the overlay ghost. Bars = all-day or multi-day (span > 1 day).
|
|
468
|
+
const ghostIsBar =
|
|
469
|
+
!!dragGhost &&
|
|
470
|
+
(dragGhost.kind !== "move" ||
|
|
471
|
+
dragGhost.proposedAllDay ||
|
|
472
|
+
dragGhost.proposedEnd.getTime() - dragGhost.proposedStart.getTime() >
|
|
473
|
+
dayMs)
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div
|
|
477
|
+
role="row"
|
|
478
|
+
data-slot="event-calendar-month-row"
|
|
479
|
+
className={cn(
|
|
480
|
+
"relative grid min-h-0 border-b last:border-b-0",
|
|
481
|
+
viewConfig.classNames?.monthRow
|
|
482
|
+
)}
|
|
483
|
+
style={{ gridTemplateColumns }}
|
|
484
|
+
>
|
|
485
|
+
{showWeekNumber && (
|
|
486
|
+
<div
|
|
487
|
+
role="rowheader"
|
|
488
|
+
data-slot="event-calendar-week-number"
|
|
489
|
+
className={cn(
|
|
490
|
+
"text-muted-foreground border-e px-2 pt-1 tabular-nums",
|
|
491
|
+
viewConfig.classNames?.weekNumber
|
|
492
|
+
)}
|
|
493
|
+
>
|
|
494
|
+
{settings.i18n.labels.week(
|
|
495
|
+
getWeek(toZoned(week[0], settings.timeZone), {
|
|
496
|
+
// locale supplies firstWeekContainsDate, so a de/ISO calendar
|
|
497
|
+
// numbers the year-boundary weeks its own way instead of falling
|
|
498
|
+
// back to US numbering; weekStartsOn stays explicit so the number
|
|
499
|
+
// keeps matching the rendered grid
|
|
500
|
+
locale: settings.locale,
|
|
501
|
+
weekStartsOn: settings.weekStartsOn,
|
|
502
|
+
})
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
{week.map((day, col) => (
|
|
507
|
+
<EventCalendarMonthCell
|
|
508
|
+
key={day.getTime()}
|
|
509
|
+
day={day}
|
|
510
|
+
cap={cap}
|
|
511
|
+
// Reserve lane space only for bars that pass through THIS cell, so a
|
|
512
|
+
// short multi-day event does not push down timed events in unrelated
|
|
513
|
+
// cells of the same row. Reserve down to the deepest covering bar
|
|
514
|
+
// lane so timed events always sit below every bar in their own cell.
|
|
515
|
+
reservedLanes={visibleBars.reduce(
|
|
516
|
+
(max, b) =>
|
|
517
|
+
covers(b, offsets[col]) ? Math.max(max, (b.lane ?? 0) + 1) : max,
|
|
518
|
+
0
|
|
519
|
+
)}
|
|
520
|
+
hiddenBarKeys={hiddenBarKeysByCol[col]}
|
|
521
|
+
isLast={col === week.length - 1}
|
|
522
|
+
autoFit={autoFit}
|
|
523
|
+
contentRef={col === 0 ? contentRef : undefined}
|
|
524
|
+
/>
|
|
525
|
+
))}
|
|
526
|
+
{/* Continuous bar overlay: one element per bar, placed by grid-column so
|
|
527
|
+
a cross-day span is a single unbroken block. pointer-events pass
|
|
528
|
+
through the gaps to the cells below. NOT aria-hidden - these are the
|
|
529
|
+
real interactive bars. */}
|
|
530
|
+
{(visibleBars.length > 0 || (dragGhost && ghostPos && ghostIsBar)) && (
|
|
531
|
+
<div
|
|
532
|
+
data-slot="event-calendar-month-bar-overlay"
|
|
533
|
+
className={cn(
|
|
534
|
+
"pointer-events-none absolute inset-x-0 top-0 z-10 grid pt-1.5",
|
|
535
|
+
viewConfig.classNames?.monthBarOverlay
|
|
536
|
+
)}
|
|
537
|
+
style={{
|
|
538
|
+
gridTemplateColumns,
|
|
539
|
+
gridAutoRows: "var(--ec-month-bar-h, 1.75rem)",
|
|
540
|
+
}}
|
|
541
|
+
>
|
|
542
|
+
{visibleBars.map((bar) => {
|
|
543
|
+
const pos = gridPos(bar.colStart ?? 0, bar.colSpan ?? 1)
|
|
544
|
+
if (!pos) return null
|
|
545
|
+
return (
|
|
546
|
+
<div
|
|
547
|
+
key={bar.occurrence.key}
|
|
548
|
+
className={cn(
|
|
549
|
+
"pointer-events-auto min-w-0 px-1",
|
|
550
|
+
viewConfig.classNames?.monthBar
|
|
551
|
+
)}
|
|
552
|
+
style={{
|
|
553
|
+
gridColumn: `${colOffset + pos.col + 1} / span ${pos.span}`,
|
|
554
|
+
gridRow: (bar.lane ?? 0) + 1,
|
|
555
|
+
}}
|
|
556
|
+
>
|
|
557
|
+
{/* lane height minus the 2px inter-lane gap */}
|
|
558
|
+
<EventCalendarEvent
|
|
559
|
+
segment={bar}
|
|
560
|
+
className="h-[calc(var(--ec-month-bar-h,1.75rem)-0.125rem)]"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
)
|
|
564
|
+
})}
|
|
565
|
+
{dragGhost && ghostPos && ghostIsBar && (
|
|
566
|
+
<div
|
|
567
|
+
aria-hidden
|
|
568
|
+
className={cn("min-w-0 px-1", viewConfig.classNames?.monthBar)}
|
|
569
|
+
style={{
|
|
570
|
+
gridColumn: `${colOffset + ghostPos.col + 1} / span ${ghostPos.span}`,
|
|
571
|
+
gridRow: ghostLane + 1,
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
<div
|
|
575
|
+
data-slot="event-calendar-drag-ghost"
|
|
576
|
+
data-kind={dragGhost.kind}
|
|
577
|
+
data-drop-invalid={!dragGhost.valid || undefined}
|
|
578
|
+
className={cn(
|
|
579
|
+
"h-[calc(var(--ec-month-bar-h,1.75rem)-0.125rem)]",
|
|
580
|
+
dragGhost.kind === "move"
|
|
581
|
+
? // faint drop placeholder only: the cursor-attached
|
|
582
|
+
// carry clone owns the visual during moves
|
|
583
|
+
cn(
|
|
584
|
+
EVENT_CALENDAR_GHOST.move,
|
|
585
|
+
!dragGhost.valid && EVENT_CALENDAR_GHOST.invalid
|
|
586
|
+
)
|
|
587
|
+
: cn(
|
|
588
|
+
EVENT_CALENDAR_GHOST.resize,
|
|
589
|
+
!dragGhost.valid && EVENT_CALENDAR_GHOST.invalidResize
|
|
590
|
+
),
|
|
591
|
+
viewConfig.classNames?.dragGhost
|
|
592
|
+
)}
|
|
593
|
+
style={
|
|
594
|
+
{
|
|
595
|
+
"--ec-event-color":
|
|
596
|
+
dragGhost.color ?? "var(--color-primary)",
|
|
597
|
+
} as CSSProperties
|
|
598
|
+
}
|
|
599
|
+
>
|
|
600
|
+
{dragGhost.kind !== "move" && (
|
|
601
|
+
<EventCalendarEvent
|
|
602
|
+
preview
|
|
603
|
+
segment={{
|
|
604
|
+
occurrence: {
|
|
605
|
+
...dragGhost.occurrence,
|
|
606
|
+
start: dragGhost.proposedStart,
|
|
607
|
+
end: dragGhost.proposedEnd,
|
|
608
|
+
},
|
|
609
|
+
day: week[ghostPos.col] ?? week[0],
|
|
610
|
+
isStart: dragGhost.isStart,
|
|
611
|
+
isEnd: dragGhost.isEnd,
|
|
612
|
+
continuesBefore: !dragGhost.isStart,
|
|
613
|
+
continuesAfter: !dragGhost.isEnd,
|
|
614
|
+
}}
|
|
615
|
+
className={cn(
|
|
616
|
+
"h-full inset-ring-0",
|
|
617
|
+
!dragGhost.valid && EVENT_CALENDAR_GHOST.invalidContent
|
|
618
|
+
)}
|
|
619
|
+
/>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function EventCalendarMonthCell({
|
|
631
|
+
day,
|
|
632
|
+
cap,
|
|
633
|
+
reservedLanes,
|
|
634
|
+
hiddenBarKeys,
|
|
635
|
+
isLast,
|
|
636
|
+
autoFit,
|
|
637
|
+
contentRef,
|
|
638
|
+
}: {
|
|
639
|
+
day: Date
|
|
640
|
+
cap: number
|
|
641
|
+
reservedLanes: number
|
|
642
|
+
/** Occurrence keys of the bars hidden in THIS column (lane >= cap), from the
|
|
643
|
+
* week row. Lets the cell list hidden bars in its overflow popover without
|
|
644
|
+
* re-listing the bars already visible in the row overlay. */
|
|
645
|
+
hiddenBarKeys: Set<string>
|
|
646
|
+
/** Last column in the row - drops the right border so the grid's outer edge
|
|
647
|
+
* is owned by the container, not a doubled cell border. Passed explicitly
|
|
648
|
+
* because the bar overlay renders after the cells, so `:last-child` is
|
|
649
|
+
* unreliable on rows that have bars. */
|
|
650
|
+
isLast: boolean
|
|
651
|
+
/** When true, the "+N more" chip is treated as taking a row so the visible
|
|
652
|
+
* chips + indicator always fit the measured cell height. */
|
|
653
|
+
autoFit: boolean
|
|
654
|
+
/** Set on the first cell only: measured to derive the "auto" cap. */
|
|
655
|
+
contentRef?: Ref<HTMLDivElement>
|
|
656
|
+
}) {
|
|
657
|
+
const instance = useEventCalendar()
|
|
658
|
+
const settings = useEventCalendarSettings()
|
|
659
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
660
|
+
const gestures = useEventCalendarGestures()
|
|
661
|
+
const { segments, isToday, isOutside } = useEventCalendarDay(day)
|
|
662
|
+
|
|
663
|
+
const dayStart = zonedStartOfDay(day, settings.timeZone)
|
|
664
|
+
const dayEnd = addDays(toZoned(dayStart, settings.timeZone), 1)
|
|
665
|
+
const { effective } = useEventCalendarViewSettings()
|
|
666
|
+
const isOff = resolveOffDay(
|
|
667
|
+
day,
|
|
668
|
+
settings.timeZone,
|
|
669
|
+
effective.offDays
|
|
670
|
+
? typeof viewConfig.offDays === "object"
|
|
671
|
+
? viewConfig.offDays
|
|
672
|
+
: true
|
|
673
|
+
: false,
|
|
674
|
+
settings.weekendDays
|
|
675
|
+
)
|
|
676
|
+
const offClassName =
|
|
677
|
+
(typeof viewConfig.offDays === "object" && viewConfig.offDays.className) ||
|
|
678
|
+
"bg-muted/25"
|
|
679
|
+
|
|
680
|
+
const isDropTarget = useEventCalendarSelector<
|
|
681
|
+
unknown,
|
|
682
|
+
"valid" | "invalid" | null
|
|
683
|
+
>((state) => {
|
|
684
|
+
const drag = state.drag
|
|
685
|
+
if (!drag) return null
|
|
686
|
+
const covered = drag.proposedStart < dayEnd && drag.proposedEnd > dayStart
|
|
687
|
+
if (!covered) return null
|
|
688
|
+
return drag.valid ? "valid" : "invalid"
|
|
689
|
+
})
|
|
690
|
+
// Hide hover affordances mid-gesture: the only intent is the drop target.
|
|
691
|
+
// Gated on the one thing that reads it: a plain global boolean flips for all
|
|
692
|
+
// 42 cells the moment a gesture starts and again when it ends, which is pure
|
|
693
|
+
// waste in the default configuration where no add button renders.
|
|
694
|
+
const isInteracting = useEventCalendarSelector<unknown, boolean>((state) =>
|
|
695
|
+
viewConfig.showDayAddButton
|
|
696
|
+
? state.drag !== null || state.slotDraft !== null
|
|
697
|
+
: false
|
|
698
|
+
)
|
|
699
|
+
const inDraft = useEventCalendarSelector<
|
|
700
|
+
unknown,
|
|
701
|
+
{ isStart: boolean; isEnd: boolean } | null
|
|
702
|
+
>(
|
|
703
|
+
(state) => {
|
|
704
|
+
const draft = state.slotDraft
|
|
705
|
+
if (!draft || !draft.allDay) return null
|
|
706
|
+
if (draft.start >= dayEnd || draft.end <= dayStart) return null
|
|
707
|
+
return {
|
|
708
|
+
isStart: draft.start >= dayStart,
|
|
709
|
+
isEnd: draft.end <= dayEnd,
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
isEqual: (a, b) =>
|
|
714
|
+
a === b ||
|
|
715
|
+
(a !== null &&
|
|
716
|
+
b !== null &&
|
|
717
|
+
a.isStart === b.isStart &&
|
|
718
|
+
a.isEnd === b.isEnd),
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
// Month drag-create selects whole DAYS, so the readout is a date range rather
|
|
723
|
+
// than a time range. Held separately from `inDraft` above, which only knows
|
|
724
|
+
// whether this cell is an edge of the selection, not what the selection is.
|
|
725
|
+
const draftRange = useEventCalendarSelector<
|
|
726
|
+
unknown,
|
|
727
|
+
{ start: Date; end: Date } | null
|
|
728
|
+
>(
|
|
729
|
+
(state) => {
|
|
730
|
+
const draft = state.slotDraft
|
|
731
|
+
if (!draft || !draft.allDay) return null
|
|
732
|
+
return { start: draft.start, end: draft.end }
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
isEqual: (a, b) =>
|
|
736
|
+
a === b ||
|
|
737
|
+
(a !== null &&
|
|
738
|
+
b !== null &&
|
|
739
|
+
a.start.getTime() === b.start.getTime() &&
|
|
740
|
+
a.end.getTime() === b.end.getTime()),
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
// A single-day timed MOVE landing on THIS day: expose the proposed
|
|
744
|
+
// minute-of-day (+ color/validity) so the cell can render a drop placeholder
|
|
745
|
+
// at the correct time-sorted position, instead of the overlay marking a bar
|
|
746
|
+
// over the first chip. Skipped for bars (they keep the overlay ghost) and for
|
|
747
|
+
// a no-op move back onto the event's own day (the dimmed source already marks
|
|
748
|
+
// the spot).
|
|
749
|
+
const inlineDrop = useEventCalendarSelector<
|
|
750
|
+
unknown,
|
|
751
|
+
{ min: number; valid: boolean; color?: string } | null
|
|
752
|
+
>(
|
|
753
|
+
(state) => {
|
|
754
|
+
const drag = state.drag
|
|
755
|
+
if (!drag || drag.kind !== "move" || drag.proposedAllDay) return null
|
|
756
|
+
if (drag.proposedEnd.getTime() - drag.proposedStart.getTime() > 86400000)
|
|
757
|
+
return null
|
|
758
|
+
const dropDayMs = zonedStartOfDay(
|
|
759
|
+
drag.proposedStart,
|
|
760
|
+
settings.timeZone
|
|
761
|
+
).getTime()
|
|
762
|
+
if (dropDayMs !== dayStart.getTime()) return null
|
|
763
|
+
if (
|
|
764
|
+
zonedStartOfDay(drag.occurrence.start, settings.timeZone).getTime() ===
|
|
765
|
+
dayStart.getTime()
|
|
766
|
+
)
|
|
767
|
+
return null
|
|
768
|
+
return {
|
|
769
|
+
min: (drag.proposedStart.getTime() - dropDayMs) / 60000,
|
|
770
|
+
valid: drag.valid,
|
|
771
|
+
color: drag.occurrence.event.color,
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
isEqual: (a, b) =>
|
|
776
|
+
a === b ||
|
|
777
|
+
(a !== null &&
|
|
778
|
+
b !== null &&
|
|
779
|
+
a.min === b.min &&
|
|
780
|
+
a.valid === b.valid &&
|
|
781
|
+
a.color === b.color),
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
// Bars (allDay/multi-day) are drawn by the week-row overlay; the cell renders
|
|
786
|
+
// only single-day timed events, below the reserved bar lanes.
|
|
787
|
+
const extraHidden = hiddenBarKeys.size
|
|
788
|
+
const hiddenBarSegs = segments.allDay.filter((s) =>
|
|
789
|
+
hiddenBarKeys.has(s.occurrence.key)
|
|
790
|
+
)
|
|
791
|
+
const m = segments.timed.length
|
|
792
|
+
const timedSlots = Math.max(0, cap - reservedLanes)
|
|
793
|
+
|
|
794
|
+
// Static (at-rest) split: what the cell shows with no drag, and - crucially -
|
|
795
|
+
// what the "+N more" popover lists. The popover carries ONLY the hidden events
|
|
796
|
+
// (hidden bars + timed past the cap), never the chips already visible in the
|
|
797
|
+
// cell, so it never duplicates them. autoFit gives up one timed row to the
|
|
798
|
+
// "+N more" indicator so the visible chips fit the clipped cell height.
|
|
799
|
+
const staticOverflow = extraHidden > 0 || m > timedSlots
|
|
800
|
+
const staticShown =
|
|
801
|
+
autoFit && staticOverflow ? Math.max(0, timedSlots - 1) : timedSlots
|
|
802
|
+
const overflowSegments = [
|
|
803
|
+
...hiddenBarSegs,
|
|
804
|
+
...segments.timed.slice(staticShown),
|
|
805
|
+
]
|
|
806
|
+
|
|
807
|
+
// Live split: while a timed chip is dragged onto this day, render the day
|
|
808
|
+
// exactly as it will look AFTER the drop. The dragged chip is a phantom in the
|
|
809
|
+
// time-sorted order and is shown as the placeholder - inline where it lands,
|
|
810
|
+
// or ON the "+N more" indicator when it lands in the overflow bucket (so the
|
|
811
|
+
// user sees the drop will push it into "more"). The "+N more" count always
|
|
812
|
+
// reflects the post-drop hidden total.
|
|
813
|
+
let visibleTimed: EventCalendarSegment[]
|
|
814
|
+
let overflowCount: number
|
|
815
|
+
let placeholderIndex: number
|
|
816
|
+
let placeholderAtMore: boolean
|
|
817
|
+
if (!inlineDrop) {
|
|
818
|
+
visibleTimed = segments.timed.slice(0, staticShown)
|
|
819
|
+
overflowCount = overflowSegments.length
|
|
820
|
+
placeholderIndex = -1
|
|
821
|
+
placeholderAtMore = false
|
|
822
|
+
} else {
|
|
823
|
+
// rank of the dragged chip in the resulting time-sorted list (chips are
|
|
824
|
+
// time-ordered, so this is the count starting at or before its time)
|
|
825
|
+
const insertRank = segments.timed.filter(
|
|
826
|
+
(s) => (s.startMin ?? 0) <= inlineDrop.min
|
|
827
|
+
).length
|
|
828
|
+
const dropOverflow = extraHidden > 0 || m + 1 > timedSlots
|
|
829
|
+
// rows for timed items INCLUDING the phantom, before the "+N more" row
|
|
830
|
+
const vis = !dropOverflow
|
|
831
|
+
? m + 1
|
|
832
|
+
: autoFit
|
|
833
|
+
? Math.max(0, timedSlots - 1)
|
|
834
|
+
: timedSlots
|
|
835
|
+
placeholderAtMore = insertRank >= vis
|
|
836
|
+
visibleTimed = placeholderAtMore
|
|
837
|
+
? segments.timed.slice(0, vis)
|
|
838
|
+
: segments.timed.slice(0, Math.max(0, vis - 1))
|
|
839
|
+
placeholderIndex = placeholderAtMore ? -1 : insertRank
|
|
840
|
+
overflowCount =
|
|
841
|
+
extraHidden + (m - visibleTimed.length) + (placeholderAtMore ? 1 : 0)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// No dep array: the replacement for a chip that lost focus to a commit can
|
|
845
|
+
// appear on any re-render of any cell, and a cell with nothing to restore
|
|
846
|
+
// bails after two comparisons.
|
|
847
|
+
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
848
|
+
useIsoLayoutEffect(() => {
|
|
849
|
+
restoreChipFocus(rootRef.current, visibleTimed)
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
// Faint dashed drop placeholder, tinted to the dragged event's color, echoing
|
|
853
|
+
// the move ghost (EVENT_CALENDAR_GHOST.move); one chip-height tall so chips
|
|
854
|
+
// shift by exactly one row when it is inserted.
|
|
855
|
+
const dropPlaceholder = inlineDrop ? (
|
|
856
|
+
<div
|
|
857
|
+
key="ec-drop-placeholder"
|
|
858
|
+
aria-hidden
|
|
859
|
+
data-slot="event-calendar-drop-placeholder"
|
|
860
|
+
data-drop-invalid={!inlineDrop.valid || undefined}
|
|
861
|
+
className={cn(
|
|
862
|
+
"shrink-0 rounded-sm border border-dashed",
|
|
863
|
+
inlineDrop.valid
|
|
864
|
+
? "border-(--ec-event-color)/50 bg-(--ec-event-color)/8"
|
|
865
|
+
: "border-destructive/70 bg-destructive/10",
|
|
866
|
+
viewConfig.classNames?.dragGhost
|
|
867
|
+
)}
|
|
868
|
+
style={
|
|
869
|
+
{
|
|
870
|
+
"--ec-event-color": inlineDrop.color ?? "var(--color-primary)",
|
|
871
|
+
height: "calc(var(--ec-month-bar-h, 1.75rem) - 0.125rem)",
|
|
872
|
+
} as CSSProperties
|
|
873
|
+
}
|
|
874
|
+
/>
|
|
875
|
+
) : null
|
|
876
|
+
|
|
877
|
+
const defaultContent = (
|
|
878
|
+
<>
|
|
879
|
+
<div
|
|
880
|
+
ref={contentRef}
|
|
881
|
+
className={cn(
|
|
882
|
+
// gap-0.5 pairs with the 0.125rem subtraction in the lane spacer
|
|
883
|
+
// below; changing the gap requires renderMonthCell
|
|
884
|
+
// px-1 matches the all-day bar wrapper inset so single-day chips and
|
|
885
|
+
// multi-day bars line up on the same left/right edge in a cell
|
|
886
|
+
"flex min-h-0 flex-1 flex-col gap-0.5 overflow-hidden px-1 pt-1.5",
|
|
887
|
+
viewConfig.classNames?.monthCellContent
|
|
888
|
+
)}
|
|
889
|
+
>
|
|
890
|
+
{reservedLanes > 0 && (
|
|
891
|
+
<div
|
|
892
|
+
aria-hidden
|
|
893
|
+
className="shrink-0"
|
|
894
|
+
style={{
|
|
895
|
+
// lane height already carries the 2px inter-lane gap; subtract
|
|
896
|
+
// it so spacer + the column's own gap-0.5 = the SAME 2px rhythm
|
|
897
|
+
// between the last bar and the first timed chip
|
|
898
|
+
height: `calc(${reservedLanes} * var(--ec-month-bar-h, 1.75rem) - 0.125rem)`,
|
|
899
|
+
}}
|
|
900
|
+
/>
|
|
901
|
+
)}
|
|
902
|
+
{visibleTimed.flatMap((segment, i) => {
|
|
903
|
+
const chip = (
|
|
904
|
+
<EventCalendarEvent
|
|
905
|
+
key={segment.occurrence.key}
|
|
906
|
+
segment={segment}
|
|
907
|
+
// Remember the chip holding focus so a commit that re-keys it can
|
|
908
|
+
// hand focus back to the remounted one (see restoreChipFocus)
|
|
909
|
+
onFocus={(e) => {
|
|
910
|
+
focusedChip = {
|
|
911
|
+
node: e.currentTarget,
|
|
912
|
+
eventId: segment.occurrence.eventId,
|
|
913
|
+
recurrenceIndex: segment.occurrence.recurrenceIndex,
|
|
914
|
+
}
|
|
915
|
+
}}
|
|
916
|
+
// A chip still in the tree lost focus on its own, so there is
|
|
917
|
+
// nothing to restore; only a blur from the remount is kept.
|
|
918
|
+
onBlur={(e) => {
|
|
919
|
+
if (e.currentTarget.isConnected) focusedChip = null
|
|
920
|
+
}}
|
|
921
|
+
// Hold a fixed height like the all-day lane above; without this
|
|
922
|
+
// the chip flex-shrinks to whatever room the cell has left, so
|
|
923
|
+
// cells with a reserved bar lane or a second chip render shorter
|
|
924
|
+
// chips.
|
|
925
|
+
className="shrink-0"
|
|
926
|
+
/>
|
|
927
|
+
)
|
|
928
|
+
return i === placeholderIndex ? [dropPlaceholder, chip] : [chip]
|
|
929
|
+
})}
|
|
930
|
+
{placeholderIndex >= 0 &&
|
|
931
|
+
placeholderIndex >= visibleTimed.length &&
|
|
932
|
+
dropPlaceholder}
|
|
933
|
+
{overflowCount > 0 && (
|
|
934
|
+
<EventCalendarMoreIndicator
|
|
935
|
+
day={day}
|
|
936
|
+
count={overflowCount}
|
|
937
|
+
segments={overflowSegments}
|
|
938
|
+
dropInto={
|
|
939
|
+
placeholderAtMore && inlineDrop
|
|
940
|
+
? { color: inlineDrop.color, valid: inlineDrop.valid }
|
|
941
|
+
: undefined
|
|
942
|
+
}
|
|
943
|
+
/>
|
|
944
|
+
)}
|
|
945
|
+
</div>
|
|
946
|
+
{/* Day number + add affordance, bottom-right (Notion-style) */}
|
|
947
|
+
<div
|
|
948
|
+
className={cn(
|
|
949
|
+
"flex items-center justify-end gap-1 px-2 pb-1.5",
|
|
950
|
+
viewConfig.classNames?.monthCellFooter
|
|
951
|
+
)}
|
|
952
|
+
>
|
|
953
|
+
{viewConfig.showDayAddButton && !isInteracting && (
|
|
954
|
+
<button
|
|
955
|
+
type="button"
|
|
956
|
+
data-slot="event-calendar-day-add"
|
|
957
|
+
aria-label={settings.i18n.labels.addEvent}
|
|
958
|
+
// a different icon/markup is a renderMonthCell job
|
|
959
|
+
className={cn(
|
|
960
|
+
"bg-primary text-primary-foreground flex size-5 cursor-pointer items-center justify-center rounded-sm opacity-0 transition-opacity group-hover/ec-cell:opacity-100 focus-visible:opacity-100",
|
|
961
|
+
viewConfig.classNames?.dayAddButton
|
|
962
|
+
)}
|
|
963
|
+
onClick={(e) => {
|
|
964
|
+
e.stopPropagation()
|
|
965
|
+
settings.onSlotClick?.(
|
|
966
|
+
{ date: day, allDay: true, view: "month" },
|
|
967
|
+
e
|
|
968
|
+
)
|
|
969
|
+
}}
|
|
970
|
+
>
|
|
971
|
+
<PlusIcon className="size-3.5" aria-hidden="true" />
|
|
972
|
+
</button>
|
|
973
|
+
)}
|
|
974
|
+
<span
|
|
975
|
+
data-slot="event-calendar-month-day-number"
|
|
976
|
+
className={cn(
|
|
977
|
+
"flex size-5 items-center justify-center rounded-full",
|
|
978
|
+
isOutside && "text-muted-foreground",
|
|
979
|
+
// the filled circle already marks today; keep the number the same
|
|
980
|
+
// weight/size as the other days so it does not read as larger
|
|
981
|
+
// Same font-size as every other day; a lighter weight cancels the
|
|
982
|
+
// way white digits on the filled circle read bolder/larger than the
|
|
983
|
+
// dark-on-light numbers around them.
|
|
984
|
+
isToday && "bg-primary text-primary-foreground font-light",
|
|
985
|
+
viewConfig.classNames?.monthDayNumber
|
|
986
|
+
)}
|
|
987
|
+
>
|
|
988
|
+
{format(
|
|
989
|
+
toZoned(day, settings.timeZone),
|
|
990
|
+
settings.i18n.formats.monthCellDay,
|
|
991
|
+
{ locale: settings.locale }
|
|
992
|
+
)}
|
|
993
|
+
</span>
|
|
994
|
+
</div>
|
|
995
|
+
</>
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
const content =
|
|
999
|
+
viewConfig.renderMonthCell?.({
|
|
1000
|
+
day,
|
|
1001
|
+
segments,
|
|
1002
|
+
isToday,
|
|
1003
|
+
isOutside,
|
|
1004
|
+
overflowCount,
|
|
1005
|
+
defaultContent,
|
|
1006
|
+
}) ?? defaultContent
|
|
1007
|
+
|
|
1008
|
+
return (
|
|
1009
|
+
<div
|
|
1010
|
+
ref={rootRef}
|
|
1011
|
+
role="gridcell"
|
|
1012
|
+
data-slot="event-calendar-month-cell"
|
|
1013
|
+
data-today={isToday || undefined}
|
|
1014
|
+
data-outside={isOutside || undefined}
|
|
1015
|
+
data-weekend={
|
|
1016
|
+
settings.weekendDays.includes(
|
|
1017
|
+
toZoned(day, settings.timeZone).getDay()
|
|
1018
|
+
) || undefined
|
|
1019
|
+
}
|
|
1020
|
+
data-ec-day={dayStart.getTime()}
|
|
1021
|
+
data-drop-target={isDropTarget ?? undefined}
|
|
1022
|
+
data-off={isOff || undefined}
|
|
1023
|
+
data-draft={inDraft ? "" : undefined}
|
|
1024
|
+
aria-label={format(
|
|
1025
|
+
toZoned(day, settings.timeZone),
|
|
1026
|
+
settings.i18n.formats.monthCellAriaLabel,
|
|
1027
|
+
{ locale: settings.locale }
|
|
1028
|
+
)}
|
|
1029
|
+
className={cn(
|
|
1030
|
+
"group/ec-cell relative flex min-h-0 min-w-0 flex-col overflow-hidden",
|
|
1031
|
+
!isLast && "border-e",
|
|
1032
|
+
isOutside && !settings.showOutsideDays && "invisible",
|
|
1033
|
+
isOff && offClassName,
|
|
1034
|
+
isToday &&
|
|
1035
|
+
cn(
|
|
1036
|
+
"bg-primary/3 border-b-primary/40 relative border-b-2",
|
|
1037
|
+
viewConfig.todayClassName
|
|
1038
|
+
),
|
|
1039
|
+
viewConfig.dayClassName?.(day),
|
|
1040
|
+
// No drop-target bg fill on move/resize - the dragged bar + not-allowed
|
|
1041
|
+
// cursor carry the feedback; a cell-wide color wash is too distracting.
|
|
1042
|
+
// data-drop-target stays as an opt-in styling hook.
|
|
1043
|
+
inDraft && EVENT_CALENDAR_SLOT_DRAFT.surface,
|
|
1044
|
+
viewConfig.classNames?.monthCell
|
|
1045
|
+
)}
|
|
1046
|
+
onPointerDown={(e) => {
|
|
1047
|
+
const target = e.target as HTMLElement
|
|
1048
|
+
if (target.closest("[data-slot=event-calendar-event]")) return
|
|
1049
|
+
if (target.closest("[data-slot=event-calendar-more]")) return
|
|
1050
|
+
gestures.beginCreate(e, day, true)
|
|
1051
|
+
}}
|
|
1052
|
+
onClick={(e) => {
|
|
1053
|
+
if (wasRecentDrag() || wasRecentChipPress()) return
|
|
1054
|
+
settings.onSlotClick?.({ date: day, allDay: true, view: "month" }, e)
|
|
1055
|
+
}}
|
|
1056
|
+
>
|
|
1057
|
+
{inDraft && (
|
|
1058
|
+
<span
|
|
1059
|
+
aria-hidden
|
|
1060
|
+
data-slot="event-calendar-slot-draft"
|
|
1061
|
+
className={cn(
|
|
1062
|
+
"pointer-events-none absolute inset-0 z-10",
|
|
1063
|
+
EVENT_CALENDAR_SLOT_DRAFT.segment,
|
|
1064
|
+
inDraft.isStart && EVENT_CALENDAR_SLOT_DRAFT.segmentStart,
|
|
1065
|
+
inDraft.isEnd && EVENT_CALENDAR_SLOT_DRAFT.segmentEnd,
|
|
1066
|
+
viewConfig.classNames?.slotDraft
|
|
1067
|
+
)}
|
|
1068
|
+
>
|
|
1069
|
+
{/* Date-range readout, the month-view counterpart of the time-grid
|
|
1070
|
+
one. Rendered only on the FIRST cell of the selection, otherwise
|
|
1071
|
+
a three-day drag would repeat the same label three times. Pinned
|
|
1072
|
+
to the bottom because the day number owns the top of the cell,
|
|
1073
|
+
and given its own surface so it stays legible over event chips. */}
|
|
1074
|
+
{inDraft.isStart && draftRange && (
|
|
1075
|
+
<span
|
|
1076
|
+
className={cn(
|
|
1077
|
+
"absolute inset-x-1 bottom-1",
|
|
1078
|
+
EVENT_CALENDAR_SLOT_DRAFT.label
|
|
1079
|
+
)}
|
|
1080
|
+
>
|
|
1081
|
+
{settings.i18n.functions.formatDayRange(
|
|
1082
|
+
{
|
|
1083
|
+
start: toZoned(draftRange.start, settings.timeZone),
|
|
1084
|
+
end: toZoned(draftRange.end, settings.timeZone),
|
|
1085
|
+
},
|
|
1086
|
+
{ locale: settings.locale }
|
|
1087
|
+
)}
|
|
1088
|
+
</span>
|
|
1089
|
+
)}
|
|
1090
|
+
</span>
|
|
1091
|
+
)}
|
|
1092
|
+
{content}
|
|
1093
|
+
</div>
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
interface EventCalendarMoreIndicatorProps {
|
|
1098
|
+
day: Date
|
|
1099
|
+
count: number
|
|
1100
|
+
/** The OVERFLOW (hidden) segments for this day - bars first, then timed. The
|
|
1101
|
+
* popover lists only these, never the chips already visible in the cell. */
|
|
1102
|
+
segments: EventCalendarSegment[]
|
|
1103
|
+
/** Set while a timed chip is dragged and will land in THIS overflow bucket:
|
|
1104
|
+
* the indicator itself becomes the drop placeholder (dashed, event-tinted) so
|
|
1105
|
+
* it reads as "the chip joins the +N more list". */
|
|
1106
|
+
dropInto?: { color?: string; valid: boolean }
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* "+N more" trigger opening a popover with the day's full event list.
|
|
1111
|
+
* onMoreClick returning false suppresses the built-in popover.
|
|
1112
|
+
*/
|
|
1113
|
+
function EventCalendarMoreIndicator({
|
|
1114
|
+
day,
|
|
1115
|
+
count,
|
|
1116
|
+
segments,
|
|
1117
|
+
dropInto,
|
|
1118
|
+
}: EventCalendarMoreIndicatorProps) {
|
|
1119
|
+
const settings = useEventCalendarSettings()
|
|
1120
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
1121
|
+
const [open, setOpen] = useState(false)
|
|
1122
|
+
const headerId = useId()
|
|
1123
|
+
|
|
1124
|
+
// Grabbing a chip from this list starts a drag; close the popover so it does
|
|
1125
|
+
// not sit over the drop target while the event is carried to another day.
|
|
1126
|
+
const isDragging = useEventCalendarSelector<unknown, boolean>(
|
|
1127
|
+
(state) => state.drag !== null
|
|
1128
|
+
)
|
|
1129
|
+
useEffect(() => {
|
|
1130
|
+
if (isDragging) setOpen(false)
|
|
1131
|
+
}, [isDragging])
|
|
1132
|
+
|
|
1133
|
+
return (
|
|
1134
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
1135
|
+
<PopoverTrigger
|
|
1136
|
+
data-slot="event-calendar-more"
|
|
1137
|
+
data-drop-into={dropInto ? "" : undefined}
|
|
1138
|
+
data-drop-invalid={dropInto && !dropInto.valid ? "" : undefined}
|
|
1139
|
+
className={cn(
|
|
1140
|
+
"text-muted-foreground hover:text-foreground cursor-pointer truncate rounded-sm px-1.5 text-start",
|
|
1141
|
+
// While a dragged chip will land in this overflow bucket, the "+N more"
|
|
1142
|
+
// link itself becomes the drop placeholder: dashed, event-tinted, one
|
|
1143
|
+
// chip tall, so the drop target is unmistakable.
|
|
1144
|
+
dropInto &&
|
|
1145
|
+
cn(
|
|
1146
|
+
"flex shrink-0 items-center border border-dashed",
|
|
1147
|
+
dropInto.valid
|
|
1148
|
+
? "text-foreground border-(--ec-event-color)/50 bg-(--ec-event-color)/8"
|
|
1149
|
+
: "border-destructive/70 bg-destructive/10 text-destructive"
|
|
1150
|
+
),
|
|
1151
|
+
viewConfig.classNames?.moreIndicator
|
|
1152
|
+
)}
|
|
1153
|
+
style={
|
|
1154
|
+
dropInto
|
|
1155
|
+
? ({
|
|
1156
|
+
"--ec-event-color": dropInto.color ?? "var(--color-primary)",
|
|
1157
|
+
height: "calc(var(--ec-month-bar-h, 1.75rem) - 0.125rem)",
|
|
1158
|
+
} as CSSProperties)
|
|
1159
|
+
: undefined
|
|
1160
|
+
}
|
|
1161
|
+
onClick={(e: React.MouseEvent) => {
|
|
1162
|
+
e.stopPropagation()
|
|
1163
|
+
const verdict = settings.onMoreClick?.(
|
|
1164
|
+
day,
|
|
1165
|
+
segments.map((segment) => segment.occurrence),
|
|
1166
|
+
e
|
|
1167
|
+
)
|
|
1168
|
+
if (verdict === false) {
|
|
1169
|
+
e.preventDefault()
|
|
1170
|
+
setOpen(false)
|
|
1171
|
+
}
|
|
1172
|
+
}}
|
|
1173
|
+
>
|
|
1174
|
+
{viewConfig.renderMoreIndicator?.({ day, count, segments }) ??
|
|
1175
|
+
settings.i18n.labels.more(count)}
|
|
1176
|
+
</PopoverTrigger>
|
|
1177
|
+
<PopoverContent
|
|
1178
|
+
data-slot="event-calendar-more-popover"
|
|
1179
|
+
align={viewConfig.morePopoverAlign}
|
|
1180
|
+
// The popover is a dialog, so it needs a name. The built-in body already
|
|
1181
|
+
// renders the day header this list belongs to, so point at that; a
|
|
1182
|
+
// consumer body has no header to point at and gets the same formatted
|
|
1183
|
+
// day as a label instead.
|
|
1184
|
+
aria-labelledby={viewConfig.renderMoreContent ? undefined : headerId}
|
|
1185
|
+
aria-label={
|
|
1186
|
+
viewConfig.renderMoreContent
|
|
1187
|
+
? format(
|
|
1188
|
+
toZoned(day, settings.timeZone),
|
|
1189
|
+
settings.i18n.formats.moreDayHeader,
|
|
1190
|
+
{ locale: settings.locale }
|
|
1191
|
+
)
|
|
1192
|
+
: undefined
|
|
1193
|
+
}
|
|
1194
|
+
// PopoverContent is unlayered (flex-col gap-4 p-4); override with !.
|
|
1195
|
+
// text-xs re-establishes the calendar's base type here because this
|
|
1196
|
+
// content is portaled out of the root subtree and cannot inherit it.
|
|
1197
|
+
className={cn(
|
|
1198
|
+
"w-64 gap-1! p-2! text-xs",
|
|
1199
|
+
viewConfig.classNames?.morePopover
|
|
1200
|
+
)}
|
|
1201
|
+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
1202
|
+
>
|
|
1203
|
+
{viewConfig.renderMoreContent ? (
|
|
1204
|
+
viewConfig.renderMoreContent({
|
|
1205
|
+
day,
|
|
1206
|
+
segments,
|
|
1207
|
+
close: () => setOpen(false),
|
|
1208
|
+
})
|
|
1209
|
+
) : (
|
|
1210
|
+
<EventCalendarMoreDefaultContent
|
|
1211
|
+
day={day}
|
|
1212
|
+
segments={segments}
|
|
1213
|
+
headerId={headerId}
|
|
1214
|
+
/>
|
|
1215
|
+
)}
|
|
1216
|
+
</PopoverContent>
|
|
1217
|
+
</Popover>
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** Built-in "+N more" popover body: day header + the day's chips. */
|
|
1222
|
+
function EventCalendarMoreDefaultContent({
|
|
1223
|
+
day,
|
|
1224
|
+
segments,
|
|
1225
|
+
headerId,
|
|
1226
|
+
}: {
|
|
1227
|
+
day: Date
|
|
1228
|
+
segments: EventCalendarSegment[]
|
|
1229
|
+
/** Names the popover dialog: the header IS the list's accessible name. */
|
|
1230
|
+
headerId?: string
|
|
1231
|
+
}) {
|
|
1232
|
+
const settings = useEventCalendarSettings()
|
|
1233
|
+
const viewConfig = useEventCalendarViewConfig()
|
|
1234
|
+
return (
|
|
1235
|
+
<>
|
|
1236
|
+
<div
|
|
1237
|
+
id={headerId}
|
|
1238
|
+
className={cn(
|
|
1239
|
+
"text-muted-foreground px-1 py-1 text-xs font-medium",
|
|
1240
|
+
viewConfig.classNames?.morePopoverHeader
|
|
1241
|
+
)}
|
|
1242
|
+
>
|
|
1243
|
+
{format(
|
|
1244
|
+
toZoned(day, settings.timeZone),
|
|
1245
|
+
settings.i18n.formats.moreDayHeader,
|
|
1246
|
+
{ locale: settings.locale }
|
|
1247
|
+
)}
|
|
1248
|
+
</div>
|
|
1249
|
+
{/* The scroll region breaks out of the popover's right padding (-me-2)
|
|
1250
|
+
so the scrollbar sits flush in the gutter; the list then pads itself
|
|
1251
|
+
back (ps-1 aligns with the header, pe-4 clears the ~10px bar with a
|
|
1252
|
+
gap) and adds py-1 so the first/last focus ring is not clipped by
|
|
1253
|
+
the overflow. Layout is identical with or without a scrollbar. */}
|
|
1254
|
+
{viewConfig.scrollbars === "native" ? (
|
|
1255
|
+
<div
|
|
1256
|
+
data-ec-native-scroll=""
|
|
1257
|
+
className="-me-2 max-h-(--ec-more-max-height,16rem) min-h-0 overflow-y-auto"
|
|
1258
|
+
>
|
|
1259
|
+
<div className="flex flex-col gap-1 py-1 ps-1 pe-4">
|
|
1260
|
+
{segments.map((segment) => (
|
|
1261
|
+
<EventCalendarEvent
|
|
1262
|
+
key={segment.occurrence.key}
|
|
1263
|
+
segment={segment}
|
|
1264
|
+
className="py-0.5"
|
|
1265
|
+
/>
|
|
1266
|
+
))}
|
|
1267
|
+
</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
) : (
|
|
1270
|
+
<ScrollArea className="-me-2 min-h-0 **:data-[slot=scroll-area-viewport]:max-h-(--ec-more-max-height,16rem)">
|
|
1271
|
+
<div className="flex flex-col gap-1 py-1 ps-1 pe-4">
|
|
1272
|
+
{segments.map((segment) => (
|
|
1273
|
+
<EventCalendarEvent
|
|
1274
|
+
key={segment.occurrence.key}
|
|
1275
|
+
segment={segment}
|
|
1276
|
+
className="py-0.5"
|
|
1277
|
+
/>
|
|
1278
|
+
))}
|
|
1279
|
+
</div>
|
|
1280
|
+
</ScrollArea>
|
|
1281
|
+
)}
|
|
1282
|
+
</>
|
|
1283
|
+
)
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export { EventCalendarMonthView, EventCalendarMoreIndicator }
|
|
1287
|
+
export type { EventCalendarMonthViewProps, EventCalendarMoreIndicatorProps }
|