@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,1000 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react"
|
|
2
|
+
import {
|
|
3
|
+
resolveScheduleMode,
|
|
4
|
+
useGantt,
|
|
5
|
+
useGanttViewConfig,
|
|
6
|
+
type GanttInstance,
|
|
7
|
+
} from "@/components/reui/gantt/gantt"
|
|
8
|
+
import {
|
|
9
|
+
findResource,
|
|
10
|
+
snapMinutes,
|
|
11
|
+
toZoned,
|
|
12
|
+
zonedStartOfDay,
|
|
13
|
+
} from "@/components/reui/gantt/gantt-lib"
|
|
14
|
+
import type {
|
|
15
|
+
GanttProposedUpdate,
|
|
16
|
+
GanttScheduleMode,
|
|
17
|
+
GanttSegment,
|
|
18
|
+
} from "@/components/reui/gantt/gantt-types"
|
|
19
|
+
import { addDays, differenceInCalendarDays } from "date-fns"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Activation policy (dnd-kit parity where proven):
|
|
23
|
+
* mouse move 5px before a drag starts (below = click), create 4px;
|
|
24
|
+
* touch long-press 250ms with 5px tolerance (movement past tolerance
|
|
25
|
+
* before the delay cancels the drag so taps stay taps).
|
|
26
|
+
*/
|
|
27
|
+
const GANTT_ACTIVATION = {
|
|
28
|
+
moveDistancePx: 5,
|
|
29
|
+
createDistancePx: 4,
|
|
30
|
+
touchDelayMs: 250,
|
|
31
|
+
touchTolerancePx: 5,
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
type GestureKind = "move" | "resize-start" | "resize-end" | "create"
|
|
35
|
+
|
|
36
|
+
interface GanttSurface {
|
|
37
|
+
rect: DOMRect
|
|
38
|
+
rangeStart: number
|
|
39
|
+
rangeEnd: number
|
|
40
|
+
snapMin: number
|
|
41
|
+
/** Mirrored axis: in RTL the range START sits at the rect's RIGHT edge. */
|
|
42
|
+
isRtl: boolean
|
|
43
|
+
rows: Array<{ resourceId: string; rect: DOMRect }>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pointer x (viewport px) to minutes from the range start, clamped to the
|
|
48
|
+
* track. The single place the horizontal axis direction is resolved: every
|
|
49
|
+
* gesture mapping (move, both resizes, create, grab offset) goes through it.
|
|
50
|
+
*/
|
|
51
|
+
function surfaceMinutesAt(tl: GanttSurface, x: number): number {
|
|
52
|
+
const clamped = Math.min(Math.max(x, tl.rect.left), tl.rect.right)
|
|
53
|
+
const traveled = tl.isRtl ? tl.rect.right - clamped : clamped - tl.rect.left
|
|
54
|
+
return (traveled / tl.rect.width) * ((tl.rangeEnd - tl.rangeStart) / 60000)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Module flag so bar onClick can ignore the click that ends a drag. */
|
|
58
|
+
let lastGestureEndedAt = 0
|
|
59
|
+
function wasRecentDrag(): boolean {
|
|
60
|
+
return performance.now() - lastGestureEndedAt < 250
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mark a non-dnd gesture (e.g. a timeline pan) so the click it ends is ignored. */
|
|
64
|
+
function markGestureEnd(): void {
|
|
65
|
+
lastGestureEndedAt = performance.now()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registry of in-flight gesture cancels. A gesture measures its surface
|
|
70
|
+
* (axis + row rects) once at activation, so the VIEW - not the bar, bars
|
|
71
|
+
* legitimately unmount mid-gesture - must be able to abort gestures when it
|
|
72
|
+
* unmounts or when the measured geometry changes under them (zoom, scale,
|
|
73
|
+
* range growth, splitter). Cancel fully reverts: listeners, overlays and the
|
|
74
|
+
* body drag state all clear, and no update is committed.
|
|
75
|
+
*/
|
|
76
|
+
const activeGestureCancels = new Set<() => void>()
|
|
77
|
+
|
|
78
|
+
/** Cancel (and fully revert) every in-flight gantt pointer gesture. */
|
|
79
|
+
function cancelActiveGanttGestures(): void {
|
|
80
|
+
for (const cancel of [...activeGestureCancels]) cancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* View-level teardown, mounted once by GanttView: aborts any in-flight
|
|
85
|
+
* gesture on unmount so window listeners, body-appended overlays and the
|
|
86
|
+
* gantt-dragging body class never outlive the gantt.
|
|
87
|
+
*/
|
|
88
|
+
function useGanttGestureTeardown(): void {
|
|
89
|
+
useEffect(() => cancelActiveGanttGestures, [])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Snap a translate offset to the device pixel grid. The cursor-following
|
|
94
|
+
* overlays (the move clone and the resize indicator) are their own
|
|
95
|
+
* `will-change: transform` compositing layers: the GPU rasterizes their text
|
|
96
|
+
* once and repositions that texture each frame, so a subpixel translate
|
|
97
|
+
* (getBoundingClientRect and raw clientX/Y are routinely fractional) resamples
|
|
98
|
+
* the texture and blurs the text. Rounding each offset to a whole device pixel
|
|
99
|
+
* lands the layer on the grid so glyphs stay crisp, without giving up the
|
|
100
|
+
* per-frame GPU transform.
|
|
101
|
+
*/
|
|
102
|
+
function snapToPixel(value: number): number {
|
|
103
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
|
|
104
|
+
return Math.round(value * dpr) / dpr
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function collectSurface(root: HTMLElement | null): GanttSurface | null {
|
|
108
|
+
if (!root) return null
|
|
109
|
+
const axis = root.querySelector<HTMLElement>("[data-gantt-axis]")
|
|
110
|
+
if (!axis) return null
|
|
111
|
+
return {
|
|
112
|
+
rect: axis.getBoundingClientRect(),
|
|
113
|
+
rangeStart: Number(axis.dataset.ganttRangeStart),
|
|
114
|
+
rangeEnd: Number(axis.dataset.ganttRangeEnd),
|
|
115
|
+
snapMin: Number(axis.dataset.ganttSnap) || 15,
|
|
116
|
+
isRtl: getComputedStyle(axis).direction === "rtl",
|
|
117
|
+
rows: [...root.querySelectorAll<HTMLElement>("[data-gantt-row]")]
|
|
118
|
+
// static rows (parents that aggregate their subtree) take no drops
|
|
119
|
+
.filter((row) => row.dataset.ganttRowStatic === undefined)
|
|
120
|
+
.map((row) => ({
|
|
121
|
+
resourceId: row.dataset.ganttResource ?? "",
|
|
122
|
+
rect: row.getBoundingClientRect(),
|
|
123
|
+
})),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface BeginGestureConfig<TData> {
|
|
128
|
+
instance: GanttInstance<TData>
|
|
129
|
+
kind: GestureKind
|
|
130
|
+
origin: HTMLElement
|
|
131
|
+
startEvent: PointerEvent
|
|
132
|
+
segment?: GanttSegment<TData>
|
|
133
|
+
/** Consumer renders the move preview (renderDragPreview); the engine only positions it. */
|
|
134
|
+
customMoveOverlay?: boolean
|
|
135
|
+
/** Consumer renders the resize indicator; the engine only positions it. */
|
|
136
|
+
customResizeOverlay?: boolean
|
|
137
|
+
/** View-level cardinality default; a node's own scheduleMode wins. */
|
|
138
|
+
scheduleMode?: GanttScheduleMode
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function beginGesture<TData>(config: BeginGestureConfig<TData>) {
|
|
142
|
+
const {
|
|
143
|
+
instance,
|
|
144
|
+
kind,
|
|
145
|
+
origin,
|
|
146
|
+
startEvent,
|
|
147
|
+
segment,
|
|
148
|
+
customMoveOverlay,
|
|
149
|
+
customResizeOverlay,
|
|
150
|
+
} = config
|
|
151
|
+
const { settings, internals, api } = instance
|
|
152
|
+
const activation = { ...GANTT_ACTIVATION, ...settings.activation }
|
|
153
|
+
const startX = startEvent.clientX
|
|
154
|
+
const startY = startEvent.clientY
|
|
155
|
+
const pointerId = startEvent.pointerId
|
|
156
|
+
// stable ancestors: bar nodes may be replaced by re-renders mid-gesture
|
|
157
|
+
const viewRoot = origin.closest<HTMLElement>("[data-slot=gantt-view]")
|
|
158
|
+
const ganttRoot = origin.closest<HTMLElement>("[data-slot=gantt]")
|
|
159
|
+
const announcer = ganttRoot?.querySelector<HTMLElement>(
|
|
160
|
+
"[data-slot=gantt-announcer]"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The cursor-following overlays are appended to document.body so no ancestor
|
|
165
|
+
* transform or overflow can clip them - which also cuts them off from the
|
|
166
|
+
* gantt root, and the root is what OWNS the type scale (its `text-xs` is
|
|
167
|
+
* what every resting label inherits). Without this the clone's label jumps
|
|
168
|
+
* to the document default and reads visibly bigger than the bar it left.
|
|
169
|
+
* Copying the ROOT's resolved metrics - rather than hardcoding a size -
|
|
170
|
+
* keeps the documented contract that one class on the root (e.g.
|
|
171
|
+
* className="text-sm") rescales the whole gantt, drag clone included.
|
|
172
|
+
*/
|
|
173
|
+
const adoptRootTypography = (el: HTMLElement) => {
|
|
174
|
+
if (!ganttRoot) return
|
|
175
|
+
const rootStyle = getComputedStyle(ganttRoot)
|
|
176
|
+
el.style.fontSize = rootStyle.fontSize
|
|
177
|
+
el.style.lineHeight = rootStyle.lineHeight
|
|
178
|
+
el.style.fontFamily = rootStyle.fontFamily
|
|
179
|
+
el.style.letterSpacing = rootStyle.letterSpacing
|
|
180
|
+
// physical positioning, logical content: the flex row mirrors so the
|
|
181
|
+
// label lands on the same side of the bar as the resting one in RTL
|
|
182
|
+
el.style.direction = rootStyle.direction
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isTouch = startEvent.pointerType === "touch"
|
|
186
|
+
// resize activates immediately on precise pointers; on touch it waits for
|
|
187
|
+
// the same long-press as a move, so a stray brush over a bar edge can
|
|
188
|
+
// never start an accidental resize
|
|
189
|
+
let active = kind.startsWith("resize") && !isTouch
|
|
190
|
+
let surface: GanttSurface | null = active ? collectSurface(viewRoot) : null
|
|
191
|
+
let lastProposalKey = ""
|
|
192
|
+
let touchTimer: ReturnType<typeof setTimeout> | null = null
|
|
193
|
+
let lastPointer: PointerEvent = startEvent
|
|
194
|
+
|
|
195
|
+
const occurrence = segment?.occurrence
|
|
196
|
+
|
|
197
|
+
// ----- neighbour awareness: the other schedules in the SAME node -----
|
|
198
|
+
// A node in "single" mode rejects any concurrency regardless of the
|
|
199
|
+
// overlap option; otherwise the option decides. "allow" short-circuits
|
|
200
|
+
// everything below, so the default gesture path is untouched.
|
|
201
|
+
const nodeId = occurrence?.event.resourceId
|
|
202
|
+
const nodeMode = resolveScheduleMode(
|
|
203
|
+
nodeId === undefined ? null : findResource(settings.resources, nodeId),
|
|
204
|
+
config.scheduleMode
|
|
205
|
+
)
|
|
206
|
+
const overlapPolicy =
|
|
207
|
+
nodeMode === "single" ? ("reject" as const) : settings.overlap
|
|
208
|
+
// Read once per gesture: the gantt never mutates events mid-drag, so the
|
|
209
|
+
// neighbours cannot move under us.
|
|
210
|
+
let neighbourCache: Array<{ start: number; end: number }> | null = null
|
|
211
|
+
const getNeighbours = () => {
|
|
212
|
+
if (neighbourCache) return neighbourCache
|
|
213
|
+
neighbourCache =
|
|
214
|
+
!occurrence || nodeId === undefined || overlapPolicy === "allow"
|
|
215
|
+
? []
|
|
216
|
+
: api
|
|
217
|
+
.getOccurrences()
|
|
218
|
+
.filter(
|
|
219
|
+
(other) =>
|
|
220
|
+
other.event.resourceId === nodeId &&
|
|
221
|
+
other.key !== occurrence.key
|
|
222
|
+
)
|
|
223
|
+
.map((other) => ({
|
|
224
|
+
start: other.start.getTime(),
|
|
225
|
+
end: other.end.getTime(),
|
|
226
|
+
}))
|
|
227
|
+
return neighbourCache
|
|
228
|
+
}
|
|
229
|
+
const overlapsNeighbour = (start: Date, end: Date) =>
|
|
230
|
+
getNeighbours().some(
|
|
231
|
+
(other) => other.start < end.getTime() && other.end > start.getTime()
|
|
232
|
+
)
|
|
233
|
+
/**
|
|
234
|
+
* Stop the gesture at the neighbour's edge. Runs AFTER snapping so the
|
|
235
|
+
* clamp always wins, and only against neighbours that sit clear of the
|
|
236
|
+
* bar's CURRENT span - a pre-existing overlap has no edge to stop at.
|
|
237
|
+
*/
|
|
238
|
+
const clampToNeighbours = (
|
|
239
|
+
start: Date,
|
|
240
|
+
end: Date
|
|
241
|
+
): { start: Date; end: Date } => {
|
|
242
|
+
if (overlapPolicy !== "clamp" || !occurrence) return { start, end }
|
|
243
|
+
const anchorStart = occurrence.start.getTime()
|
|
244
|
+
const anchorEnd = occurrence.end.getTime()
|
|
245
|
+
let floor = -Infinity
|
|
246
|
+
let ceiling = Infinity
|
|
247
|
+
for (const other of getNeighbours()) {
|
|
248
|
+
if (other.end <= anchorStart) floor = Math.max(floor, other.end)
|
|
249
|
+
else if (other.start >= anchorEnd)
|
|
250
|
+
ceiling = Math.min(ceiling, other.start)
|
|
251
|
+
}
|
|
252
|
+
if (floor === -Infinity && ceiling === Infinity) return { start, end }
|
|
253
|
+
let from = start.getTime()
|
|
254
|
+
let to = end.getTime()
|
|
255
|
+
if (kind === "resize-start") {
|
|
256
|
+
from = Math.min(Math.max(from, floor), to)
|
|
257
|
+
} else if (kind === "resize-end") {
|
|
258
|
+
to = Math.max(Math.min(to, ceiling), from)
|
|
259
|
+
} else {
|
|
260
|
+
// a move keeps its duration and parks against whichever edge it meets
|
|
261
|
+
const duration = to - from
|
|
262
|
+
if (from < floor) {
|
|
263
|
+
from = floor
|
|
264
|
+
to = from + duration
|
|
265
|
+
}
|
|
266
|
+
if (to > ceiling) {
|
|
267
|
+
to = ceiling
|
|
268
|
+
from = to - duration
|
|
269
|
+
}
|
|
270
|
+
// window narrower than the bar itself: park at the earlier edge
|
|
271
|
+
if (from < floor) {
|
|
272
|
+
from = floor
|
|
273
|
+
to = from + duration
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { start: new Date(from), end: new Date(to) }
|
|
277
|
+
}
|
|
278
|
+
// Set by applyProposal when a "reject" policy refuses the current proposal;
|
|
279
|
+
// read on pointerup so the commit is actually blocked, not merely styled.
|
|
280
|
+
let overlapRejected = false
|
|
281
|
+
|
|
282
|
+
// Preserve the grab offset so the bar does not jump to the pointer
|
|
283
|
+
let grabOffsetMin = 0
|
|
284
|
+
// Smooth cursor-following clone for a move: a real-looking bar that tracks
|
|
285
|
+
// the pointer's x via transform (no per-frame React), lifted with a shadow.
|
|
286
|
+
let overlay: HTMLDivElement | null = null
|
|
287
|
+
let grabOffsetPx = 0
|
|
288
|
+
let barTop = 0
|
|
289
|
+
let barWidth = 0
|
|
290
|
+
let barHeight = 0
|
|
291
|
+
|
|
292
|
+
const createMoveOverlay = () => {
|
|
293
|
+
if (kind !== "move" || !occurrence || overlay || barWidth === 0) return
|
|
294
|
+
// consumer-rendered preview (renderDragPreview): the view mounts it from
|
|
295
|
+
// drag state; positionOverlay adopts it lazily and only writes transforms
|
|
296
|
+
if (customMoveOverlay) return
|
|
297
|
+
const color = occurrence.event.color ?? "var(--color-primary)"
|
|
298
|
+
overlay = document.createElement("div")
|
|
299
|
+
overlay.setAttribute("data-slot", "gantt-drag-overlay")
|
|
300
|
+
// container: the bar + its label ride together; the label stays OUTSIDE
|
|
301
|
+
// the bar (to the right), matching the resting look - no in-bar text
|
|
302
|
+
overlay.className =
|
|
303
|
+
// physical left-0 anchor: the clone is positioned by translate3d from
|
|
304
|
+
// raw clientX, which is physical - a logical start-0 anchor would pin
|
|
305
|
+
// it to the RIGHT edge in RTL and fling the clone off screen
|
|
306
|
+
"pointer-events-none fixed top-0 left-0 z-100 flex items-center gap-2 will-change-transform"
|
|
307
|
+
adoptRootTypography(overlay)
|
|
308
|
+
overlay.style.height = `${barHeight}px`
|
|
309
|
+
const barEl = document.createElement("div")
|
|
310
|
+
if (occurrence.end.getTime() === occurrence.start.getTime()) {
|
|
311
|
+
// a MILESTONE stays a diamond while it travels: the resting glyph,
|
|
312
|
+
// lifted with a shadow - a rectangle here reads as a different object
|
|
313
|
+
barEl.className = "flex shrink-0 items-center justify-center"
|
|
314
|
+
barEl.style.width = `${barWidth}px`
|
|
315
|
+
barEl.style.height = "100%"
|
|
316
|
+
const diamond = document.createElement("span")
|
|
317
|
+
diamond.className = "rotate-45 rounded-[2px] shadow-lg"
|
|
318
|
+
diamond.style.width = "10px"
|
|
319
|
+
diamond.style.height = "10px"
|
|
320
|
+
diamond.style.background = `color-mix(in oklab, ${color} 80%, var(--color-background))`
|
|
321
|
+
diamond.style.border = `1px solid ${color}`
|
|
322
|
+
barEl.appendChild(diamond)
|
|
323
|
+
} else {
|
|
324
|
+
barEl.className = "shrink-0 rounded-sm shadow-lg"
|
|
325
|
+
barEl.style.width = `${barWidth}px`
|
|
326
|
+
barEl.style.height = "100%"
|
|
327
|
+
barEl.style.background = `color-mix(in oklab, ${color} 22%, var(--color-background))`
|
|
328
|
+
barEl.style.outline = `1px solid color-mix(in oklab, ${color} 55%, transparent)`
|
|
329
|
+
}
|
|
330
|
+
overlay.appendChild(barEl)
|
|
331
|
+
const label = document.createElement("span")
|
|
332
|
+
label.className = "text-foreground truncate font-medium whitespace-nowrap"
|
|
333
|
+
label.textContent = occurrence.event.title
|
|
334
|
+
overlay.appendChild(label)
|
|
335
|
+
document.body.appendChild(overlay)
|
|
336
|
+
positionOverlay(lastPointer)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const positionOverlay = (e: PointerEvent) => {
|
|
340
|
+
if (!overlay && customMoveOverlay && active && kind === "move") {
|
|
341
|
+
overlay = document.querySelector<HTMLDivElement>(
|
|
342
|
+
"[data-slot=gantt-drag-overlay][data-custom]"
|
|
343
|
+
)
|
|
344
|
+
if (overlay) overlay.style.visibility = "visible"
|
|
345
|
+
}
|
|
346
|
+
if (!overlay) return
|
|
347
|
+
// x follows the pointer freely (smooth); y stays on the bar's own row
|
|
348
|
+
overlay.style.transform = `translate3d(${snapToPixel(e.clientX - grabOffsetPx)}px, ${snapToPixel(barTop)}px, 0)`
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Resize status indicator: a smooth cursor-following edge line plus a live
|
|
352
|
+
// range + duration chip. The dashed ghost still shows the SNAPPED landing;
|
|
353
|
+
// this overlay is the continuous feedback between snap steps.
|
|
354
|
+
let resizeOverlay: HTMLDivElement | null = null
|
|
355
|
+
let resizeLine: HTMLDivElement | null = null
|
|
356
|
+
let resizeRange: HTMLSpanElement | null = null
|
|
357
|
+
let resizeDot: HTMLSpanElement | null = null
|
|
358
|
+
let resizeDuration: HTMLSpanElement | null = null
|
|
359
|
+
|
|
360
|
+
const positionResizeOverlay = (e: PointerEvent) => {
|
|
361
|
+
if (!resizeOverlay && customResizeOverlay && kind.startsWith("resize")) {
|
|
362
|
+
resizeOverlay = document.querySelector<HTMLDivElement>(
|
|
363
|
+
"[data-slot=gantt-resize-indicator][data-custom]"
|
|
364
|
+
)
|
|
365
|
+
if (resizeOverlay) resizeOverlay.style.visibility = "visible"
|
|
366
|
+
}
|
|
367
|
+
if (!resizeOverlay || !surface) return
|
|
368
|
+
// x follows the pointer freely (clamped to the track); y stays on the bar
|
|
369
|
+
const x = Math.min(
|
|
370
|
+
Math.max(e.clientX, surface.rect.left),
|
|
371
|
+
surface.rect.right
|
|
372
|
+
)
|
|
373
|
+
resizeOverlay.style.transform = `translate3d(${snapToPixel(x)}px, ${snapToPixel(barTop)}px, 0)`
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const createResizeOverlay = () => {
|
|
377
|
+
if (!kind.startsWith("resize") || !occurrence || resizeOverlay) return
|
|
378
|
+
const barEl = origin.closest<HTMLElement>("[data-slot=gantt-bar]")
|
|
379
|
+
const rect = (barEl ?? origin).getBoundingClientRect()
|
|
380
|
+
barTop = rect.top
|
|
381
|
+
barHeight = rect.height
|
|
382
|
+
// consumer-rendered indicator: rect capture above still runs (the engine
|
|
383
|
+
// positions the consumer's wrapper), only the default DOM is skipped
|
|
384
|
+
if (customResizeOverlay) return
|
|
385
|
+
const color = occurrence.event.color ?? "var(--color-primary)"
|
|
386
|
+
resizeOverlay = document.createElement("div")
|
|
387
|
+
resizeOverlay.setAttribute("data-slot", "gantt-resize-indicator")
|
|
388
|
+
resizeOverlay.className =
|
|
389
|
+
// physical left-0 anchor, same reason as the move clone above
|
|
390
|
+
"pointer-events-none fixed top-0 left-0 z-100 will-change-transform"
|
|
391
|
+
adoptRootTypography(resizeOverlay)
|
|
392
|
+
resizeOverlay.style.height = `${barHeight}px`
|
|
393
|
+
resizeLine = document.createElement("div")
|
|
394
|
+
resizeLine.className = "h-full w-0.5 -translate-x-1/2 rounded-full"
|
|
395
|
+
resizeLine.style.background = color
|
|
396
|
+
resizeOverlay.appendChild(resizeLine)
|
|
397
|
+
const chip = document.createElement("div")
|
|
398
|
+
chip.className =
|
|
399
|
+
// physical left-0: centered with a physical translate on a physical anchor
|
|
400
|
+
// no text size of its own: it inherits the root scale adopted above, so
|
|
401
|
+
// the chip tracks a consumer rescale instead of pinning itself to 12px
|
|
402
|
+
"bg-foreground text-background absolute bottom-full left-0 mb-1.5 flex -translate-x-1/2 items-center gap-1.5 rounded-md px-2 py-1 font-medium whitespace-nowrap"
|
|
403
|
+
resizeRange = document.createElement("span")
|
|
404
|
+
chip.appendChild(resizeRange)
|
|
405
|
+
resizeDot = document.createElement("span")
|
|
406
|
+
resizeDot.className = "bg-background/40 size-1 shrink-0 rounded-full"
|
|
407
|
+
resizeDot.setAttribute("aria-hidden", "true")
|
|
408
|
+
chip.appendChild(resizeDot)
|
|
409
|
+
resizeDuration = document.createElement("span")
|
|
410
|
+
chip.appendChild(resizeDuration)
|
|
411
|
+
// The arrow. Every other bubble in the gantt has one pointing at what it
|
|
412
|
+
// describes; this chip had none, so a resize looked like a different
|
|
413
|
+
// component from the hover hint. Physical left-1/2 to match the chip's own
|
|
414
|
+
// physical anchor, and out of flow so the chip's flex gap ignores it.
|
|
415
|
+
const chipArrow = document.createElement("span")
|
|
416
|
+
chipArrow.setAttribute("aria-hidden", "true")
|
|
417
|
+
chipArrow.className =
|
|
418
|
+
"bg-foreground absolute -bottom-1 left-1/2 size-2.5 -translate-x-1/2 rotate-45 rounded-[2px]"
|
|
419
|
+
chip.appendChild(chipArrow)
|
|
420
|
+
resizeOverlay.appendChild(chip)
|
|
421
|
+
document.body.appendChild(resizeOverlay)
|
|
422
|
+
// seed the chip with the CURRENT range so it never flashes empty;
|
|
423
|
+
// zoned so the label names the same day the grid shows
|
|
424
|
+
resizeRange.textContent = settings.i18n.functions.formatEventTime(
|
|
425
|
+
toZoned(occurrence.start, settings.timeZone),
|
|
426
|
+
toZoned(occurrence.end, settings.timeZone),
|
|
427
|
+
occurrence.allDay ?? false,
|
|
428
|
+
settings.locale
|
|
429
|
+
)
|
|
430
|
+
const days = Math.round(
|
|
431
|
+
(occurrence.end.getTime() - occurrence.start.getTime()) / 86_400_000
|
|
432
|
+
)
|
|
433
|
+
if (days >= 1) {
|
|
434
|
+
resizeDuration.textContent = settings.i18n.labels.durationDays(days)
|
|
435
|
+
} else {
|
|
436
|
+
resizeDot.style.display = "none"
|
|
437
|
+
resizeDuration.style.display = "none"
|
|
438
|
+
}
|
|
439
|
+
positionResizeOverlay(startEvent)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// resize activates immediately, so its indicator mounts with the gesture
|
|
443
|
+
if (active) createResizeOverlay()
|
|
444
|
+
|
|
445
|
+
const activationDistance =
|
|
446
|
+
kind === "create" ? activation.createDistancePx : activation.moveDistancePx
|
|
447
|
+
|
|
448
|
+
// Each gesture keeps its own cursor: a resize must stay ew-resize for the
|
|
449
|
+
// whole drag (flipping to grabbing reads as a move), a move grabs.
|
|
450
|
+
const gestureCursor = kind.startsWith("resize") ? "ew-resize" : "grabbing"
|
|
451
|
+
const setBodyDragging = (on: boolean, invalid = false) => {
|
|
452
|
+
document.body.classList.toggle("gantt-dragging", on)
|
|
453
|
+
document.body.style.cursor = on
|
|
454
|
+
? invalid
|
|
455
|
+
? "not-allowed"
|
|
456
|
+
: gestureCursor
|
|
457
|
+
: ""
|
|
458
|
+
document.body.style.userSelect = on ? "none" : ""
|
|
459
|
+
if (!on) document.body.style.removeProperty("-webkit-user-select")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const activate = () => {
|
|
463
|
+
if (active) return
|
|
464
|
+
active = true
|
|
465
|
+
surface = collectSurface(viewRoot)
|
|
466
|
+
// touch resize activates here (long-press) instead of at gesture start,
|
|
467
|
+
// so its indicator mounts now; the guard inside makes this a no-op for
|
|
468
|
+
// every other path
|
|
469
|
+
createResizeOverlay()
|
|
470
|
+
if (kind === "move" && occurrence && surface) {
|
|
471
|
+
const pointerMin = surfaceMinutesAt(surface, startX)
|
|
472
|
+
// TRUE start, never clamped to the range: a bar that begins before the
|
|
473
|
+
// visible window (negative minutes) must keep its real grab offset, or
|
|
474
|
+
// the first snapped proposal teleports its start to the range edge
|
|
475
|
+
const occStartMin =
|
|
476
|
+
(occurrence.start.getTime() - surface.rangeStart) / 60000
|
|
477
|
+
grabOffsetMin = pointerMin - occStartMin
|
|
478
|
+
const rect = origin.getBoundingClientRect()
|
|
479
|
+
barTop = rect.top
|
|
480
|
+
barWidth = rect.width
|
|
481
|
+
barHeight = rect.height
|
|
482
|
+
grabOffsetPx = startX - rect.left
|
|
483
|
+
createMoveOverlay()
|
|
484
|
+
}
|
|
485
|
+
setBodyDragging(true)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const computeProposal = (
|
|
489
|
+
e: PointerEvent
|
|
490
|
+
): {
|
|
491
|
+
start: Date
|
|
492
|
+
end: Date
|
|
493
|
+
allDay: boolean
|
|
494
|
+
resourceId?: string
|
|
495
|
+
} | null => {
|
|
496
|
+
if (!surface) return null
|
|
497
|
+
const tl = surface
|
|
498
|
+
const rangeMinutes = (tl.rangeEnd - tl.rangeStart) / 60000
|
|
499
|
+
const minutesAt = (x: number) => surfaceMinutesAt(tl, x)
|
|
500
|
+
// Day-grid scales snap to real zoned midnights, not 1440-minute
|
|
501
|
+
// multiples from the range start - those drift by an hour across DST
|
|
502
|
+
const snapMin = (minutes: number) => {
|
|
503
|
+
if (tl.snapMin < 24 * 60) return snapMinutes(minutes, tl.snapMin)
|
|
504
|
+
const ms = tl.rangeStart + minutes * 60000
|
|
505
|
+
const dayStart = zonedStartOfDay(new Date(ms), settings.timeZone)
|
|
506
|
+
const dayEnd = zonedStartOfDay(
|
|
507
|
+
addDays(toZoned(new Date(ms), settings.timeZone), 1),
|
|
508
|
+
settings.timeZone
|
|
509
|
+
)
|
|
510
|
+
const snapped =
|
|
511
|
+
ms - dayStart.getTime() < dayEnd.getTime() - ms ? dayStart : dayEnd
|
|
512
|
+
return (snapped.getTime() - tl.rangeStart) / 60000
|
|
513
|
+
}
|
|
514
|
+
const rowAt = (y: number) => {
|
|
515
|
+
let best = tl.rows[0]
|
|
516
|
+
for (const row of tl.rows) {
|
|
517
|
+
if (y >= row.rect.top && y < row.rect.bottom) return row
|
|
518
|
+
if (
|
|
519
|
+
best &&
|
|
520
|
+
Math.abs(y - (row.rect.top + row.rect.height / 2)) <
|
|
521
|
+
Math.abs(y - (best.rect.top + best.rect.height / 2))
|
|
522
|
+
) {
|
|
523
|
+
best = row
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return best
|
|
527
|
+
}
|
|
528
|
+
const at = (minutes: number) => new Date(tl.rangeStart + minutes * 60000)
|
|
529
|
+
|
|
530
|
+
if (kind === "create") {
|
|
531
|
+
const anchorMin = snapMin(minutesAt(startX))
|
|
532
|
+
const curMin = snapMin(minutesAt(e.clientX))
|
|
533
|
+
const lo = Math.min(anchorMin, curMin)
|
|
534
|
+
// a bare click still yields a usable slot: at least slotDuration long
|
|
535
|
+
const hi = Math.max(
|
|
536
|
+
anchorMin,
|
|
537
|
+
curMin,
|
|
538
|
+
lo + Math.max(tl.snapMin, settings.slotDuration)
|
|
539
|
+
)
|
|
540
|
+
return {
|
|
541
|
+
start: at(lo),
|
|
542
|
+
end: at(hi),
|
|
543
|
+
allDay: false,
|
|
544
|
+
resourceId: rowAt(startY)?.resourceId,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!occurrence) return null
|
|
548
|
+
const midnightAligned = (d: Date) =>
|
|
549
|
+
zonedStartOfDay(d, settings.timeZone).getTime() === d.getTime()
|
|
550
|
+
if (kind === "move") {
|
|
551
|
+
// x-axis only: the bar slides along its OWN row, never across rows.
|
|
552
|
+
// The proposal preserves the pointer DELTA - no clamping to the visible
|
|
553
|
+
// range, or bars crossing the window edge would teleport to it.
|
|
554
|
+
const start = at(snapMin(minutesAt(e.clientX) - grabOffsetMin))
|
|
555
|
+
// Day-snapped scales preserve the CALENDAR span for day-aligned bars:
|
|
556
|
+
// a 3-day bar dragged across a DST change stays midnight-to-midnight
|
|
557
|
+
// (72h +/- 1h), never drifting to a 23:00 end. Sub-day events keep
|
|
558
|
+
// their exact ms duration.
|
|
559
|
+
let end: Date
|
|
560
|
+
if (occurrence.end.getTime() === occurrence.start.getTime()) {
|
|
561
|
+
// a milestone is an instant: moving it moves the point - the
|
|
562
|
+
// day-span branch below would silently stretch it to a full day
|
|
563
|
+
end = start
|
|
564
|
+
} else if (
|
|
565
|
+
tl.snapMin >= 24 * 60 &&
|
|
566
|
+
(occurrence.allDay ||
|
|
567
|
+
(midnightAligned(occurrence.start) &&
|
|
568
|
+
midnightAligned(occurrence.end)))
|
|
569
|
+
) {
|
|
570
|
+
const daySpan = Math.max(
|
|
571
|
+
differenceInCalendarDays(
|
|
572
|
+
toZoned(occurrence.end, settings.timeZone),
|
|
573
|
+
toZoned(occurrence.start, settings.timeZone)
|
|
574
|
+
),
|
|
575
|
+
1
|
|
576
|
+
)
|
|
577
|
+
end = zonedStartOfDay(
|
|
578
|
+
addDays(toZoned(start, settings.timeZone), daySpan),
|
|
579
|
+
settings.timeZone
|
|
580
|
+
)
|
|
581
|
+
} else {
|
|
582
|
+
end = new Date(
|
|
583
|
+
start.getTime() +
|
|
584
|
+
(occurrence.end.getTime() - occurrence.start.getTime())
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
const bounded = clampToNeighbours(start, end)
|
|
588
|
+
return {
|
|
589
|
+
start: bounded.start,
|
|
590
|
+
end: bounded.end,
|
|
591
|
+
allDay: occurrence.allDay,
|
|
592
|
+
resourceId: occurrence.event.resourceId,
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const min = snapMin(minutesAt(e.clientX))
|
|
596
|
+
if (kind === "resize-start") {
|
|
597
|
+
const endMin = (occurrence.end.getTime() - tl.rangeStart) / 60000
|
|
598
|
+
// Minimum length = one snap unit; on day grids that unit is the LAST
|
|
599
|
+
// zoned midnight before the end (raw 1440-minute arithmetic lands off
|
|
600
|
+
// the midnight grid across DST changes).
|
|
601
|
+
const maxStartMin =
|
|
602
|
+
tl.snapMin >= 24 * 60
|
|
603
|
+
? (zonedStartOfDay(
|
|
604
|
+
midnightAligned(occurrence.end)
|
|
605
|
+
? addDays(toZoned(occurrence.end, settings.timeZone), -1)
|
|
606
|
+
: occurrence.end,
|
|
607
|
+
settings.timeZone
|
|
608
|
+
).getTime() -
|
|
609
|
+
tl.rangeStart) /
|
|
610
|
+
60000
|
|
611
|
+
: endMin - tl.snapMin
|
|
612
|
+
const clamped = Math.min(Math.max(min, 0), maxStartMin)
|
|
613
|
+
const bounded = clampToNeighbours(at(clamped), occurrence.end)
|
|
614
|
+
return {
|
|
615
|
+
start: bounded.start,
|
|
616
|
+
end: bounded.end,
|
|
617
|
+
allDay: occurrence.allDay,
|
|
618
|
+
resourceId: occurrence.event.resourceId,
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const startMin = (occurrence.start.getTime() - tl.rangeStart) / 60000
|
|
622
|
+
// Mirror of the resize-start bound: the FIRST zoned midnight after the
|
|
623
|
+
// start on day grids, plain snap arithmetic otherwise.
|
|
624
|
+
const minEndMin =
|
|
625
|
+
tl.snapMin >= 24 * 60
|
|
626
|
+
? (zonedStartOfDay(
|
|
627
|
+
addDays(toZoned(occurrence.start, settings.timeZone), 1),
|
|
628
|
+
settings.timeZone
|
|
629
|
+
).getTime() -
|
|
630
|
+
tl.rangeStart) /
|
|
631
|
+
60000
|
|
632
|
+
: startMin + tl.snapMin
|
|
633
|
+
const clamped = Math.max(Math.min(min, rangeMinutes), minEndMin)
|
|
634
|
+
const bounded = clampToNeighbours(occurrence.start, at(clamped))
|
|
635
|
+
return {
|
|
636
|
+
start: bounded.start,
|
|
637
|
+
end: bounded.end,
|
|
638
|
+
allDay: occurrence.allDay,
|
|
639
|
+
resourceId: occurrence.event.resourceId,
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const applyProposal = (e: PointerEvent) => {
|
|
644
|
+
const proposal = computeProposal(e)
|
|
645
|
+
if (!proposal) return
|
|
646
|
+
const key = `${proposal.start.getTime()}-${proposal.end.getTime()}-${proposal.allDay}-${proposal.resourceId ?? ""}`
|
|
647
|
+
if (key === lastProposalKey) return
|
|
648
|
+
lastProposalKey = key
|
|
649
|
+
|
|
650
|
+
if (kind === "create") {
|
|
651
|
+
const draft = { ...proposal }
|
|
652
|
+
if (settings.canSelectSlot && !settings.canSelectSlot(draft)) return
|
|
653
|
+
internals.setSlotDraft(draft)
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
const update: GanttProposedUpdate<TData> = {
|
|
657
|
+
event: occurrence!.event,
|
|
658
|
+
occurrence: occurrence!,
|
|
659
|
+
...proposal,
|
|
660
|
+
source:
|
|
661
|
+
kind === "move" ? "drag" : (kind as "resize-start" | "resize-end"),
|
|
662
|
+
}
|
|
663
|
+
// "reject" is the one veto the engine owns: it both styles the ghost AND
|
|
664
|
+
// blocks the commit below. canDropEvent stays advisory unless the
|
|
665
|
+
// consumer opts into enforceCanDrop, which is checked at release.
|
|
666
|
+
overlapRejected =
|
|
667
|
+
overlapPolicy === "reject" &&
|
|
668
|
+
overlapsNeighbour(proposal.start, proposal.end)
|
|
669
|
+
const valid =
|
|
670
|
+
!overlapRejected &&
|
|
671
|
+
(settings.canDropEvent ? settings.canDropEvent(update) : true)
|
|
672
|
+
// live status: the indicator chip always names the CURRENT proposed
|
|
673
|
+
// range; the edge line flips to destructive on an invalid drop
|
|
674
|
+
if (resizeRange && resizeDot && resizeDuration) {
|
|
675
|
+
resizeRange.textContent = settings.i18n.functions.formatEventTime(
|
|
676
|
+
toZoned(proposal.start, settings.timeZone),
|
|
677
|
+
toZoned(proposal.end, settings.timeZone),
|
|
678
|
+
proposal.allDay,
|
|
679
|
+
settings.locale
|
|
680
|
+
)
|
|
681
|
+
const days = Math.round(
|
|
682
|
+
(proposal.end.getTime() - proposal.start.getTime()) / 86_400_000
|
|
683
|
+
)
|
|
684
|
+
const showDays = days >= 1
|
|
685
|
+
resizeDot.style.display = showDays ? "" : "none"
|
|
686
|
+
resizeDuration.style.display = showDays ? "" : "none"
|
|
687
|
+
if (showDays) {
|
|
688
|
+
resizeDuration.textContent = settings.i18n.labels.durationDays(days)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (resizeLine) {
|
|
692
|
+
resizeLine.style.background = valid
|
|
693
|
+
? (occurrence!.event.color ?? "var(--color-primary)")
|
|
694
|
+
: "var(--color-destructive)"
|
|
695
|
+
}
|
|
696
|
+
setBodyDragging(true, !valid)
|
|
697
|
+
internals.setDrag({
|
|
698
|
+
kind: kind === "move" ? "move" : (kind as "resize-start" | "resize-end"),
|
|
699
|
+
occurrence: occurrence!,
|
|
700
|
+
proposedStart: proposal.start,
|
|
701
|
+
proposedEnd: proposal.end,
|
|
702
|
+
proposedAllDay: proposal.allDay,
|
|
703
|
+
proposedResourceId: proposal.resourceId,
|
|
704
|
+
valid,
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ----- edge auto-scroll: pan the timeline while dragging near its edge -----
|
|
709
|
+
// The pointer is clamped to the visible track, so without this a bar can
|
|
710
|
+
// never travel past the window. Holding the pointer inside the edge zone
|
|
711
|
+
// scrolls the viewport (speed eased by proximity), refreshes the track rect
|
|
712
|
+
// (the axis moved under the pointer) and re-derives the proposal from the
|
|
713
|
+
// same pointer position. Programmatic scrolls never mark user intent, so
|
|
714
|
+
// this can never trigger infinite-range growth mid-gesture.
|
|
715
|
+
const AUTO_SCROLL_EDGE_PX = 24
|
|
716
|
+
const AUTO_SCROLL_MAX_SPEED = 14
|
|
717
|
+
let autoScrollRaf = 0
|
|
718
|
+
const timelineViewport = viewRoot?.querySelector<HTMLElement>(
|
|
719
|
+
"[data-slot=gantt-timeline-pane] [data-slot=scroll-area-viewport]"
|
|
720
|
+
)
|
|
721
|
+
const autoScrollTick = () => {
|
|
722
|
+
autoScrollRaf = 0
|
|
723
|
+
if (finished || !active || !surface || !timelineViewport) return
|
|
724
|
+
const paneRect = timelineViewport.getBoundingClientRect()
|
|
725
|
+
const x = lastPointer.clientX
|
|
726
|
+
let speed = 0
|
|
727
|
+
if (x < paneRect.left + AUTO_SCROLL_EDGE_PX) {
|
|
728
|
+
speed =
|
|
729
|
+
-((paneRect.left + AUTO_SCROLL_EDGE_PX - x) / AUTO_SCROLL_EDGE_PX) *
|
|
730
|
+
AUTO_SCROLL_MAX_SPEED
|
|
731
|
+
} else if (x > paneRect.right - AUTO_SCROLL_EDGE_PX) {
|
|
732
|
+
speed =
|
|
733
|
+
((x - (paneRect.right - AUTO_SCROLL_EDGE_PX)) / AUTO_SCROLL_EDGE_PX) *
|
|
734
|
+
AUTO_SCROLL_MAX_SPEED
|
|
735
|
+
}
|
|
736
|
+
if (speed === 0) return
|
|
737
|
+
const before = timelineViewport.scrollLeft
|
|
738
|
+
timelineViewport.scrollLeft = before + speed
|
|
739
|
+
if (timelineViewport.scrollLeft === before) return // parked on the end
|
|
740
|
+
const axis = viewRoot?.querySelector<HTMLElement>("[data-gantt-axis]")
|
|
741
|
+
if (axis) surface.rect = axis.getBoundingClientRect()
|
|
742
|
+
applyProposal(lastPointer)
|
|
743
|
+
positionResizeOverlay(lastPointer)
|
|
744
|
+
scheduleAutoScroll()
|
|
745
|
+
}
|
|
746
|
+
const scheduleAutoScroll = () => {
|
|
747
|
+
if (!autoScrollRaf) autoScrollRaf = requestAnimationFrame(autoScrollTick)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// idempotent: pointerup, pointercancel, Escape, blur and the view-level
|
|
751
|
+
// teardown can race; whichever lands first wins and the rest no-op
|
|
752
|
+
let finished = false
|
|
753
|
+
const cleanup = () => {
|
|
754
|
+
if (finished) return
|
|
755
|
+
finished = true
|
|
756
|
+
activeGestureCancels.delete(cancel)
|
|
757
|
+
if (autoScrollRaf) cancelAnimationFrame(autoScrollRaf)
|
|
758
|
+
try {
|
|
759
|
+
origin.releasePointerCapture(pointerId)
|
|
760
|
+
} catch {
|
|
761
|
+
// capture already released (pointer gone or origin detached)
|
|
762
|
+
}
|
|
763
|
+
window.removeEventListener("pointermove", onPointerMove)
|
|
764
|
+
window.removeEventListener("pointerup", onPointerUp)
|
|
765
|
+
window.removeEventListener("pointercancel", onCancel)
|
|
766
|
+
window.removeEventListener("blur", onWindowBlur)
|
|
767
|
+
window.removeEventListener("keydown", onKeyDown, true)
|
|
768
|
+
if (touchTimer) clearTimeout(touchTimer)
|
|
769
|
+
// consumer-rendered overlays are React-owned: they unmount when the drag
|
|
770
|
+
// state clears, so the engine must never removeChild them itself
|
|
771
|
+
if (!customMoveOverlay) overlay?.remove()
|
|
772
|
+
overlay = null
|
|
773
|
+
if (!customResizeOverlay) resizeOverlay?.remove()
|
|
774
|
+
resizeOverlay = null
|
|
775
|
+
setBodyDragging(false)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const cancel = () => {
|
|
779
|
+
cleanup()
|
|
780
|
+
if (active) {
|
|
781
|
+
lastGestureEndedAt = performance.now()
|
|
782
|
+
internals.setDrag(null)
|
|
783
|
+
internals.setSlotDraft(null)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
788
|
+
if (e.key === "Escape") {
|
|
789
|
+
e.stopPropagation()
|
|
790
|
+
cancel()
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// focus loss mid-gesture (alt-tab, OS dialogs) means the release may never
|
|
795
|
+
// be delivered; treat it as a cancel so the gesture cannot get stuck
|
|
796
|
+
const onWindowBlur = () => cancel()
|
|
797
|
+
|
|
798
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
799
|
+
if (e.pointerId !== pointerId) return
|
|
800
|
+
lastPointer = e
|
|
801
|
+
if (!active) {
|
|
802
|
+
const distance = Math.hypot(e.clientX - startX, e.clientY - startY)
|
|
803
|
+
if (isTouch) {
|
|
804
|
+
// Long-press pending: moving past tolerance means scroll, not drag
|
|
805
|
+
if (distance > activation.touchTolerancePx) cancel()
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
if (distance < activationDistance) return
|
|
809
|
+
activate()
|
|
810
|
+
}
|
|
811
|
+
applyProposal(e)
|
|
812
|
+
positionOverlay(e)
|
|
813
|
+
positionResizeOverlay(e)
|
|
814
|
+
scheduleAutoScroll()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
818
|
+
if (e.pointerId !== pointerId) return
|
|
819
|
+
cleanup()
|
|
820
|
+
if (!active) return
|
|
821
|
+
lastGestureEndedAt = performance.now()
|
|
822
|
+
|
|
823
|
+
const state = instance.getState()
|
|
824
|
+
if (kind === "create") {
|
|
825
|
+
const draft = state.slotDraft
|
|
826
|
+
internals.setSlotDraft(null)
|
|
827
|
+
if (draft) {
|
|
828
|
+
api.select({
|
|
829
|
+
slot: { start: draft.start, end: draft.end, allDay: draft.allDay },
|
|
830
|
+
})
|
|
831
|
+
settings.onSelectSlot?.(draft)
|
|
832
|
+
}
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
const drag = state.drag
|
|
836
|
+
internals.setDrag(null)
|
|
837
|
+
if (!drag || !occurrence) return
|
|
838
|
+
// the node refuses concurrency: revert instead of committing an overlap
|
|
839
|
+
if (overlapRejected) return
|
|
840
|
+
// enforceCanDrop makes the advisory verdict binding: a release whose
|
|
841
|
+
// last canDropEvent answer was false reverts exactly like "reject" -
|
|
842
|
+
// the red ghost becomes a promise instead of a suggestion
|
|
843
|
+
if (settings.enforceCanDrop && !drag.valid) return
|
|
844
|
+
const unchanged =
|
|
845
|
+
drag.proposedStart.getTime() === occurrence.start.getTime() &&
|
|
846
|
+
drag.proposedEnd.getTime() === occurrence.end.getTime() &&
|
|
847
|
+
(drag.proposedResourceId === undefined ||
|
|
848
|
+
drag.proposedResourceId === occurrence.event.resourceId)
|
|
849
|
+
if (unchanged) return
|
|
850
|
+
// Commit through the one validation funnel; consumer reject = automatic
|
|
851
|
+
// revert because the gantt never mutated during the gesture.
|
|
852
|
+
const accepted = internals.applyProposedUpdate({
|
|
853
|
+
event: occurrence.event,
|
|
854
|
+
occurrence,
|
|
855
|
+
start: drag.proposedStart,
|
|
856
|
+
end: drag.proposedEnd,
|
|
857
|
+
allDay: drag.proposedAllDay,
|
|
858
|
+
resourceId: drag.proposedResourceId,
|
|
859
|
+
source:
|
|
860
|
+
kind === "move" ? "drag" : (kind as "resize-start" | "resize-end"),
|
|
861
|
+
})
|
|
862
|
+
if (accepted && announcer) {
|
|
863
|
+
announcer.textContent = `${occurrence.event.title}, ${settings.i18n.functions.formatEventTime(
|
|
864
|
+
toZoned(drag.proposedStart, settings.timeZone),
|
|
865
|
+
toZoned(drag.proposedEnd, settings.timeZone),
|
|
866
|
+
drag.proposedAllDay,
|
|
867
|
+
settings.locale
|
|
868
|
+
)}`
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const onCancel = (e: PointerEvent) => {
|
|
873
|
+
if (e.pointerId !== pointerId) return
|
|
874
|
+
cancel()
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
window.addEventListener("pointermove", onPointerMove)
|
|
878
|
+
window.addEventListener("pointerup", onPointerUp)
|
|
879
|
+
window.addEventListener("pointercancel", onCancel)
|
|
880
|
+
window.addEventListener("blur", onWindowBlur)
|
|
881
|
+
window.addEventListener("keydown", onKeyDown, true)
|
|
882
|
+
activeGestureCancels.add(cancel)
|
|
883
|
+
|
|
884
|
+
// Capture the pointer so a release OUTSIDE the OS window still delivers
|
|
885
|
+
// pointerup here instead of leaving the gesture stuck. Captured events keep
|
|
886
|
+
// bubbling to the window listeners above, and if the origin node is removed
|
|
887
|
+
// mid-gesture the capture auto-releases - behavior then degrades to plain
|
|
888
|
+
// window listeners, never worse than before. Guarded: the pointer can
|
|
889
|
+
// already be gone by now (fast flicks, synthetic events).
|
|
890
|
+
try {
|
|
891
|
+
origin.setPointerCapture(pointerId)
|
|
892
|
+
} catch {
|
|
893
|
+
// capture is an enhancement, never a requirement
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Touch: long-press activation (movement past tolerance cancels above)
|
|
897
|
+
if (isTouch && !active) {
|
|
898
|
+
touchTimer = setTimeout(() => {
|
|
899
|
+
activate()
|
|
900
|
+
applyProposal(lastPointer)
|
|
901
|
+
}, activation.touchDelayMs)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Per-bar / per-row pointer gesture wiring. */
|
|
906
|
+
function useGanttGestures<TData = unknown>() {
|
|
907
|
+
const instance = useGantt<TData>()
|
|
908
|
+
const viewConfig = useGanttViewConfig<TData>()
|
|
909
|
+
// presence flags only: the engine skips its default overlay DOM and
|
|
910
|
+
// positions the consumer-rendered node instead
|
|
911
|
+
const customMoveOverlay = !!viewConfig.renderDragPreview
|
|
912
|
+
const customResizeOverlay = !!viewConfig.renderResizeIndicator
|
|
913
|
+
|
|
914
|
+
const canDrag = useCallback(
|
|
915
|
+
(segment: GanttSegment<TData>) => {
|
|
916
|
+
const { interactions } = instance.getState()
|
|
917
|
+
const event = segment.occurrence.event
|
|
918
|
+
return interactions.drag && !event.readOnly && event.draggable !== false
|
|
919
|
+
},
|
|
920
|
+
[instance]
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
const beginMove = useCallback(
|
|
924
|
+
(e: React.PointerEvent, segment: GanttSegment<TData>) => {
|
|
925
|
+
if (e.button !== 0 || !canDrag(segment)) return
|
|
926
|
+
beginGesture({
|
|
927
|
+
instance,
|
|
928
|
+
kind: "move",
|
|
929
|
+
origin: e.currentTarget as HTMLElement,
|
|
930
|
+
startEvent: e.nativeEvent,
|
|
931
|
+
segment,
|
|
932
|
+
customMoveOverlay,
|
|
933
|
+
scheduleMode: viewConfig.scheduleMode,
|
|
934
|
+
})
|
|
935
|
+
},
|
|
936
|
+
[instance, canDrag, customMoveOverlay, viewConfig.scheduleMode]
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
const canResize = useCallback(
|
|
940
|
+
(segment: GanttSegment<TData>) => {
|
|
941
|
+
const { interactions } = instance.getState()
|
|
942
|
+
const event = segment.occurrence.event
|
|
943
|
+
return (
|
|
944
|
+
interactions.resize &&
|
|
945
|
+
!event.readOnly &&
|
|
946
|
+
event.resizable !== false &&
|
|
947
|
+
// a milestone is an instant: it has no edges to resize
|
|
948
|
+
segment.occurrence.end.getTime() > segment.occurrence.start.getTime()
|
|
949
|
+
)
|
|
950
|
+
},
|
|
951
|
+
[instance]
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
const beginResize = useCallback(
|
|
955
|
+
(
|
|
956
|
+
e: React.PointerEvent,
|
|
957
|
+
segment: GanttSegment<TData>,
|
|
958
|
+
edge: "start" | "end"
|
|
959
|
+
) => {
|
|
960
|
+
if (e.button !== 0 || !canResize(segment)) return
|
|
961
|
+
e.stopPropagation()
|
|
962
|
+
e.preventDefault()
|
|
963
|
+
beginGesture({
|
|
964
|
+
instance,
|
|
965
|
+
kind: edge === "start" ? "resize-start" : "resize-end",
|
|
966
|
+
origin: e.currentTarget as HTMLElement,
|
|
967
|
+
startEvent: e.nativeEvent,
|
|
968
|
+
segment,
|
|
969
|
+
customResizeOverlay,
|
|
970
|
+
scheduleMode: viewConfig.scheduleMode,
|
|
971
|
+
})
|
|
972
|
+
},
|
|
973
|
+
[instance, canResize, customResizeOverlay, viewConfig.scheduleMode]
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
const beginCreate = useCallback(
|
|
977
|
+
(e: React.PointerEvent) => {
|
|
978
|
+
if (e.button !== 0) return
|
|
979
|
+
if (!instance.getState().interactions.selectSlot) return
|
|
980
|
+
beginGesture({
|
|
981
|
+
instance,
|
|
982
|
+
kind: "create",
|
|
983
|
+
origin: e.currentTarget as HTMLElement,
|
|
984
|
+
startEvent: e.nativeEvent,
|
|
985
|
+
})
|
|
986
|
+
},
|
|
987
|
+
[instance]
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
return { beginMove, beginResize, beginCreate, canDrag, canResize }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export {
|
|
994
|
+
cancelActiveGanttGestures,
|
|
995
|
+
GANTT_ACTIVATION,
|
|
996
|
+
markGestureEnd,
|
|
997
|
+
useGanttGestures,
|
|
998
|
+
useGanttGestureTeardown,
|
|
999
|
+
wasRecentDrag,
|
|
1000
|
+
}
|