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