@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,1062 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo } from "react"
|
|
2
|
+
import {
|
|
3
|
+
useEventCalendar,
|
|
4
|
+
useEventCalendarViewConfig,
|
|
5
|
+
type EventCalendarInstance,
|
|
6
|
+
} from "@/components/reui/event-calendar/event-calendar"
|
|
7
|
+
import {
|
|
8
|
+
snapMinutes,
|
|
9
|
+
toZoned,
|
|
10
|
+
zonedStartOfDay,
|
|
11
|
+
} from "@/components/reui/event-calendar/event-calendar-lib"
|
|
12
|
+
import type {
|
|
13
|
+
EventCalendarProposedUpdate,
|
|
14
|
+
EventCalendarSegment,
|
|
15
|
+
} from "@/components/reui/event-calendar/event-calendar-types"
|
|
16
|
+
import { addDays, addMinutes, differenceInCalendarDays } from "date-fns"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Activation policy (dnd-kit parity where proven):
|
|
20
|
+
* mouse move 5px before a drag starts (below = click), create 4px;
|
|
21
|
+
* touch long-press 250ms with 5px tolerance (movement past tolerance
|
|
22
|
+
* before the delay cancels the drag so taps stay taps).
|
|
23
|
+
*/
|
|
24
|
+
const EVENT_CALENDAR_ACTIVATION = {
|
|
25
|
+
moveDistancePx: 5,
|
|
26
|
+
createDistancePx: 4,
|
|
27
|
+
touchDelayMs: 250,
|
|
28
|
+
touchTolerancePx: 5,
|
|
29
|
+
autoScrollEdgePx: 48,
|
|
30
|
+
autoScrollMaxStepPx: 15,
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
type GestureKind = "move" | "resize-start" | "resize-end" | "create"
|
|
34
|
+
|
|
35
|
+
interface TimeColumnRect {
|
|
36
|
+
day: Date
|
|
37
|
+
rect: DOMRect
|
|
38
|
+
boundsStartMin: number
|
|
39
|
+
boundsEndMin: number
|
|
40
|
+
resourceId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DayCellRect {
|
|
44
|
+
day: Date
|
|
45
|
+
rect: DOMRect
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Surface {
|
|
49
|
+
/** Minute-precise day columns (week/day/days) or resource columns. */
|
|
50
|
+
columns: TimeColumnRect[]
|
|
51
|
+
/** Day-precise cells (month grid, all-day row). */
|
|
52
|
+
cells: DayCellRect[]
|
|
53
|
+
viewport: HTMLElement | null
|
|
54
|
+
/**
|
|
55
|
+
* Viewport rect captured ONCE at gesture start. The scroll container does
|
|
56
|
+
* not move on screen while a pointer drag is captured (auto-scroll changes
|
|
57
|
+
* its scrollTop, not its box), so reusing this avoids a per-pointermove
|
|
58
|
+
* getBoundingClientRect - that read forces a synchronous full-document
|
|
59
|
+
* reflow, which is cheap on a bare demo page but ~200ms on a long docs page
|
|
60
|
+
* (big prop tables re-lay-out on every flush), turning drag into a slideshow.
|
|
61
|
+
*/
|
|
62
|
+
viewportRect: DOMRect | null
|
|
63
|
+
viewportStartScrollTop: number
|
|
64
|
+
/**
|
|
65
|
+
* Live scrollTop, seeded from the start value and advanced by auto-scroll
|
|
66
|
+
* itself. Tracking it here lets pointerMinutes read a number instead of the
|
|
67
|
+
* DOM `scrollTop` property every move (another forced reflow).
|
|
68
|
+
*/
|
|
69
|
+
scrollTop: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Module flag so chip onClick can ignore the click that ends a drag. */
|
|
73
|
+
let lastGestureEndedAt = 0
|
|
74
|
+
function wasRecentDrag(): boolean {
|
|
75
|
+
return performance.now() - lastGestureEndedAt < 250
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A press that started on an event chip. Slot-create clicks consult this so a
|
|
80
|
+
* refused drag (e.g. a locked chip that never registers a gesture) whose
|
|
81
|
+
* trailing native click retargets to the empty grid does NOT open a create
|
|
82
|
+
* dialog. Refreshed on release so it covers long presses; the chip's own
|
|
83
|
+
* click-to-edit is unaffected (only grid slot-clicks check it).
|
|
84
|
+
*/
|
|
85
|
+
let lastChipPressAt = 0
|
|
86
|
+
function markChipPress(): void {
|
|
87
|
+
lastChipPressAt = performance.now()
|
|
88
|
+
window.addEventListener(
|
|
89
|
+
"pointerup",
|
|
90
|
+
() => {
|
|
91
|
+
lastChipPressAt = performance.now()
|
|
92
|
+
},
|
|
93
|
+
{ once: true, capture: true }
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
function wasRecentChipPress(): boolean {
|
|
97
|
+
return performance.now() - lastChipPressAt < 300
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Registry of in-flight gesture cancels. A gesture measures its surface (day
|
|
102
|
+
* columns and cells) once at activation and then lives on window listeners, so
|
|
103
|
+
* the CALENDAR - not the chip, chips legitimately unmount mid-gesture (lane
|
|
104
|
+
* repacking, a "+N more" popover closing) - is what must be able to abort it.
|
|
105
|
+
* Cancel fully reverts: listeners, overlays and the body drag state all clear,
|
|
106
|
+
* and no update is committed.
|
|
107
|
+
*/
|
|
108
|
+
const activeGestureCancels = new Set<() => void>()
|
|
109
|
+
|
|
110
|
+
/** Cancel (and fully revert) every in-flight event calendar pointer gesture. */
|
|
111
|
+
function cancelActiveEventCalendarGestures(): void {
|
|
112
|
+
for (const cancel of [...activeGestureCancels]) cancel()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Mounted-consumer count for the gestures hook. Every chip holds one, so only
|
|
117
|
+
* the LAST consumer leaving means the calendar itself is gone; aborting when
|
|
118
|
+
* any single chip unmounts would kill a drag the user is still holding.
|
|
119
|
+
*/
|
|
120
|
+
let gestureConsumers = 0
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Snap a translate offset to the device pixel grid. The cursor-following
|
|
124
|
+
* overlays (carry clone, drop hint) are their own `will-change: transform`
|
|
125
|
+
* compositing layers: the GPU rasterizes their text once and repositions that
|
|
126
|
+
* texture each frame, so a subpixel translate (getBoundingClientRect and raw
|
|
127
|
+
* clientX/Y are routinely fractional) resamples the texture and blurs the
|
|
128
|
+
* text. Rounding each offset to a whole device pixel lands the layer on the
|
|
129
|
+
* grid so glyphs stay crisp, without giving up the per-frame GPU transform.
|
|
130
|
+
*/
|
|
131
|
+
function snapToPixel(value: number): number {
|
|
132
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
|
|
133
|
+
return Math.round(value * dpr) / dpr
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectSurface(
|
|
137
|
+
origin: HTMLElement,
|
|
138
|
+
fallbackRoot?: HTMLElement | null
|
|
139
|
+
): Surface {
|
|
140
|
+
// A gesture from a portaled surface (the "+N more" popover) has no calendar
|
|
141
|
+
// ancestor, so fall back to the root the host registered on the instance.
|
|
142
|
+
const root =
|
|
143
|
+
origin.closest<HTMLElement>("[data-slot=event-calendar-time-grid]") ??
|
|
144
|
+
origin.closest<HTMLElement>("[data-slot=event-calendar-resource-view]") ??
|
|
145
|
+
origin.closest<HTMLElement>("[data-slot=event-calendar-month-view]") ??
|
|
146
|
+
origin.closest<HTMLElement>("[data-slot=event-calendar]") ??
|
|
147
|
+
fallbackRoot ??
|
|
148
|
+
null
|
|
149
|
+
|
|
150
|
+
const columns: TimeColumnRect[] = []
|
|
151
|
+
const cells: DayCellRect[] = []
|
|
152
|
+
if (root) {
|
|
153
|
+
for (const el of root.querySelectorAll<HTMLElement>("[data-ec-day]")) {
|
|
154
|
+
const day = new Date(Number(el.dataset.ecDay))
|
|
155
|
+
if (el.dataset.ecBoundsStart !== undefined) {
|
|
156
|
+
columns.push({
|
|
157
|
+
day,
|
|
158
|
+
rect: el.getBoundingClientRect(),
|
|
159
|
+
boundsStartMin: Number(el.dataset.ecBoundsStart),
|
|
160
|
+
boundsEndMin: Number(el.dataset.ecBoundsEnd),
|
|
161
|
+
resourceId: el.dataset.ecResource,
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
cells.push({ day, rect: el.getBoundingClientRect() })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const viewport =
|
|
169
|
+
root?.querySelector<HTMLElement>("[data-slot=scroll-area-viewport]") ?? null
|
|
170
|
+
const viewportStartScrollTop = viewport?.scrollTop ?? 0
|
|
171
|
+
return {
|
|
172
|
+
columns,
|
|
173
|
+
cells,
|
|
174
|
+
viewport,
|
|
175
|
+
viewportRect: viewport?.getBoundingClientRect() ?? null,
|
|
176
|
+
viewportStartScrollTop,
|
|
177
|
+
scrollTop: viewportStartScrollTop,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findColumn(
|
|
182
|
+
surface: Surface,
|
|
183
|
+
clientX: number
|
|
184
|
+
): TimeColumnRect | undefined {
|
|
185
|
+
const scrollAdjusted = surface.columns
|
|
186
|
+
let best: TimeColumnRect | undefined
|
|
187
|
+
for (const col of scrollAdjusted) {
|
|
188
|
+
if (clientX >= col.rect.left && clientX < col.rect.right) return col
|
|
189
|
+
if (!best) best = col
|
|
190
|
+
// clamp to nearest horizontal column
|
|
191
|
+
const bestDist = Math.min(
|
|
192
|
+
Math.abs(clientX - best.rect.left),
|
|
193
|
+
Math.abs(clientX - best.rect.right)
|
|
194
|
+
)
|
|
195
|
+
const dist = Math.min(
|
|
196
|
+
Math.abs(clientX - col.rect.left),
|
|
197
|
+
Math.abs(clientX - col.rect.right)
|
|
198
|
+
)
|
|
199
|
+
if (dist < bestDist) best = col
|
|
200
|
+
}
|
|
201
|
+
return best
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function findCell(
|
|
205
|
+
surface: Surface,
|
|
206
|
+
x: number,
|
|
207
|
+
y: number
|
|
208
|
+
): DayCellRect | undefined {
|
|
209
|
+
return surface.cells.find(
|
|
210
|
+
(cell) =>
|
|
211
|
+
x >= cell.rect.left &&
|
|
212
|
+
x < cell.rect.right &&
|
|
213
|
+
y >= cell.rect.top &&
|
|
214
|
+
y < cell.rect.bottom
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pointerMinutes(
|
|
219
|
+
surface: Surface,
|
|
220
|
+
col: TimeColumnRect,
|
|
221
|
+
clientY: number
|
|
222
|
+
): number {
|
|
223
|
+
const scrollDelta = surface.scrollTop - surface.viewportStartScrollTop
|
|
224
|
+
const boundsMinutes = col.boundsEndMin - col.boundsStartMin
|
|
225
|
+
const pxPerMinute = col.rect.height / Math.max(1, boundsMinutes)
|
|
226
|
+
const y = clientY - col.rect.top + scrollDelta
|
|
227
|
+
return col.boundsStartMin + y / pxPerMinute
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface BeginGestureConfig<TData> {
|
|
231
|
+
instance: EventCalendarInstance<TData>
|
|
232
|
+
kind: GestureKind
|
|
233
|
+
origin: HTMLElement
|
|
234
|
+
startEvent: PointerEvent
|
|
235
|
+
segment?: EventCalendarSegment<TData>
|
|
236
|
+
/** create only */
|
|
237
|
+
createDay?: Date
|
|
238
|
+
createAllDay?: boolean
|
|
239
|
+
/**
|
|
240
|
+
* Consumer classNames for the engine's vanilla-DOM overlays (carry clone,
|
|
241
|
+
* validation hint pill), forwarded by the gestures hook - the engine itself
|
|
242
|
+
* must never import from the React chip module.
|
|
243
|
+
*/
|
|
244
|
+
ui?: {
|
|
245
|
+
dragCarry?: string
|
|
246
|
+
dragCarryInvalid?: string
|
|
247
|
+
dropHint?: string
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A move/resize was refused (locked, per-event disabled, or interaction off).
|
|
253
|
+
* Rather than silently swallow the press, track the pointer: once it crosses
|
|
254
|
+
* the activation threshold - i.e. the user genuinely tried to drag - show a
|
|
255
|
+
* not-allowed cursor and broadcast once via onDragBlocked so the consumer can
|
|
256
|
+
* explain it. The calendar picks no message. Fires at most once per gesture.
|
|
257
|
+
*/
|
|
258
|
+
function beginBlockedGesture<TData>(
|
|
259
|
+
instance: EventCalendarInstance<TData>,
|
|
260
|
+
startEvent: PointerEvent,
|
|
261
|
+
segment: EventCalendarSegment<TData>,
|
|
262
|
+
gesture: "move" | "resize"
|
|
263
|
+
) {
|
|
264
|
+
const startX = startEvent.clientX
|
|
265
|
+
const startY = startEvent.clientY
|
|
266
|
+
const activation = {
|
|
267
|
+
...EVENT_CALENDAR_ACTIVATION,
|
|
268
|
+
...instance.settings.activation,
|
|
269
|
+
}
|
|
270
|
+
const event = segment.occurrence.event
|
|
271
|
+
const reason: "readOnly" | "disabled" | "interactions-off" = event.readOnly
|
|
272
|
+
? "readOnly"
|
|
273
|
+
: (gesture === "move" ? event.draggable : event.resizable) === false
|
|
274
|
+
? "disabled"
|
|
275
|
+
: "interactions-off"
|
|
276
|
+
const pointerId = startEvent.pointerId
|
|
277
|
+
let activated = false
|
|
278
|
+
let finished = false
|
|
279
|
+
const onMove = (e: PointerEvent) => {
|
|
280
|
+
if (e.pointerId !== pointerId || activated) return
|
|
281
|
+
if (
|
|
282
|
+
Math.hypot(e.clientX - startX, e.clientY - startY) <
|
|
283
|
+
activation.moveDistancePx
|
|
284
|
+
) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
activated = true
|
|
288
|
+
document.body.style.cursor = "not-allowed"
|
|
289
|
+
// body class alongside the inline cursor so consumer CSS can restyle or
|
|
290
|
+
// detect the blocked-drag state (mirrors "ec-dragging")
|
|
291
|
+
document.body.classList.add("ec-drag-blocked")
|
|
292
|
+
// only now is there anything to get stuck: focus loss means the release
|
|
293
|
+
// may never be delivered, leaving the cursor not-allowed document-wide
|
|
294
|
+
window.addEventListener("blur", cleanup)
|
|
295
|
+
instance.settings.onDragBlocked?.(segment.occurrence, { gesture, reason })
|
|
296
|
+
}
|
|
297
|
+
const cleanup = () => {
|
|
298
|
+
if (finished) return
|
|
299
|
+
finished = true
|
|
300
|
+
window.removeEventListener("pointermove", onMove)
|
|
301
|
+
window.removeEventListener("pointerup", onRelease)
|
|
302
|
+
window.removeEventListener("pointercancel", onRelease)
|
|
303
|
+
window.removeEventListener("blur", cleanup)
|
|
304
|
+
if (activated) {
|
|
305
|
+
document.body.style.cursor = ""
|
|
306
|
+
document.body.classList.remove("ec-drag-blocked")
|
|
307
|
+
// the pointer travelled: suppress the trailing click so the event's own
|
|
308
|
+
// dialog does not open on top of the rejection message
|
|
309
|
+
lastGestureEndedAt = performance.now()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// only the pointer that started the press may end it: a second finger
|
|
313
|
+
// lifting must not clear the not-allowed cursor out from under it
|
|
314
|
+
const onRelease = (e: PointerEvent) => {
|
|
315
|
+
if (e.pointerId !== pointerId) return
|
|
316
|
+
cleanup()
|
|
317
|
+
}
|
|
318
|
+
window.addEventListener("pointermove", onMove)
|
|
319
|
+
window.addEventListener("pointerup", onRelease)
|
|
320
|
+
window.addEventListener("pointercancel", onRelease)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function beginGesture<TData>(config: BeginGestureConfig<TData>) {
|
|
324
|
+
const { instance, kind, origin, startEvent, segment, ui } = config
|
|
325
|
+
const { settings, internals, api } = instance
|
|
326
|
+
const timeZone = settings.timeZone
|
|
327
|
+
const snap = settings.snapDuration
|
|
328
|
+
// per-calendar tuning shallow-merged over the module defaults
|
|
329
|
+
const activation = { ...EVENT_CALENDAR_ACTIVATION, ...settings.activation }
|
|
330
|
+
const startX = startEvent.clientX
|
|
331
|
+
const startY = startEvent.clientY
|
|
332
|
+
const pointerId = startEvent.pointerId
|
|
333
|
+
// Resolved once: the chip can be re-rendered away mid-gesture. A gesture
|
|
334
|
+
// from a portaled surface (the "+N more" popover) has no calendar ancestor,
|
|
335
|
+
// so fall back to the registered root - same reason as collectSurface.
|
|
336
|
+
const announcer =
|
|
337
|
+
origin
|
|
338
|
+
.closest<HTMLElement>("[data-slot=event-calendar]")
|
|
339
|
+
?.querySelector<HTMLElement>("[data-slot=event-calendar-announcer]") ??
|
|
340
|
+
internals
|
|
341
|
+
.getRootEl()
|
|
342
|
+
?.querySelector<HTMLElement>("[data-slot=event-calendar-announcer]") ??
|
|
343
|
+
null
|
|
344
|
+
|
|
345
|
+
const isTouch = startEvent.pointerType === "touch"
|
|
346
|
+
// resize activates immediately on precise pointers; on touch it waits for
|
|
347
|
+
// the same long-press as a move, so a finger landing on the (invisible on
|
|
348
|
+
// touch) handle strip can never start a resize the user never asked for
|
|
349
|
+
let active = kind.startsWith("resize") && !isTouch
|
|
350
|
+
let surface: Surface | null = active
|
|
351
|
+
? collectSurface(origin, internals.getRootEl())
|
|
352
|
+
: null
|
|
353
|
+
let rafScroll: number | null = null
|
|
354
|
+
let lastProposalKey = ""
|
|
355
|
+
let touchTimer: ReturnType<typeof setTimeout> | null = null
|
|
356
|
+
let hintEl: HTMLDivElement | null = null
|
|
357
|
+
let lastValid = true
|
|
358
|
+
// Drag-create anchor minute, frozen at the first proposal: re-deriving it
|
|
359
|
+
// from the stale startY client coordinate + the CURRENT scroll delta would
|
|
360
|
+
// make the anchor drift with auto-scroll instead of staying pinned.
|
|
361
|
+
let createAnchorMin: number | null = null
|
|
362
|
+
let lastPointer: PointerEvent = startEvent
|
|
363
|
+
|
|
364
|
+
const occurrence = segment?.occurrence
|
|
365
|
+
const isBar = occurrence
|
|
366
|
+
? occurrence.allDay ||
|
|
367
|
+
occurrence.end.getTime() - occurrence.start.getTime() >
|
|
368
|
+
24 * 60 * 60 * 1000
|
|
369
|
+
: false
|
|
370
|
+
|
|
371
|
+
// Preserve the grab offset so the event does not jump to the pointer
|
|
372
|
+
let grabOffsetMin = 0
|
|
373
|
+
/**
|
|
374
|
+
* The grabbed CHIP, not the whole occurrence. A cross-midnight event renders
|
|
375
|
+
* one chip per day, so the offset above is measured in the grabbed chip's own
|
|
376
|
+
* day frame (mixing the two frames jumps a tail chip a full day on grab) and
|
|
377
|
+
* these two carry that chip back to the occurrence. For a chip that fits in
|
|
378
|
+
* one day the lead is 0 and the duration is the occurrence's, i.e. unchanged.
|
|
379
|
+
*/
|
|
380
|
+
let grabLeadMs = 0
|
|
381
|
+
let grabSegDurationMs = occurrence
|
|
382
|
+
? occurrence.end.getTime() - occurrence.start.getTime()
|
|
383
|
+
: 0
|
|
384
|
+
/**
|
|
385
|
+
* Which day of a multi-day bar was grabbed. Without it the day-granular move
|
|
386
|
+
* slides the bar's START under the pointer, so a Mon-Fri bar grabbed on
|
|
387
|
+
* Wednesday teleports two days forward on the first nudge.
|
|
388
|
+
*/
|
|
389
|
+
let grabDayOffset = 0
|
|
390
|
+
|
|
391
|
+
const activationDistance =
|
|
392
|
+
kind === "create" ? activation.createDistancePx : activation.moveDistancePx
|
|
393
|
+
|
|
394
|
+
// Each gesture keeps its own cursor: a resize must stay ns/ew-resize for
|
|
395
|
+
// the whole drag (flipping to grabbing reads as a move) - vertical for
|
|
396
|
+
// timed blocks, horizontal for day-granular bars. Moves grab.
|
|
397
|
+
const gestureCursor = kind.startsWith("resize")
|
|
398
|
+
? isBar
|
|
399
|
+
? "ew-resize"
|
|
400
|
+
: "ns-resize"
|
|
401
|
+
: "grabbing"
|
|
402
|
+
const setBodyDragging = (on: boolean, invalid = false) => {
|
|
403
|
+
document.body.classList.toggle("ec-dragging", on)
|
|
404
|
+
document.body.style.cursor = on
|
|
405
|
+
? invalid
|
|
406
|
+
? "not-allowed"
|
|
407
|
+
: gestureCursor
|
|
408
|
+
: ""
|
|
409
|
+
document.body.style.userSelect = on ? "none" : ""
|
|
410
|
+
if (!on) document.body.style.removeProperty("-webkit-user-select")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const activate = () => {
|
|
414
|
+
if (active) return
|
|
415
|
+
active = true
|
|
416
|
+
// armed with the gesture, not with the press: a press that never activates
|
|
417
|
+
// holds no drag state, no overlay and no body class to strand
|
|
418
|
+
window.addEventListener("blur", onWindowBlur)
|
|
419
|
+
surface = collectSurface(origin, internals.getRootEl())
|
|
420
|
+
if (kind === "move" && occurrence) {
|
|
421
|
+
const grabCell = findCell(surface, startX, startY)
|
|
422
|
+
if (grabCell) {
|
|
423
|
+
const originDay = zonedStartOfDay(occurrence.start, timeZone)
|
|
424
|
+
// end is exclusive, so step back an instant for the last covered day
|
|
425
|
+
const lastDay = zonedStartOfDay(
|
|
426
|
+
new Date(
|
|
427
|
+
Math.max(occurrence.end.getTime() - 1, occurrence.start.getTime())
|
|
428
|
+
),
|
|
429
|
+
timeZone
|
|
430
|
+
)
|
|
431
|
+
const offset = differenceInCalendarDays(
|
|
432
|
+
zonedStartOfDay(grabCell.day, timeZone),
|
|
433
|
+
originDay
|
|
434
|
+
)
|
|
435
|
+
// A gesture from the "+N more" popover grabs over whatever cell that
|
|
436
|
+
// floating surface happens to cover, so only a day the event actually
|
|
437
|
+
// spans can be the grabbed day.
|
|
438
|
+
if (
|
|
439
|
+
offset >= 0 &&
|
|
440
|
+
offset <= differenceInCalendarDays(lastDay, originDay)
|
|
441
|
+
) {
|
|
442
|
+
grabDayOffset = offset
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (surface.columns.length > 0 && !isBar) {
|
|
446
|
+
const col = findColumn(surface, startX)
|
|
447
|
+
if (col) {
|
|
448
|
+
const colDayStart = zonedStartOfDay(col.day, timeZone)
|
|
449
|
+
const segStartMs = Math.max(
|
|
450
|
+
occurrence.start.getTime(),
|
|
451
|
+
colDayStart.getTime()
|
|
452
|
+
)
|
|
453
|
+
grabLeadMs = segStartMs - occurrence.start.getTime()
|
|
454
|
+
grabSegDurationMs =
|
|
455
|
+
Math.min(
|
|
456
|
+
occurrence.end.getTime(),
|
|
457
|
+
addDays(colDayStart, 1).getTime()
|
|
458
|
+
) - segStartMs
|
|
459
|
+
grabOffsetMin =
|
|
460
|
+
pointerMinutes(surface, col, startY) -
|
|
461
|
+
(segStartMs - colDayStart.getTime()) / 60000
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
setBodyDragging(true)
|
|
466
|
+
createCarry()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const computeProposal = (
|
|
470
|
+
e: PointerEvent
|
|
471
|
+
): {
|
|
472
|
+
start: Date
|
|
473
|
+
end: Date
|
|
474
|
+
allDay: boolean
|
|
475
|
+
dayGranular?: boolean
|
|
476
|
+
resourceId?: string
|
|
477
|
+
} | null => {
|
|
478
|
+
if (!surface) return null
|
|
479
|
+
|
|
480
|
+
// ---- create: select a slot range
|
|
481
|
+
if (kind === "create") {
|
|
482
|
+
if (config.createAllDay || surface.columns.length === 0) {
|
|
483
|
+
const anchor = zonedStartOfDay(config.createDay!, timeZone)
|
|
484
|
+
const cell = findCell(surface, e.clientX, e.clientY)
|
|
485
|
+
const target = cell ? zonedStartOfDay(cell.day, timeZone) : anchor
|
|
486
|
+
const start = anchor <= target ? anchor : target
|
|
487
|
+
const end = addDays(anchor <= target ? target : anchor, 1)
|
|
488
|
+
return { start, end, allDay: true, dayGranular: true }
|
|
489
|
+
}
|
|
490
|
+
const col = findColumn(surface, startX)
|
|
491
|
+
if (!col) return null
|
|
492
|
+
if (createAnchorMin === null) {
|
|
493
|
+
createAnchorMin = snapMinutes(
|
|
494
|
+
pointerMinutes(surface, col, startY),
|
|
495
|
+
snap
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
const anchorMin = createAnchorMin
|
|
499
|
+
const curMin = snapMinutes(pointerMinutes(surface, col, e.clientY), snap)
|
|
500
|
+
const lo = Math.max(col.boundsStartMin, Math.min(anchorMin, curMin))
|
|
501
|
+
const hi = Math.min(
|
|
502
|
+
col.boundsEndMin,
|
|
503
|
+
Math.max(anchorMin, curMin, lo + snap)
|
|
504
|
+
)
|
|
505
|
+
const dayStart = zonedStartOfDay(col.day, timeZone)
|
|
506
|
+
return {
|
|
507
|
+
start: addMinutes(dayStart, lo),
|
|
508
|
+
end: addMinutes(dayStart, hi),
|
|
509
|
+
allDay: false,
|
|
510
|
+
resourceId: col.resourceId,
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!occurrence) return null
|
|
515
|
+
const durationMs = occurrence.end.getTime() - occurrence.start.getTime()
|
|
516
|
+
|
|
517
|
+
// ---- day-granularity: month cells and bars in the all-day row
|
|
518
|
+
const overCell = findCell(surface, e.clientX, e.clientY)
|
|
519
|
+
|
|
520
|
+
// A timed event dropped on the all-day lane (a day cell inside a surface
|
|
521
|
+
// that also has time columns) converts to a full-day event on that day.
|
|
522
|
+
if (
|
|
523
|
+
kind === "move" &&
|
|
524
|
+
!isBar &&
|
|
525
|
+
surface.columns.length > 0 &&
|
|
526
|
+
overCell !== undefined
|
|
527
|
+
) {
|
|
528
|
+
const targetDay = zonedStartOfDay(overCell.day, timeZone)
|
|
529
|
+
return {
|
|
530
|
+
start: targetDay,
|
|
531
|
+
end: addDays(targetDay, 1),
|
|
532
|
+
allDay: true,
|
|
533
|
+
dayGranular: true,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const useCells =
|
|
538
|
+
surface.columns.length === 0 || (isBar && overCell !== undefined)
|
|
539
|
+
|
|
540
|
+
if (useCells) {
|
|
541
|
+
const cell = overCell ?? findCell(surface, startX, startY)
|
|
542
|
+
if (!cell) return null
|
|
543
|
+
const targetDay = zonedStartOfDay(cell.day, timeZone)
|
|
544
|
+
if (kind === "move") {
|
|
545
|
+
const originDay = zonedStartOfDay(occurrence.start, timeZone)
|
|
546
|
+
// minus the grabbed day: the bar follows the pointer by the distance
|
|
547
|
+
// travelled, it does not re-anchor its start under the pointer
|
|
548
|
+
const delta =
|
|
549
|
+
differenceInCalendarDays(targetDay, originDay) - grabDayOffset
|
|
550
|
+
const start = addDays(toZoned(occurrence.start, timeZone), delta)
|
|
551
|
+
return {
|
|
552
|
+
start,
|
|
553
|
+
end: new Date(start.getTime() + durationMs),
|
|
554
|
+
allDay: occurrence.allDay,
|
|
555
|
+
dayGranular: true,
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// bar edge resize: day granularity
|
|
559
|
+
if (kind === "resize-start") {
|
|
560
|
+
const time =
|
|
561
|
+
occurrence.start.getTime() -
|
|
562
|
+
zonedStartOfDay(occurrence.start, timeZone).getTime()
|
|
563
|
+
const start = new Date(targetDay.getTime() + time)
|
|
564
|
+
if (start >= occurrence.end) return null
|
|
565
|
+
return {
|
|
566
|
+
start,
|
|
567
|
+
end: occurrence.end,
|
|
568
|
+
allDay: occurrence.allDay,
|
|
569
|
+
dayGranular: true,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const time = occurrence.allDay
|
|
573
|
+
? 0
|
|
574
|
+
: occurrence.end.getTime() -
|
|
575
|
+
zonedStartOfDay(occurrence.end, timeZone).getTime()
|
|
576
|
+
const end = occurrence.allDay
|
|
577
|
+
? addDays(targetDay, 1)
|
|
578
|
+
: new Date(addDays(targetDay, time > 0 ? 0 : 1).getTime() + time)
|
|
579
|
+
if (end <= occurrence.start) return null
|
|
580
|
+
return {
|
|
581
|
+
start: occurrence.start,
|
|
582
|
+
end,
|
|
583
|
+
allDay: occurrence.allDay,
|
|
584
|
+
dayGranular: true,
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ---- minute-granularity: time-grid columns
|
|
589
|
+
const col = findColumn(surface, e.clientX)
|
|
590
|
+
if (!col) return null
|
|
591
|
+
const dayStart = zonedStartOfDay(col.day, timeZone)
|
|
592
|
+
const rawMin = pointerMinutes(surface, col, e.clientY)
|
|
593
|
+
|
|
594
|
+
if (kind === "move") {
|
|
595
|
+
const newStartMin = snapMinutes(rawMin - grabOffsetMin, snap)
|
|
596
|
+
// the clamp keeps the grabbed CHIP inside the column it is over; the
|
|
597
|
+
// lead then carries the rest of a cross-midnight occurrence with it
|
|
598
|
+
const chipDurationMin = Math.round(grabSegDurationMs / 60000)
|
|
599
|
+
const clamped = Math.min(
|
|
600
|
+
Math.max(newStartMin, col.boundsStartMin),
|
|
601
|
+
col.boundsEndMin - chipDurationMin
|
|
602
|
+
)
|
|
603
|
+
const start = new Date(
|
|
604
|
+
addMinutes(dayStart, clamped).getTime() - grabLeadMs
|
|
605
|
+
)
|
|
606
|
+
return {
|
|
607
|
+
start,
|
|
608
|
+
end: new Date(start.getTime() + durationMs),
|
|
609
|
+
allDay: false,
|
|
610
|
+
resourceId: col.resourceId,
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const min = snapMinutes(rawMin, snap)
|
|
615
|
+
// Both endpoints expressed in the POINTED column's day coordinates.
|
|
616
|
+
// Anchoring each endpoint to its OWN day breaks cross-midnight events:
|
|
617
|
+
// a 23:30 pointer in the start day's column would apply 1410 minutes to
|
|
618
|
+
// the end's next-day anchor and jump the end a full day late (and a
|
|
619
|
+
// midnight-ending event computes occEndMin 0 against its own day).
|
|
620
|
+
const occStartMinInCol = Math.round(
|
|
621
|
+
(occurrence.start.getTime() - dayStart.getTime()) / 60000
|
|
622
|
+
)
|
|
623
|
+
const occEndMinInCol = Math.round(
|
|
624
|
+
(occurrence.end.getTime() - dayStart.getTime()) / 60000
|
|
625
|
+
)
|
|
626
|
+
if (kind === "resize-start") {
|
|
627
|
+
const clamped = Math.min(
|
|
628
|
+
Math.max(min, col.boundsStartMin),
|
|
629
|
+
Math.min(occEndMinInCol - snap, col.boundsEndMin)
|
|
630
|
+
)
|
|
631
|
+
const start = addMinutes(dayStart, clamped)
|
|
632
|
+
if (start >= occurrence.end) return null
|
|
633
|
+
return { start, end: occurrence.end, allDay: false }
|
|
634
|
+
}
|
|
635
|
+
const clamped = Math.max(
|
|
636
|
+
Math.min(min, col.boundsEndMin),
|
|
637
|
+
Math.max(occStartMinInCol + snap, col.boundsStartMin)
|
|
638
|
+
)
|
|
639
|
+
const end = addMinutes(dayStart, clamped)
|
|
640
|
+
if (end <= occurrence.start) return null
|
|
641
|
+
return { start: occurrence.start, end, allDay: false }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const applyProposal = (e: PointerEvent) => {
|
|
645
|
+
const proposal = computeProposal(e)
|
|
646
|
+
if (!proposal) return
|
|
647
|
+
const key = `${proposal.start.getTime()}-${proposal.end.getTime()}-${proposal.allDay}-${proposal.resourceId ?? ""}`
|
|
648
|
+
if (key === lastProposalKey) return
|
|
649
|
+
lastProposalKey = key
|
|
650
|
+
|
|
651
|
+
if (kind === "create") {
|
|
652
|
+
const draft = { ...proposal, view: instance.getState().view }
|
|
653
|
+
if (settings.canSelectSlot && !settings.canSelectSlot(draft)) return
|
|
654
|
+
internals.setSlotDraft(draft)
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
const update: EventCalendarProposedUpdate<TData> = {
|
|
658
|
+
event: occurrence!.event,
|
|
659
|
+
occurrence: occurrence!,
|
|
660
|
+
...proposal,
|
|
661
|
+
source: kind as "drag" | "resize-start" | "resize-end",
|
|
662
|
+
}
|
|
663
|
+
if (kind === "move") update.source = "drag"
|
|
664
|
+
const valid = settings.canDropEvent ? settings.canDropEvent(update) : true
|
|
665
|
+
lastValid = valid
|
|
666
|
+
setBodyDragging(true, !valid)
|
|
667
|
+
internals.setDrag({
|
|
668
|
+
kind: kind === "move" ? "move" : (kind as "resize-start" | "resize-end"),
|
|
669
|
+
occurrence: occurrence!,
|
|
670
|
+
proposedStart: proposal.start,
|
|
671
|
+
proposedEnd: proposal.end,
|
|
672
|
+
proposedAllDay: proposal.allDay,
|
|
673
|
+
proposedDayGranular: proposal.dayGranular ?? false,
|
|
674
|
+
proposedResourceId: proposal.resourceId,
|
|
675
|
+
valid,
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Cursor-attached carry clone for MOVE gestures: the event travels freely
|
|
680
|
+
// with the pointer, like an absolutely positioned overlay, while the
|
|
681
|
+
// in-grid ghost renders only a faint dashed placeholder at the snapped
|
|
682
|
+
// drop slot (EVENT_CALENDAR_GHOST.move). Vanilla DOM: cloned once at
|
|
683
|
+
// activation, transformed per pointermove, zero React work per frame.
|
|
684
|
+
let carryEl: HTMLDivElement | null = null
|
|
685
|
+
let carryDX = 0
|
|
686
|
+
let carryDY = 0
|
|
687
|
+
const CARRY_CLASS =
|
|
688
|
+
"bg-background pointer-events-none fixed top-0 left-0 z-100 overflow-hidden rounded-sm opacity-90 shadow-md will-change-transform" +
|
|
689
|
+
(ui?.dragCarry ? " " + ui.dragCarry : "")
|
|
690
|
+
const CARRY_INVALID_CLASS =
|
|
691
|
+
`${CARRY_CLASS} ring-destructive/60 ring-1` +
|
|
692
|
+
(ui?.dragCarryInvalid ? " " + ui.dragCarryInvalid : "")
|
|
693
|
+
|
|
694
|
+
const createCarry = () => {
|
|
695
|
+
if (kind !== "move") return
|
|
696
|
+
const chip =
|
|
697
|
+
origin.closest<HTMLElement>("[data-slot=event-calendar-event]") ?? origin
|
|
698
|
+
const rect = chip.getBoundingClientRect()
|
|
699
|
+
// The clone is re-parented to <body>, escaping the calendar's inherited
|
|
700
|
+
// font-size, so copy the source chip's resolved type - this keeps the carry
|
|
701
|
+
// matching the grid at any consumer text scale (e.g. root text-sm).
|
|
702
|
+
const chipFont = getComputedStyle(chip)
|
|
703
|
+
carryDX = startX - rect.left
|
|
704
|
+
carryDY = startY - rect.top
|
|
705
|
+
carryEl = document.createElement("div")
|
|
706
|
+
carryEl.setAttribute("data-slot", "event-calendar-drag-carry")
|
|
707
|
+
carryEl.setAttribute("aria-hidden", "true")
|
|
708
|
+
carryEl.className = CARRY_CLASS
|
|
709
|
+
carryEl.style.width = `${rect.width}px`
|
|
710
|
+
carryEl.style.height = `${rect.height}px`
|
|
711
|
+
carryEl.style.fontSize = chipFont.fontSize
|
|
712
|
+
carryEl.style.lineHeight = chipFont.lineHeight
|
|
713
|
+
carryEl.style.transform = `translate3d(${snapToPixel(rect.left)}px, ${snapToPixel(rect.top)}px, 0)`
|
|
714
|
+
const clone = chip.cloneNode(true) as HTMLElement
|
|
715
|
+
clone.removeAttribute("data-dragging")
|
|
716
|
+
clone.style.width = "100%"
|
|
717
|
+
clone.style.height = "100%"
|
|
718
|
+
carryEl.appendChild(clone)
|
|
719
|
+
document.body.appendChild(carryEl)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const positionCarry = (e: PointerEvent) => {
|
|
723
|
+
if (!carryEl) return
|
|
724
|
+
carryEl.style.transform = `translate3d(${snapToPixel(e.clientX - carryDX)}px, ${snapToPixel(e.clientY - carryDY)}px, 0)`
|
|
725
|
+
const cls = lastValid ? CARRY_CLASS : CARRY_INVALID_CLASS
|
|
726
|
+
if (carryEl.className !== cls) carryEl.className = cls
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Cursor-following validation hint, visible ONLY while the proposal is
|
|
730
|
+
// rejected (canDropEvent / bounds). Vanilla DOM: zero React work per frame;
|
|
731
|
+
// pairs with the not-allowed cursor and the ghost's destructive marking.
|
|
732
|
+
const updateHint = (e: PointerEvent) => {
|
|
733
|
+
if (lastValid || !active) {
|
|
734
|
+
hintEl?.remove()
|
|
735
|
+
hintEl = null
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
if (!hintEl) {
|
|
739
|
+
hintEl = document.createElement("div")
|
|
740
|
+
hintEl.setAttribute("data-slot", "event-calendar-drop-hint")
|
|
741
|
+
// physical left-0 anchor: translate3d positions in physical clientX
|
|
742
|
+
// coordinates, so a logical start-0 anchor would fling it off-screen
|
|
743
|
+
// in RTL documents
|
|
744
|
+
hintEl.className =
|
|
745
|
+
"bg-background text-destructive border-destructive/40 pointer-events-none fixed top-0 left-0 z-100 rounded-md border px-2 py-0.5 text-xs font-medium shadow-sm" +
|
|
746
|
+
(ui?.dropHint ? " " + ui.dropHint : "")
|
|
747
|
+
hintEl.textContent = settings.i18n.labels.dropNotAllowed
|
|
748
|
+
document.body.appendChild(hintEl)
|
|
749
|
+
}
|
|
750
|
+
hintEl.style.transform = `translate3d(${snapToPixel(e.clientX + 12)}px, ${snapToPixel(e.clientY + 16)}px, 0)`
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const autoScroll = (e: PointerEvent) => {
|
|
754
|
+
// Cached rect (see Surface.viewportRect) - never getBoundingClientRect per
|
|
755
|
+
// move; the box is stable while the pointer is captured.
|
|
756
|
+
const rect = surface?.viewportRect
|
|
757
|
+
if (!surface?.viewport || !rect) return
|
|
758
|
+
const edge = activation.autoScrollEdgePx
|
|
759
|
+
const step = activation.autoScrollMaxStepPx
|
|
760
|
+
let delta = 0
|
|
761
|
+
/**
|
|
762
|
+
* The scroller wraps the time track ONLY - the day headers and the all-day
|
|
763
|
+
* row sit above it, outside the box - so its top edge is a hard floor. A
|
|
764
|
+
* pointer above that floor is over one of those rows, and both are drop
|
|
765
|
+
* targets in their own right (a bar moving across dates, a timed chip
|
|
766
|
+
* lifted onto the all-day lane to convert it); they are already fully
|
|
767
|
+
* visible, so reaching them is never a request to scroll. Without the
|
|
768
|
+
* floor, "past the top edge" is true on every single move of a horizontal
|
|
769
|
+
* all-day drag and the track pans out from under a gesture that only wants
|
|
770
|
+
* to change the date - fast, because the proximity ratio grows past 1 the
|
|
771
|
+
* further above the box the pointer sits.
|
|
772
|
+
*
|
|
773
|
+
* Below the track there is no such row, so overshooting the bottom keeps
|
|
774
|
+
* scrolling - that is how a grid is normally asked to keep going - but the
|
|
775
|
+
* eased speed is capped at one step per frame for the same reason.
|
|
776
|
+
*/
|
|
777
|
+
if (e.clientY >= rect.top) {
|
|
778
|
+
if (e.clientY < rect.top + edge) {
|
|
779
|
+
delta = -step * ((rect.top + edge - e.clientY) / edge)
|
|
780
|
+
} else if (e.clientY > rect.bottom - edge) {
|
|
781
|
+
delta = step * Math.min(1, (e.clientY - (rect.bottom - edge)) / edge)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (rafScroll) cancelAnimationFrame(rafScroll)
|
|
785
|
+
if (delta !== 0) {
|
|
786
|
+
const tick = () => {
|
|
787
|
+
// Write-then-readback: the browser clamps scrollTop at the scroll
|
|
788
|
+
// extent, so mirror only the APPLIED delta - otherwise parking the
|
|
789
|
+
// pointer in the edge zone at the limit keeps inflating the tracked
|
|
790
|
+
// value past reality and poisons every later minute mapping. When
|
|
791
|
+
// parked (nothing applied), stop the loop; the next pointermove
|
|
792
|
+
// restarts it.
|
|
793
|
+
const before = surface!.viewport!.scrollTop
|
|
794
|
+
surface!.viewport!.scrollTop = before + delta
|
|
795
|
+
const applied = surface!.viewport!.scrollTop - before
|
|
796
|
+
if (applied === 0) return
|
|
797
|
+
// keep the tracked scrollTop in step so pointerMinutes stays a pure
|
|
798
|
+
// number read (no DOM scrollTop, no forced reflow)
|
|
799
|
+
surface!.scrollTop += applied
|
|
800
|
+
applyProposal(e)
|
|
801
|
+
rafScroll = requestAnimationFrame(tick)
|
|
802
|
+
}
|
|
803
|
+
rafScroll = requestAnimationFrame(tick)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// idempotent: pointerup, pointercancel, Escape, blur and the calendar-level
|
|
808
|
+
// teardown can race; whichever lands first wins and the rest no-op
|
|
809
|
+
let finished = false
|
|
810
|
+
const cleanup = () => {
|
|
811
|
+
if (finished) return
|
|
812
|
+
finished = true
|
|
813
|
+
activeGestureCancels.delete(cancel)
|
|
814
|
+
window.removeEventListener("pointermove", onPointerMove)
|
|
815
|
+
window.removeEventListener("pointerup", onPointerUp)
|
|
816
|
+
window.removeEventListener("pointercancel", onCancel)
|
|
817
|
+
window.removeEventListener("blur", onWindowBlur)
|
|
818
|
+
window.removeEventListener("keydown", onKeyDown, true)
|
|
819
|
+
if (rafScroll) cancelAnimationFrame(rafScroll)
|
|
820
|
+
if (touchTimer) clearTimeout(touchTimer)
|
|
821
|
+
hintEl?.remove()
|
|
822
|
+
hintEl = null
|
|
823
|
+
carryEl?.remove()
|
|
824
|
+
carryEl = null
|
|
825
|
+
setBodyDragging(false)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const cancel = () => {
|
|
829
|
+
cleanup()
|
|
830
|
+
if (active) {
|
|
831
|
+
lastGestureEndedAt = performance.now()
|
|
832
|
+
internals.setDrag(null)
|
|
833
|
+
internals.setSlotDraft(null)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
838
|
+
if (e.key === "Escape") {
|
|
839
|
+
e.stopPropagation()
|
|
840
|
+
cancel()
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// focus loss mid-gesture (alt-tab, OS dialogs) means the release may never
|
|
845
|
+
// be delivered; treat it as a cancel so the gesture cannot get stuck and
|
|
846
|
+
// commit its stale proposal on the next click
|
|
847
|
+
const onWindowBlur = () => cancel()
|
|
848
|
+
|
|
849
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
850
|
+
// a second finger must not drive - or cancel the pending long press of -
|
|
851
|
+
// the gesture this pointer started
|
|
852
|
+
if (e.pointerId !== pointerId) return
|
|
853
|
+
lastPointer = e
|
|
854
|
+
if (!active) {
|
|
855
|
+
const distance = Math.hypot(e.clientX - startX, e.clientY - startY)
|
|
856
|
+
if (isTouch) {
|
|
857
|
+
// Long-press pending: moving past tolerance means scroll, not drag
|
|
858
|
+
if (distance > activation.touchTolerancePx) cancel()
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
if (distance < activationDistance) return
|
|
862
|
+
activate()
|
|
863
|
+
}
|
|
864
|
+
applyProposal(e)
|
|
865
|
+
autoScroll(e)
|
|
866
|
+
updateHint(e)
|
|
867
|
+
positionCarry(e)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
871
|
+
if (e.pointerId !== pointerId) return
|
|
872
|
+
cleanup()
|
|
873
|
+
if (!active) return
|
|
874
|
+
lastGestureEndedAt = performance.now()
|
|
875
|
+
|
|
876
|
+
const state = instance.getState()
|
|
877
|
+
if (kind === "create") {
|
|
878
|
+
const draft = state.slotDraft
|
|
879
|
+
internals.setSlotDraft(null)
|
|
880
|
+
if (draft) {
|
|
881
|
+
api.select({
|
|
882
|
+
slot: { start: draft.start, end: draft.end, allDay: draft.allDay },
|
|
883
|
+
})
|
|
884
|
+
settings.onSelectSlot?.(draft)
|
|
885
|
+
}
|
|
886
|
+
return
|
|
887
|
+
}
|
|
888
|
+
const drag = state.drag
|
|
889
|
+
internals.setDrag(null)
|
|
890
|
+
if (!drag || !occurrence) return
|
|
891
|
+
const unchanged =
|
|
892
|
+
drag.proposedStart.getTime() === occurrence.start.getTime() &&
|
|
893
|
+
drag.proposedEnd.getTime() === occurrence.end.getTime() &&
|
|
894
|
+
(drag.proposedResourceId === undefined ||
|
|
895
|
+
drag.proposedResourceId === occurrence.event.resourceId)
|
|
896
|
+
if (unchanged) return
|
|
897
|
+
// Commit through the one validation funnel; consumer reject = automatic
|
|
898
|
+
// revert because the calendar never mutated during the gesture.
|
|
899
|
+
const accepted = internals.applyProposedUpdate({
|
|
900
|
+
event: occurrence.event,
|
|
901
|
+
occurrence,
|
|
902
|
+
start: drag.proposedStart,
|
|
903
|
+
end: drag.proposedEnd,
|
|
904
|
+
allDay: drag.proposedAllDay,
|
|
905
|
+
resourceId: drag.proposedResourceId,
|
|
906
|
+
source:
|
|
907
|
+
kind === "move" ? "drag" : (kind as "resize-start" | "resize-end"),
|
|
908
|
+
})
|
|
909
|
+
// the polite live region is the only feedback a screen-reader user gets
|
|
910
|
+
// that the drop landed, and on what; a vetoed commit stays silent
|
|
911
|
+
if (accepted && announcer) {
|
|
912
|
+
announcer.textContent = `${occurrence.event.title}, ${settings.i18n.functions.formatEventTime(
|
|
913
|
+
toZoned(drag.proposedStart, timeZone),
|
|
914
|
+
toZoned(drag.proposedEnd, timeZone),
|
|
915
|
+
drag.proposedAllDay,
|
|
916
|
+
{ locale: settings.locale }
|
|
917
|
+
)}`
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const onCancel = (e: PointerEvent) => {
|
|
922
|
+
if (e.pointerId !== pointerId) return
|
|
923
|
+
cancel()
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
window.addEventListener("pointermove", onPointerMove)
|
|
927
|
+
window.addEventListener("pointerup", onPointerUp)
|
|
928
|
+
window.addEventListener("pointercancel", onCancel)
|
|
929
|
+
// a resize on a precise pointer is live from the first frame, so it never
|
|
930
|
+
// reaches activate() to arm its own blur cancel
|
|
931
|
+
if (active) window.addEventListener("blur", onWindowBlur)
|
|
932
|
+
window.addEventListener("keydown", onKeyDown, true)
|
|
933
|
+
activeGestureCancels.add(cancel)
|
|
934
|
+
|
|
935
|
+
// Touch: long-press activation (movement past tolerance cancels above)
|
|
936
|
+
if (isTouch && !active) {
|
|
937
|
+
touchTimer = setTimeout(() => {
|
|
938
|
+
activate()
|
|
939
|
+
applyProposal(lastPointer)
|
|
940
|
+
}, activation.touchDelayMs)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Per-chip / per-surface pointer gesture wiring. */
|
|
945
|
+
function useEventCalendarGestures<TData = unknown>() {
|
|
946
|
+
const instance = useEventCalendar<TData>()
|
|
947
|
+
// Overlay classNames bridged from React config into the vanilla-DOM engine
|
|
948
|
+
// (which must never import from the chip module - circular).
|
|
949
|
+
const { classNames } = useEventCalendarViewConfig<TData>()
|
|
950
|
+
const ui = useMemo(
|
|
951
|
+
() => ({
|
|
952
|
+
dragCarry: classNames?.dragCarry,
|
|
953
|
+
dragCarryInvalid: classNames?.dragCarryInvalid,
|
|
954
|
+
dropHint: classNames?.dropHint,
|
|
955
|
+
}),
|
|
956
|
+
[classNames]
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
// An in-flight gesture lives on window listeners and body-appended overlays,
|
|
960
|
+
// so it must never outlive the calendar. Chips hold this hook too and they
|
|
961
|
+
// legitimately unmount mid-gesture, so only the LAST consumer leaving - the
|
|
962
|
+
// calendar itself going away - aborts.
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
gestureConsumers += 1
|
|
965
|
+
return () => {
|
|
966
|
+
gestureConsumers -= 1
|
|
967
|
+
if (gestureConsumers === 0) cancelActiveEventCalendarGestures()
|
|
968
|
+
}
|
|
969
|
+
}, [])
|
|
970
|
+
|
|
971
|
+
const canDrag = useCallback(
|
|
972
|
+
(segment: EventCalendarSegment<TData>) => {
|
|
973
|
+
const { interactions } = instance.getState()
|
|
974
|
+
const event = segment.occurrence.event
|
|
975
|
+
return interactions.drag && !event.readOnly && event.draggable !== false
|
|
976
|
+
},
|
|
977
|
+
[instance]
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
const canResize = useCallback(
|
|
981
|
+
(segment: EventCalendarSegment<TData>) => {
|
|
982
|
+
const { interactions } = instance.getState()
|
|
983
|
+
const event = segment.occurrence.event
|
|
984
|
+
return interactions.resize && !event.readOnly && event.resizable !== false
|
|
985
|
+
},
|
|
986
|
+
[instance]
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
const beginMove = useCallback(
|
|
990
|
+
(e: React.PointerEvent, segment: EventCalendarSegment<TData>) => {
|
|
991
|
+
if (e.button !== 0) return
|
|
992
|
+
if (!canDrag(segment)) {
|
|
993
|
+
// refused, but still give feedback + broadcast on a real drag attempt
|
|
994
|
+
beginBlockedGesture(instance, e.nativeEvent, segment, "move")
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
beginGesture({
|
|
998
|
+
instance,
|
|
999
|
+
kind: "move",
|
|
1000
|
+
origin: e.currentTarget as HTMLElement,
|
|
1001
|
+
startEvent: e.nativeEvent,
|
|
1002
|
+
segment,
|
|
1003
|
+
ui,
|
|
1004
|
+
})
|
|
1005
|
+
},
|
|
1006
|
+
[instance, canDrag, ui]
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
const beginResize = useCallback(
|
|
1010
|
+
(
|
|
1011
|
+
e: React.PointerEvent,
|
|
1012
|
+
segment: EventCalendarSegment<TData>,
|
|
1013
|
+
edge: "start" | "end"
|
|
1014
|
+
) => {
|
|
1015
|
+
if (e.button !== 0) return
|
|
1016
|
+
if (!canResize(segment)) {
|
|
1017
|
+
e.stopPropagation()
|
|
1018
|
+
beginBlockedGesture(instance, e.nativeEvent, segment, "resize")
|
|
1019
|
+
return
|
|
1020
|
+
}
|
|
1021
|
+
e.stopPropagation()
|
|
1022
|
+
e.preventDefault()
|
|
1023
|
+
beginGesture({
|
|
1024
|
+
instance,
|
|
1025
|
+
kind: edge === "start" ? "resize-start" : "resize-end",
|
|
1026
|
+
origin: e.currentTarget as HTMLElement,
|
|
1027
|
+
startEvent: e.nativeEvent,
|
|
1028
|
+
segment,
|
|
1029
|
+
ui,
|
|
1030
|
+
})
|
|
1031
|
+
},
|
|
1032
|
+
[instance, canResize, ui]
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
const beginCreate = useCallback(
|
|
1036
|
+
(e: React.PointerEvent, day: Date, allDay: boolean) => {
|
|
1037
|
+
if (e.button !== 0) return
|
|
1038
|
+
if (!instance.getState().interactions.selectSlot) return
|
|
1039
|
+
beginGesture({
|
|
1040
|
+
instance,
|
|
1041
|
+
kind: "create",
|
|
1042
|
+
origin: e.currentTarget as HTMLElement,
|
|
1043
|
+
startEvent: e.nativeEvent,
|
|
1044
|
+
createDay: day,
|
|
1045
|
+
createAllDay: allDay,
|
|
1046
|
+
ui,
|
|
1047
|
+
})
|
|
1048
|
+
},
|
|
1049
|
+
[instance, ui]
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
return { beginMove, beginResize, beginCreate, canDrag, canResize }
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export {
|
|
1056
|
+
EVENT_CALENDAR_ACTIVATION,
|
|
1057
|
+
cancelActiveEventCalendarGestures,
|
|
1058
|
+
markChipPress,
|
|
1059
|
+
useEventCalendarGestures,
|
|
1060
|
+
wasRecentChipPress,
|
|
1061
|
+
wasRecentDrag,
|
|
1062
|
+
}
|