@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,1227 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import * as React from "react"
4
+ import type { CSSProperties, ReactNode } from "react"
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext,
9
+ useLayoutEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ useSyncExternalStore,
14
+ useEffect,
15
+ } from "react"
16
+ import { mergeProps } from "@base-ui/react/merge-props"
17
+ import { useRender } from "@base-ui/react/use-render"
18
+ import type {
19
+ Active,
20
+ CollisionDetection,
21
+ DragCancelEvent,
22
+ DragEndEvent,
23
+ DragMoveEvent,
24
+ DragOverEvent,
25
+ DragStartEvent,
26
+ DropAnimation,
27
+ Modifiers,
28
+ Over,
29
+ UniqueIdentifier,
30
+ } from "@dnd-kit/core"
31
+ import {
32
+ closestCenter,
33
+ DndContext,
34
+ DragOverlay,
35
+ getFirstCollision,
36
+ MeasuringStrategy,
37
+ MouseSensor,
38
+ pointerWithin,
39
+ rectIntersection,
40
+ TouchSensor,
41
+ useSensor,
42
+ useSensors,
43
+ type DraggableAttributes,
44
+ type DraggableSyntheticListeners,
45
+ } from "@dnd-kit/core"
46
+ import {
47
+ arrayMove,
48
+ defaultAnimateLayoutChanges,
49
+ rectSortingStrategy,
50
+ SortableContext,
51
+ useSortable,
52
+ verticalListSortingStrategy,
53
+ type AnimateLayoutChanges,
54
+ } from "@dnd-kit/sortable"
55
+ import { CSS } from "@dnd-kit/utilities"
56
+ import { createPortal } from "react-dom"
57
+
58
+ import { cn } from "@/lib/utils"
59
+
60
+ import {
61
+ KanbanKeyboardSensor,
62
+ makeKanbanKeyboardCoordinateGetter,
63
+ } from "./kanban-keyboard"
64
+
65
+ interface KanbanDataContextProps<T> {
66
+ columns: Record<string, T[]>
67
+ setColumns: (columns: Record<string, T[]>) => void
68
+ getItemId: (item: T) => string
69
+ columnIds: string[]
70
+ findContainer: (id: UniqueIdentifier) => string | undefined
71
+ }
72
+
73
+ /**
74
+ * 拖拽状态与数据状态分居两个 context:`columns` 在每次 onDragOver 都换新对象,
75
+ * 而 `activeId` 只在拖拽起止变化。同处一个 context 时,只读 `activeId` 的
76
+ * KanbanItem/KanbanColumn 会被列内容更新拖着整板重渲染(大看板下是主要开销)。
77
+ */
78
+ interface KanbanDragContextProps {
79
+ activeId: UniqueIdentifier | null
80
+ isColumn: (id: UniqueIdentifier) => boolean
81
+ modifiers?: Modifiers
82
+ }
83
+
84
+ const KanbanDataContext = createContext<KanbanDataContextProps<any>>({
85
+ columns: {},
86
+ setColumns: () => {},
87
+ getItemId: () => "",
88
+ columnIds: [],
89
+ findContainer: () => undefined,
90
+ })
91
+
92
+ const KanbanDragContext = createContext<KanbanDragContextProps>({
93
+ activeId: null,
94
+ isColumn: () => false,
95
+ modifiers: undefined,
96
+ })
97
+
98
+ const ColumnContext = createContext<{
99
+ attributes: DraggableAttributes
100
+ listeners: DraggableSyntheticListeners | undefined
101
+ isDragging?: boolean
102
+ disabled?: boolean
103
+ }>({
104
+ attributes: {} as DraggableAttributes,
105
+ listeners: undefined,
106
+ isDragging: false,
107
+ disabled: false,
108
+ })
109
+
110
+ const ItemContext = createContext<{
111
+ listeners: DraggableSyntheticListeners | undefined
112
+ isDragging?: boolean
113
+ disabled?: boolean
114
+ }>({
115
+ listeners: undefined,
116
+ isDragging: false,
117
+ disabled: false,
118
+ })
119
+
120
+ const IsOverlayContext = createContext(false)
121
+
122
+ /**
123
+ * 拖拽落点指示:拖拽期由 Kanban 写入目标列 + 插入位,列内容按列订阅后渲染落点槽、
124
+ * 并用 transform 让位(不插入 DOM、不改数据)。
125
+ *
126
+ * 为什么不用 context state:context 更新会穿透 memo 让整板列内容一起重渲染
127
+ * (200 卡下即卡顿);外部 store + 按列选取器让非目标列拿到稳定的 null,
128
+ * 完全不参与重渲染。也不能用「插入真实占位元素」的写法:布局变化会触发
129
+ * dnd-kit 的 droppable 重测(拖拽期 queue 已空 → 全量 re-measure),
130
+ * 同样卡顿;transform 不触发布局,故最稳。
131
+ */
132
+ export interface KanbanDropIndicator {
133
+ /** 目标列 key */
134
+ container: string
135
+ /** 目标列当前顺序中的插入下标(0..len) */
136
+ insertIndex: number
137
+ /** 拖拽起始列 */
138
+ sourceContainer: string
139
+ /** 拖拽起始下标(同列原位判定用) */
140
+ sourceIndex: number
141
+ /** 拖拽卡高度(px):落点槽高度与让位位移都用它 */
142
+ height: number
143
+ /** 被拖卡片的 item key:落点处渲染同款淡化卡 */
144
+ itemId: string
145
+ /** 落点 hover 到的卡(或列)的 id:用于判断落在哪张卡上/哪一子分组末尾 */
146
+ overItemKey: string
147
+ }
148
+
149
+ /** 按列取用的视图数据;非目标列恒为 null(引用稳定 → 不触发重渲染) */
150
+ export interface KanbanDropIndicatorView {
151
+ insertIndex: number
152
+ height: number
153
+ /** 被拖卡片的 item key */
154
+ itemId: string
155
+ /** 落点 hover 到的卡(或列)的 id */
156
+ overItemKey: string
157
+ }
158
+
159
+ interface KanbanDropIndicatorStore {
160
+ subscribe: (listener: () => void) => () => void
161
+ set: (state: KanbanDropIndicator | null) => void
162
+ getForColumn: (column: string) => KanbanDropIndicatorView | null
163
+ }
164
+
165
+ /** 落点指示器 store 工厂:导出供测试与自定义落点渲染使用 */
166
+ export function createDropIndicatorStore(): KanbanDropIndicatorStore {
167
+ let state: KanbanDropIndicator | null = null
168
+ let derivedColumn: string | null = null
169
+ let derivedView: KanbanDropIndicatorView | null = null
170
+ const listeners = new Set<() => void>()
171
+ const publish = () => {
172
+ derivedColumn = state ? state.container : null
173
+ derivedView = state
174
+ ? {
175
+ insertIndex: state.insertIndex,
176
+ height: state.height,
177
+ itemId: state.itemId,
178
+ overItemKey: state.overItemKey,
179
+ }
180
+ : null
181
+ listeners.forEach((listener) => listener())
182
+ }
183
+ return {
184
+ subscribe(listener) {
185
+ listeners.add(listener)
186
+ return () => {
187
+ listeners.delete(listener)
188
+ }
189
+ },
190
+ set(next) {
191
+ if (next === null) {
192
+ if (state === null) return
193
+ state = null
194
+ publish()
195
+ return
196
+ }
197
+ // onDragMove 每帧调用:仅在目标列 / 插入位 / 卡高变化时才通知
198
+ const prev = state
199
+ if (
200
+ prev !== null &&
201
+ prev.container === next.container &&
202
+ prev.insertIndex === next.insertIndex &&
203
+ prev.height === next.height &&
204
+ prev.itemId === next.itemId &&
205
+ prev.overItemKey === next.overItemKey
206
+ ) {
207
+ return
208
+ }
209
+ state = next
210
+ publish()
211
+ },
212
+ getForColumn(column) {
213
+ return derivedColumn === column ? derivedView : null
214
+ },
215
+ }
216
+ }
217
+
218
+ const KanbanDropIndicatorContext =
219
+ createContext<KanbanDropIndicatorStore | null>(null)
220
+
221
+ export function useKanbanDropIndicator(
222
+ column: string
223
+ ): KanbanDropIndicatorView | null {
224
+ const store = useContext(KanbanDropIndicatorContext)
225
+ const getSnapshot = useCallback(
226
+ () => (store ? store.getForColumn(column) : null),
227
+ [store, column]
228
+ )
229
+ return useSyncExternalStore(
230
+ store ? store.subscribe : subscribeToNothing,
231
+ getSnapshot,
232
+ getSnapshot
233
+ )
234
+ }
235
+
236
+ const animateLayoutChanges: AnimateLayoutChanges = (args) =>
237
+ defaultAnimateLayoutChanges({ ...args, wasDragging: true })
238
+
239
+ // drop 后 overlay 的收尾动画必须关掉:默认动画把 overlay 从指针位置平移回
240
+ // activeNodeRect(源卡片位置),而源卡片在拖拽期已经不再移动,动画会先把卡片
241
+ // 「飞回原位」再跳到落点列。落位由 drop 时的 re-render 直接呈现,无动画更准。
242
+ const dropAnimationConfig: DropAnimation | null = null
243
+
244
+ /**
245
+ * Client-mount gate for the `createPortal` call in KanbanOverlay, which needs
246
+ * `document.body` and so must not run on the server or during hydration.
247
+ */
248
+ const subscribeToNothing = () => () => {}
249
+ const getIsMounted = () => true
250
+ const getIsMountedOnServer = () => false
251
+
252
+ const MOUSE_SENSOR_OPTIONS = { activationConstraint: { distance: 10 } }
253
+ const TOUCH_SENSOR_OPTIONS = {
254
+ activationConstraint: { delay: 250, tolerance: 5 },
255
+ }
256
+ const MEASURING_CONFIG = {
257
+ droppable: { strategy: MeasuringStrategy.WhileDragging },
258
+ }
259
+
260
+ export interface KanbanMoveEvent {
261
+ event: DragEndEvent
262
+ activeContainer: string
263
+ activeIndex: number
264
+ overContainer: string
265
+ overIndex: number
266
+ }
267
+
268
+ export interface KanbanCommitMeta<T> {
269
+ kind: "item" | "column"
270
+ event: DragEndEvent
271
+ activeContainer: string
272
+ activeIndex: number
273
+ overContainer: string
274
+ overIndex: number
275
+ previousValue: Record<string, T[]>
276
+ }
277
+
278
+ export interface KanbanRootProps<T> extends Omit<
279
+ useRender.ComponentProps<"div">,
280
+ "children" | "onDragStart" | "onDragEnd"
281
+ > {
282
+ value: Record<string, T[]>
283
+ onValueChange: (value: Record<string, T[]>) => void
284
+ getItemValue: (item: T) => string
285
+ children: ReactNode
286
+ onMove?: (event: KanbanMoveEvent) => void
287
+ onValueCommit?: (
288
+ value: Record<string, T[]>,
289
+ meta: KanbanCommitMeta<T>
290
+ ) => void
291
+ restoreOnCancel?: boolean
292
+ onDragStart?: (event: DragStartEvent) => void
293
+ onDragEnd?: (event: DragEndEvent) => void
294
+ onDragCancel?: (event?: DragCancelEvent) => void
295
+ accessibility?: React.ComponentProps<typeof DndContext>["accessibility"]
296
+ modifiers?: Modifiers
297
+ }
298
+
299
+ function Kanban<T>({
300
+ value,
301
+ onValueChange,
302
+ getItemValue,
303
+ children,
304
+ className,
305
+ render,
306
+ onMove,
307
+ onValueCommit,
308
+ restoreOnCancel = false,
309
+ onDragStart,
310
+ onDragEnd,
311
+ onDragCancel,
312
+ accessibility,
313
+ modifiers,
314
+ ...props
315
+ }: KanbanRootProps<T>) {
316
+ const columns = value
317
+ const setColumns = onValueChange
318
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
319
+ // 落点指示器的外部 store:列内容按列订阅,非目标列不参与重渲染(见 createDropIndicatorStore)
320
+ const [dropIndicatorStore] = useState(createDropIndicatorStore)
321
+
322
+ // Always-current mirrors so the drag handlers can read fresh values without
323
+ // widening their dependency arrays (keeps handler identity stable).
324
+ const valueRef = useRef(value)
325
+ const getItemValueRef = useRef(getItemValue)
326
+ useLayoutEffect(() => {
327
+ valueRef.current = value
328
+ getItemValueRef.current = getItemValue
329
+ })
330
+ // 被拖卡片在「列内」的真实高度:dnd-kit 的 active/over rect 都取自 DragOverlay 节点
331
+ // (overlay 用 w-72,比列内卡宽 16px → 少折一行、矮 ~20px),直接用会让位步长与落点
332
+ // 淡化卡高度对不齐(实测重叠 11px)。拖拽开始时从激活事件的目标节点量一次即可。
333
+ const dragHeightRef = useRef(0)
334
+ // Esc 软取消标记:pointer 传感器不处理 Esc(dnd-kit 只在键盘传感器实现),
335
+ // 这里自己补:Esc 时清空拖拽视觉态,等真实 mouseup 触发的 handleDragEnd 跳过提交
336
+ const cancelledRef = useRef(false)
337
+ const dragOriginRef = useRef<{
338
+ value: Record<string, T[]>
339
+ container: string | undefined
340
+ index: number
341
+ } | null>(null)
342
+
343
+ const columnIds = useMemo(() => {
344
+ const keys = Object.keys(columns)
345
+ if (process.env.NODE_ENV !== "production") {
346
+ const seen = new Set<string>()
347
+ for (const key of keys) {
348
+ for (const item of columns[key]) {
349
+ const itemId = getItemValue(item)
350
+ if (seen.has(itemId)) {
351
+ console.warn(
352
+ `[Kanban] Duplicate item id "${itemId}". Item ids must be unique across all columns, or drag and drop will misbehave.`
353
+ )
354
+ break
355
+ }
356
+ seen.add(itemId)
357
+ }
358
+ }
359
+ }
360
+ return keys
361
+ }, [columns, getItemValue])
362
+
363
+ const isColumn = useCallback(
364
+ (id: UniqueIdentifier) => columnIds.includes(id as string),
365
+ [columnIds]
366
+ )
367
+
368
+ const keyboardCoordinateGetter = useMemo(
369
+ () => makeKanbanKeyboardCoordinateGetter(isColumn),
370
+ [isColumn]
371
+ )
372
+
373
+ const sensors = useSensors(
374
+ useSensor(MouseSensor, MOUSE_SENSOR_OPTIONS),
375
+ useSensor(TouchSensor, TOUCH_SENSOR_OPTIONS),
376
+ // 键盘传感器为 fork 内实现(kanban-keyboard.ts):上游对视口外目标「只滚动不
377
+ // 下发 move」,窄屏下列拖拽原地踏步(#470)
378
+ useSensor(KanbanKeyboardSensor, { coordinateGetter: keyboardCoordinateGetter })
379
+ )
380
+
381
+ const findContainer = useCallback(
382
+ (id: UniqueIdentifier) => {
383
+ if (isColumn(id)) return id as string
384
+ return columnIds.find((key) =>
385
+ columns[key].some((item) => getItemValue(item) === id)
386
+ )
387
+ },
388
+ [columns, columnIds, getItemValue, isColumn]
389
+ )
390
+
391
+ // The droppable under the pointer, not the one the dragged rect overlaps most.
392
+ const lastOverIdRef = useRef<UniqueIdentifier | null>(null)
393
+ const collisionDetection = useCallback<CollisionDetection>(
394
+ (args) => {
395
+ if (isColumn(args.active.id)) {
396
+ return closestCenter({
397
+ ...args,
398
+ droppableContainers: args.droppableContainers.filter((container) =>
399
+ isColumn(container.id)
400
+ ),
401
+ })
402
+ }
403
+
404
+ // Keyboard drags carry no pointer.
405
+ if (!args.pointerCoordinates) return rectIntersection(args)
406
+
407
+ let overId = getFirstCollision(pointerWithin(args), "id")
408
+ if (overId != null) {
409
+ // Over a column's empty space: resolve to its closest item, if any.
410
+ if (isColumn(overId)) {
411
+ const itemIds = new Set(columns[overId as string].map(getItemValue))
412
+ overId =
413
+ closestCenter({
414
+ ...args,
415
+ droppableContainers: args.droppableContainers.filter(
416
+ (container) => itemIds.has(container.id as string)
417
+ ),
418
+ })[0]?.id ?? overId
419
+ }
420
+ lastOverIdRef.current = overId
421
+ return [{ id: overId }]
422
+ }
423
+
424
+ // Between droppables: hold the last target so the preview stays put.
425
+ return lastOverIdRef.current != null
426
+ ? [{ id: lastOverIdRef.current }]
427
+ : rectIntersection(args)
428
+ },
429
+ [columns, getItemValue, isColumn]
430
+ )
431
+
432
+ const commitChange = useCallback(
433
+ (
434
+ finalValue: Record<string, T[]>,
435
+ event: DragEndEvent,
436
+ kind: "item" | "column"
437
+ ) => {
438
+ if (!onValueCommit) return
439
+ const origin = dragOriginRef.current
440
+ if (!origin) return
441
+
442
+ const id = event.active.id
443
+
444
+ if (kind === "column") {
445
+ const keys = Object.keys(finalValue)
446
+ const overIndex = keys.indexOf(id as string)
447
+ if (overIndex === -1 || overIndex === origin.index) return
448
+ onValueCommit(finalValue, {
449
+ kind: "column",
450
+ event,
451
+ activeContainer: id as string,
452
+ activeIndex: origin.index,
453
+ overContainer: String(event.over?.id ?? id),
454
+ overIndex,
455
+ previousValue: origin.value,
456
+ })
457
+ return
458
+ }
459
+
460
+ const getId = getItemValueRef.current
461
+ let overContainer: string | undefined
462
+ let overIndex = -1
463
+ for (const key of Object.keys(finalValue)) {
464
+ const found = finalValue[key].findIndex((item) => getId(item) === id)
465
+ if (found !== -1) {
466
+ overContainer = key
467
+ overIndex = found
468
+ break
469
+ }
470
+ }
471
+ if (overContainer === undefined) return
472
+ if (overContainer === origin.container && overIndex === origin.index) {
473
+ return
474
+ }
475
+ onValueCommit(finalValue, {
476
+ kind: "item",
477
+ event,
478
+ activeContainer: origin.container ?? overContainer,
479
+ activeIndex: origin.index,
480
+ overContainer,
481
+ overIndex,
482
+ previousValue: origin.value,
483
+ })
484
+ },
485
+ [onValueCommit]
486
+ )
487
+
488
+ /**
489
+ * 解析落点插入位(目标列当前顺序中的下标)。
490
+ * 拖拽卡中心越过 hover 卡中线 → 插到它下面(+1)。落点指示与 drop 提交必须共用
491
+ * 同一份解析:否则「看到的淡化卡位置」与「真正落库的位置」会差一位,跨子分组时
492
+ * 就会落到相邻子分组里。
493
+ */
494
+ const resolveInsertIndex = useCallback(
495
+ (active: Active, over: Over): number | null => {
496
+ const overItems = columns[findContainer(over.id) ?? ""]
497
+ if (!overItems) return null
498
+ if (isColumn(over.id)) return overItems.length
499
+ const overIndex = overItems.findIndex(
500
+ (item: T) => getItemValue(item) === over.id
501
+ )
502
+ if (overIndex === -1) return null
503
+ const translated = active.rect.current.translated
504
+ const below =
505
+ translated != null &&
506
+ translated.top + translated.height / 2 >
507
+ over.rect.top + over.rect.height / 2
508
+ return below ? overIndex + 1 : overIndex
509
+ },
510
+ [columns, findContainer, getItemValue, isColumn]
511
+ )
512
+
513
+ /**
514
+ * 计算并发布拖拽落点(目标列 + 插入位)。只读 dnd-kit 事件里已测好的 rect
515
+ * (`over.rect` 是拖拽开始时的测量值,不会被落点槽的 transform 影响),
516
+ * 因此不会产生「插入 → 位置变化 → 落点抖动」的反馈环,也不需要额外的
517
+ * getBoundingClientRect(整板测量是拖拽卡顿的主因之一)。
518
+ */
519
+ const updateDropIndicator = useCallback(
520
+ (event: DragMoveEvent | DragOverEvent) => {
521
+ const { active, over } = event
522
+ if (!over || isColumn(active.id)) {
523
+ dropIndicatorStore.set(null)
524
+ return
525
+ }
526
+ const activeContainer = findContainer(active.id)
527
+ const overContainer = findContainer(over.id)
528
+ if (!activeContainer || !overContainer) {
529
+ dropIndicatorStore.set(null)
530
+ return
531
+ }
532
+ const insertIndex = resolveInsertIndex(active, over)
533
+ if (insertIndex === null) {
534
+ dropIndicatorStore.set(null)
535
+ return
536
+ }
537
+ const activeItems = columns[activeContainer] ?? []
538
+ const sourceIndex = activeItems.findIndex(
539
+ (item) => getItemValue(item) === active.id
540
+ )
541
+ // 同列且落回原位(插入点等于自身位置或紧邻其后)→ 不显示落点
542
+ if (
543
+ activeContainer === overContainer &&
544
+ (insertIndex === sourceIndex || insertIndex === sourceIndex + 1)
545
+ ) {
546
+ dropIndicatorStore.set(null)
547
+ return
548
+ }
549
+ dropIndicatorStore.set({
550
+ container: overContainer,
551
+ insertIndex,
552
+ sourceContainer: activeContainer,
553
+ sourceIndex,
554
+ itemId: String(active.id),
555
+ overItemKey: String(over.id),
556
+ height: dragHeightRef.current,
557
+ })
558
+ },
559
+ [columns, dropIndicatorStore, findContainer, getItemValue, isColumn, resolveInsertIndex]
560
+ )
561
+
562
+ // Esc 取消拖拽(pointer 拖拽):清视觉态 + 打标记,mouseup 时按取消收尾
563
+ useEffect(() => {
564
+ if (activeId === null) return
565
+ const onKeyDown = (event: KeyboardEvent) => {
566
+ if (event.key !== "Escape" && event.code !== "Escape") return
567
+ if (cancelledRef.current) return
568
+ cancelledRef.current = true
569
+ const origin = dragOriginRef.current
570
+ if (restoreOnCancel && origin && !onMove) {
571
+ setColumns(origin.value)
572
+ }
573
+ dropIndicatorStore.set(null)
574
+ lastOverIdRef.current = null
575
+ setActiveId(null)
576
+ onDragCancel?.()
577
+ }
578
+ document.addEventListener("keydown", onKeyDown)
579
+ return () => document.removeEventListener("keydown", onKeyDown)
580
+ }, [activeId, restoreOnCancel, onMove, setColumns, dropIndicatorStore, onDragCancel])
581
+
582
+ const handleDragStart = useCallback(
583
+ (event: DragStartEvent) => {
584
+ lastOverIdRef.current = null
585
+ dropIndicatorStore.set(null)
586
+ const activator = event.activatorEvent as Event | null
587
+ const target = (activator?.target ?? null) as Element | null
588
+ const cardEl = target?.closest?.('[data-slot="kanban-item"]')
589
+ dragHeightRef.current = cardEl instanceof HTMLElement ? cardEl.offsetHeight : 0
590
+ setActiveId(event.active.id)
591
+ onDragStart?.(event)
592
+
593
+ if (onValueCommit || restoreOnCancel) {
594
+ const snapshot = valueRef.current
595
+ const id = event.active.id
596
+ const keys = Object.keys(snapshot)
597
+ if (keys.includes(id as string)) {
598
+ dragOriginRef.current = {
599
+ value: snapshot,
600
+ container: id as string,
601
+ index: keys.indexOf(id as string),
602
+ }
603
+ } else {
604
+ const getId = getItemValueRef.current
605
+ let container: string | undefined
606
+ let index = -1
607
+ for (const key of keys) {
608
+ const found = snapshot[key].findIndex((item) => getId(item) === id)
609
+ if (found !== -1) {
610
+ container = key
611
+ index = found
612
+ break
613
+ }
614
+ }
615
+ dragOriginRef.current = { value: snapshot, container, index }
616
+ }
617
+ }
618
+ },
619
+ [dropIndicatorStore, onDragStart, onValueCommit, restoreOnCancel]
620
+ )
621
+
622
+ // 拖拽期不做任何顺序变更:每次 dragOver 提交都会让 dnd-kit 的 useDerivedTransform
623
+ // 在 render 阶段逐项 getClientRect,强制刷新已脏的整板布局(实测每次提交固定
624
+ // 100–155ms 长任务,板内 200 卡 / 16k 节点),拖拽中的重排节点数越多越卡。
625
+ // 顺序变更统一推迟到 drop 时一次性提交(见 handleDragEnd),拖拽期仅由 transform
626
+ // 与 DragOverlay 提供视觉反馈。
627
+
628
+ const handleDragCancel = useCallback(
629
+ (event: DragCancelEvent) => {
630
+ const origin = dragOriginRef.current
631
+
632
+ // 拖拽期已不改写顺序,正常取消无需恢复;仅在拖拽期间值确实被整体替换
633
+ // (服务端刷新、或上游插入幽灵空列)时才回滚快照。
634
+ if (
635
+ restoreOnCancel &&
636
+ origin &&
637
+ !onMove &&
638
+ origin.value !== valueRef.current
639
+ ) {
640
+ setColumns(origin.value)
641
+ }
642
+
643
+ dragOriginRef.current = null
644
+ lastOverIdRef.current = null
645
+ dropIndicatorStore.set(null)
646
+ setActiveId(null)
647
+ onDragCancel?.(event)
648
+ },
649
+ [dropIndicatorStore, restoreOnCancel, onMove, setColumns, onDragCancel]
650
+ )
651
+
652
+ const handleDragEnd = useCallback(
653
+ (event: DragEndEvent) => {
654
+ const { active, over } = event
655
+ lastOverIdRef.current = null
656
+ dropIndicatorStore.set(null)
657
+ if (cancelledRef.current) {
658
+ // Esc 已取消:视觉态在 Esc 时已清,这里只需丢弃这次提交
659
+ cancelledRef.current = false
660
+ dragOriginRef.current = null
661
+ setActiveId(null)
662
+ return
663
+ }
664
+ setActiveId(null)
665
+ onDragEnd?.(event)
666
+
667
+ if (!over) {
668
+ // Released over nothing. In default mode the live preview during
669
+ // dragOver may have already moved the item, so commit the current value.
670
+ commitChange(valueRef.current, event, "item")
671
+ dragOriginRef.current = null
672
+ return
673
+ }
674
+
675
+ // Handle item move callback
676
+ if (onMove && !isColumn(active.id)) {
677
+ const activeContainer = findContainer(active.id)
678
+ const overContainer = findContainer(over.id)
679
+
680
+ if (activeContainer && overContainer) {
681
+ const activeIndex = columns[activeContainer].findIndex(
682
+ (item: T) => getItemValue(item) === active.id
683
+ )
684
+ const overIndex = isColumn(over.id)
685
+ ? columns[overContainer].length
686
+ : columns[overContainer].findIndex(
687
+ (item: T) => getItemValue(item) === over.id
688
+ )
689
+
690
+ onMove({
691
+ event,
692
+ activeContainer,
693
+ activeIndex,
694
+ overContainer,
695
+ overIndex,
696
+ })
697
+ }
698
+ // In onMove mode the consumer owns applying the item move, so do not
699
+ // fire onValueCommit for item moves; column reorders still commit below.
700
+ dragOriginRef.current = null
701
+ return
702
+ }
703
+
704
+ // Handle column reordering
705
+ if (isColumn(active.id) && isColumn(over.id)) {
706
+ const activeIndex = columnIds.indexOf(active.id as string)
707
+ const overIndex = columnIds.indexOf(over.id as string)
708
+ if (activeIndex !== overIndex) {
709
+ const newOrder = arrayMove(
710
+ Object.keys(columns),
711
+ activeIndex,
712
+ overIndex
713
+ )
714
+ const newColumns: Record<string, T[]> = {}
715
+ newOrder.forEach((key) => {
716
+ newColumns[key] = columns[key]
717
+ })
718
+ setColumns(newColumns)
719
+ commitChange(newColumns, event, "column")
720
+ }
721
+ dragOriginRef.current = null
722
+ return
723
+ }
724
+
725
+ // A column drag that ends over a non-column droppable is not an item move.
726
+ if (isColumn(active.id)) {
727
+ dragOriginRef.current = null
728
+ return
729
+ }
730
+
731
+ const activeContainer = findContainer(active.id)
732
+ const overContainer = findContainer(over.id)
733
+
734
+ // Handle item reordering within the same column
735
+ if (
736
+ activeContainer &&
737
+ overContainer &&
738
+ activeContainer === overContainer
739
+ ) {
740
+ const container = activeContainer
741
+ const activeIndex = columns[container].findIndex(
742
+ (item: T) => getItemValue(item) === active.id
743
+ )
744
+ // 与落点指示共用插入位解析(含「越过中线插到下面」),否则提交位置与
745
+ // 用户看到的淡化卡位置差一位
746
+ const rawIndex = resolveInsertIndex(active, over)
747
+ const insertIndex =
748
+ rawIndex === null ? activeIndex : rawIndex
749
+ if (activeIndex !== -1 && insertIndex !== activeIndex) {
750
+ const items = [...columns[container]]
751
+ const [moved] = items.splice(activeIndex, 1)
752
+ items.splice(
753
+ insertIndex > activeIndex ? insertIndex - 1 : insertIndex,
754
+ 0,
755
+ moved
756
+ )
757
+ const newColumns = { ...columns, [container]: items }
758
+ setColumns(newColumns)
759
+ commitChange(newColumns, event, "item")
760
+ } else {
761
+ commitChange(columns, event, "item")
762
+ }
763
+ } else if (activeContainer && overContainer) {
764
+ // 跨列落位:拖拽期不再即时改写顺序,改为在这里一次性提交
765
+ const activeItems = columns[activeContainer]
766
+ const overItems = columns[overContainer]
767
+
768
+ const activeIndex = activeItems.findIndex(
769
+ (item: T) => getItemValue(item) === active.id
770
+ )
771
+ const overIndex = resolveInsertIndex(active, over)
772
+
773
+ if (activeIndex === -1 || overIndex === null) {
774
+ commitChange(columns, event, "item")
775
+ } else {
776
+ const newActiveItems = [...activeItems]
777
+ const newOverItems = [...overItems]
778
+ const [movedItem] = newActiveItems.splice(activeIndex, 1)
779
+ newOverItems.splice(overIndex, 0, movedItem)
780
+ const newColumns = {
781
+ ...columns,
782
+ [activeContainer]: newActiveItems,
783
+ [overContainer]: newOverItems,
784
+ }
785
+ setColumns(newColumns)
786
+ commitChange(newColumns, event, "item")
787
+ }
788
+ } else {
789
+ commitChange(columns, event, "item")
790
+ }
791
+ dragOriginRef.current = null
792
+ },
793
+ [
794
+ columnIds,
795
+ columns,
796
+ findContainer,
797
+ getItemValue,
798
+ isColumn,
799
+ setColumns,
800
+ onMove,
801
+ onDragEnd,
802
+ commitChange,
803
+ dropIndicatorStore,
804
+ resolveInsertIndex,
805
+ ]
806
+ )
807
+
808
+ const dataContextValue = useMemo(
809
+ () => ({
810
+ columns,
811
+ setColumns,
812
+ getItemId: getItemValue,
813
+ columnIds,
814
+ findContainer,
815
+ }),
816
+ [columns, setColumns, getItemValue, columnIds, findContainer]
817
+ )
818
+
819
+ const dragContextValue = useMemo(
820
+ () => ({ activeId, isColumn, modifiers }),
821
+ [activeId, isColumn, modifiers]
822
+ )
823
+
824
+ const defaultProps = {
825
+ "data-slot": "kanban",
826
+ "data-dragging": activeId !== null,
827
+ className: cn(activeId !== null && "cursor-grabbing!", className),
828
+ children,
829
+ }
830
+
831
+ return (
832
+ <KanbanDragContext.Provider value={dragContextValue}>
833
+ <KanbanDataContext.Provider value={dataContextValue}>
834
+ <KanbanDropIndicatorContext.Provider value={dropIndicatorStore}>
835
+ <DndContext
836
+ sensors={sensors}
837
+ modifiers={modifiers}
838
+ accessibility={accessibility}
839
+ measuring={MEASURING_CONFIG}
840
+ collisionDetection={collisionDetection}
841
+ onDragStart={handleDragStart}
842
+ onDragMove={updateDropIndicator}
843
+ onDragOver={updateDropIndicator}
844
+ onDragEnd={handleDragEnd}
845
+ onDragCancel={handleDragCancel}
846
+ >
847
+ {useRender({
848
+ defaultTagName: "div",
849
+ render,
850
+ props: mergeProps<"div">(defaultProps, props),
851
+ })}
852
+ </DndContext>
853
+ </KanbanDropIndicatorContext.Provider>
854
+ </KanbanDataContext.Provider>
855
+ </KanbanDragContext.Provider>
856
+ )
857
+ }
858
+
859
+ export type KanbanBoardProps = useRender.ComponentProps<"div">
860
+
861
+ function KanbanBoard({ className, render, ...props }: KanbanBoardProps) {
862
+ const { columnIds } = useContext(KanbanDataContext)
863
+
864
+ const defaultProps = {
865
+ "data-slot": "kanban-board",
866
+ className: cn(
867
+ "flex size-full items-stretch gap-3 overflow-x-auto overflow-y-hidden",
868
+ className
869
+ ),
870
+ children: props.children,
871
+ }
872
+
873
+ return (
874
+ <SortableContext items={columnIds} strategy={rectSortingStrategy}>
875
+ {useRender({
876
+ defaultTagName: "div",
877
+ render,
878
+ props: mergeProps<"div">(defaultProps, props),
879
+ })}
880
+ </SortableContext>
881
+ )
882
+ }
883
+
884
+ export interface KanbanColumnProps extends useRender.ComponentProps<"div"> {
885
+ value: string
886
+ disabled?: boolean
887
+ }
888
+
889
+ function KanbanColumn({
890
+ value,
891
+ className,
892
+ render,
893
+ disabled,
894
+ ...props
895
+ }: KanbanColumnProps) {
896
+ const isOverlay = useContext(IsOverlayContext)
897
+
898
+ const {
899
+ setNodeRef,
900
+ transform,
901
+ transition,
902
+ attributes,
903
+ listeners,
904
+ isDragging: isSortableDragging,
905
+ } = useSortable({
906
+ id: value,
907
+ disabled: disabled || isOverlay,
908
+ animateLayoutChanges,
909
+ })
910
+
911
+ // Hooks must run unconditionally; the derived value below is used only in the non-overlay branch.
912
+ const { activeId, isColumn } = useContext(KanbanDragContext)
913
+ const isColumnDragging = activeId ? isColumn(activeId) : false
914
+
915
+ const style = {
916
+ transition,
917
+ transform: CSS.Transform.toString(transform),
918
+ } as CSSProperties
919
+
920
+ const defaultProps = isOverlay
921
+ ? {
922
+ "data-slot": "kanban-column",
923
+ "data-value": value,
924
+ "data-dragging": true,
925
+ className: cn("group/kanban-column flex flex-col", className),
926
+ children: props.children,
927
+ }
928
+ : {
929
+ "data-slot": "kanban-column",
930
+ "data-value": value,
931
+ "data-dragging": isSortableDragging,
932
+ "data-disabled": disabled,
933
+ ref: setNodeRef,
934
+ style,
935
+ className: cn(
936
+ "group/kanban-column flex flex-col",
937
+ isSortableDragging && "opacity-50 z-50",
938
+ disabled && "opacity-50",
939
+ className
940
+ ),
941
+ children: props.children,
942
+ }
943
+
944
+ return (
945
+ <ColumnContext.Provider
946
+ value={
947
+ isOverlay
948
+ ? {
949
+ attributes: {} as DraggableAttributes,
950
+ listeners: undefined,
951
+ isDragging: true,
952
+ disabled: false,
953
+ }
954
+ : { attributes, listeners, isDragging: isColumnDragging, disabled }
955
+ }
956
+ >
957
+ {useRender({
958
+ defaultTagName: "div",
959
+ render,
960
+ props: mergeProps<"div">(defaultProps, props),
961
+ })}
962
+ </ColumnContext.Provider>
963
+ )
964
+ }
965
+
966
+ export interface KanbanColumnHandleProps extends useRender.ComponentProps<"div"> {
967
+ cursor?: boolean
968
+ }
969
+
970
+ function KanbanColumnHandle({
971
+ className,
972
+ render,
973
+ cursor = true,
974
+ ...props
975
+ }: KanbanColumnHandleProps) {
976
+ const { attributes, listeners, isDragging, disabled } =
977
+ useContext(ColumnContext)
978
+
979
+ const defaultProps = {
980
+ "data-slot": "kanban-column-handle",
981
+ "data-dragging": isDragging,
982
+ "data-disabled": disabled,
983
+ ...attributes,
984
+ ...listeners,
985
+ className: cn(
986
+ /* Keyboard focus shows it too. focus-within would also match the focus
987
+ a mousedown gives the handle, so it would stay shown after a drag. */
988
+ "opacity-0 transition-opacity group-hover/kanban-column:opacity-100 focus-visible:opacity-100 has-focus-visible:opacity-100",
989
+ cursor && (isDragging ? "cursor-grabbing!" : "cursor-grab!"),
990
+ className
991
+ ),
992
+ children: props.children,
993
+ }
994
+
995
+ return useRender({
996
+ defaultTagName: "div",
997
+ render,
998
+ props: mergeProps<"div">(defaultProps, props),
999
+ })
1000
+ }
1001
+
1002
+ export interface KanbanItemProps extends useRender.ComponentProps<"div"> {
1003
+ value: string
1004
+ disabled?: boolean
1005
+ }
1006
+
1007
+ function KanbanItem({
1008
+ value,
1009
+ className,
1010
+ render,
1011
+ disabled,
1012
+ ...props
1013
+ }: KanbanItemProps) {
1014
+ const isOverlay = useContext(IsOverlayContext)
1015
+
1016
+ const {
1017
+ setNodeRef,
1018
+ transform,
1019
+ transition,
1020
+ attributes,
1021
+ listeners,
1022
+ isDragging: isSortableDragging,
1023
+ } = useSortable({
1024
+ id: value,
1025
+ disabled: disabled || isOverlay,
1026
+ animateLayoutChanges,
1027
+ })
1028
+
1029
+ // 故意不消费 KanbanDragContext:拖拽激活时 activeId 会变,若每张卡都订阅,
1030
+ // 整板 200 张卡会同时重渲染(实测起手一帧 700ms:200 次 setProp + 108ms 样式重算 +
1031
+ // GC)。handle 的 grabbing 光标用 useSortable 本地的 isSortableDragging 即可,
1032
+ // 语义相同(只有被拖的那张卡才需要 grabbing)。
1033
+ const style = {
1034
+ transition,
1035
+ transform: CSS.Transform.toString(transform),
1036
+ } as CSSProperties
1037
+
1038
+ const defaultProps = isOverlay
1039
+ ? {
1040
+ "data-slot": "kanban-item",
1041
+ "data-value": value,
1042
+ "data-dragging": true,
1043
+ className: cn(className),
1044
+ children: props.children,
1045
+ }
1046
+ : {
1047
+ "data-slot": "kanban-item",
1048
+ "data-value": value,
1049
+ "data-dragging": isSortableDragging,
1050
+ "data-disabled": disabled,
1051
+ ref: setNodeRef,
1052
+ style,
1053
+ ...attributes,
1054
+ className: cn(
1055
+ isSortableDragging && "opacity-50 z-50",
1056
+ disabled && "opacity-50",
1057
+ className
1058
+ ),
1059
+ children: props.children,
1060
+ }
1061
+
1062
+ return (
1063
+ <ItemContext.Provider
1064
+ value={
1065
+ isOverlay
1066
+ ? { listeners: undefined, isDragging: true, disabled: false }
1067
+ : { listeners, isDragging: isSortableDragging, disabled }
1068
+ }
1069
+ >
1070
+ {useRender({
1071
+ defaultTagName: "div",
1072
+ render,
1073
+ props: mergeProps<"div">(defaultProps, props),
1074
+ })}
1075
+ </ItemContext.Provider>
1076
+ )
1077
+ }
1078
+
1079
+ export interface KanbanItemHandleProps extends useRender.ComponentProps<"div"> {
1080
+ cursor?: boolean
1081
+ }
1082
+
1083
+ function KanbanItemHandle({
1084
+ className,
1085
+ render,
1086
+ cursor = true,
1087
+ ...props
1088
+ }: KanbanItemHandleProps) {
1089
+ const { listeners, isDragging, disabled } = useContext(ItemContext)
1090
+
1091
+ const defaultProps = {
1092
+ "data-slot": "kanban-item-handle",
1093
+ "data-dragging": isDragging,
1094
+ "data-disabled": disabled,
1095
+ ...listeners,
1096
+ className: cn(
1097
+ cursor && (isDragging ? "cursor-grabbing!" : "cursor-grab!"),
1098
+ className
1099
+ ),
1100
+ children: props.children,
1101
+ }
1102
+
1103
+ return useRender({
1104
+ defaultTagName: "div",
1105
+ render,
1106
+ props: mergeProps<"div">(defaultProps, props),
1107
+ })
1108
+ }
1109
+
1110
+ export interface KanbanColumnContentProps extends useRender.ComponentProps<"div"> {
1111
+ value: string
1112
+ }
1113
+
1114
+ function KanbanColumnContent({
1115
+ value,
1116
+ className,
1117
+ render,
1118
+ ...props
1119
+ }: KanbanColumnContentProps) {
1120
+ const { columns, getItemId } = useContext(KanbanDataContext)
1121
+
1122
+ const items = columns[value]
1123
+ const itemIdsCache = useRef<{
1124
+ ids: string[]
1125
+ source: unknown
1126
+ getItemId: (item: unknown) => string
1127
+ } | null>(null)
1128
+
1129
+ const itemIds = useMemo(() => {
1130
+ if (!items) {
1131
+ throw new Error(
1132
+ `KanbanColumnContent: column "${value}" was not found in the Kanban value. ` +
1133
+ `Available columns: ${Object.keys(columns).join(", ") || "(none)"}.`
1134
+ )
1135
+ }
1136
+ const cached = itemIdsCache.current
1137
+ if (cached && cached.source === items && cached.getItemId === getItemId) {
1138
+ return cached.ids
1139
+ }
1140
+ const next = items.map(getItemId)
1141
+ // 列内 id 序列逐元素相同时复用旧数组:SortableContext 的 items 引用一变,
1142
+ // 该列所有 useSortable 都会重渲染并重新测量,拖拽期重建数组会让整板抖动。
1143
+ if (
1144
+ cached &&
1145
+ cached.ids.length === next.length &&
1146
+ cached.ids.every((id, index) => id === next[index])
1147
+ ) {
1148
+ itemIdsCache.current = { ids: cached.ids, source: items, getItemId }
1149
+ return cached.ids
1150
+ }
1151
+ itemIdsCache.current = { ids: next, source: items, getItemId }
1152
+ return next
1153
+ }, [columns, getItemId, items, value])
1154
+
1155
+ const defaultProps = {
1156
+ "data-slot": "kanban-column-content",
1157
+ className: cn("flex flex-col gap-2", className),
1158
+ children: props.children,
1159
+ }
1160
+
1161
+ return (
1162
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
1163
+ {useRender({
1164
+ defaultTagName: "div",
1165
+ render,
1166
+ props: mergeProps<"div">(defaultProps, props),
1167
+ })}
1168
+ </SortableContext>
1169
+ )
1170
+ }
1171
+
1172
+ export interface KanbanOverlayProps extends Omit<
1173
+ React.ComponentProps<typeof DragOverlay>,
1174
+ "children"
1175
+ > {
1176
+ children?:
1177
+ | ReactNode
1178
+ | ((params: {
1179
+ value: UniqueIdentifier
1180
+ variant: "column" | "item"
1181
+ }) => ReactNode)
1182
+ }
1183
+
1184
+ function KanbanOverlay({ children, className, ...props }: KanbanOverlayProps) {
1185
+ const { activeId, isColumn, modifiers } = useContext(KanbanDragContext)
1186
+ const mounted = useSyncExternalStore(
1187
+ subscribeToNothing,
1188
+ getIsMounted,
1189
+ getIsMountedOnServer
1190
+ )
1191
+
1192
+ const variant = activeId ? (isColumn(activeId) ? "column" : "item") : "item"
1193
+
1194
+ const content =
1195
+ activeId && children
1196
+ ? typeof children === "function"
1197
+ ? children({ value: activeId, variant })
1198
+ : children
1199
+ : null
1200
+
1201
+ if (!mounted) return null
1202
+
1203
+ return createPortal(
1204
+ <DragOverlay
1205
+ dropAnimation={dropAnimationConfig}
1206
+ modifiers={modifiers}
1207
+ className={cn("z-50", activeId && "cursor-grabbing", className)}
1208
+ {...props}
1209
+ >
1210
+ <IsOverlayContext.Provider value={true}>
1211
+ {content}
1212
+ </IsOverlayContext.Provider>
1213
+ </DragOverlay>,
1214
+ document.body
1215
+ )
1216
+ }
1217
+
1218
+ export {
1219
+ Kanban,
1220
+ KanbanBoard,
1221
+ KanbanColumn,
1222
+ KanbanColumnHandle,
1223
+ KanbanItem,
1224
+ KanbanItemHandle,
1225
+ KanbanColumnContent,
1226
+ KanbanOverlay,
1227
+ }