@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,1822 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ type ReactNode,
10
+ type RefObject,
11
+ } from "react"
12
+ import {
13
+ mergeGanttI18n,
14
+ type GanttI18nConfig,
15
+ type GanttI18nOverrides,
16
+ } from "@/components/reui/gantt/gantt-i18n"
17
+ import {
18
+ buildEventIndex,
19
+ defaultEventOrder,
20
+ eventsOverlap,
21
+ findResource,
22
+ getGanttDateRange,
23
+ getRangeKey,
24
+ stepGanttDate,
25
+ toZoned,
26
+ type GanttIndex,
27
+ type WeekStartsOn,
28
+ } from "@/components/reui/gantt/gantt-lib"
29
+ import type {
30
+ GanttBarId,
31
+ GanttBaseline,
32
+ GanttBaselineVariance,
33
+ GanttDateRange,
34
+ GanttDragState,
35
+ GanttEvent,
36
+ GanttInteractions,
37
+ GanttOccurrence,
38
+ GanttOffDaysConfig,
39
+ GanttOverlapPolicy,
40
+ GanttProposedUpdate,
41
+ GanttRangeInfo,
42
+ GanttResource,
43
+ GanttResourceReorder,
44
+ GanttRowAlign,
45
+ GanttScale,
46
+ GanttScheduleMode,
47
+ GanttSegment,
48
+ GanttSelection,
49
+ GanttSlotDraft,
50
+ GanttSlotInfo,
51
+ GanttState,
52
+ GanttUpdateResult,
53
+ } from "@/components/reui/gantt/gantt-types"
54
+ import { mergeProps } from "@base-ui/react/merge-props"
55
+ import { useRender } from "@base-ui/react/use-render"
56
+ import type { Locale } from "date-fns"
57
+
58
+ import { cn } from "cn"
59
+
60
+ const DEFAULT_INTERACTIONS: GanttInteractions = {
61
+ drag: true,
62
+ resize: true,
63
+ selectSlot: true,
64
+ }
65
+
66
+ /** Infinite-scroll growth cap, in whole periods per side. */
67
+ const MAX_RANGE_WINDOW = 12
68
+
69
+ /** A node holds as many concurrent schedules as it needs unless told otherwise. */
70
+ const DEFAULT_SCHEDULE_MODE: GanttScheduleMode = "multiple"
71
+
72
+ /** Tree label sits on the first schedule's baseline, not the grown row's middle. */
73
+ const DEFAULT_ROW_ALIGN: GanttRowAlign = "start"
74
+
75
+ /**
76
+ * A node's cardinality: its own override wins over the view-level default.
77
+ * Shared by the layout pass and the gesture engine so both read one rule.
78
+ */
79
+ function resolveScheduleMode(
80
+ node: GanttResource | null | undefined,
81
+ scheduleMode: GanttScheduleMode | undefined
82
+ ): GanttScheduleMode {
83
+ return node?.scheduleMode ?? scheduleMode ?? DEFAULT_SCHEDULE_MODE
84
+ }
85
+
86
+ const EMPTY_SELECTION: GanttSelection = { eventKeys: [], slot: null }
87
+
88
+ interface GanttCallbacks<TData = unknown> {
89
+ onEventClick?: (
90
+ occurrence: GanttOccurrence<TData>,
91
+ e: React.MouseEvent
92
+ ) => void
93
+ onEventDoubleClick?: (
94
+ occurrence: GanttOccurrence<TData>,
95
+ e: React.MouseEvent
96
+ ) => void
97
+ onEventUpdate?: (update: GanttProposedUpdate<TData>) => GanttUpdateResult
98
+ canDropEvent?: (update: GanttProposedUpdate<TData>) => boolean
99
+ onSlotClick?: (slot: GanttSlotInfo, e: React.MouseEvent) => void
100
+ onSelectSlot?: (slot: GanttSlotDraft) => void
101
+ canSelectSlot?: (slot: GanttSlotDraft) => boolean
102
+ /** Fires when the "add task" hint is activated; create a new tree row. */
103
+ onCreateTask?: (ctx: { parentId: string | null; index: number }) => void
104
+ /**
105
+ * Gates the "add task" hint. The shipped view offers root-level creation
106
+ * only (parentId = null); parentId stays in the contract for group-level
107
+ * affordances a consumer builds via its own UI + onCreateTask.
108
+ */
109
+ canCreateTask?: (ctx: { parentId: string | null }) => boolean
110
+ /** Click on a tree row's surface (chevron/checkbox/grip clicks excluded). */
111
+ onResourceClick?: (ctx: GanttColumnContext, e: React.MouseEvent) => void
112
+ onResourceDoubleClick?: (ctx: GanttColumnContext, e: React.MouseEvent) => void
113
+ onRangeChange?: (info: GanttRangeInfo) => void
114
+ onScaleChange?: (scale: GanttScale) => void
115
+ onDateChange?: (date: Date) => void
116
+ onSelectionChange?: (selection: GanttSelection) => void
117
+ onInteractionsChange?: (interactions: GanttInteractions) => void
118
+ onEventsChange?: (events: GanttEvent<TData>[]) => void
119
+ /**
120
+ * Commit gate for timeline resource-row drag reorder. Return false to
121
+ * reject; apply the move by adopting proposal.resources into your
122
+ * `resources` state (controlled - the calendar never self-mutates).
123
+ */
124
+ onResourceReorder?: (proposal: GanttResourceReorder) => void | false
125
+ /** Live validity predicate while a resource row is being dragged. */
126
+ canReorderResource?: (proposal: GanttResourceReorder) => boolean
127
+ /**
128
+ * Fires when a reorder gesture is released on a position rejected by
129
+ * `canReorderResource` (e.g. a pinned row). Use it to explain the rejection
130
+ * (a toast) - the destructive drop indicator already shows it live.
131
+ */
132
+ onResourceReorderReject?: (proposal: GanttResourceReorder) => void
133
+ }
134
+
135
+ interface UseGanttStateOptions<TData = unknown> extends GanttCallbacks<TData> {
136
+ events?: GanttEvent<TData>[]
137
+ defaultEvents?: GanttEvent<TData>[]
138
+ scale?: GanttScale
139
+ defaultScale?: GanttScale
140
+ date?: Date
141
+ defaultDate?: Date
142
+ selection?: GanttSelection
143
+ defaultSelection?: GanttSelection
144
+ interactions?: Partial<GanttInteractions>
145
+ defaultInteractions?: Partial<GanttInteractions>
146
+ loading?: boolean
147
+ timeZone?: string
148
+ locale?: Locale
149
+ weekStartsOn?: WeekStartsOn
150
+ slotDuration?: number
151
+ snapDuration?: number
152
+ i18n?: GanttI18nOverrides
153
+ /**
154
+ * Hard travel bounds for infinite scrolling; either side may be omitted
155
+ * for unlimited travel in that direction.
156
+ */
157
+ rangeBounds?: { min?: Date; max?: Date }
158
+ /** Pointer-activation threshold overrides for drag/resize/create. */
159
+ activation?: GanttActivationConfig
160
+ /**
161
+ * Infinite-scroll growth cap in whole periods per side; past it the
162
+ * anchor slides instead (DOM stays bounded). Default 12.
163
+ */
164
+ maxRangeWindow?: number
165
+ /** Tree nodes of the gantt (GanttNode is the preferred type name). */
166
+ resources?: GanttResource[]
167
+ /**
168
+ * What a gesture may do when it would overlap another schedule in the SAME
169
+ * node: "allow" (default), "clamp" to the neighbour's edge, or "reject".
170
+ * Policy only - overlapping data always renders. A node in "single"
171
+ * scheduleMode rejects regardless.
172
+ */
173
+ overlap?: GanttOverlapPolicy
174
+ /**
175
+ * Make canDropEvent binding: releasing a gesture whose last verdict was
176
+ * invalid reverts it, like the "reject" overlap policy, instead of
177
+ * committing anyway. Default false - canDropEvent alone stays advisory
178
+ * (it styles the ghost; onEventUpdate is the commit gate).
179
+ */
180
+ enforceCanDrop?: boolean
181
+ getEventPriority?: (event: GanttEvent<TData>) => number
182
+ eventOrder?: (a: GanttOccurrence<TData>, b: GanttOccurrence<TData>) => number
183
+ getOccurrences?: (
184
+ event: GanttEvent<TData>,
185
+ range: GanttDateRange,
186
+ ctx: { timeZone: string }
187
+ ) => Array<{ start: Date; end: Date }> | null
188
+ }
189
+
190
+ /**
191
+ * Resolved configuration: every UseGanttStateOptions field except the
192
+ * controlled/uncontrolled state pairs, with defaults applied and i18n merged.
193
+ * Read via ref semantics - callback identity changes never re-render the grid.
194
+ */
195
+ interface GanttSettings<TData = unknown> extends GanttCallbacks<TData> {
196
+ timeZone: string
197
+ locale?: Locale
198
+ weekStartsOn: WeekStartsOn
199
+ slotDuration: number
200
+ snapDuration: number
201
+ i18n: GanttI18nConfig
202
+ rangeBounds?: { min?: Date; max?: Date }
203
+ activation?: GanttActivationConfig
204
+ maxRangeWindow?: number
205
+ resources: GanttResource[]
206
+ overlap: GanttOverlapPolicy
207
+ enforceCanDrop?: boolean
208
+ getEventPriority: (event: GanttEvent<TData>) => number
209
+ eventOrder: (a: GanttOccurrence<TData>, b: GanttOccurrence<TData>) => number
210
+ getOccurrences?: (
211
+ event: GanttEvent<TData>,
212
+ range: GanttDateRange,
213
+ ctx: { timeZone: string }
214
+ ) => Array<{ start: Date; end: Date }> | null
215
+ }
216
+
217
+ interface GanttApi<TData = unknown> {
218
+ next(): void
219
+ prev(): void
220
+ today(): void
221
+ goTo(date: Date): void
222
+ setScale(scale: GanttScale): void
223
+ getEvents(): GanttEvent<TData>[]
224
+ getEvent(id: GanttBarId): GanttEvent<TData> | undefined
225
+ setEvents(events: GanttEvent<TData>[]): void
226
+ addEvent(event: GanttEvent<TData>): void
227
+ updateEvent(id: GanttBarId, patch: Partial<GanttEvent<TData>>): void
228
+ removeEvent(id: GanttBarId): void
229
+ getOccurrences(range?: GanttDateRange): GanttOccurrence<TData>[]
230
+ findOverlapping(candidate: {
231
+ start: Date
232
+ end: Date
233
+ excludeEventId?: string
234
+ }): GanttOccurrence<TData>[]
235
+ select(selection: Partial<GanttSelection>): void
236
+ selectEvent(key: string, opts?: { additive?: boolean }): void
237
+ clearSelection(): void
238
+ setInteractions(patch: Partial<GanttInteractions>): void
239
+ getVisibleRange(): GanttDateRange
240
+ getActiveRange(): GanttDateRange
241
+ /** TZDate in the gantt's display time zone. */
242
+ toZoned(date: Date): Date
243
+ }
244
+
245
+ /** Cross-file plumbing for sibling view/interaction modules; not public API. */
246
+ interface GanttInternals<TData = unknown> {
247
+ getIndex(): GanttIndex<TData>
248
+ setDrag(drag: GanttDragState<TData> | null): void
249
+ setSlotDraft(draft: GanttSlotDraft | null): void
250
+ applyProposedUpdate(update: GanttProposedUpdate<TData>): boolean
251
+ getSettingsVersion(): number
252
+ /**
253
+ * Grow visibleRange by whole periods for infinite scrolling; resets on
254
+ * date/scale changes. Returns false once the growth cap is reached.
255
+ */
256
+ extendRange(direction: "before" | "after"): boolean
257
+ /**
258
+ * True when the LAST anchor-date change was an extendRange window slide
259
+ * (not a navigation) - the view keeps its scroll guard across slides.
260
+ */
261
+ didAnchorSlide(): boolean
262
+ /** View reports the visible-center instant (or null) for the nav title. */
263
+ setViewportCenter(date: Date | null): void
264
+ }
265
+
266
+ interface GanttInstance<TData = unknown> {
267
+ getState(): GanttState<TData>
268
+ subscribe(listener: () => void): () => void
269
+ api: GanttApi<TData>
270
+ settings: GanttSettings<TData>
271
+ internals: GanttInternals<TData>
272
+ }
273
+
274
+ function resolveSettings<TData>(
275
+ options: UseGanttStateOptions<TData>
276
+ ): GanttSettings<TData> {
277
+ const {
278
+ // strip state pairs; the rest flows into settings
279
+ events: _e,
280
+ defaultEvents: _de,
281
+ scale: _v,
282
+ defaultScale: _dv,
283
+ date: _d,
284
+ defaultDate: _dd,
285
+ selection: _s,
286
+ defaultSelection: _ds,
287
+ interactions: _i,
288
+ defaultInteractions: _di,
289
+ loading: _l,
290
+ ...rest
291
+ } = options
292
+ const getEventPriority =
293
+ options.getEventPriority ??
294
+ ((event: GanttEvent<TData>) => event.priority ?? 0)
295
+ return {
296
+ ...rest,
297
+ timeZone:
298
+ options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
299
+ locale: options.locale,
300
+ // locale-first default: a de/fr locale gets Monday weeks without also
301
+ // having to set weekStartsOn; an explicit weekStartsOn always wins
302
+ weekStartsOn:
303
+ options.weekStartsOn ?? options.locale?.options?.weekStartsOn ?? 0,
304
+ slotDuration: options.slotDuration ?? 30,
305
+ snapDuration: options.snapDuration ?? 15,
306
+ i18n: mergeGanttI18n(options.i18n),
307
+ rangeBounds: options.rangeBounds,
308
+ resources: options.resources ?? [],
309
+ overlap: options.overlap ?? "allow",
310
+ getEventPriority,
311
+ // priority-aware default: higher getEventPriority packs/orders first
312
+ eventOrder:
313
+ options.eventOrder ??
314
+ ((a, b) =>
315
+ getEventPriority(b.event) - getEventPriority(a.event) ||
316
+ defaultEventOrder(a, b)),
317
+ getOccurrences: options.getOccurrences,
318
+ }
319
+ }
320
+
321
+ const warned = new Set<string>()
322
+ function warnOnce(key: string, message: string) {
323
+ if (process.env.NODE_ENV !== "production" && !warned.has(key)) {
324
+ warned.add(key)
325
+ console.warn(`[gantt] ${message}`)
326
+ }
327
+ }
328
+
329
+ interface GanttStore<TData> {
330
+ instance: GanttInstance<TData>
331
+ setOptions(next: UseGanttStateOptions<TData>): boolean
332
+ notify(): void
333
+ emitRangeIfChanged(): void
334
+ }
335
+
336
+ function createGanttStore<TData>(
337
+ initial: UseGanttStateOptions<TData>
338
+ ): GanttStore<TData> {
339
+ let options = initial
340
+ let settings = resolveSettings(initial)
341
+ let settingsVersion = 0
342
+
343
+ const listeners = new Set<() => void>()
344
+
345
+ const internal = {
346
+ scale: initial.defaultScale ?? "day",
347
+ date: initial.defaultDate ?? new Date(),
348
+ events: initial.defaultEvents ?? [],
349
+ selection: initial.defaultSelection ?? EMPTY_SELECTION,
350
+ interactions: { ...DEFAULT_INTERACTIONS, ...initial.defaultInteractions },
351
+ drag: null as GanttDragState<TData> | null,
352
+ slotDraft: null as GanttSlotDraft | null,
353
+ /** Whole extra periods rendered on each side (infinite scroll). */
354
+ rangeWindow: { before: 0, after: 0 },
355
+ /** Visible-center instant reported by the view; drives the nav title. */
356
+ viewportCenter: null as Date | null,
357
+ }
358
+
359
+ let snapshot: GanttState<TData> | null = null
360
+ let indexCache: {
361
+ events: GanttEvent<TData>[]
362
+ rangeKey: string
363
+ timeZone: string
364
+ index: GanttIndex<TData>
365
+ } | null = null
366
+ let lastEmittedRangeKey: string | null = null
367
+ /** Whether the last anchor change came from an extendRange window slide. */
368
+ let lastAnchorChangeWasSlide = false
369
+
370
+ const invalidate = () => {
371
+ snapshot = null
372
+ }
373
+
374
+ const notify = () => {
375
+ listeners.forEach((listener) => listener())
376
+ emitRangeIfChanged()
377
+ }
378
+
379
+ const getState = (): GanttState<TData> => {
380
+ if (snapshot) return snapshot
381
+ const scale = options.scale ?? internal.scale
382
+ const date = options.date ?? internal.date
383
+ const rangeOpts = {
384
+ timeZone: settings.timeZone,
385
+ weekStartsOn: settings.weekStartsOn,
386
+ }
387
+ const { visibleRange: baseRange, activeRange } = getGanttDateRange(
388
+ scale,
389
+ date,
390
+ rangeOpts
391
+ )
392
+ // Infinite scroll: widen by whole periods; the anchor period stays put
393
+ const { before, after } = internal.rangeWindow
394
+ let visibleRange = baseRange
395
+ if (before > 0 || after > 0) {
396
+ let earlier = date
397
+ for (let i = 0; i < before; i++) {
398
+ earlier = stepGanttDate(scale, earlier, -1, rangeOpts)
399
+ }
400
+ let later = date
401
+ for (let i = 0; i < after; i++) {
402
+ later = stepGanttDate(scale, later, 1, rangeOpts)
403
+ }
404
+ visibleRange = {
405
+ start: getGanttDateRange(scale, earlier, rangeOpts).visibleRange.start,
406
+ end: getGanttDateRange(scale, later, rangeOpts).visibleRange.end,
407
+ }
408
+ }
409
+ snapshot = {
410
+ scale,
411
+ date,
412
+ visibleRange,
413
+ activeRange,
414
+ events: options.events ?? internal.events,
415
+ selection: options.selection ?? internal.selection,
416
+ interactions: options.interactions
417
+ ? { ...DEFAULT_INTERACTIONS, ...options.interactions }
418
+ : internal.interactions,
419
+ loading: options.loading ?? false,
420
+ drag: internal.drag,
421
+ slotDraft: internal.slotDraft,
422
+ viewportCenter: internal.viewportCenter,
423
+ }
424
+ return snapshot
425
+ }
426
+
427
+ const emitRangeIfChanged = () => {
428
+ if (!settings.onRangeChange) return
429
+ const state = getState()
430
+ const key = `${state.scale}:${getRangeKey(state.visibleRange)}:${settings.timeZone}`
431
+ if (key === lastEmittedRangeKey) return
432
+ lastEmittedRangeKey = key
433
+ settings.onRangeChange({
434
+ range: state.visibleRange,
435
+ activeRange: state.activeRange,
436
+ scale: state.scale,
437
+ date: state.date,
438
+ timeZone: settings.timeZone,
439
+ })
440
+ }
441
+
442
+ type ControlledKey =
443
+ | "scale"
444
+ | "date"
445
+ | "events"
446
+ | "selection"
447
+ | "interactions"
448
+
449
+ const setField = <K extends ControlledKey>(
450
+ key: K,
451
+ value: GanttState<TData>[K extends "events" ? "events" : K]
452
+ ) => {
453
+ const controlled = options[key] !== undefined
454
+ if (key === "date" || key === "scale") {
455
+ // value-equal sets are no-ops: they must not touch store state (the
456
+ // controlled path would mutate without notify) nor drop infinite-
457
+ // scroll growth for a navigation that never happened
458
+ const current = getState()[key]
459
+ const same =
460
+ key === "date"
461
+ ? (current as Date).getTime() === (value as Date).getTime()
462
+ : current === value
463
+ if (same) return
464
+ // navigating re-anchors the axis; drop any infinite-scroll growth and
465
+ // let the title follow the anchor again until the user scrolls
466
+ internal.rangeWindow = { before: 0, after: 0 }
467
+ internal.viewportCenter = null
468
+ lastAnchorChangeWasSlide = false
469
+ invalidate()
470
+ }
471
+ if (!controlled) {
472
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
473
+ ;(internal as any)[key] = value
474
+ invalidate()
475
+ }
476
+ const callbacks: Record<ControlledKey, ((v: never) => void) | undefined> = {
477
+ scale: settings.onScaleChange as never,
478
+ date: settings.onDateChange as never,
479
+ events: settings.onEventsChange as never,
480
+ selection: settings.onSelectionChange as never,
481
+ interactions: settings.onInteractionsChange as never,
482
+ }
483
+ callbacks[key]?.(value as never)
484
+ if (!controlled) notify()
485
+ }
486
+
487
+ const applyProposedUpdate = (
488
+ update: GanttProposedUpdate<TData>,
489
+ // extra non-timing fields committed in the SAME events emission: a second
490
+ // setField pass would read stale controlled options.events and emit an
491
+ // array without the timing change
492
+ extra?: Partial<GanttEvent<TData>>
493
+ ): boolean => {
494
+ const result = settings.onEventUpdate?.(update)
495
+ if (result === false) return false
496
+ const adjusted: Partial<GanttEvent<TData>> =
497
+ result && typeof result === "object"
498
+ ? {
499
+ start: result.start ?? update.start,
500
+ end: result.end ?? update.end,
501
+ allDay: result.allDay ?? update.allDay,
502
+ }
503
+ : { start: update.start, end: update.end, allDay: update.allDay }
504
+ if (update.resourceId !== undefined) adjusted.resourceId = update.resourceId
505
+ const merged = extra ? { ...extra, ...adjusted } : adjusted
506
+ const events = getState().events
507
+ const next = events.map((event) =>
508
+ event.id === update.event.id ? { ...event, ...merged } : event
509
+ )
510
+ setField("events", next)
511
+ return true
512
+ }
513
+
514
+ const getIndex = (): GanttIndex<TData> => {
515
+ const state = getState()
516
+ const rangeKey = getRangeKey(state.visibleRange)
517
+ if (
518
+ indexCache &&
519
+ indexCache.events === state.events &&
520
+ indexCache.rangeKey === rangeKey &&
521
+ indexCache.timeZone === settings.timeZone
522
+ ) {
523
+ return indexCache.index
524
+ }
525
+ const index = buildEventIndex(state.events, state.visibleRange, {
526
+ timeZone: settings.timeZone,
527
+ eventOrder: settings.eventOrder,
528
+ getOccurrences: settings.getOccurrences,
529
+ })
530
+ indexCache = {
531
+ events: state.events,
532
+ rangeKey,
533
+ timeZone: settings.timeZone,
534
+ index,
535
+ }
536
+ return index
537
+ }
538
+
539
+ /** Anchor clamp: navigation may never leave the configured bounds. */
540
+ const clampToBounds = (date: Date): Date => {
541
+ const bounds = settings.rangeBounds
542
+ if (!bounds) return date
543
+ if (bounds.min && date.getTime() < bounds.min.getTime()) return bounds.min
544
+ if (bounds.max && date.getTime() > bounds.max.getTime()) return bounds.max
545
+ return date
546
+ }
547
+
548
+ const api: GanttApi<TData> = {
549
+ next() {
550
+ const state = getState()
551
+ setField(
552
+ "date",
553
+ clampToBounds(
554
+ stepGanttDate(state.scale, state.date, 1, {
555
+ timeZone: settings.timeZone,
556
+ })
557
+ )
558
+ )
559
+ },
560
+ prev() {
561
+ const state = getState()
562
+ setField(
563
+ "date",
564
+ clampToBounds(
565
+ stepGanttDate(state.scale, state.date, -1, {
566
+ timeZone: settings.timeZone,
567
+ })
568
+ )
569
+ )
570
+ },
571
+ today() {
572
+ setField("date", clampToBounds(new Date()))
573
+ },
574
+ goTo(date) {
575
+ setField("date", clampToBounds(date))
576
+ },
577
+ setScale(scale) {
578
+ setField("scale", scale)
579
+ },
580
+ getEvents() {
581
+ return getState().events
582
+ },
583
+ getEvent(id) {
584
+ return getState().events.find((event) => event.id === id)
585
+ },
586
+ setEvents(events) {
587
+ setField("events", events)
588
+ },
589
+ addEvent(event) {
590
+ setField("events", [...getState().events, event])
591
+ },
592
+ updateEvent(id, patch) {
593
+ const event = api.getEvent(id)
594
+ if (!event) return
595
+ const merged = { ...event, ...patch }
596
+ const timingChanged =
597
+ patch.start !== undefined ||
598
+ patch.end !== undefined ||
599
+ patch.allDay !== undefined
600
+ if (timingChanged && settings.onEventUpdate) {
601
+ // timing + rest commit as ONE events emission (a rejected update
602
+ // drops the whole patch, same as before)
603
+ const rest = { ...patch }
604
+ delete rest.start
605
+ delete rest.end
606
+ delete rest.allDay
607
+ applyProposedUpdate(
608
+ {
609
+ event: merged,
610
+ occurrence: null,
611
+ start: merged.start,
612
+ end: merged.end,
613
+ allDay: merged.allDay ?? false,
614
+ source: "api",
615
+ },
616
+ Object.keys(rest).length > 0 ? rest : undefined
617
+ )
618
+ return
619
+ }
620
+ setField(
621
+ "events",
622
+ getState().events.map((e) => (e.id === id ? merged : e))
623
+ )
624
+ },
625
+ removeEvent(id) {
626
+ setField(
627
+ "events",
628
+ getState().events.filter((event) => event.id !== id)
629
+ )
630
+ },
631
+ getOccurrences(range) {
632
+ if (!range) return getIndex().occurrences
633
+ const state = getState()
634
+ const within =
635
+ range.start >= state.visibleRange.start &&
636
+ range.end <= state.visibleRange.end
637
+ if (within) {
638
+ return getIndex().occurrences.filter((occ) => eventsOverlap(occ, range))
639
+ }
640
+ return buildEventIndex(state.events, range, {
641
+ timeZone: settings.timeZone,
642
+ eventOrder: settings.eventOrder,
643
+ getOccurrences: settings.getOccurrences,
644
+ }).occurrences
645
+ },
646
+ findOverlapping({ start, end, excludeEventId }) {
647
+ return api
648
+ .getOccurrences({ start, end })
649
+ .filter((occ) => occ.eventId !== excludeEventId)
650
+ },
651
+ select(partial) {
652
+ const current = getState().selection
653
+ setField("selection", {
654
+ eventKeys: partial.eventKeys ?? current.eventKeys,
655
+ slot: partial.slot !== undefined ? partial.slot : current.slot,
656
+ })
657
+ },
658
+ selectEvent(key, opts) {
659
+ const current = getState().selection
660
+ const eventKeys = opts?.additive
661
+ ? current.eventKeys.includes(key)
662
+ ? current.eventKeys.filter((k) => k !== key)
663
+ : [...current.eventKeys, key]
664
+ : [key]
665
+ setField("selection", { ...current, eventKeys })
666
+ },
667
+ clearSelection() {
668
+ setField("selection", EMPTY_SELECTION)
669
+ },
670
+ setInteractions(patch) {
671
+ setField("interactions", { ...getState().interactions, ...patch })
672
+ },
673
+ getVisibleRange() {
674
+ return getState().visibleRange
675
+ },
676
+ getActiveRange() {
677
+ return getState().activeRange
678
+ },
679
+ toZoned(date) {
680
+ return toZoned(date, settings.timeZone)
681
+ },
682
+ }
683
+
684
+ const internals: GanttInternals<TData> = {
685
+ getIndex,
686
+ setDrag(drag) {
687
+ internal.drag = drag
688
+ invalidate()
689
+ notify()
690
+ },
691
+ setSlotDraft(draft) {
692
+ internal.slotDraft = draft
693
+ invalidate()
694
+ notify()
695
+ },
696
+ setViewportCenter(date) {
697
+ const prev = internal.viewportCenter
698
+ if (prev?.getTime() === date?.getTime()) return
699
+ internal.viewportCenter = date
700
+ invalidate()
701
+ notify()
702
+ },
703
+ applyProposedUpdate,
704
+ getSettingsVersion() {
705
+ return settingsVersion
706
+ },
707
+ extendRange(direction) {
708
+ const state = getState()
709
+ const bounds = settings.rangeBounds
710
+ if (
711
+ direction === "before" &&
712
+ bounds?.min &&
713
+ state.visibleRange.start.getTime() <= bounds.min.getTime()
714
+ ) {
715
+ return false
716
+ }
717
+ if (
718
+ direction === "after" &&
719
+ bounds?.max &&
720
+ state.visibleRange.end.getTime() >= bounds.max.getTime()
721
+ ) {
722
+ return false
723
+ }
724
+ const cap = Math.max(1, settings.maxRangeWindow ?? MAX_RANGE_WINDOW)
725
+ const { before, after } = internal.rangeWindow
726
+ const grow = direction === "before" ? before < cap : after < cap
727
+ if (grow) {
728
+ internal.rangeWindow =
729
+ direction === "before"
730
+ ? { before: before + 1, after }
731
+ : { before, after: after + 1 }
732
+ } else {
733
+ // window is at capacity: SLIDE the anchor one period instead, so
734
+ // travel stays unbounded while the DOM stays bounded
735
+ const next = stepGanttDate(
736
+ state.scale,
737
+ state.date,
738
+ direction === "before" ? -1 : 1,
739
+ {
740
+ timeZone: settings.timeZone,
741
+ }
742
+ )
743
+ if (options.date !== undefined) {
744
+ // controlled anchor: propose the slide; nothing changes until the
745
+ // parent adopts it
746
+ settings.onDateChange?.(next)
747
+ return false
748
+ }
749
+ internal.date = next
750
+ lastAnchorChangeWasSlide = true
751
+ settings.onDateChange?.(next)
752
+ }
753
+ invalidate()
754
+ notify()
755
+ return true
756
+ },
757
+ didAnchorSlide() {
758
+ return lastAnchorChangeWasSlide
759
+ },
760
+ }
761
+
762
+ const instance: GanttInstance<TData> = {
763
+ getState,
764
+ subscribe(listener) {
765
+ listeners.add(listener)
766
+ return () => listeners.delete(listener)
767
+ },
768
+ api,
769
+ get settings() {
770
+ return settings
771
+ },
772
+ internals,
773
+ }
774
+
775
+ const STATE_KEYS = [
776
+ "events",
777
+ "scale",
778
+ "date",
779
+ "selection",
780
+ "interactions",
781
+ "loading",
782
+ ] as const
783
+ const SETTINGS_KEYS = [
784
+ "timeZone",
785
+ "locale",
786
+ "weekStartsOn",
787
+ "slotDuration",
788
+ "snapDuration",
789
+ "i18n",
790
+ "rangeBounds",
791
+ "activation",
792
+ "maxRangeWindow",
793
+ "resources",
794
+ "overlap",
795
+ "enforceCanDrop",
796
+ "getEventPriority",
797
+ "eventOrder",
798
+ "getOccurrences",
799
+ ] as const
800
+
801
+ return {
802
+ instance,
803
+ setOptions(next) {
804
+ const prev = options
805
+ options = next
806
+ // compare by value: a freshly constructed but equal controlled date
807
+ // must not wipe infinite-scroll growth on every parent re-render
808
+ if (
809
+ prev.date?.getTime() !== next.date?.getTime() ||
810
+ prev.scale !== next.scale
811
+ ) {
812
+ internal.rangeWindow = { before: 0, after: 0 }
813
+ lastAnchorChangeWasSlide = false
814
+ }
815
+ let changed = false
816
+ for (const key of STATE_KEYS) {
817
+ if (prev[key] !== next[key]) {
818
+ changed = true
819
+ break
820
+ }
821
+ }
822
+ let settingsChanged = false
823
+ for (const key of SETTINGS_KEYS) {
824
+ if (prev[key] !== next[key]) {
825
+ settingsChanged = true
826
+ break
827
+ }
828
+ }
829
+ settings = resolveSettings(next)
830
+ if (settingsChanged) {
831
+ settingsVersion++
832
+ changed = true
833
+ }
834
+ if (changed) invalidate()
835
+ return changed
836
+ },
837
+ notify,
838
+ emitRangeIfChanged,
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Headless root hook - the full calendar engine without any markup.
844
+ * Pass the returned instance to <Gantt calendar={instance}> or drive
845
+ * fully custom UI from instance.getState()/subscribe/api.
846
+ */
847
+ function useGanttState<TData = unknown>(
848
+ options: UseGanttStateOptions<TData> = {}
849
+ ): GanttInstance<TData> {
850
+ const [store] = useState(() => createGanttStore<TData>(options))
851
+ const changed = store.setOptions(options)
852
+ const changedRef = useRef(false)
853
+ if (changed) changedRef.current = true
854
+ useLayoutEffect(() => {
855
+ if (changedRef.current) {
856
+ changedRef.current = false
857
+ store.notify()
858
+ }
859
+ })
860
+ useEffect(() => {
861
+ store.emitRangeIfChanged()
862
+ // mount-only: onRangeChange fires once for the initial range
863
+ }, [])
864
+ return store.instance
865
+ }
866
+
867
+ const GanttContext =
868
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
869
+ createContext<GanttInstance<any> | null>(null)
870
+
871
+ /** The stable calendar instance; throws outside <Gantt>. */
872
+ function useGantt<TData = unknown>(): GanttInstance<TData> {
873
+ const instance = useContext(GanttContext)
874
+ if (!instance) {
875
+ throw new Error("useGantt must be used within <Gantt>")
876
+ }
877
+ return instance as GanttInstance<TData>
878
+ }
879
+
880
+ interface UseGanttSelectorOptions<TData, TSelected> {
881
+ calendar?: GanttInstance<TData>
882
+ isEqual?: (a: TSelected, b: TSelected) => boolean
883
+ }
884
+
885
+ /** Fine-grained subscription with equality memoization (Object.is default). */
886
+ function useGanttSelector<TData = unknown, TSelected = unknown>(
887
+ selector: (state: GanttState<TData>) => TSelected,
888
+ options?: UseGanttSelectorOptions<TData, TSelected>
889
+ ): TSelected {
890
+ const contextInstance = useContext(GanttContext)
891
+ const instance = options?.calendar ?? contextInstance
892
+ if (!instance) {
893
+ throw new Error(
894
+ "useGanttSelector needs an <Gantt> ancestor or an explicit `calendar` option"
895
+ )
896
+ }
897
+ const isEqual = options?.isEqual ?? Object.is
898
+ const lastRef = useRef<{ value: TSelected } | null>(null)
899
+ const selectorRef = useRef(selector)
900
+ selectorRef.current = selector
901
+
902
+ const getSnapshot = () => {
903
+ const next = selectorRef.current(instance.getState() as GanttState<TData>)
904
+ if (lastRef.current && isEqual(lastRef.current.value, next)) {
905
+ return lastRef.current.value
906
+ }
907
+ lastRef.current = { value: next }
908
+ return next
909
+ }
910
+
911
+ return useSyncExternalStore(instance.subscribe, getSnapshot, getSnapshot)
912
+ }
913
+
914
+ function useGanttScale(): {
915
+ scale: GanttScale
916
+ setScale: (scale: GanttScale) => void
917
+ } {
918
+ const instance = useGantt()
919
+ const scale = useGanttSelector((state) => state.scale)
920
+ return { scale, setScale: instance.api.setScale }
921
+ }
922
+
923
+ function useGanttNavigation(): {
924
+ date: Date
925
+ /** i18n.functions.formatTitle output for the current view. */
926
+ title: string
927
+ visibleRange: GanttDateRange
928
+ activeRange: GanttDateRange
929
+ next: () => void
930
+ prev: () => void
931
+ today: () => void
932
+ goTo: (date: Date) => void
933
+ /** True when the anchor period contains now in the display time zone. */
934
+ isToday: boolean
935
+ } {
936
+ const instance = useGantt()
937
+ const { settings } = instance
938
+ const slice = useGanttSelector(
939
+ (state) => ({
940
+ date: state.date,
941
+ scale: state.scale,
942
+ visibleRange: state.visibleRange,
943
+ activeRange: state.activeRange,
944
+ viewportCenter: state.viewportCenter,
945
+ }),
946
+ {
947
+ isEqual: (a, b) =>
948
+ a.date.getTime() === b.date.getTime() &&
949
+ a.scale === b.scale &&
950
+ a.viewportCenter?.getTime() === b.viewportCenter?.getTime() &&
951
+ getRangeKey(a.visibleRange) === getRangeKey(b.visibleRange),
952
+ }
953
+ )
954
+ useGanttSettingsVersion(instance)
955
+ // Read in a selector, which runs on every render: compiled, a render-body
956
+ // clock read is cached and Today keeps its mount-time answer.
957
+ const isToday = useGanttSelector((state) => {
958
+ const now = new Date()
959
+ return now >= state.activeRange.start && now < state.activeRange.end
960
+ })
961
+ // The title names what you are LOOKING at: the visible-center period when
962
+ // the view reports one, otherwise the anchor period.
963
+ const titleDate = slice.viewportCenter ?? slice.date
964
+ const titleActive = slice.viewportCenter
965
+ ? getGanttDateRange(slice.scale, slice.viewportCenter, {
966
+ timeZone: settings.timeZone,
967
+ weekStartsOn: settings.weekStartsOn,
968
+ }).activeRange
969
+ : slice.activeRange
970
+ return {
971
+ date: slice.date,
972
+ title: settings.i18n.functions.formatTitle(slice.scale, {
973
+ date: toZoned(titleDate, settings.timeZone),
974
+ activeRange: titleActive,
975
+ visibleRange: slice.visibleRange,
976
+ locale: settings.locale,
977
+ }),
978
+ visibleRange: slice.visibleRange,
979
+ activeRange: slice.activeRange,
980
+ next: instance.api.next,
981
+ prev: instance.api.prev,
982
+ today: instance.api.today,
983
+ goTo: instance.api.goTo,
984
+ isToday,
985
+ }
986
+ }
987
+
988
+ function useGanttSelection(): {
989
+ selection: GanttSelection
990
+ select: (selection: Partial<GanttSelection>) => void
991
+ selectEvent: (key: string, opts?: { additive?: boolean }) => void
992
+ clearSelection: () => void
993
+ } {
994
+ const instance = useGantt()
995
+ const selection = useGanttSelector((state) => state.selection)
996
+ return {
997
+ selection,
998
+ select: instance.api.select,
999
+ selectEvent: instance.api.selectEvent,
1000
+ clearSelection: instance.api.clearSelection,
1001
+ }
1002
+ }
1003
+
1004
+ function useGanttInteractions(): {
1005
+ interactions: GanttInteractions
1006
+ setInteractions: (patch: Partial<GanttInteractions>) => void
1007
+ } {
1008
+ const instance = useGantt()
1009
+ const interactions = useGanttSelector((state) => state.interactions)
1010
+ return { interactions, setInteractions: instance.api.setInteractions }
1011
+ }
1012
+
1013
+ /** Expanded, sorted occurrences; defaults to the visible range. */
1014
+ function useGanttOccurrences<TData = unknown>(
1015
+ range?: GanttDateRange
1016
+ ): GanttOccurrence<TData>[] {
1017
+ const instance = useGantt<TData>()
1018
+ return useGanttSelector<TData, GanttOccurrence<TData>[]>(
1019
+ () => instance.api.getOccurrences(range),
1020
+ {
1021
+ calendar: instance,
1022
+ // keys encode id + start only, so end edits (resize-end) and payload
1023
+ // changes (title, color, progress) must be compared explicitly
1024
+ isEqual: (a, b) =>
1025
+ a.length === b.length &&
1026
+ a.every(
1027
+ (occ, i) =>
1028
+ occ.key === b[i]?.key &&
1029
+ occ.end.getTime() === b[i].end.getTime() &&
1030
+ occ.event === b[i].event
1031
+ ),
1032
+ }
1033
+ )
1034
+ }
1035
+
1036
+ interface GanttNodeSchedules<TData = unknown> {
1037
+ /** The node itself, or null when the id is not in the tree. */
1038
+ node: GanttResource | null
1039
+ /** Cardinality in force for this node (its own override, else the default). */
1040
+ scheduleMode: GanttScheduleMode
1041
+ /** The node's occurrences in the visible range, in axis order. */
1042
+ schedules: GanttOccurrence<TData>[]
1043
+ /** Pairs of the node's schedules that overlap in time. */
1044
+ conflicts: Array<[GanttOccurrence<TData>, GanttOccurrence<TData>]>
1045
+ }
1046
+
1047
+ /**
1048
+ * Everything a consumer needs to MANAGE one node's schedules without
1049
+ * re-deriving layout: the node, its resolved cardinality, its schedules in
1050
+ * order, and the pairs that collide. Pure state - it renders nothing, so a
1051
+ * "manage schedules" panel is entirely the consumer's design.
1052
+ */
1053
+ function useGanttNodeSchedules<TData = unknown>(
1054
+ nodeId: string
1055
+ ): GanttNodeSchedules<TData> {
1056
+ const settings = useGanttSettings<TData>()
1057
+ const viewConfig = useGanttViewConfig<TData>()
1058
+ const occurrences = useGanttOccurrences<TData>()
1059
+
1060
+ const node = findResource(settings.resources, nodeId)
1061
+ const schedules = occurrences.filter(
1062
+ (occurrence) => occurrence.event.resourceId === nodeId
1063
+ )
1064
+ const conflicts: Array<[GanttOccurrence<TData>, GanttOccurrence<TData>]> = []
1065
+ for (let i = 0; i < schedules.length; i++) {
1066
+ for (let j = i + 1; j < schedules.length; j++) {
1067
+ if (eventsOverlap(schedules[i], schedules[j])) {
1068
+ conflicts.push([schedules[i], schedules[j]])
1069
+ }
1070
+ }
1071
+ }
1072
+ return {
1073
+ node,
1074
+ scheduleMode: resolveScheduleMode(node, viewConfig.scheduleMode),
1075
+ schedules,
1076
+ conflicts,
1077
+ }
1078
+ }
1079
+
1080
+ /** Subscribes to settings changes only (version counter, not state). */
1081
+ function useGanttSettingsVersion<TData>(
1082
+ instance: GanttInstance<TData>
1083
+ ): number {
1084
+ return useSyncExternalStore(
1085
+ instance.subscribe,
1086
+ instance.internals.getSettingsVersion,
1087
+ instance.internals.getSettingsVersion
1088
+ )
1089
+ }
1090
+
1091
+ /** Resolved settings incl. merged i18n; re-renders only when settings change. */
1092
+ function useGanttSettings<TData = unknown>(): GanttSettings<TData> {
1093
+ const instance = useGantt<TData>()
1094
+ useGanttSettingsVersion(instance)
1095
+ return instance.settings
1096
+ }
1097
+
1098
+ interface GanttClassNames {
1099
+ nav?: string
1100
+ toolbar?: string
1101
+ /** The gantt body (tree + track). */
1102
+ view?: string
1103
+ event?: string
1104
+ /** The planned-window (baseline) ghost drawn behind a bar. */
1105
+ baseline?: string
1106
+ /** The dependency-arrow layer (an SVG; color flows from currentColor). */
1107
+ dependencies?: string
1108
+ }
1109
+
1110
+ /** Row context handed to tree-panel column and label renderers. */
1111
+ interface GanttColumnContext {
1112
+ resource: GanttResource
1113
+ depth: number
1114
+ isGroup: boolean
1115
+ collapsed: boolean
1116
+ }
1117
+
1118
+ /** One extra tree-panel column after the built-in name column. */
1119
+ interface GanttColumn {
1120
+ /** Stable id; doubles as the default header label. */
1121
+ id: string
1122
+ /** Header label. */
1123
+ title?: ReactNode
1124
+ /** Fixed column width in px. Default 96. */
1125
+ width?: number
1126
+ /** Cell content alignment. Default "start". */
1127
+ align?: "start" | "center" | "end"
1128
+ /** Cell content per row; omit or return null for an empty cell. */
1129
+ render?: (ctx: GanttColumnContext) => ReactNode
1130
+ /** Extra classes on every cell of this column (header included). */
1131
+ className?: string
1132
+ }
1133
+
1134
+ /** Pointer-activation thresholds; unset keys keep the dnd-kit parity defaults. */
1135
+ interface GanttActivationConfig {
1136
+ /** Mouse travel (px) before a bar move starts. Default 5. */
1137
+ moveDistancePx?: number
1138
+ /** Mouse travel (px) before a drag-create starts. Default 4. */
1139
+ createDistancePx?: number
1140
+ /** Touch long-press delay in ms. Default 250. */
1141
+ touchDelayMs?: number
1142
+ /** Touch movement tolerance (px) during the long-press. Default 5. */
1143
+ touchTolerancePx?: number
1144
+ }
1145
+
1146
+ /** Layout metrics (rem unless noted); every knob falls back to its default. */
1147
+ /** A gridline: false to hide it, true for the default solid stroke, or a style. */
1148
+ type GanttGridLine = boolean | "solid" | "dashed"
1149
+
1150
+ interface GanttTimelineLines {
1151
+ /** Unit boundary lines running down the timeline. Default solid. */
1152
+ vertical?: GanttGridLine
1153
+ /** Row separator lines running across the timeline. Default solid. */
1154
+ horizontal?: GanttGridLine
1155
+ }
1156
+
1157
+ /** Resolved stroke per axis; null means the axis draws nothing. */
1158
+ interface GanttResolvedLines {
1159
+ vertical: "solid" | "dashed" | null
1160
+ horizontal: "solid" | "dashed" | null
1161
+ }
1162
+
1163
+ /**
1164
+ * One place decides what the grid draws, so the header lines, the body lines
1165
+ * and the row separators can never disagree.
1166
+ */
1167
+ function resolveTimelineLines(
1168
+ value: GanttTimelineLines | "vertical" | "both" | "none" | undefined
1169
+ ): GanttResolvedLines {
1170
+ if (value === "none") return { vertical: null, horizontal: null }
1171
+ if (value === "vertical") return { vertical: "solid", horizontal: null }
1172
+ if (value === "both" || value === undefined) {
1173
+ return { vertical: "solid", horizontal: "solid" }
1174
+ }
1175
+ const stroke = (line: GanttGridLine | undefined) =>
1176
+ line === false ? null : line === true || line === undefined ? "solid" : line
1177
+ return {
1178
+ vertical: stroke(value.vertical),
1179
+ horizontal: stroke(value.horizontal),
1180
+ }
1181
+ }
1182
+
1183
+ interface GanttMetrics {
1184
+ /** Height of one schedule bar. Default 1.25. */
1185
+ laneHeight?: number
1186
+ /** Gap between stacked schedules in one node. Default 0.1875. */
1187
+ laneGap?: number
1188
+ /**
1189
+ * Vertical inset between the row's edges and its block of schedules - the
1190
+ * breathing room around the stack, kept separate from laneGap so schedules
1191
+ * in one node can sit tight without cramping the row. Default 0.5.
1192
+ */
1193
+ rowPadding?: number
1194
+ /** Minimum row height. Default 2.5. */
1195
+ minRowHeight?: number
1196
+ /**
1197
+ * Drag-drop indicator height, centered in its lane band and published on
1198
+ * the ghost as --gantt-ghost-height so consumer styling can read the same
1199
+ * number. Default 1.25.
1200
+ */
1201
+ ghostHeight?: number
1202
+ /** barLabel "auto" flips the title outside below this bar width. Default 7. */
1203
+ autoLabelMin?: number
1204
+ /** Unit width at zoom 1, per scale. Day scale = width per interval unit. */
1205
+ unitWidths?: Partial<Record<GanttScale, number>>
1206
+ /** Minimum timeline pane width in px. Default 200. */
1207
+ minTimelineWidth?: number
1208
+ /** Scroll distance (px) from an edge that grows the range. Default 160. */
1209
+ infiniteScrollEdge?: number
1210
+ }
1211
+
1212
+ /** Live gesture snapshot handed to the drag/resize indicator render props. */
1213
+ interface GanttDragIndicatorProps<TData = unknown> {
1214
+ occurrence: GanttOccurrence<TData>
1215
+ kind: "move" | "resize-start" | "resize-end"
1216
+ /** Proposed (snapped) range of the current gesture step. */
1217
+ start: Date
1218
+ end: Date
1219
+ valid: boolean
1220
+ /** The event's planned (baseline) window, so a custom overlay can keep
1221
+ * drawing it mid-gesture; null when the event carries none. */
1222
+ baseline: GanttBaseline | null
1223
+ }
1224
+
1225
+ /** Slot handed to a custom schedule-hint renderer. */
1226
+ interface GanttScheduleHintProps {
1227
+ start: Date
1228
+ end: Date
1229
+ resource: GanttResource
1230
+ }
1231
+
1232
+ /** Parent rollup handed to a custom summary renderer. */
1233
+ interface GanttSummaryProps {
1234
+ resource: GanttResource
1235
+ start: Date
1236
+ end: Date
1237
+ progress: number | null
1238
+ }
1239
+
1240
+ /** Planned window handed to a custom baseline renderer. */
1241
+ interface GanttBaselineProps<TData = unknown> {
1242
+ event: GanttEvent<TData>
1243
+ start: Date
1244
+ end: Date
1245
+ /** Planned start and end coincide: a point in the plan, not a window. */
1246
+ milestone: boolean
1247
+ variance: GanttBaselineVariance
1248
+ }
1249
+
1250
+ /** Planned NODE window handed to a custom row-baseline renderer. */
1251
+ interface GanttRowBaselineProps {
1252
+ resource: GanttResource
1253
+ start: Date
1254
+ end: Date
1255
+ /** Planned start and end coincide: a point in the plan, not a window. */
1256
+ milestone: boolean
1257
+ /** Latest actual end across the node's (and, for a group, its subtree's)
1258
+ * events against the planned end; null when there is nothing to compare. */
1259
+ variance: GanttBaselineVariance | null
1260
+ }
1261
+
1262
+ /** Left tree-panel sizing and splitter behavior. */
1263
+ interface GanttTreePanelConfig {
1264
+ /** Initial panel width in px. Default 288. */
1265
+ width?: number
1266
+ /** Splitter lower bound in px. Default 180. */
1267
+ minWidth?: number
1268
+ /** Splitter upper bound in px. Default 640. */
1269
+ maxWidth?: number
1270
+ /** Drag/keyboard splitter between the panels. Default true. */
1271
+ resizable?: boolean
1272
+ /** Width of the sticky name column in px. Default 208. */
1273
+ nameColumnWidth?: number
1274
+ /** Fires after any user resize (drag release, keyboard, double-click reset). */
1275
+ onWidthChange?: (width: number) => void
1276
+ }
1277
+
1278
+ interface GanttRenderEventProps<TData = unknown> {
1279
+ occurrence: GanttOccurrence<TData>
1280
+ segment: GanttSegment<TData>
1281
+ isDragging: boolean
1282
+ isSelected: boolean
1283
+ }
1284
+
1285
+ /**
1286
+ * View-layer configuration: display props and render overrides. These live on
1287
+ * <Gantt> (and per-view components), never in the headless options.
1288
+ */
1289
+ interface GanttViewConfig<TData = unknown> {
1290
+ /** Red now-line on the axis. */
1291
+ nowIndicator: boolean
1292
+ /**
1293
+ * Day-scale unit interval in minutes: axis units and gridlines follow it.
1294
+ */
1295
+ interval: number
1296
+ /**
1297
+ * Scroll implementation for the gantt body: "custom" (default, shadcn
1298
+ * ScrollArea) or "native" (browser scrollbars via overflow auto).
1299
+ */
1300
+ scrollbars: "custom" | "native"
1301
+ /**
1302
+ * Placement hint over empty timeline track: a validated, snapped tile that
1303
+ * opens the schedule flow (onSlotClick, else onSelectSlot) at that day.
1304
+ * Works on every scale. Default off.
1305
+ */
1306
+ displayScheduleHint: boolean
1307
+ /**
1308
+ * Where the viewport opens. `"now"` (default) centres the current instant
1309
+ * when the anchor period contains it and falls back to the anchor; `"anchor"`
1310
+ * always centres the anchor; a Date centres that instant.
1311
+ *
1312
+ * Only `"now"` follows the wall clock - which is right for a live board and
1313
+ * wrong for a demo or a report, whose opening composition must not depend on
1314
+ * the hour it is viewed at. Those pass an explicit instant.
1315
+ */
1316
+ initialCenter: "now" | "anchor" | Date
1317
+ /**
1318
+ * Empty-track presses on schedulable rows start a drag-create gesture that
1319
+ * commits through onSelectSlot. Default off: the whole panel drags-to-pan
1320
+ * instead, and scheduling flows through the hint tile / onSlotClick.
1321
+ */
1322
+ dragCreate: boolean
1323
+ /**
1324
+ * "Add task" affordance at the foot of the tree that opens the create-task
1325
+ * flow (onCreateTask). Shown only when canCreateTask allows it. Default off.
1326
+ */
1327
+ displayCreateTaskHint: boolean
1328
+ /** Floating zoom in/out control over the track. Default on. */
1329
+ zoomControl: boolean
1330
+ /**
1331
+ * Ctrl/Cmd + wheel over the timeline zooms the time range, anchored on the
1332
+ * pointer. Trackpad pinch arrives as the same event (browsers set ctrlKey
1333
+ * on it), so this is also the pinch-to-zoom switch. Default on. The gesture
1334
+ * is handed back to the browser at the zoom limits, so page zoom still
1335
+ * works there.
1336
+ */
1337
+ wheelZoom: boolean
1338
+ /** Nav button variant; all nav buttons follow it. Default "ghost". */
1339
+ navButtonVariant: "ghost" | "outline" | "secondary" | "default"
1340
+ /** Nav button size; icon buttons use the icon twin. Default "sm". */
1341
+ navButtonSize: "sm" | "default"
1342
+ /**
1343
+ * Off-day (non-working day) marking on day/week/month scales. true =
1344
+ * weekends with a muted background; a config object customizes weekdays,
1345
+ * explicit dates, a predicate, and the marker class.
1346
+ */
1347
+ offDays?: boolean | GanttOffDaysConfig
1348
+ /**
1349
+ * Extra tree-panel columns after the built-in name column. The tree panel
1350
+ * scrolls horizontally when the columns outgrow it; the name column stays
1351
+ * pinned.
1352
+ */
1353
+ columns?: GanttColumn[]
1354
+ /**
1355
+ * Consumer slot pinned at the end of the tree-panel header - the intended
1356
+ * home for an add/remove-columns dropdown menu.
1357
+ */
1358
+ columnsMenu?: ReactNode
1359
+ /** Tree-panel width, splitter bounds, and resizability. */
1360
+ treePanel?: GanttTreePanelConfig
1361
+ /**
1362
+ * Timeline gridlines. The object form controls the two axes independently
1363
+ * and gives each its own stroke: `{ vertical: "dashed", horizontal: true }`.
1364
+ * An omitted axis stays on and solid. `true` means solid.
1365
+ *
1366
+ * The three legacy shorthands still work: "none" (bare), "vertical" (unit
1367
+ * boundaries only, rows separated by whitespace) and "both" (adds row
1368
+ * separators).
1369
+ */
1370
+ timelineLines: GanttTimelineLines | "vertical" | "both" | "none"
1371
+ /**
1372
+ * Bar title placement: "inside" (default) renders it in the bar, "outside"
1373
+ * beside the bar, "auto" moves it outside only when the bar is too short.
1374
+ */
1375
+ barLabel: "inside" | "outside" | "auto"
1376
+ /** Edge chips that scroll to bars outside the visible timeline. Default true. */
1377
+ offscreenIndicators: boolean
1378
+ /**
1379
+ * Extend the timeline into the past/future while scrolling near an edge
1380
+ * (the anchor period stays the nav title). Default true.
1381
+ */
1382
+ infiniteScroll: boolean
1383
+ /** Zoom bounds and button step for the floating control. Default 0.5 - 3, step 0.25. */
1384
+ zoomRange?: { min?: number; max?: number; step?: number }
1385
+ /** Layout metric overrides (row/lane/unit geometry, thresholds). */
1386
+ metrics?: GanttMetrics
1387
+ /** Sticky nav bar (same contract as the event calendar). Default false. */
1388
+ stickyNav: boolean
1389
+ /**
1390
+ * Leaf-row selection checkboxes in the tree panel. Default true;
1391
+ * uncontrolled unless selectedRows is passed.
1392
+ */
1393
+ rowCheckboxes: boolean
1394
+ /** Controlled selected row ids; pairs with onSelectedRowsChange. */
1395
+ selectedRows?: string[]
1396
+ onSelectedRowsChange?: (ids: string[]) => void
1397
+ /** Controlled collapsed group ids; pairs with onCollapsedGroupsChange. */
1398
+ collapsedGroups?: string[]
1399
+ /** Initial collapsed group ids (uncontrolled). */
1400
+ defaultCollapsedGroups?: string[]
1401
+ onCollapsedGroupsChange?: (ids: string[]) => void
1402
+ /** Controlled zoom multiplier; pairs with onZoomChange. */
1403
+ zoom?: number
1404
+ /** Initial zoom multiplier (uncontrolled). Default 1. */
1405
+ defaultZoom?: number
1406
+ onZoomChange?: (zoom: number) => void
1407
+ /**
1408
+ * Allow drag-create and slot clicks on rows that have children. Default
1409
+ * false: parents aggregate their subtree instead of owning bars.
1410
+ */
1411
+ parentScheduling: boolean
1412
+ /**
1413
+ * Rollup strips on parent rows without bars of their own: the envelope of
1414
+ * descendant bars with duration-weighted progress. Default true.
1415
+ */
1416
+ summaryBars: boolean
1417
+ /**
1418
+ * As-built comparison: the planned window of every bar carrying
1419
+ * baselineStart/baselineEnd, ghosted behind it on its own lane (equal
1420
+ * instants draw a planned milestone diamond), and of every NODE whose
1421
+ * GanttResource carries the same pair, drawn as a band behind its lanes.
1422
+ * Display only - dragging a bar moves the actual dates and never its
1423
+ * baseline. Default true; rows and events without baseline data draw
1424
+ * nothing.
1425
+ */
1426
+ baselineBars: boolean
1427
+ /**
1428
+ * Finish-to-start arrows between bars whose events name `dependencies`.
1429
+ * Drawn from the predecessor's end into the dependent's start, under the
1430
+ * bars; endpoints on hidden rows are skipped. Default true; nothing
1431
+ * renders when no visible event names a dependency.
1432
+ */
1433
+ dependencyLines: boolean
1434
+ /**
1435
+ * How many schedules a tree node may hold. "multiple" (default) stacks
1436
+ * concurrent schedules into stable lanes and grows the row; "single" keeps
1437
+ * one track per node - the task-gantt shape. Any node can override it with
1438
+ * its own `scheduleMode`.
1439
+ */
1440
+ scheduleMode: GanttScheduleMode
1441
+ /**
1442
+ * Vertical placement of a row's content once a node holds several lanes.
1443
+ * "start" (default) keeps the tree label on the baseline of the FIRST
1444
+ * schedule; "center" centers both against the grown row.
1445
+ */
1446
+ rowAlign: GanttRowAlign
1447
+ classNames?: GanttClassNames
1448
+ renderEvent?: (props: GanttRenderEventProps<TData>) => ReactNode
1449
+ /**
1450
+ * Right-click menu for a bar: return shadcn ContextMenu items (the primitive
1451
+ * wraps every bar in a ContextMenu and renders this as its content). Read
1452
+ * the occurrence for the subject and drive actions through the gantt api
1453
+ * (useGantt) or your own state - fully headless. Omit for no menu.
1454
+ */
1455
+ renderEventMenu?: (props: GanttRenderEventProps<TData>) => ReactNode
1456
+ /**
1457
+ * Tree-node label. Receives the resource with its tree position; return
1458
+ * any rich content (icons, badges). Default is the plain title.
1459
+ */
1460
+ renderResourceLabel?: (props: {
1461
+ resource: GanttResource
1462
+ depth: number
1463
+ isGroup: boolean
1464
+ collapsed: boolean
1465
+ }) => ReactNode
1466
+ /**
1467
+ * Right-click menu for a tree row (same contract as renderEventMenu):
1468
+ * return shadcn ContextMenu items and drive actions through your own state.
1469
+ */
1470
+ renderResourceMenu?: (ctx: GanttColumnContext) => ReactNode
1471
+ /** Rendered in the timeline body when there are no resources. */
1472
+ renderNoResources?: () => ReactNode
1473
+ /**
1474
+ * Replaces the smooth cursor-following MOVE clone. Content is React and
1475
+ * re-renders per snap step; the gantt owns the fixed wrapper and writes
1476
+ * its position imperatively per pointermove (no per-frame React).
1477
+ */
1478
+ renderDragPreview?: (props: GanttDragIndicatorProps<TData>) => ReactNode
1479
+ /**
1480
+ * Replaces the RESIZE edge line + status chip. Same positioning contract
1481
+ * as renderDragPreview: your content, gantt-owned cursor tracking.
1482
+ */
1483
+ renderResizeIndicator?: (props: GanttDragIndicatorProps<TData>) => ReactNode
1484
+ /**
1485
+ * Replaces the schedule-hint tile + bubble. Rendered inside the snapped,
1486
+ * validated, pointer-transparent wrapper: set pointer-events-auto on your
1487
+ * clickable parts and drive your own create flow from the slot.
1488
+ */
1489
+ renderScheduleHint?: (props: GanttScheduleHintProps) => ReactNode
1490
+ /** Replaces the parent rollup strip (the positioned wrapper stays gantt-owned). */
1491
+ renderSummary?: (props: GanttSummaryProps) => ReactNode
1492
+ /**
1493
+ * Replaces the baseline ghost's content, the milestone diamond included
1494
+ * (props.milestone forks the two). The positioned, pointer-transparent
1495
+ * wrapper stays gantt-owned.
1496
+ */
1497
+ renderBaseline?: (props: GanttBaselineProps<TData>) => ReactNode
1498
+ /**
1499
+ * Replaces the ROW baseline band's content (same wrapper contract as
1500
+ * renderBaseline).
1501
+ */
1502
+ renderRowBaseline?: (props: GanttRowBaselineProps) => ReactNode
1503
+ /**
1504
+ * Replaces the rollup MATH: return 0-100 (or null to hide) for a group
1505
+ * from its descendant events. Default: duration-weighted mean progress.
1506
+ */
1507
+ getSummaryProgress?: (ctx: {
1508
+ resource: GanttResource
1509
+ events: GanttEvent<TData>[]
1510
+ }) => number | null
1511
+ }
1512
+
1513
+ const DEFAULT_VIEW_CONFIG: GanttViewConfig = {
1514
+ nowIndicator: true,
1515
+ interval: 60,
1516
+ scrollbars: "custom",
1517
+ displayScheduleHint: false,
1518
+ initialCenter: "now",
1519
+ displayCreateTaskHint: false,
1520
+ dragCreate: false,
1521
+ zoomControl: true,
1522
+ wheelZoom: true,
1523
+ navButtonVariant: "ghost",
1524
+ navButtonSize: "sm",
1525
+ timelineLines: "vertical",
1526
+ barLabel: "inside",
1527
+ offscreenIndicators: true,
1528
+ infiniteScroll: true,
1529
+ stickyNav: false,
1530
+ rowCheckboxes: true,
1531
+ parentScheduling: false,
1532
+ summaryBars: true,
1533
+ baselineBars: true,
1534
+ dependencyLines: true,
1535
+ scheduleMode: "multiple",
1536
+ rowAlign: "start",
1537
+ }
1538
+
1539
+ const GanttViewConfigContext = createContext<
1540
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1541
+ GanttViewConfig<any>
1542
+ >(DEFAULT_VIEW_CONFIG)
1543
+
1544
+ /** Root-level display props + render overrides, for view components. */
1545
+ function useGanttViewConfig<TData = unknown>(): GanttViewConfig<TData> {
1546
+ return useContext(GanttViewConfigContext)
1547
+ }
1548
+
1549
+ const VIEW_CONFIG_KEYS: Array<keyof GanttViewConfig> = [
1550
+ "nowIndicator",
1551
+ "interval",
1552
+ "scrollbars",
1553
+ "displayScheduleHint",
1554
+ "initialCenter",
1555
+ "displayCreateTaskHint",
1556
+ "dragCreate",
1557
+ "zoomControl",
1558
+ "wheelZoom",
1559
+ "navButtonVariant",
1560
+ "navButtonSize",
1561
+ "offDays",
1562
+ "columns",
1563
+ "columnsMenu",
1564
+ "treePanel",
1565
+ "metrics",
1566
+ "timelineLines",
1567
+ "barLabel",
1568
+ "offscreenIndicators",
1569
+ "infiniteScroll",
1570
+ "zoomRange",
1571
+ "stickyNav",
1572
+ "rowCheckboxes",
1573
+ "selectedRows",
1574
+ "onSelectedRowsChange",
1575
+ "collapsedGroups",
1576
+ "defaultCollapsedGroups",
1577
+ "onCollapsedGroupsChange",
1578
+ "zoom",
1579
+ "defaultZoom",
1580
+ "onZoomChange",
1581
+ "parentScheduling",
1582
+ "summaryBars",
1583
+ "baselineBars",
1584
+ "dependencyLines",
1585
+ "scheduleMode",
1586
+ "rowAlign",
1587
+ "classNames",
1588
+ "renderEvent",
1589
+ "renderEventMenu",
1590
+ "renderResourceLabel",
1591
+ "renderResourceMenu",
1592
+ "renderNoResources",
1593
+ "renderDragPreview",
1594
+ "renderResizeIndicator",
1595
+ "renderScheduleHint",
1596
+ "renderSummary",
1597
+ "renderBaseline",
1598
+ "renderRowBaseline",
1599
+ "getSummaryProgress",
1600
+ ]
1601
+
1602
+ interface GanttProps<TData = unknown>
1603
+ extends
1604
+ UseGanttStateOptions<TData>,
1605
+ Partial<GanttViewConfig<TData>>,
1606
+ Omit<useRender.ComponentProps<"div">, "children" | "defaultValue"> {
1607
+ /** Adopt a hoisted useGanttState instance; option props are then ignored. */
1608
+ calendar?: GanttInstance<TData>
1609
+ /** Imperative escape hatch usable from outside the tree. */
1610
+ apiRef?: RefObject<GanttApi<TData> | null>
1611
+ children?: ReactNode
1612
+ }
1613
+
1614
+ const OPTION_KEYS: Array<keyof UseGanttStateOptions> = [
1615
+ "events",
1616
+ "defaultEvents",
1617
+ "scale",
1618
+ "defaultScale",
1619
+ "date",
1620
+ "defaultDate",
1621
+ "selection",
1622
+ "defaultSelection",
1623
+ "interactions",
1624
+ "defaultInteractions",
1625
+ "loading",
1626
+ "timeZone",
1627
+ "locale",
1628
+ "weekStartsOn",
1629
+ "slotDuration",
1630
+ "snapDuration",
1631
+ "i18n",
1632
+ "rangeBounds",
1633
+ "activation",
1634
+ "maxRangeWindow",
1635
+ "resources",
1636
+ "overlap",
1637
+ "enforceCanDrop",
1638
+ "getEventPriority",
1639
+ "eventOrder",
1640
+ "getOccurrences",
1641
+ "onEventClick",
1642
+ "onEventDoubleClick",
1643
+ "onEventUpdate",
1644
+ "canDropEvent",
1645
+ "onSlotClick",
1646
+ "onSelectSlot",
1647
+ "canSelectSlot",
1648
+ "onCreateTask",
1649
+ "canCreateTask",
1650
+ "onResourceClick",
1651
+ "onResourceDoubleClick",
1652
+ "onRangeChange",
1653
+ "onScaleChange",
1654
+ "onDateChange",
1655
+ "onSelectionChange",
1656
+ "onInteractionsChange",
1657
+ "onEventsChange",
1658
+ "onResourceReorder",
1659
+ "onResourceReorderReject",
1660
+ "canReorderResource",
1661
+ ]
1662
+
1663
+ function shallowEqualRecord(
1664
+ a: Record<string, unknown>,
1665
+ b: Record<string, unknown>
1666
+ ): boolean {
1667
+ const aKeys = Object.keys(a)
1668
+ if (aKeys.length !== Object.keys(b).length) return false
1669
+ for (const key of aKeys) {
1670
+ if (!Object.is(a[key], b[key])) return false
1671
+ }
1672
+ return true
1673
+ }
1674
+
1675
+ function splitOptions<TData>(props: Record<string, unknown>): {
1676
+ options: UseGanttStateOptions<TData>
1677
+ viewConfig: GanttViewConfig<TData>
1678
+ rest: Record<string, unknown>
1679
+ } {
1680
+ const options: Record<string, unknown> = {}
1681
+ const viewConfig: Record<string, unknown> = { ...DEFAULT_VIEW_CONFIG }
1682
+ const rest: Record<string, unknown> = {}
1683
+ for (const [key, value] of Object.entries(props)) {
1684
+ if ((OPTION_KEYS as string[]).includes(key)) options[key] = value
1685
+ else if ((VIEW_CONFIG_KEYS as string[]).includes(key)) {
1686
+ if (value !== undefined) viewConfig[key] = value
1687
+ } else rest[key] = value
1688
+ }
1689
+ return {
1690
+ options: options as UseGanttStateOptions<TData>,
1691
+ viewConfig: viewConfig as unknown as GanttViewConfig<TData>,
1692
+ rest,
1693
+ }
1694
+ }
1695
+
1696
+ /**
1697
+ * Root provider + container. Composition contract:
1698
+ * <Gantt><GanttNav/><GanttToolbar/><GanttView/></Gantt>
1699
+ */
1700
+ function Gantt<TData = unknown>({
1701
+ calendar,
1702
+ apiRef,
1703
+ className,
1704
+ render,
1705
+ children,
1706
+ ...props
1707
+ }: GanttProps<TData>) {
1708
+ const { options, viewConfig, rest } = splitOptions<TData>(
1709
+ props as Record<string, unknown>
1710
+ )
1711
+
1712
+ // Stable context identity: splitOptions builds a fresh object per render,
1713
+ // and every row subscribes to this context - hand out the previous object
1714
+ // unless a config value actually changed.
1715
+ const viewConfigRef = useRef(viewConfig)
1716
+ if (
1717
+ !shallowEqualRecord(
1718
+ viewConfigRef.current as unknown as Record<string, unknown>,
1719
+ viewConfig as unknown as Record<string, unknown>
1720
+ )
1721
+ ) {
1722
+ viewConfigRef.current = viewConfig
1723
+ }
1724
+ const stableViewConfig = viewConfigRef.current
1725
+
1726
+ if (calendar && Object.keys(options).length > 0) {
1727
+ warnOnce(
1728
+ "calendar-and-options",
1729
+ "both `calendar` and option props were passed; option props are ignored when adopting an instance."
1730
+ )
1731
+ }
1732
+
1733
+ const own = useGanttState<TData>(calendar ? {} : options)
1734
+ const instance = calendar ?? own
1735
+
1736
+ useEffect(() => {
1737
+ if (apiRef) apiRef.current = instance.api
1738
+ }, [apiRef, instance])
1739
+
1740
+ const defaultProps = {
1741
+ "data-slot": "gantt",
1742
+ // own the foreground (previews and consumer shells may not set body
1743
+ // color) and the type scale: every gantt label inherits the root's text
1744
+ // size, so one class here (or on the consumer's className) rescales the
1745
+ // whole component - e.g. className="text-sm" for a roomier grid
1746
+ className: cn(
1747
+ "text-foreground flex min-h-0 min-w-0 flex-col text-xs",
1748
+ className
1749
+ ),
1750
+ children: (
1751
+ <>
1752
+ {children}
1753
+ <div
1754
+ data-slot="gantt-announcer"
1755
+ aria-live="polite"
1756
+ className="sr-only"
1757
+ />
1758
+ </>
1759
+ ),
1760
+ }
1761
+
1762
+ return (
1763
+ <GanttContext.Provider value={instance}>
1764
+ <GanttViewConfigContext.Provider value={stableViewConfig}>
1765
+ {useRender({
1766
+ defaultTagName: "div",
1767
+ render,
1768
+ props: mergeProps<"div">(defaultProps, rest),
1769
+ })}
1770
+ </GanttViewConfigContext.Provider>
1771
+ </GanttContext.Provider>
1772
+ )
1773
+ }
1774
+
1775
+ export {
1776
+ DEFAULT_ROW_ALIGN,
1777
+ DEFAULT_SCHEDULE_MODE,
1778
+ DEFAULT_VIEW_CONFIG,
1779
+ Gantt,
1780
+ GanttContext,
1781
+ GanttViewConfigContext,
1782
+ resolveScheduleMode,
1783
+ resolveTimelineLines,
1784
+ useGantt,
1785
+ useGanttInteractions,
1786
+ useGanttNavigation,
1787
+ useGanttNodeSchedules,
1788
+ useGanttOccurrences,
1789
+ useGanttScale,
1790
+ useGanttSelection,
1791
+ useGanttSelector,
1792
+ useGanttSettings,
1793
+ useGanttSettingsVersion,
1794
+ useGanttState,
1795
+ useGanttViewConfig,
1796
+ }
1797
+ export type {
1798
+ GanttActivationConfig,
1799
+ GanttApi,
1800
+ GanttBaselineProps,
1801
+ GanttCallbacks,
1802
+ GanttClassNames,
1803
+ GanttColumn,
1804
+ GanttColumnContext,
1805
+ GanttDragIndicatorProps,
1806
+ GanttGridLine,
1807
+ GanttInstance,
1808
+ GanttInternals,
1809
+ GanttMetrics,
1810
+ GanttNodeSchedules,
1811
+ GanttProps,
1812
+ GanttRenderEventProps,
1813
+ GanttResolvedLines,
1814
+ GanttRowBaselineProps,
1815
+ GanttScheduleHintProps,
1816
+ GanttSettings,
1817
+ GanttSummaryProps,
1818
+ GanttTimelineLines,
1819
+ GanttTreePanelConfig,
1820
+ GanttViewConfig,
1821
+ UseGanttStateOptions,
1822
+ }