@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.
Files changed (761) hide show
  1. package/package.json +66 -51
  2. package/src/__tests__/components/auth/auth-layout.test.tsx +13 -1
  3. package/src/__tests__/{knowledge-public → knowledge/public}/canonical-id.test.ts +1 -1
  4. package/src/__tests__/{knowledge-public → knowledge/public}/favicon.test.ts +1 -1
  5. package/src/__tests__/{knowledge-public → knowledge/public}/github-sync.test.ts +1 -1
  6. package/src/__tests__/{knowledge-public → knowledge/public}/share-merge.test.ts +7 -7
  7. package/src/__tests__/security/xss-sanitize.test.ts +9 -8
  8. package/src/components/ai/__tests__/chat-thread-id-stability.test.tsx +125 -0
  9. package/src/components/ai/ai-chat-prompt.tsx +8 -2
  10. package/src/components/ai/ai-prompt-input/index.tsx +2 -1
  11. package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +1 -2
  12. package/src/components/ai/global-harness-sidebar/index.tsx +22 -10
  13. package/src/components/bug/__tests__/board-subgroups.test.tsx +512 -0
  14. package/src/components/bug/__tests__/board-view.test.tsx +350 -0
  15. package/src/components/bug/__tests__/bug-list-loading-defer.test.tsx +85 -0
  16. package/src/components/bug/__tests__/bug-toolbar.test.tsx +7 -1
  17. package/src/components/bug/__tests__/comment-reactions.test.tsx +182 -0
  18. package/src/components/bug/__tests__/date-and-lightbox.test.tsx +96 -0
  19. package/src/components/bug/__tests__/export-entry.test.tsx +217 -0
  20. package/src/components/bug/__tests__/filters-compat.test.tsx +157 -0
  21. package/src/components/bug/__tests__/fix-request-race.test.tsx +396 -0
  22. package/src/components/bug/__tests__/peek-modes.test.tsx +365 -0
  23. package/src/components/bug/__tests__/saved-views.test.tsx +309 -0
  24. package/src/components/bug/__tests__/story-bugs-panel.test.tsx +146 -0
  25. package/src/components/bug/__tests__/subgroup-header.test.tsx +150 -0
  26. package/src/components/bug/__tests__/toolbar-shell.test.tsx +369 -0
  27. package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +58 -0
  28. package/src/components/bug/__tests__/view-type.test.tsx +247 -0
  29. package/src/components/bug/bug-list-view.tsx +310 -13
  30. package/src/components/bug/bug-panel-header.tsx +84 -8
  31. package/src/components/bug/bug-toolbar.tsx +268 -116
  32. package/src/components/bug/create-bug-dialog.tsx +1 -1
  33. package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -0
  34. package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +236 -0
  35. package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +2 -1
  36. package/src/components/bug/detail/activity-timeline.tsx +1 -1
  37. package/src/components/bug/detail/bug-detail-body.tsx +316 -18
  38. package/src/components/bug/detail/comment-item.tsx +7 -2
  39. package/src/components/bug/detail/comment-reactions.tsx +138 -0
  40. package/src/components/bug/detail/error-context.tsx +29 -9
  41. package/src/components/bug/detail/props-section.tsx +31 -3
  42. package/src/components/bug/detail/repro-report-card.tsx +1 -1
  43. package/src/components/bug/display/constants.ts +4 -2
  44. package/src/components/bug/display/display-dropdown.tsx +76 -280
  45. package/src/components/bug/display/types.ts +24 -8
  46. package/src/components/bug/display/use-paged-bug-groups.ts +183 -32
  47. package/src/components/bug/filters/{filter-value-input.tsx → field-configs.tsx} +218 -106
  48. package/src/components/bug/filters/index.tsx +14 -5
  49. package/src/components/bug/inline-editors/assignee-editor.tsx +2 -2
  50. package/src/components/bug/inline-editors/labels-editor.tsx +1 -1
  51. package/src/components/bug/inline-editors/severity-editor.tsx +37 -18
  52. package/src/components/bug/inline-editors/state-editor.tsx +1 -1
  53. package/src/components/bug/peek-panel.tsx +153 -35
  54. package/src/components/bug/select-bug-dialog.tsx +500 -0
  55. package/src/components/bug/story-bugs-panel.tsx +1090 -0
  56. package/src/components/bug/types.ts +9 -2
  57. package/src/components/bug/views/board-view.tsx +661 -0
  58. package/src/components/bug/views/bug-list/batch-toolbar.tsx +25 -34
  59. package/src/components/bug/views/bug-list/group.tsx +28 -13
  60. package/src/components/bug/views/bug-list/index.tsx +223 -59
  61. package/src/components/bug/views/bug-list/row.tsx +7 -1
  62. package/src/components/bug/views/create-view-dialog.tsx +160 -0
  63. package/src/components/bug/views/view-list-section.tsx +140 -0
  64. package/src/components/bug/views/view-selector-button.tsx +145 -0
  65. package/src/components/bug/views/view-switcher.tsx +8 -6
  66. package/src/components/bug/views/view-types.ts +162 -0
  67. package/src/components/collab/__tests__/activity-attribution.test.tsx +211 -0
  68. package/src/components/collab/__tests__/canvas-collab-panel.test.tsx +118 -0
  69. package/src/components/collab/__tests__/collab-presence-header.test.tsx +335 -0
  70. package/src/components/collab/__tests__/entity-presence-badge.test.tsx +226 -0
  71. package/src/components/collab/activity-attribution.tsx +221 -0
  72. package/src/components/collab/canvas-collab-panel.tsx +104 -0
  73. package/src/components/collab/collab-presence-header.tsx +220 -0
  74. package/src/components/collab/entity-presence-badge.tsx +130 -0
  75. package/src/components/collab/field-editing-pill.tsx +137 -0
  76. package/src/components/command/command-palette.tsx +6 -6
  77. package/src/components/design/canvas/document/__tests__/canvas-tree-fixture-snapshot.ts +589 -0
  78. package/src/components/design/canvas/document/__tests__/canvas-tree-reconcile.test.ts +159 -0
  79. package/src/components/design/canvas/panels/toolbar.tsx +3 -2
  80. package/src/components/design/canvas/plugins/presence/__tests__/canvas-collab-wiring.test.tsx +660 -0
  81. package/src/components/design/canvas/plugins/presence/__tests__/layout.test.ts +292 -0
  82. package/src/components/design/canvas/plugins/presence/__tests__/presence-overlay.test.tsx +139 -0
  83. package/src/components/design/canvas/plugins/presence/layout.ts +222 -0
  84. package/src/components/design/canvas/plugins/presence/presence-layer.tsx +353 -0
  85. package/src/components/design/canvas/plugins/presence/presence-overlay.tsx +155 -0
  86. package/src/components/design/canvas/plugins/presence/viewer-badge.tsx +80 -0
  87. package/src/components/design/canvas/react/__tests__/hooks.test.tsx +4 -4
  88. package/src/components/design/editor/collaboration/__tests__/design-agent-panel-regenerate.test.tsx +3 -1
  89. package/src/components/design/editor/collaboration/__tests__/design-agent-panel-stream.test.tsx +3 -1
  90. package/src/components/design/editor/collaboration/design-agent-panel.tsx +3 -0
  91. package/src/components/design/editor/collaboration/design-comment-review-panel.tsx +5 -5
  92. package/src/components/design/editor/dashboard/create-design-dialog.tsx +1 -1
  93. package/src/components/design/editor/dashboard/design-dashboard.tsx +1 -10
  94. package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +153 -0
  95. package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +8 -0
  96. package/src/components/design/editor/layout/design-editor-layout.tsx +28 -1
  97. package/src/components/design/editor/layout/design-navigation.tsx +0 -4
  98. package/src/components/design/editor/layout/hooks/__tests__/use-design-auto-save-collab.test.tsx +130 -0
  99. package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +18 -1
  100. package/src/components/design/editor/share/share-comment-panel.tsx +37 -20
  101. package/src/components/design/editor/share/share-layout.tsx +3 -0
  102. package/src/components/design/editor/share/share-nav-bar.tsx +7 -1
  103. package/src/components/design/editor/share/share-theme-toggle.tsx +10 -15
  104. package/src/components/design/editor/tiptap/node/paragraph-node/paragraph-node.scss +1 -1
  105. package/src/components/harness/__tests__/composer-draft.test.ts +126 -0
  106. package/src/components/harness/composer/composer-editor.tsx +64 -6
  107. package/src/components/harness/composer/harness-composer.tsx +1 -1
  108. package/src/components/harness/harness-layout.tsx +1 -1
  109. package/src/components/harness/harness-workbench-page.tsx +6 -1
  110. package/src/components/harness/turn-group.tsx +1 -1
  111. package/src/components/hotkeys/shortcut-badge.tsx +4 -4
  112. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +1 -1
  113. package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +542 -0
  114. package/src/components/knowledge/editor/doc-header.tsx +7 -49
  115. package/src/components/knowledge/editor/knowledge-doc-editor.tsx +264 -51
  116. package/src/components/knowledge/editor/mindmap-doc-editor.tsx +3 -19
  117. package/src/components/knowledge/knowledge-homepage.tsx +3 -3
  118. package/src/components/knowledge/knowledge-toolbar.tsx +8 -8
  119. package/src/components/{knowledge-public → knowledge/public}/bookmark-form-dialog.tsx +4 -4
  120. package/src/components/{knowledge-public → knowledge/public}/knowledge-card.tsx +1 -15
  121. package/src/components/{knowledge-public → knowledge/public}/knowledge-form-dialog.tsx +9 -9
  122. package/src/components/{knowledge-public → knowledge/public}/legal-compliance-editor.tsx +35 -28
  123. package/src/components/{knowledge-public → knowledge/public}/sharers-avatar-group.tsx +3 -2
  124. package/src/components/layout/layout-responsive-provider.tsx +79 -0
  125. package/src/components/layout/sidebar-03/__tests__/presence-avatar-stack.test.tsx +148 -0
  126. package/src/components/layout/sidebar-03/app-sidebar.tsx +9 -3
  127. package/src/components/layout/sidebar-03/index.tsx +19 -4
  128. package/src/components/layout/sidebar-03/nav-main.tsx +28 -10
  129. package/src/components/layout/sidebar-03/presence-avatar-stack.tsx +112 -0
  130. package/src/components/layout/sidebar-03/user-menu.tsx +3 -3
  131. package/src/components/project/create-project-dialog.tsx +1 -1
  132. package/src/components/project/invite-member-dialog.tsx +1 -1
  133. package/src/components/project/project-activity-list.tsx +2 -2
  134. package/src/components/project/project-member-manager.tsx +2 -2
  135. package/src/components/reui/__tests__/kanban-drop-indicator.test.ts +88 -0
  136. package/src/components/reui/__tests__/kanban-keyboard.test.ts +262 -0
  137. package/src/components/reui/event-calendar/event-calendar-agenda-view.tsx +236 -0
  138. package/src/components/reui/event-calendar/event-calendar-content.tsx +80 -0
  139. package/src/components/reui/event-calendar/event-calendar-dnd.tsx +1062 -0
  140. package/src/components/reui/event-calendar/event-calendar-event.tsx +555 -0
  141. package/src/components/reui/event-calendar/event-calendar-i18n.tsx +291 -0
  142. package/src/components/reui/event-calendar/event-calendar-lib.tsx +677 -0
  143. package/src/components/reui/event-calendar/event-calendar-month-view.tsx +1287 -0
  144. package/src/components/reui/event-calendar/event-calendar-nav.tsx +615 -0
  145. package/src/components/reui/event-calendar/event-calendar-recurrence.tsx +592 -0
  146. package/src/components/reui/event-calendar/event-calendar-resource-view.tsx +810 -0
  147. package/src/components/reui/event-calendar/event-calendar-time-grid.tsx +1366 -0
  148. package/src/components/reui/event-calendar/event-calendar-types.tsx +240 -0
  149. package/src/components/reui/event-calendar/event-calendar.tsx +1916 -0
  150. package/src/components/reui/gantt/__tests__/gantt-row-window.test.ts +243 -0
  151. package/src/components/reui/gantt/__tests__/gantt-view-virtualize.test.tsx +243 -0
  152. package/src/components/reui/gantt/gantt-bar.tsx +439 -0
  153. package/src/components/reui/gantt/gantt-dnd.tsx +1000 -0
  154. package/src/components/reui/gantt/gantt-i18n.tsx +287 -0
  155. package/src/components/reui/gantt/gantt-lib.tsx +695 -0
  156. package/src/components/reui/gantt/gantt-nav.tsx +446 -0
  157. package/src/components/reui/gantt/gantt-recurrence.tsx +402 -0
  158. package/src/components/reui/gantt/gantt-row-window.ts +119 -0
  159. package/src/components/reui/gantt/gantt-types.tsx +283 -0
  160. package/src/components/reui/gantt/gantt-view.tsx +4466 -0
  161. package/src/components/reui/gantt/gantt.tsx +1822 -0
  162. package/src/components/reui/gantt/use-gantt-row-window.ts +159 -0
  163. package/src/components/reui/icon-stack.tsx +90 -0
  164. package/src/components/reui/kanban-keyboard.ts +268 -0
  165. package/src/components/reui/kanban.tsx +1227 -0
  166. package/src/components/settings/personal-team/account/account-client.tsx +3 -2
  167. package/src/components/settings/personal-team/team/invitation-cards/invitation-history-list.tsx +5 -5
  168. package/src/components/settings/personal-team/team/team-client.tsx +2 -1
  169. package/src/components/settings/personal-team/team/team-member-list.tsx +4 -4
  170. package/src/components/share/share-presence-bar.tsx +182 -0
  171. package/src/components/shared-kit/auth/auth-client.ts +5 -0
  172. package/src/components/shared-kit/avatars/__tests__/avatar-shape.test.ts +44 -0
  173. package/src/components/shared-kit/avatars/__tests__/user-avatar.test.tsx +46 -0
  174. package/src/components/{tags/tags-collection.ts → shared-kit/avatars/avatar-shape.ts} +14 -20
  175. package/src/components/shared-kit/avatars/notion-avatar.tsx +62 -11
  176. package/src/components/shared-kit/avatars/user-avatar.tsx +7 -4
  177. package/src/components/shared-kit/list-kit/__tests__/batch-toolbar-shell.test.tsx +133 -0
  178. package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +89 -0
  179. package/src/components/shared-kit/list-kit/__tests__/level-badge.test.tsx +85 -0
  180. package/src/components/shared-kit/list-kit/__tests__/toolbar-shell.test.tsx +178 -0
  181. package/src/components/shared-kit/list-kit/batch-toolbar-shell.tsx +111 -0
  182. package/src/components/shared-kit/list-kit/csv-download.ts +46 -0
  183. package/src/components/shared-kit/list-kit/display/__tests__/dropdown.test.tsx +215 -0
  184. package/src/components/shared-kit/list-kit/display/dropdown.tsx +582 -0
  185. package/src/components/shared-kit/list-kit/display/types.ts +64 -0
  186. package/src/components/shared-kit/list-kit/display/values.ts +60 -0
  187. package/src/components/shared-kit/list-kit/filters/__tests__/create-filters-module.test.tsx +181 -0
  188. package/src/components/shared-kit/list-kit/filters/__tests__/types.test.ts +133 -0
  189. package/src/components/shared-kit/list-kit/filters/index.tsx +757 -0
  190. package/src/components/shared-kit/list-kit/filters/types.ts +68 -0
  191. package/src/components/shared-kit/list-kit/frozen-header.ts +45 -0
  192. package/src/components/shared-kit/list-kit/icon-semantics.ts +50 -0
  193. package/src/components/shared-kit/list-kit/kanban/__tests__/drop-layout.test.ts +139 -0
  194. package/src/components/shared-kit/list-kit/kanban/__tests__/frozen-header.test.ts +111 -0
  195. package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +130 -0
  196. package/src/components/shared-kit/list-kit/kanban/__tests__/kanban-board-view.test.tsx +901 -0
  197. package/src/components/shared-kit/list-kit/kanban/__tests__/manual-order.test.ts +35 -0
  198. package/src/components/shared-kit/list-kit/kanban/drop-commit.ts +212 -0
  199. package/src/components/shared-kit/list-kit/kanban/drop-layout.ts +98 -0
  200. package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +88 -0
  201. package/src/components/shared-kit/list-kit/kanban/instance-key.ts +83 -0
  202. package/src/components/shared-kit/list-kit/kanban/kanban-board-view.tsx +1414 -0
  203. package/src/components/shared-kit/list-kit/kanban/manual-order.ts +39 -0
  204. package/src/components/shared-kit/list-kit/level-badge.tsx +71 -0
  205. package/src/components/shared-kit/list-kit/toolbar-shell.tsx +103 -0
  206. package/src/components/shared-kit/loading/__tests__/use-deferred-flag.test.tsx +115 -0
  207. package/src/components/shared-kit/loading/use-deferred-flag.ts +35 -0
  208. package/src/components/shared-kit/peek/dock-slot.ts +62 -0
  209. package/src/components/shared-kit/peek/peek-mode.ts +34 -0
  210. package/src/components/shared-kit/theme/animated-theme-toggler.tsx +13 -7
  211. package/src/components/shared-kit/theme/theme-toggle.tsx +7 -18
  212. package/src/components/shared-kit/views/view-icon.tsx +49 -0
  213. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +215 -0
  214. package/src/components/story/__tests__/story-list-loading-defer.test.tsx +88 -0
  215. package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +242 -0
  216. package/src/components/story/__tests__/story-list-view-restore.test.tsx +45 -3
  217. package/src/components/story/__tests__/story-list-view-ssr.test.tsx +0 -1
  218. package/src/components/story/ai/__tests__/action-registry.test.ts +11 -11
  219. package/src/components/story/ai/action-registry.ts +5 -7
  220. package/src/components/story/ai/handlers/stream-text.ts +1 -3
  221. package/src/components/story/ai/story-ai-toolbar.tsx +58 -16
  222. package/src/components/story/ai/use-toolbar-overflow.ts +4 -7
  223. package/src/components/story/columns.tsx +2 -2
  224. package/src/components/story/constants.ts +17 -1
  225. package/src/components/story/create-story-dialog.tsx +8 -7
  226. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +38 -4
  227. package/src/components/story/detail/__tests__/categories.test.tsx +52 -26
  228. package/src/components/story/detail/__tests__/story-collab.test.tsx +897 -0
  229. package/src/components/story/detail/activity-tabs.tsx +5 -4
  230. package/src/components/story/detail/activity-timeline.tsx +15 -3
  231. package/src/components/story/detail/comment-input.tsx +2 -2
  232. package/src/components/story/detail/comment-item.tsx +6 -3
  233. package/src/components/story/detail/comment-thread.tsx +1 -0
  234. package/src/components/story/detail/reaction-bar.tsx +1 -1
  235. package/src/components/story/detail/story-detail-body.tsx +501 -76
  236. package/src/components/story/detail/story-detail-toolbar/__tests__/related-test-cases.test.tsx +173 -10
  237. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +144 -8
  238. package/src/components/story/detail/story-detail-toolbar/categories.tsx +82 -38
  239. package/src/components/story/detail/story-detail-toolbar/children.tsx +254 -174
  240. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +140 -58
  241. package/src/components/story/detail/story-detail-toolbar/index.tsx +613 -0
  242. package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +1010 -87
  243. package/src/components/story/detail/story-detail-toolbar/scroll-hint.tsx +57 -0
  244. package/src/components/story/detail/story-detail-toolbar/use-panel-frozen-groups.ts +97 -0
  245. package/src/components/story/detail/story-properties.tsx +9 -5
  246. package/src/components/story/display/__tests__/display-dropdown-helpers.test.ts +17 -1
  247. package/src/components/story/display/__tests__/display-dropdown.test.tsx +9 -0
  248. package/src/components/story/display/__tests__/flatten-story-groups.test.ts +42 -0
  249. package/src/components/story/display/__tests__/group-fields.test.ts +81 -1
  250. package/src/components/story/display/__tests__/group-resolver.test.ts +153 -1
  251. package/src/components/story/display/__tests__/use-collapsed-subgroups.test.ts +158 -0
  252. package/src/components/story/display/__tests__/use-display-options.test.ts +32 -3
  253. package/src/components/story/display/__tests__/use-fit-count.test.ts +198 -0
  254. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +77 -3
  255. package/src/components/story/display/constants.ts +23 -2
  256. package/src/components/story/display/display-dropdown.tsx +95 -397
  257. package/src/components/story/display/flatten-story-groups.ts +39 -0
  258. package/src/components/story/display/group-fields.ts +32 -1
  259. package/src/components/story/display/group-resolver.tsx +128 -5
  260. package/src/components/story/display/types.ts +23 -42
  261. package/src/components/story/display/use-collapsed-subgroups.ts +109 -0
  262. package/src/components/story/display/use-display-options.ts +21 -5
  263. package/src/components/story/display/use-fit-count.ts +124 -0
  264. package/src/components/story/display/use-paged-story-groups.ts +174 -31
  265. package/src/components/story/filters/field-configs.tsx +263 -0
  266. package/src/components/story/filters/index.tsx +14 -5
  267. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +2 -0
  268. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +15 -0
  269. package/src/components/story/inline-editors/assignee-editor.tsx +7 -5
  270. package/src/components/story/inline-editors/date-editor.tsx +7 -2
  271. package/src/components/story/inline-editors/inline-trigger.tsx +17 -4
  272. package/src/components/story/inline-editors/labels-editor.tsx +243 -35
  273. package/src/components/story/inline-editors/parent-story-editor.tsx +1 -1
  274. package/src/components/story/inline-editors/priority-editor.tsx +6 -4
  275. package/src/components/story/inline-editors/state-editor.tsx +6 -4
  276. package/src/components/story/inline-editors/story-select-dialog.tsx +56 -19
  277. package/src/components/story/peek-panel.tsx +234 -51
  278. package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +131 -0
  279. package/src/components/story/relations/attachment-manage-dialog.tsx +1 -1
  280. package/src/components/story/relations/design-select-dialog.tsx +285 -0
  281. package/src/components/story/relations/knowledge-select-dialog.tsx +3 -3
  282. package/src/components/story/relations/link-manage-dialog.tsx +2 -2
  283. package/src/components/story/story-list-view.tsx +175 -17
  284. package/src/components/story/story-panel-header.tsx +18 -11
  285. package/src/components/story/story-toolbar.tsx +177 -126
  286. package/src/components/story/transfer-story-popover.tsx +1 -1
  287. package/src/components/story/types.ts +11 -2
  288. package/src/components/story/views/__tests__/calendar-view.test.tsx +262 -0
  289. package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +68 -0
  290. package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +184 -0
  291. package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +179 -0
  292. package/src/components/story/views/__tests__/timeline-view.test.tsx +453 -0
  293. package/src/components/story/views/__tests__/view-icon.test.tsx +45 -0
  294. package/src/components/story/views/__tests__/view-types.test.ts +0 -8
  295. package/src/components/story/views/calendar/__tests__/mappers.test.ts +201 -0
  296. package/src/components/story/views/calendar/__tests__/slot-draft.test.ts +78 -0
  297. package/src/components/story/views/calendar/drag-blocked.ts +41 -0
  298. package/src/components/story/views/calendar/i18n.ts +91 -0
  299. package/src/components/story/views/calendar/mappers.ts +119 -0
  300. package/src/components/story/views/calendar/slot-draft.ts +48 -0
  301. package/src/components/story/views/calendar-view.tsx +490 -223
  302. package/src/components/story/views/create-view-dialog.tsx +6 -6
  303. package/src/components/story/views/kanban-view.tsx +618 -0
  304. package/src/components/story/views/{__tests__ → story-list/__tests__}/use-flat-rows.test.ts +1 -1
  305. package/src/components/story/views/story-list/batch-preview-dialog.tsx +3 -3
  306. package/src/components/story/views/story-list/batch-toolbar.tsx +17 -34
  307. package/src/components/story/views/story-list/group.tsx +4 -2
  308. package/src/components/story/views/story-list/index.tsx +142 -34
  309. package/src/components/story/views/story-list/row.tsx +4 -0
  310. package/src/components/story/views/{use-flat-rows.ts → story-list/use-flat-rows.ts} +2 -2
  311. package/src/components/story/views/timeline/__tests__/mappers.test.ts +742 -0
  312. package/src/components/story/views/timeline/__tests__/sentinel-geometry.test.ts +76 -0
  313. package/src/components/story/views/timeline/__tests__/use-date-commit.test.tsx +372 -0
  314. package/src/components/story/views/timeline/i18n.ts +96 -0
  315. package/src/components/story/views/timeline/mappers.ts +506 -0
  316. package/src/components/story/views/timeline/sentinel-geometry.ts +43 -0
  317. package/src/components/story/views/timeline/use-date-commit.ts +169 -0
  318. package/src/components/story/views/timeline-view.tsx +593 -222
  319. package/src/components/story/views/view-list-section.tsx +4 -4
  320. package/src/components/story/views/view-selector-button.tsx +9 -10
  321. package/src/components/story/views/view-switcher.tsx +7 -1
  322. package/src/components/story/views/view-types.ts +1 -5
  323. package/src/components/testcase/__tests__/batch-toolbar.test.tsx +245 -0
  324. package/src/components/testcase/__tests__/board-view.test.tsx +241 -0
  325. package/src/components/testcase/__tests__/case-row.test.tsx +24 -10
  326. package/src/components/testcase/__tests__/constants.test.ts +151 -0
  327. package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +173 -0
  328. package/src/components/testcase/__tests__/detail-properties.test.tsx +19 -0
  329. package/src/components/testcase/__tests__/display-options.test.tsx +154 -0
  330. package/src/components/testcase/__tests__/export-entry.test.tsx +279 -0
  331. package/src/components/testcase/__tests__/filters.test.tsx +280 -0
  332. package/src/components/testcase/__tests__/list-view-scope.test.tsx +290 -0
  333. package/src/components/testcase/__tests__/list-view.test.tsx +552 -7
  334. package/src/components/testcase/__tests__/manual-move.test.ts +69 -0
  335. package/src/components/testcase/__tests__/peek-panel.test.tsx +89 -8
  336. package/src/components/testcase/__tests__/toolbar.test.tsx +302 -0
  337. package/src/components/testcase/__tests__/view-fallback.test.tsx +272 -0
  338. package/src/components/testcase/__tests__/view-management.test.tsx +179 -0
  339. package/src/components/testcase/ai/generate-dialog.tsx +1 -1
  340. package/src/components/testcase/constants.ts +83 -0
  341. package/src/components/testcase/create-testcase-dialog.tsx +76 -8
  342. package/src/components/testcase/detail/__tests__/select-cases-dialog.test.tsx +163 -0
  343. package/src/components/testcase/detail/case-detail-body.tsx +311 -30
  344. package/src/components/testcase/detail/select-cases-dialog.tsx +465 -0
  345. package/src/components/testcase/detail/story-link-editor.tsx +1 -1
  346. package/src/components/testcase/display/display-dropdown.tsx +147 -0
  347. package/src/components/testcase/display/field-configs.ts +80 -0
  348. package/src/components/testcase/display/use-testcase-display-options.ts +59 -0
  349. package/src/components/testcase/filters/field-configs.tsx +748 -0
  350. package/src/components/testcase/filters/index.tsx +35 -127
  351. package/src/components/testcase/inline-editors/assignee-editor.tsx +6 -6
  352. package/src/components/testcase/inline-editors/folder-editor.tsx +9 -9
  353. package/src/components/testcase/inline-editors/priority-editor.tsx +47 -31
  354. package/src/components/testcase/inline-editors/state-editor.tsx +14 -12
  355. package/src/components/testcase/inline-editors/type-editor.tsx +31 -20
  356. package/src/components/testcase/list/case-row.tsx +60 -2
  357. package/src/components/testcase/list/grouped-list.tsx +3 -2
  358. package/src/components/testcase/list-view.tsx +633 -137
  359. package/src/components/testcase/peek-panel.tsx +69 -11
  360. package/src/components/testcase/plans/plan-list.tsx +6 -1
  361. package/src/components/testcase/step-editor.tsx +34 -3
  362. package/src/components/testcase/testcase-toolbar.tsx +349 -94
  363. package/src/components/testcase/views/batch-toolbar.tsx +203 -0
  364. package/src/components/testcase/views/board-view.tsx +546 -0
  365. package/src/components/testcase/views/create-view-dialog.tsx +26 -14
  366. package/src/components/testcase/views/move.ts +56 -0
  367. package/src/components/testcase/views/view-selector-button.tsx +82 -30
  368. package/src/components/testcase/views/view-switcher.tsx +12 -5
  369. package/src/components/testcase/views/view-types.ts +50 -27
  370. package/src/components/tiptap/mindmap/lib/__tests__/shortcuts.test.ts +5 -4
  371. package/src/components/tiptap/mindmap/lib/shortcuts.ts +4 -3
  372. package/src/components/tiptap/node/doc-card-node-view.tsx +2 -1
  373. package/src/components/tiptap/notion-like/editor-extensions.ts +231 -0
  374. package/src/components/tiptap/notion-like/notion-like-editor.tsx +29 -156
  375. package/src/components/tiptap/ui/ai-menu/ai-menu.tsx +6 -1
  376. package/src/components/tiptap/ui/doc-reference-interaction/doc-reference-interaction.tsx +2 -1
  377. package/src/components/ui/checkbox.tsx +4 -4
  378. package/src/components/ui/popover.tsx +1 -1
  379. package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +1 -1
  380. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +1 -1
  381. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +1 -1
  382. package/src/components/workspace/members/workspace-member-item.tsx +2 -2
  383. package/src/components/workspace/members/workspace-members-manager.tsx +2 -2
  384. package/src/config/sidebar-routes.tsx +11 -13
  385. package/src/design-engine/document/serialize-leafer-json.ts +2 -1
  386. package/src/design-engine/engine/__tests__/engine-split.test.ts +1 -1
  387. package/src/design-engine/engine/__tests__/engine.test.ts +20 -0
  388. package/src/design-engine/engine/app.ts +5 -0
  389. package/src/design-engine/engine/engine-base.ts +17 -0
  390. package/src/design-engine/engine/engine-doc.ts +32 -0
  391. package/src/hooks/__tests__/use-commands.test.tsx +17 -5
  392. package/src/hooks/use-commands.ts +4 -4
  393. package/src/hooks/use-toolbar-search.ts +7 -2
  394. package/src/lib/__tests__/datetime-local.test.ts +46 -0
  395. package/src/lib/__tests__/datetime.test.ts +7 -0
  396. package/src/lib/__tests__/layout-responsive.test.ts +123 -0
  397. package/src/lib/__tests__/sidebar-utils.test.ts +4 -4
  398. package/src/lib/ai/story-ai-stream.ts +1 -1
  399. package/src/lib/ai/use-selected-model.ts +15 -5
  400. package/src/lib/changelog/sdk-versions.ts +22 -22
  401. package/src/lib/collab/__tests__/bindings-fields.test.ts +496 -0
  402. package/src/lib/collab/__tests__/bindings-steps.test.ts +332 -0
  403. package/src/lib/collab/__tests__/presence-store.test.ts +414 -0
  404. package/src/lib/collab/__tests__/provider-handle-contract.test.ts +89 -0
  405. package/src/lib/collab/__tests__/provider.test.ts +504 -0
  406. package/src/lib/collab/__tests__/use-presence-list-refresh.test.tsx +309 -0
  407. package/src/lib/collab/__tests__/use-presence-report.test.ts +356 -0
  408. package/src/lib/collab/__tests__/use-share-presence.test.ts +555 -0
  409. package/src/lib/collab/__tests__/user-color.test.ts +53 -0
  410. package/src/lib/collab/bindings/__tests__/canvas-local.test.ts +583 -0
  411. package/src/lib/collab/bindings/__tests__/canvas-presence.test.ts +378 -0
  412. package/src/lib/collab/bindings/__tests__/canvas-remote.test.ts +454 -0
  413. package/src/lib/collab/bindings/__tests__/canvas-tree-perf.test.ts +134 -0
  414. package/src/lib/collab/bindings/canvas-local.ts +415 -0
  415. package/src/lib/collab/bindings/canvas-presence.ts +420 -0
  416. package/src/lib/collab/bindings/canvas-remote.ts +226 -0
  417. package/src/lib/collab/bindings/fields.ts +463 -0
  418. package/src/lib/collab/bindings/steps.ts +332 -0
  419. package/src/lib/collab/env.ts +33 -0
  420. package/src/lib/collab/module-map.ts +131 -0
  421. package/src/lib/collab/presence-store.ts +350 -0
  422. package/src/lib/collab/provider.ts +378 -0
  423. package/src/lib/collab/use-collab-doc.ts +100 -0
  424. package/src/lib/collab/use-presence-list-refresh.ts +142 -0
  425. package/src/lib/collab/use-presence-report.ts +176 -0
  426. package/src/lib/collab/use-self-profile-broadcast.ts +113 -0
  427. package/src/lib/collab/use-share-presence.ts +561 -0
  428. package/src/lib/collab/user-color.ts +40 -0
  429. package/src/lib/collections/index.ts +0 -1
  430. package/src/lib/collections/reference/bookmark-categories-collection.ts +1 -1
  431. package/src/lib/commands/__tests__/registry.test.ts +2 -5
  432. package/src/lib/commands/registry.ts +0 -3
  433. package/src/lib/commands/types.ts +1 -1
  434. package/src/lib/datetime.ts +20 -3
  435. package/src/lib/hooks/__tests__/use-synced-state.test.tsx +54 -0
  436. package/src/lib/hooks/use-story-panel-open.ts +134 -0
  437. package/src/lib/hooks/use-synced-state.ts +17 -0
  438. package/src/lib/hotkeys/__tests__/registry.test.ts +1 -1
  439. package/src/lib/hotkeys/registry.ts +0 -3
  440. package/src/lib/layout-responsive.ts +140 -0
  441. package/src/lib/pacer.ts +10 -1
  442. package/src/lib/peek-dock.ts +32 -0
  443. package/src/lib/plugin-runtime/__tests__/plugin-tool-schema-parity.test.ts +140 -0
  444. package/src/lib/plugin-runtime/plugin-client-tools.ts +28 -46
  445. package/src/lib/plugin-runtime/plugin-tool-defs.ts +152 -0
  446. package/src/lib/project-module-meta.ts +1 -8
  447. package/src/lib/rate-limit.ts +15 -6
  448. package/src/lib/sanitize.ts +122 -10
  449. package/src/lib/share-events.ts +20 -11
  450. package/src/lib/sidebar-utils.ts +1 -1
  451. package/src/lib/theme/__tests__/theme-mode.test.ts +96 -0
  452. package/src/lib/theme/theme-mode.ts +77 -0
  453. package/src/paraglide/messages/_index.js +5312 -1591
  454. package/src/paraglide/messages/en.js +1880 -685
  455. package/src/paraglide/messages/zh.js +1877 -682
  456. package/src/providers/index.tsx +4 -1
  457. package/src/providers/theme-mode-provider.tsx +135 -0
  458. package/src/routeTree.gen.ts +78 -101
  459. package/src/routes/$lang/docs/$/route.lazy.tsx +7 -2
  460. package/src/routes/-tests/chat-body-validation.test.ts +61 -3
  461. package/src/routes/__tests__/-edition-routing.test.tsx +12 -8
  462. package/src/routes/_nav/-components/nav-not-found.tsx +2 -2
  463. package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +16 -2
  464. package/src/routes/_nav/bugs/$bugId/route.tsx +11 -2
  465. package/src/routes/_nav/dashboard/-components/avatar-group.tsx +2 -2
  466. package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.lazy.tsx +31 -17
  467. package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.tsx +12 -1
  468. package/src/routes/_nav/knowledge/$kbId/index.lazy.tsx +4 -2
  469. package/src/routes/_nav/knowledge/$kbId/route.lazy.tsx +7 -3
  470. package/src/routes/_nav/knowledge/bookmarks/$id.lazy.tsx +6 -6
  471. package/src/routes/_nav/knowledge/bookmarks/index.lazy.tsx +10 -10
  472. package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -9
  473. package/src/routes/_nav/knowledge/compliance/edit/$id.lazy.tsx +1 -1
  474. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +8 -8
  475. package/src/routes/_nav/knowledge/dictionaries/$id.lazy.tsx +4 -4
  476. package/src/routes/_nav/knowledge/dictionaries/index.lazy.tsx +6 -6
  477. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  478. package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +9 -9
  479. package/src/routes/_nav/knowledge/laws/edit/$id.lazy.tsx +1 -1
  480. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +11 -11
  481. package/src/routes/_nav/knowledge/summary/index.lazy.tsx +1 -1
  482. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +14 -6
  483. package/src/routes/_nav/route.lazy.tsx +11 -6
  484. package/src/routes/_nav/settings/account/route.tsx +3 -2
  485. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +52 -8
  486. package/src/routes/_nav/stories/$storyId/route.tsx +11 -0
  487. package/src/routes/_nav/stories/-components/story-detail-skeleton.tsx +1 -1
  488. package/src/{components/tags/pages/tag-detail-page.tsx → routes/_nav/tasks/$taskId/route.lazy.tsx} +20 -10
  489. package/src/routes/_nav/tasks/$taskId/route.tsx +43 -0
  490. package/src/routes/_nav/tasks/-components/ai-task-detail.tsx +307 -0
  491. package/src/routes/_nav/tasks/-components/ai-task-list.tsx +217 -0
  492. package/src/routes/_nav/tasks/-components/ai-task-status-badge.tsx +46 -0
  493. package/src/routes/_nav/tasks/-components/assigned-list.tsx +157 -0
  494. package/src/routes/_nav/tasks/-components/create-ai-task-dialog.tsx +228 -0
  495. package/src/routes/_nav/tasks/-components/source-ai-exec-dialog.tsx +175 -0
  496. package/src/routes/_nav/tasks/-components/task-constants.ts +115 -0
  497. package/src/routes/_nav/tasks/-components/use-task-filter.ts +64 -0
  498. package/src/routes/_nav/tasks/-tests/create-ai-task-dialog.test.tsx +175 -0
  499. package/src/routes/_nav/tasks/-tests/task-filter.test.ts +134 -0
  500. package/src/routes/_nav/tasks/-tests/task-styles.test.tsx +119 -0
  501. package/src/routes/_nav/tasks/index.lazy.tsx +114 -0
  502. package/src/routes/_nav/tasks/index.tsx +47 -0
  503. package/src/routes/_nav/{tags → tasks}/route.tsx +3 -3
  504. package/src/routes/_nav/test-cases/$caseId/route.lazy.tsx +3 -1
  505. package/src/routes/_nav/test-cases/$caseId/route.tsx +11 -2
  506. package/src/routes/api/chat.ts +23 -19
  507. package/src/routes/api/design/prototype/route.ts +2 -4
  508. package/src/routes/{_nav/acceptance-criteria/route.tsx → api/internal/collab-reindex.ts} +11 -8
  509. package/src/routes/api/knowledge/sync-github.ts +1 -1
  510. package/src/routes/design-files/$projectId/$artboardId/$filename.ts +9 -0
  511. package/src/routes/invite/$token/route.lazy.tsx +2 -2
  512. package/src/routes/share/$versionId/route.lazy.tsx +8 -0
  513. package/src/routes/share/compliance/$id/route.lazy.tsx +2 -2
  514. package/src/routes/share/compliance/$id/route.tsx +1 -1
  515. package/src/routes/share/law/$id/route.lazy.tsx +2 -2
  516. package/src/routes/share/law/$id/route.tsx +1 -1
  517. package/src/server/__tests__/bug-groups-two-level.test.ts +263 -0
  518. package/src/server/__tests__/builtin-project.test.ts +45 -1
  519. package/src/server/__tests__/csv-export.test.ts +319 -0
  520. package/src/server/__tests__/share-version.test.ts +7 -2
  521. package/src/server/__tests__/task-assigned-query.test.ts +232 -0
  522. package/src/server/__tests__/task-context-builder.test.ts +175 -0
  523. package/src/server/__tests__/task-executor.test.ts +349 -0
  524. package/src/server/__tests__/task-priority-map.test.ts +41 -0
  525. package/src/server/__tests__/task-serverfn-crud.test.ts +639 -0
  526. package/src/server/__tests__/task-serverfn-queries.test.ts +403 -0
  527. package/src/server/__tests__/testcase-board.test.ts +301 -0
  528. package/src/server/__tests__/testcase-bulk.test.ts +155 -0
  529. package/src/server/__tests__/testcase-list-scope.test.ts +215 -0
  530. package/src/server/ai-gateway/__tests__/chat-handler-prompt-key.test.ts +106 -0
  531. package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +9 -3
  532. package/src/server/ai-gateway/__tests__/conformance.test.ts +4 -3
  533. package/src/server/ai-gateway/adapter.ts +4 -2
  534. package/src/server/ai-gateway/chat-handler.ts +13 -5
  535. package/src/server/ai-gateway/chat-stream-builder.ts +12 -7
  536. package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +88 -0
  537. package/src/server/ai-gateway/sandbox/harness-sandbox.ts +1 -1
  538. package/src/server/ai-gateway/story-handler.ts +7 -2
  539. package/src/server/ai-gateway/tools/plugin-lifecycle-defs.ts +8 -99
  540. package/src/server/ai-gateway/tools/plugin-tools.ts +6 -10
  541. package/src/server/ai-task/assigned-query.ts +167 -0
  542. package/src/server/ai-task/context-builder.ts +182 -0
  543. package/src/server/ai-task/executor.ts +296 -0
  544. package/src/server/ai-task/priority-map.ts +37 -0
  545. package/src/server/ai-task/types.ts +15 -0
  546. package/src/server/bug-report/__tests__/report-handler.test.ts +55 -4
  547. package/src/server/bug-report/report-handler.ts +54 -8
  548. package/src/server/builtin-project.ts +24 -0
  549. package/src/server/cloudflare/prisma-wasm-base64-stub.ts +5 -0
  550. package/src/server/collab/__tests__/collab-permissions.test.ts +412 -0
  551. package/src/server/collab/__tests__/knowledge-additions.test.ts +121 -0
  552. package/src/server/collab/__tests__/reindex-endpoint.test.ts +168 -0
  553. package/src/server/collab/__tests__/resolve.test.ts +249 -0
  554. package/src/server/collab/__tests__/touch-snapshot.test.ts +58 -0
  555. package/src/server/collab/__tests__/write-guard-sdk.test.ts +501 -0
  556. package/src/server/collab/__tests__/write-guard.test.ts +373 -0
  557. package/src/server/collab/knowledge-additions.ts +37 -0
  558. package/src/server/collab/reindex-endpoint.ts +75 -0
  559. package/src/server/collab/resolve.ts +122 -0
  560. package/src/server/collab/seed.ts +408 -0
  561. package/src/server/collab/ticket-utils.ts +107 -0
  562. package/src/server/collab/touch-snapshot.ts +55 -0
  563. package/src/server/collab/write-guard.ts +258 -0
  564. package/src/server/collab-permissions.ts +248 -0
  565. package/src/server/csv.ts +61 -0
  566. package/src/server/{knowledge-public → knowledge/public}/github-sync.ts +12 -2
  567. package/src/server/{knowledge-public → knowledge/public}/share-merge.ts +31 -8
  568. package/src/server/lib/__tests__/sequence-number-retry.test.ts +160 -0
  569. package/src/server/lib/rate-limit.ts +13 -2
  570. package/src/server/lib/sequence-number-retry.ts +85 -0
  571. package/src/server/mcp/__tests__/tools.test.ts +10 -0
  572. package/src/server/mcp/tools.ts +26 -2
  573. package/src/server/middlewares/auth-rate-limit.ts +18 -9
  574. package/src/server/notifications/event-bus.ts +3 -0
  575. package/src/server/serverFns/__tests__/collab-snapshot.test.ts +665 -0
  576. package/src/server/serverFns/__tests__/collab.test.ts +459 -0
  577. package/src/server/serverFns/__tests__/knowledge-delete.test.ts +167 -0
  578. package/src/server/serverFns/__tests__/ownership-merge.test.ts +34 -2
  579. package/src/server/serverFns/__tests__/read-write-merge.test.ts +2 -2
  580. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +12 -2
  581. package/src/server/serverFns/__tests__/story-crud.test.ts +51 -11
  582. package/src/server/serverFns/__tests__/story-tree.test.ts +66 -34
  583. package/src/server/serverFns/__tests__/story-types.test.ts +14 -9
  584. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +325 -0
  585. package/src/server/serverFns/__tests__/update-document-guard.test.ts +285 -0
  586. package/src/server/serverFns/__tests__/update-page-guard.test.ts +276 -0
  587. package/src/server/serverFns/__tests__/update-story-guard.test.ts +436 -0
  588. package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +293 -0
  589. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +25 -0
  590. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +315 -15
  591. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +36 -18
  592. package/src/server/serverFns/bug/__tests__/bug-list-query.test.ts +21 -0
  593. package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +335 -0
  594. package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +165 -0
  595. package/src/server/serverFns/bug/__tests__/bug-story.test.ts +4 -1
  596. package/src/server/serverFns/bug/bug-comments.ts +5 -0
  597. package/src/server/serverFns/bug/bug-crud.ts +321 -72
  598. package/src/server/serverFns/bug/bug-display-options.ts +11 -0
  599. package/src/server/serverFns/bug/bug-export.ts +137 -0
  600. package/src/server/serverFns/bug/bug-group-query.ts +16 -5
  601. package/src/server/serverFns/bug/bug-groups.ts +39 -15
  602. package/src/server/serverFns/bug/bug-list-query.ts +8 -1
  603. package/src/server/serverFns/bug/bug-reactions.ts +123 -0
  604. package/src/server/serverFns/bug/bug-reorder.ts +64 -0
  605. package/src/server/serverFns/bug/bug-story-bulk.ts +280 -0
  606. package/src/server/serverFns/bug/bug-story.ts +47 -39
  607. package/src/server/serverFns/bug/index.ts +6 -0
  608. package/src/server/serverFns/collab-presence.ts +119 -0
  609. package/src/server/serverFns/collab-snapshot.ts +201 -0
  610. package/src/server/serverFns/collab.ts +153 -0
  611. package/src/server/serverFns/design/__tests__/designs.test.ts +0 -2
  612. package/src/server/serverFns/design/designs.ts +0 -64
  613. package/src/server/serverFns/design/detail.ts +36 -2
  614. package/src/server/serverFns/design/pages.ts +73 -10
  615. package/src/server/serverFns/iteration/iteration-crud.ts +25 -1
  616. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/create.ts +3 -3
  617. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/detail.ts +2 -2
  618. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/list.ts +3 -2
  619. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/refresh-favicon.ts +1 -1
  620. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/share.ts +1 -1
  621. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/sync-github.ts +1 -1
  622. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/update.ts +5 -4
  623. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/create.ts +1 -1
  624. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/detail.ts +2 -2
  625. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/list.ts +3 -2
  626. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/share.ts +1 -1
  627. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/update.ts +3 -2
  628. package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/create.ts +1 -1
  629. package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/detail.ts +2 -2
  630. package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/list.ts +3 -2
  631. package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/share.ts +1 -1
  632. package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/update.ts +3 -2
  633. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/create.ts +1 -1
  634. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/detail.ts +2 -2
  635. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/list.ts +3 -2
  636. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/share.ts +1 -1
  637. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/update.ts +3 -2
  638. package/src/server/serverFns/knowledge.ts +135 -19
  639. package/src/server/serverFns/project/module-overview.ts +1 -2
  640. package/src/server/serverFns/share/version.ts +7 -1
  641. package/src/server/serverFns/story/__tests__/bulk.test.ts +6 -0
  642. package/src/server/serverFns/story/__tests__/create-story-internal.test.ts +112 -3
  643. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +77 -6
  644. package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +187 -0
  645. package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +163 -0
  646. package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -0
  647. package/src/server/serverFns/story/__tests__/story-list-query.test.ts +20 -0
  648. package/src/server/serverFns/story/__tests__/story-search.test.ts +5 -0
  649. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +64 -1
  650. package/src/server/serverFns/story/index.ts +2 -2
  651. package/src/server/serverFns/story/story-assoc.ts +44 -14
  652. package/src/server/serverFns/story/story-crud/archive.ts +52 -28
  653. package/src/server/serverFns/story/story-crud/bulk.ts +14 -4
  654. package/src/server/serverFns/story/story-crud/create-story-internal.ts +39 -21
  655. package/src/server/serverFns/story/story-crud.ts +283 -116
  656. package/src/server/serverFns/story/story-dependencies.ts +31 -0
  657. package/src/server/serverFns/story/story-groups.ts +15 -11
  658. package/src/server/serverFns/story/story-list-query.ts +16 -4
  659. package/src/server/serverFns/story/story-search.ts +9 -6
  660. package/src/server/serverFns/story/story-transfer.ts +99 -42
  661. package/src/server/serverFns/story/story-tree.ts +70 -48
  662. package/src/server/serverFns/story-display-options.ts +21 -10
  663. package/src/server/serverFns/story-view.ts +2 -2
  664. package/src/server/serverFns/tasks/task-crud.ts +264 -0
  665. package/src/server/serverFns/tasks/task-queries.ts +172 -0
  666. package/src/server/serverFns/testcase/__tests__/generation.test.ts +3 -0
  667. package/src/server/serverFns/testcase/__tests__/stats.test.ts +18 -4
  668. package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +113 -0
  669. package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +62 -14
  670. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +32 -10
  671. package/src/server/serverFns/testcase/bulk.ts +195 -0
  672. package/src/server/serverFns/testcase/generation.ts +10 -0
  673. package/src/server/serverFns/testcase/stats.ts +22 -8
  674. package/src/server/serverFns/testcase/testcase-board.ts +198 -0
  675. package/src/server/serverFns/testcase/testcase-crud.ts +165 -33
  676. package/src/server/serverFns/testcase/testcase-display-options.ts +166 -0
  677. package/src/server/serverFns/testcase/testcase-export.ts +125 -0
  678. package/src/server/serverFns/testcase/testcase-filter-views.ts +36 -9
  679. package/src/server/serverFns/testcase/testcase-groups.ts +27 -23
  680. package/src/server/serverFns/testcase/testcase-reorder.ts +66 -0
  681. package/src/server/story/__tests__/date-key.test.ts +57 -0
  682. package/src/server/story/date-key.ts +36 -0
  683. package/src/server/story/types.ts +3 -7
  684. package/src/server/testcase/__tests__/list-query.test.ts +269 -0
  685. package/src/server/testcase/list-query.ts +145 -0
  686. package/src/server-entry.ts +32 -0
  687. package/src/shared/bug/__tests__/constants.test.ts +44 -0
  688. package/src/shared/bug/constants.ts +37 -1
  689. package/src/start.ts +7 -2
  690. package/src/styles/globals.css +24 -3
  691. package/src/types/project.ts +2 -3
  692. package/vite.shared.ts +110 -3
  693. package/src/__tests__/tag-badge.test.tsx +0 -41
  694. package/src/components/bug/__tests__/bug-list-table.test.tsx +0 -289
  695. package/src/components/bug/bug-list-table.tsx +0 -187
  696. package/src/components/bug/columns.tsx +0 -193
  697. package/src/components/bug/filters/add-filter-dropdown.tsx +0 -70
  698. package/src/components/bug/filters/applied-filters.tsx +0 -214
  699. package/src/components/bug/filters/filter-condition.tsx +0 -151
  700. package/src/components/bug/filters/filter-row.tsx +0 -104
  701. package/src/components/bug/filters/filter-toggle.tsx +0 -61
  702. package/src/components/bug/filters/filter-types.ts +0 -146
  703. package/src/components/story/detail/story-detail-toolbar.tsx +0 -382
  704. package/src/components/story/filters/add-filter-dropdown.tsx +0 -70
  705. package/src/components/story/filters/applied-filters.tsx +0 -225
  706. package/src/components/story/filters/filter-condition.tsx +0 -152
  707. package/src/components/story/filters/filter-row.tsx +0 -116
  708. package/src/components/story/filters/filter-toggle.tsx +0 -60
  709. package/src/components/story/filters/filter-types.ts +0 -120
  710. package/src/components/story/filters/filter-value-input.tsx +0 -182
  711. package/src/components/story/views/kanban-board.tsx +0 -464
  712. package/src/components/story/views/kanban-card.tsx +0 -100
  713. package/src/components/tags/__tests__/tags.test.ts +0 -388
  714. package/src/components/tags/nav-tag.tsx +0 -121
  715. package/src/components/tags/pages/tag-settings-page.tsx +0 -32
  716. package/src/components/tags/pages/tags-index-page.tsx +0 -32
  717. package/src/components/tags/tag-badge.tsx +0 -56
  718. package/src/components/tags/tag-color-classes.ts +0 -61
  719. package/src/components/tags/tag-color-picker.tsx +0 -54
  720. package/src/components/tags/tag-components-index.ts +0 -23
  721. package/src/components/tags/tag-manager.tsx +0 -208
  722. package/src/components/tags/tag-selector.tsx +0 -164
  723. package/src/components/testcase/__tests__/table-view.test.tsx +0 -328
  724. package/src/components/testcase/plans/add-to-plan-bar.tsx +0 -64
  725. package/src/components/testcase/table/columns.tsx +0 -170
  726. package/src/components/testcase/table/table-view.tsx +0 -152
  727. package/src/lib/tag-colors.ts +0 -35
  728. package/src/routes/_nav/tags/$id/route.lazy.tsx +0 -23
  729. package/src/routes/_nav/tags/$id/route.tsx +0 -20
  730. package/src/routes/_nav/tags/index.lazy.tsx +0 -23
  731. package/src/routes/_nav/tags/index.tsx +0 -20
  732. package/src/routes/_nav/tags/settings/route.lazy.tsx +0 -23
  733. package/src/routes/_nav/tags/settings/route.tsx +0 -23
  734. package/src/server/serverFns/tags.ts +0 -138
  735. /package/src/components/{knowledge-public → knowledge/public}/bookmark-card.tsx +0 -0
  736. /package/src/components/{knowledge-public → knowledge/public}/category-icon.tsx +0 -0
  737. /package/src/components/{knowledge-public → knowledge/public}/chip-utils.ts +0 -0
  738. /package/src/components/{knowledge-public → knowledge/public}/knowledge-card-grid.tsx +0 -0
  739. /package/src/components/{knowledge-public → knowledge/public}/knowledge-filters.tsx +0 -0
  740. /package/src/components/{knowledge-public → knowledge/public}/knowledge-search-bar.tsx +0 -0
  741. /package/src/components/{knowledge-public → knowledge/public}/markdown-editor.tsx +0 -0
  742. /package/src/components/{knowledge-public → knowledge/public}/markdown-view.tsx +0 -0
  743. /package/src/components/{knowledge-public → knowledge/public}/tag-filter.tsx +0 -0
  744. /package/src/components/{story/detail → shared-kit/comments}/emoji-picker.tsx +0 -0
  745. /package/src/components/story/{schemas/story-create.ts → story-create-schema.ts} +0 -0
  746. /package/src/server/{knowledge-public → knowledge/public}/canonical-id.ts +0 -0
  747. /package/src/server/{knowledge-public → knowledge/public}/favicon.ts +0 -0
  748. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/categories.ts +0 -0
  749. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/delete.ts +0 -0
  750. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/favorite.ts +0 -0
  751. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/unshare.ts +0 -0
  752. /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/delete.ts +0 -0
  753. /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/public.ts +0 -0
  754. /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/unshare.ts +0 -0
  755. /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/delete.ts +0 -0
  756. /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/unshare.ts +0 -0
  757. /package/src/server/serverFns/{knowledge-public → knowledge/public}/entity-tags.ts +0 -0
  758. /package/src/server/serverFns/{knowledge-public → knowledge/public}/favorite.ts +0 -0
  759. /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/delete.ts +0 -0
  760. /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/public.ts +0 -0
  761. /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
+ }