@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,1287 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type CSSProperties,
10
+ type Ref,
11
+ } from "react"
12
+ import {
13
+ EventCalendarViewContext,
14
+ useEventCalendar,
15
+ useEventCalendarDay,
16
+ useEventCalendarSelector,
17
+ useEventCalendarSettings,
18
+ useEventCalendarViewConfig,
19
+ useEventCalendarViewSettings,
20
+ useEventCalendarWeek,
21
+ } from "@/components/reui/event-calendar/event-calendar"
22
+ import {
23
+ useEventCalendarGestures,
24
+ wasRecentChipPress,
25
+ wasRecentDrag,
26
+ } from "@/components/reui/event-calendar/event-calendar-dnd"
27
+ import {
28
+ EVENT_CALENDAR_GHOST,
29
+ EVENT_CALENDAR_SLOT_DRAFT,
30
+ EventCalendarEvent,
31
+ } from "@/components/reui/event-calendar/event-calendar-event"
32
+ import {
33
+ getDayKey,
34
+ getRangeKey,
35
+ resolveOffDay,
36
+ toZoned,
37
+ zonedStartOfDay,
38
+ } from "@/components/reui/event-calendar/event-calendar-lib"
39
+ import type {
40
+ EventCalendarDateRange,
41
+ EventCalendarDragState,
42
+ EventCalendarEventId,
43
+ EventCalendarSegment,
44
+ } from "@/components/reui/event-calendar/event-calendar-types"
45
+ import { mergeProps } from "@base-ui/react/merge-props"
46
+ import { useRender } from "@base-ui/react/use-render"
47
+ import { addDays, format, getWeek } from "date-fns"
48
+
49
+ import { cn } from "cn"
50
+ import {
51
+ Popover,
52
+ PopoverContent,
53
+ PopoverTrigger,
54
+ } from "@/components/ui/popover"
55
+ import { ScrollArea } from "@/components/ui/scroll-area"
56
+ import { PlusIcon } from "lucide-react"
57
+
58
+ // Layout-effect on the client (measure before paint, no flash), plain effect on
59
+ // the server (never runs there) to avoid the SSR useLayoutEffect warning.
60
+ const useIsoLayoutEffect =
61
+ typeof window !== "undefined" ? useLayoutEffect : useEffect
62
+
63
+ // An occurrence key encodes the start instant and is also the chip's React key,
64
+ // so committing a move re-keys the chip: React remounts it and the browser
65
+ // drops focus to <body>. The chip that owns focus is recorded here so the cell
66
+ // rendering its replacement can hand focus back. Module scope because the drop
67
+ // can land in a different cell than the one the chip left, and only one element
68
+ // holds focus at a time anyway.
69
+ let focusedChip: {
70
+ node: HTMLElement
71
+ eventId: EventCalendarEventId
72
+ recurrenceIndex?: number
73
+ } | null = null
74
+
75
+ /** Give focus back to the recorded chip's replacement, if `root` renders it. */
76
+ function restoreChipFocus(
77
+ root: HTMLElement | null,
78
+ segments: EventCalendarSegment[]
79
+ ) {
80
+ const pending = focusedChip
81
+ // Only a chip removed WHILE focused needs help: a node still in the tree, or
82
+ // a focus that has already moved on by itself, is left alone.
83
+ if (!root || !pending || pending.node.isConnected) return
84
+ const active = document.activeElement
85
+ if (active && active !== document.body) return
86
+ const index = segments.findIndex(
87
+ (segment) =>
88
+ segment.occurrence.eventId === pending.eventId &&
89
+ segment.occurrence.recurrenceIndex === pending.recurrenceIndex
90
+ )
91
+ if (index < 0) return
92
+ const chip = root.querySelectorAll<HTMLElement>(
93
+ "[data-slot=event-calendar-event]"
94
+ )[index]
95
+ if (!chip) return
96
+ // cleared first: focus() re-records through the new chip's own onFocus
97
+ focusedChip = null
98
+ chip.focus()
99
+ }
100
+
101
+ interface EventCalendarMonthViewProps extends useRender.ComponentProps<"div"> {
102
+ maxEventsPerCell?: number | "auto"
103
+ }
104
+
105
+ function EventCalendarMonthView({
106
+ className,
107
+ render,
108
+ maxEventsPerCell,
109
+ ...props
110
+ }: EventCalendarMonthViewProps) {
111
+ const instance = useEventCalendar()
112
+ const settings = useEventCalendarSettings()
113
+ const viewConfig = useEventCalendarViewConfig()
114
+ const visibleRange = useEventCalendarSelector<
115
+ unknown,
116
+ EventCalendarDateRange
117
+ >((state) => state.visibleRange, {
118
+ isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),
119
+ })
120
+ const anchorDate = useEventCalendarSelector((state) => state.date)
121
+
122
+ const { effective } = useEventCalendarViewSettings()
123
+ const weeks = useMemo(() => {
124
+ const days: Date[] = []
125
+ let cursor = zonedStartOfDay(visibleRange.start, settings.timeZone)
126
+ while (cursor < visibleRange.end) {
127
+ days.push(cursor)
128
+ cursor = zonedStartOfDay(
129
+ addDays(toZoned(cursor, settings.timeZone), 1),
130
+ settings.timeZone
131
+ )
132
+ }
133
+ const rows: Date[][] = []
134
+ for (let i = 0; i < days.length; i += 7) rows.push(days.slice(i, i + 7))
135
+ if (effective.weekends) return rows
136
+ return rows.map((row) =>
137
+ row.filter(
138
+ (day) =>
139
+ !settings.weekendDays.includes(
140
+ toZoned(day, settings.timeZone).getDay()
141
+ )
142
+ )
143
+ )
144
+ }, [
145
+ visibleRange,
146
+ settings.timeZone,
147
+ settings.weekendDays,
148
+ effective.weekends,
149
+ ])
150
+
151
+ const headerDays = weeks[0] ?? []
152
+ const title = settings.i18n.functions.formatTitle("month", {
153
+ date: toZoned(anchorDate, settings.timeZone),
154
+ activeRange: instance.api.getActiveRange(),
155
+ visibleRange,
156
+ locale: settings.locale,
157
+ })
158
+
159
+ const gridTemplateColumns = `${effective.weekNumbers ? "var(--ec-week-number-w, 2.75rem) " : ""}repeat(${headerDays.length}, minmax(0, 1fr))`
160
+ const cap = maxEventsPerCell ?? viewConfig.maxEventsPerCell
161
+ const contained = viewConfig.scrollMode !== "page"
162
+
163
+ // "auto" fits as many event rows as the cell height allows and rolls the rest
164
+ // into "+N more". Only the contained mode gives a cell a bounded height to
165
+ // measure; page mode grows to fit, so "auto" there keeps the fixed fallback.
166
+ const autoFit = cap === "auto" && contained
167
+ // slotProbe resolves the event-row height (--ec-month-bar-h) to px, honoring
168
+ // the current font size and any consumer override; contentProbe is the first
169
+ // cell's flex-1 content area, whose height is the event space per cell.
170
+ const slotProbeRef = useRef<HTMLDivElement | null>(null)
171
+ const contentProbeRef = useRef<HTMLDivElement | null>(null)
172
+ const [autoCap, setAutoCap] = useState<number | null>(null)
173
+ const measureCap = useCallback(() => {
174
+ const content = contentProbeRef.current
175
+ const slot = slotProbeRef.current
176
+ if (!content || !slot) return
177
+ const laneH = slot.getBoundingClientRect().height
178
+ if (laneH <= 0) return
179
+ const cs = getComputedStyle(content)
180
+ const inner = content.clientHeight - (parseFloat(cs.paddingTop) || 0)
181
+ const gap = parseFloat(cs.rowGap) || 0
182
+ // N rows occupy N*laneH - gap (the last row has no trailing gap)
183
+ setAutoCap(Math.max(1, Math.floor((inner + gap) / laneH)))
184
+ }, [])
185
+ useIsoLayoutEffect(() => {
186
+ if (!autoFit) {
187
+ setAutoCap(null)
188
+ return
189
+ }
190
+ const content = contentProbeRef.current
191
+ if (!content || typeof ResizeObserver === "undefined") return
192
+ measureCap()
193
+ const observer = new ResizeObserver(measureCap)
194
+ observer.observe(content)
195
+ return () => observer.disconnect()
196
+ // re-observe the first cell after a re-layout (row count or month change)
197
+ }, [autoFit, measureCap, weeks.length, anchorDate])
198
+ const resolvedCap = cap === "auto" ? (autoFit ? (autoCap ?? 3) : 3) : cap
199
+
200
+ const defaultProps = {
201
+ "data-slot": "event-calendar-month-view",
202
+ "data-view": "month",
203
+ role: "grid",
204
+ "aria-label": title,
205
+ className: cn(
206
+ "flex flex-col border-t",
207
+ contained && "min-h-0 flex-1 overflow-hidden",
208
+ viewConfig.classNames?.monthView,
209
+ className
210
+ ),
211
+ children: (
212
+ <>
213
+ <div
214
+ role="row"
215
+ data-slot="event-calendar-month-header"
216
+ // @container scopes the narrow-label breakpoint to the header row
217
+ className={cn(
218
+ "@container grid border-b",
219
+ viewConfig.classNames?.monthHeader
220
+ )}
221
+ style={{ gridTemplateColumns }}
222
+ >
223
+ {effective.weekNumbers && (
224
+ <div
225
+ role="columnheader"
226
+ aria-hidden
227
+ className={cn(
228
+ "border-e px-2 py-1.5",
229
+ viewConfig.classNames?.weekNumber
230
+ )}
231
+ />
232
+ )}
233
+ {headerDays.map((day) => (
234
+ <div
235
+ key={day.getTime()}
236
+ role="columnheader"
237
+ className={cn(
238
+ "text-muted-foreground truncate px-2 py-1.5 font-medium",
239
+ viewConfig.classNames?.monthDayHeader
240
+ )}
241
+ >
242
+ {viewConfig.renderDayHeader?.({
243
+ day,
244
+ view: "month",
245
+ isToday:
246
+ getDayKey(day, settings.timeZone) ===
247
+ getDayKey(new Date(), settings.timeZone),
248
+ }) ?? (
249
+ <>
250
+ <span className="@max-[36rem]:hidden">
251
+ {format(
252
+ toZoned(day, settings.timeZone),
253
+ settings.i18n.formats.monthDayHeader,
254
+ { locale: settings.locale }
255
+ )}
256
+ </span>
257
+ <span className="hidden @max-[36rem]:inline">
258
+ {format(
259
+ toZoned(day, settings.timeZone),
260
+ settings.i18n.formats.monthDayHeaderNarrow,
261
+ { locale: settings.locale }
262
+ )}
263
+ </span>
264
+ </>
265
+ )}
266
+ </div>
267
+ ))}
268
+ </div>
269
+ <div
270
+ data-slot="event-calendar-month-body"
271
+ className={cn(
272
+ "grid",
273
+ contained && "min-h-0 flex-1",
274
+ viewConfig.classNames?.monthBody
275
+ )}
276
+ style={{
277
+ gridTemplateRows: contained
278
+ ? `repeat(${weeks.length}, minmax(0, 1fr))`
279
+ : `repeat(${weeks.length}, minmax(var(--ec-month-row-min-h, 8rem), auto))`,
280
+ }}
281
+ >
282
+ {weeks.map((week, rowIndex) => (
283
+ <EventCalendarMonthWeek
284
+ key={rowIndex}
285
+ week={week}
286
+ gridTemplateColumns={gridTemplateColumns}
287
+ showWeekNumber={effective.weekNumbers}
288
+ cap={resolvedCap}
289
+ autoFit={autoFit}
290
+ contentRef={rowIndex === 0 ? contentProbeRef : undefined}
291
+ />
292
+ ))}
293
+ </div>
294
+ {autoFit && (
295
+ <div
296
+ ref={slotProbeRef}
297
+ aria-hidden
298
+ className="pointer-events-none invisible absolute h-[var(--ec-month-bar-h,1.75rem)] w-0"
299
+ />
300
+ )}
301
+ </>
302
+ ),
303
+ }
304
+
305
+ return (
306
+ <EventCalendarViewContext.Provider value={{ view: "month" }}>
307
+ {useRender({
308
+ defaultTagName: "div",
309
+ render,
310
+ props: mergeProps<"div">(defaultProps, props),
311
+ })}
312
+ </EventCalendarViewContext.Provider>
313
+ )
314
+ }
315
+
316
+ /**
317
+ * One month week row. Multi-day / all-day events render as CONTINUOUS bars in
318
+ * an overlay grid that spans day columns (colStart -> colSpan) and stacks by
319
+ * lane; single-day timed events render inside each cell below the reserved bar
320
+ * lanes. This is what makes a cross-day event read as one whole block instead
321
+ * of a chip repeated per cell.
322
+ */
323
+ function EventCalendarMonthWeek({
324
+ week,
325
+ gridTemplateColumns,
326
+ showWeekNumber,
327
+ cap,
328
+ autoFit,
329
+ contentRef,
330
+ }: {
331
+ week: Date[]
332
+ gridTemplateColumns: string
333
+ showWeekNumber: boolean
334
+ cap: number
335
+ autoFit: boolean
336
+ /** Set on the first week only: forwarded to its first cell's content area so
337
+ * the view can measure the per-cell event height for "auto". */
338
+ contentRef?: Ref<HTMLDivElement>
339
+ }) {
340
+ const settings = useEventCalendarSettings()
341
+ const viewConfig = useEventCalendarViewConfig()
342
+ const { bars, rowStart } = useEventCalendarWeek(week[0])
343
+ const colOffset = showWeekNumber ? 1 : 0
344
+ const dayMs = 86400000
345
+ const rowStartMs = zonedStartOfDay(
346
+ rowStart ?? week[0],
347
+ settings.timeZone
348
+ ).getTime()
349
+ // Day offsets from the TRUE row start (0-6) for each visible column, so a
350
+ // weekends-hidden month still places bars on the right days.
351
+ const offsets = week.map((d) =>
352
+ Math.round(
353
+ (zonedStartOfDay(d, settings.timeZone).getTime() - rowStartMs) / dayMs
354
+ )
355
+ )
356
+ /** Clamp a day-offset span onto the visible columns; null = fully hidden. */
357
+ const gridPos = (colStart: number, colSpan: number) => {
358
+ let start = -1
359
+ let end = -1
360
+ for (let o = colStart; o < colStart + colSpan; o++) {
361
+ const col = offsets.indexOf(o)
362
+ if (col === -1) continue
363
+ if (start === -1) start = col
364
+ end = col
365
+ }
366
+ return start === -1 ? null : { col: start, span: end - start + 1 }
367
+ }
368
+ // bars fit within the cap; deeper lanes fall into each day's "+N more"
369
+ const visibleBars = bars.filter((b) => (b.lane ?? 0) < cap)
370
+ const covers = (b: EventCalendarSegment, dayOffset: number) =>
371
+ (b.colStart ?? 0) <= dayOffset &&
372
+ dayOffset < (b.colStart ?? 0) + (b.colSpan ?? 1)
373
+ // Occurrence keys of the bars hidden in each column (lane >= cap). Threaded to
374
+ // the cell so its "+N more" popover can list the hidden bars WITHOUT re-listing
375
+ // the visible ones (day buckets carry no lane, so the week row - which owns bar
376
+ // laning - is the only place that knows which bars are hidden).
377
+ const hiddenBarKeysByCol = week.map(
378
+ (_, col) =>
379
+ new Set(
380
+ bars
381
+ .filter((b) => (b.lane ?? 0) >= cap && covers(b, offsets[col]))
382
+ .map((b) => b.occurrence.key)
383
+ )
384
+ )
385
+
386
+ // Live move/resize ghost at the PROPOSED day span. Standardized treatment
387
+ // (EVENT_CALENDAR_GHOST): move = the event carried as a full clone, resize =
388
+ // the same clone with a dashed boundary; invalid adds destructive marking
389
+ // while the engine shows the not-allowed cursor + validation hint.
390
+ const dragGhost = useEventCalendarSelector<
391
+ unknown,
392
+ | (Pick<
393
+ EventCalendarDragState,
394
+ | "kind"
395
+ | "valid"
396
+ | "occurrence"
397
+ | "proposedStart"
398
+ | "proposedEnd"
399
+ | "proposedAllDay"
400
+ > & {
401
+ colStart: number
402
+ colSpan: number
403
+ isStart: boolean
404
+ isEnd: boolean
405
+ color?: string
406
+ })
407
+ | null
408
+ >(
409
+ (state) => {
410
+ const drag = state.drag
411
+ if (!drag) return null
412
+ const rowEndMs = rowStartMs + 7 * dayMs
413
+ const startDayMs = zonedStartOfDay(
414
+ drag.proposedStart,
415
+ settings.timeZone
416
+ ).getTime()
417
+ // exclusive end -> the last covered day
418
+ const lastDayMs = zonedStartOfDay(
419
+ new Date(drag.proposedEnd.getTime() - 1),
420
+ settings.timeZone
421
+ ).getTime()
422
+ if (startDayMs >= rowEndMs || lastDayMs < rowStartMs) return null
423
+ const startCol = Math.max(
424
+ 0,
425
+ Math.round((startDayMs - rowStartMs) / dayMs)
426
+ )
427
+ const endCol = Math.min(6, Math.round((lastDayMs - rowStartMs) / dayMs))
428
+ if (endCol < startCol) return null
429
+ return {
430
+ kind: drag.kind,
431
+ valid: drag.valid,
432
+ occurrence: drag.occurrence,
433
+ proposedStart: drag.proposedStart,
434
+ proposedEnd: drag.proposedEnd,
435
+ proposedAllDay: drag.proposedAllDay,
436
+ colStart: startCol,
437
+ colSpan: endCol - startCol + 1,
438
+ isStart: startDayMs >= rowStartMs,
439
+ isEnd: lastDayMs < rowEndMs,
440
+ color: drag.occurrence.event.color,
441
+ }
442
+ },
443
+ {
444
+ isEqual: (a, b) =>
445
+ a === b ||
446
+ (a !== null &&
447
+ b !== null &&
448
+ a.colStart === b.colStart &&
449
+ a.colSpan === b.colSpan &&
450
+ a.valid === b.valid &&
451
+ a.kind === b.kind &&
452
+ a.proposedAllDay === b.proposedAllDay &&
453
+ a.proposedStart.getTime() === b.proposedStart.getTime() &&
454
+ a.proposedEnd.getTime() === b.proposedEnd.getTime()),
455
+ }
456
+ )
457
+ const ghostPos = dragGhost
458
+ ? gridPos(dragGhost.colStart, dragGhost.colSpan)
459
+ : null
460
+ const ghostLane = dragGhost
461
+ ? (bars.find((b) => b.occurrence.key === dragGhost.occurrence.key)?.lane ??
462
+ 0)
463
+ : 0
464
+ // A single-day timed MOVE is indicated INLINE in the target cell (a
465
+ // placeholder at the time-sorted position, rendered by EventCalendarMonthCell)
466
+ // rather than as a bar in this overlay, so only bar drags and every resize
467
+ // render the overlay ghost. Bars = all-day or multi-day (span > 1 day).
468
+ const ghostIsBar =
469
+ !!dragGhost &&
470
+ (dragGhost.kind !== "move" ||
471
+ dragGhost.proposedAllDay ||
472
+ dragGhost.proposedEnd.getTime() - dragGhost.proposedStart.getTime() >
473
+ dayMs)
474
+
475
+ return (
476
+ <div
477
+ role="row"
478
+ data-slot="event-calendar-month-row"
479
+ className={cn(
480
+ "relative grid min-h-0 border-b last:border-b-0",
481
+ viewConfig.classNames?.monthRow
482
+ )}
483
+ style={{ gridTemplateColumns }}
484
+ >
485
+ {showWeekNumber && (
486
+ <div
487
+ role="rowheader"
488
+ data-slot="event-calendar-week-number"
489
+ className={cn(
490
+ "text-muted-foreground border-e px-2 pt-1 tabular-nums",
491
+ viewConfig.classNames?.weekNumber
492
+ )}
493
+ >
494
+ {settings.i18n.labels.week(
495
+ getWeek(toZoned(week[0], settings.timeZone), {
496
+ // locale supplies firstWeekContainsDate, so a de/ISO calendar
497
+ // numbers the year-boundary weeks its own way instead of falling
498
+ // back to US numbering; weekStartsOn stays explicit so the number
499
+ // keeps matching the rendered grid
500
+ locale: settings.locale,
501
+ weekStartsOn: settings.weekStartsOn,
502
+ })
503
+ )}
504
+ </div>
505
+ )}
506
+ {week.map((day, col) => (
507
+ <EventCalendarMonthCell
508
+ key={day.getTime()}
509
+ day={day}
510
+ cap={cap}
511
+ // Reserve lane space only for bars that pass through THIS cell, so a
512
+ // short multi-day event does not push down timed events in unrelated
513
+ // cells of the same row. Reserve down to the deepest covering bar
514
+ // lane so timed events always sit below every bar in their own cell.
515
+ reservedLanes={visibleBars.reduce(
516
+ (max, b) =>
517
+ covers(b, offsets[col]) ? Math.max(max, (b.lane ?? 0) + 1) : max,
518
+ 0
519
+ )}
520
+ hiddenBarKeys={hiddenBarKeysByCol[col]}
521
+ isLast={col === week.length - 1}
522
+ autoFit={autoFit}
523
+ contentRef={col === 0 ? contentRef : undefined}
524
+ />
525
+ ))}
526
+ {/* Continuous bar overlay: one element per bar, placed by grid-column so
527
+ a cross-day span is a single unbroken block. pointer-events pass
528
+ through the gaps to the cells below. NOT aria-hidden - these are the
529
+ real interactive bars. */}
530
+ {(visibleBars.length > 0 || (dragGhost && ghostPos && ghostIsBar)) && (
531
+ <div
532
+ data-slot="event-calendar-month-bar-overlay"
533
+ className={cn(
534
+ "pointer-events-none absolute inset-x-0 top-0 z-10 grid pt-1.5",
535
+ viewConfig.classNames?.monthBarOverlay
536
+ )}
537
+ style={{
538
+ gridTemplateColumns,
539
+ gridAutoRows: "var(--ec-month-bar-h, 1.75rem)",
540
+ }}
541
+ >
542
+ {visibleBars.map((bar) => {
543
+ const pos = gridPos(bar.colStart ?? 0, bar.colSpan ?? 1)
544
+ if (!pos) return null
545
+ return (
546
+ <div
547
+ key={bar.occurrence.key}
548
+ className={cn(
549
+ "pointer-events-auto min-w-0 px-1",
550
+ viewConfig.classNames?.monthBar
551
+ )}
552
+ style={{
553
+ gridColumn: `${colOffset + pos.col + 1} / span ${pos.span}`,
554
+ gridRow: (bar.lane ?? 0) + 1,
555
+ }}
556
+ >
557
+ {/* lane height minus the 2px inter-lane gap */}
558
+ <EventCalendarEvent
559
+ segment={bar}
560
+ className="h-[calc(var(--ec-month-bar-h,1.75rem)-0.125rem)]"
561
+ />
562
+ </div>
563
+ )
564
+ })}
565
+ {dragGhost && ghostPos && ghostIsBar && (
566
+ <div
567
+ aria-hidden
568
+ className={cn("min-w-0 px-1", viewConfig.classNames?.monthBar)}
569
+ style={{
570
+ gridColumn: `${colOffset + ghostPos.col + 1} / span ${ghostPos.span}`,
571
+ gridRow: ghostLane + 1,
572
+ }}
573
+ >
574
+ <div
575
+ data-slot="event-calendar-drag-ghost"
576
+ data-kind={dragGhost.kind}
577
+ data-drop-invalid={!dragGhost.valid || undefined}
578
+ className={cn(
579
+ "h-[calc(var(--ec-month-bar-h,1.75rem)-0.125rem)]",
580
+ dragGhost.kind === "move"
581
+ ? // faint drop placeholder only: the cursor-attached
582
+ // carry clone owns the visual during moves
583
+ cn(
584
+ EVENT_CALENDAR_GHOST.move,
585
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalid
586
+ )
587
+ : cn(
588
+ EVENT_CALENDAR_GHOST.resize,
589
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidResize
590
+ ),
591
+ viewConfig.classNames?.dragGhost
592
+ )}
593
+ style={
594
+ {
595
+ "--ec-event-color":
596
+ dragGhost.color ?? "var(--color-primary)",
597
+ } as CSSProperties
598
+ }
599
+ >
600
+ {dragGhost.kind !== "move" && (
601
+ <EventCalendarEvent
602
+ preview
603
+ segment={{
604
+ occurrence: {
605
+ ...dragGhost.occurrence,
606
+ start: dragGhost.proposedStart,
607
+ end: dragGhost.proposedEnd,
608
+ },
609
+ day: week[ghostPos.col] ?? week[0],
610
+ isStart: dragGhost.isStart,
611
+ isEnd: dragGhost.isEnd,
612
+ continuesBefore: !dragGhost.isStart,
613
+ continuesAfter: !dragGhost.isEnd,
614
+ }}
615
+ className={cn(
616
+ "h-full inset-ring-0",
617
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidContent
618
+ )}
619
+ />
620
+ )}
621
+ </div>
622
+ </div>
623
+ )}
624
+ </div>
625
+ )}
626
+ </div>
627
+ )
628
+ }
629
+
630
+ function EventCalendarMonthCell({
631
+ day,
632
+ cap,
633
+ reservedLanes,
634
+ hiddenBarKeys,
635
+ isLast,
636
+ autoFit,
637
+ contentRef,
638
+ }: {
639
+ day: Date
640
+ cap: number
641
+ reservedLanes: number
642
+ /** Occurrence keys of the bars hidden in THIS column (lane >= cap), from the
643
+ * week row. Lets the cell list hidden bars in its overflow popover without
644
+ * re-listing the bars already visible in the row overlay. */
645
+ hiddenBarKeys: Set<string>
646
+ /** Last column in the row - drops the right border so the grid's outer edge
647
+ * is owned by the container, not a doubled cell border. Passed explicitly
648
+ * because the bar overlay renders after the cells, so `:last-child` is
649
+ * unreliable on rows that have bars. */
650
+ isLast: boolean
651
+ /** When true, the "+N more" chip is treated as taking a row so the visible
652
+ * chips + indicator always fit the measured cell height. */
653
+ autoFit: boolean
654
+ /** Set on the first cell only: measured to derive the "auto" cap. */
655
+ contentRef?: Ref<HTMLDivElement>
656
+ }) {
657
+ const instance = useEventCalendar()
658
+ const settings = useEventCalendarSettings()
659
+ const viewConfig = useEventCalendarViewConfig()
660
+ const gestures = useEventCalendarGestures()
661
+ const { segments, isToday, isOutside } = useEventCalendarDay(day)
662
+
663
+ const dayStart = zonedStartOfDay(day, settings.timeZone)
664
+ const dayEnd = addDays(toZoned(dayStart, settings.timeZone), 1)
665
+ const { effective } = useEventCalendarViewSettings()
666
+ const isOff = resolveOffDay(
667
+ day,
668
+ settings.timeZone,
669
+ effective.offDays
670
+ ? typeof viewConfig.offDays === "object"
671
+ ? viewConfig.offDays
672
+ : true
673
+ : false,
674
+ settings.weekendDays
675
+ )
676
+ const offClassName =
677
+ (typeof viewConfig.offDays === "object" && viewConfig.offDays.className) ||
678
+ "bg-muted/25"
679
+
680
+ const isDropTarget = useEventCalendarSelector<
681
+ unknown,
682
+ "valid" | "invalid" | null
683
+ >((state) => {
684
+ const drag = state.drag
685
+ if (!drag) return null
686
+ const covered = drag.proposedStart < dayEnd && drag.proposedEnd > dayStart
687
+ if (!covered) return null
688
+ return drag.valid ? "valid" : "invalid"
689
+ })
690
+ // Hide hover affordances mid-gesture: the only intent is the drop target.
691
+ // Gated on the one thing that reads it: a plain global boolean flips for all
692
+ // 42 cells the moment a gesture starts and again when it ends, which is pure
693
+ // waste in the default configuration where no add button renders.
694
+ const isInteracting = useEventCalendarSelector<unknown, boolean>((state) =>
695
+ viewConfig.showDayAddButton
696
+ ? state.drag !== null || state.slotDraft !== null
697
+ : false
698
+ )
699
+ const inDraft = useEventCalendarSelector<
700
+ unknown,
701
+ { isStart: boolean; isEnd: boolean } | null
702
+ >(
703
+ (state) => {
704
+ const draft = state.slotDraft
705
+ if (!draft || !draft.allDay) return null
706
+ if (draft.start >= dayEnd || draft.end <= dayStart) return null
707
+ return {
708
+ isStart: draft.start >= dayStart,
709
+ isEnd: draft.end <= dayEnd,
710
+ }
711
+ },
712
+ {
713
+ isEqual: (a, b) =>
714
+ a === b ||
715
+ (a !== null &&
716
+ b !== null &&
717
+ a.isStart === b.isStart &&
718
+ a.isEnd === b.isEnd),
719
+ }
720
+ )
721
+
722
+ // Month drag-create selects whole DAYS, so the readout is a date range rather
723
+ // than a time range. Held separately from `inDraft` above, which only knows
724
+ // whether this cell is an edge of the selection, not what the selection is.
725
+ const draftRange = useEventCalendarSelector<
726
+ unknown,
727
+ { start: Date; end: Date } | null
728
+ >(
729
+ (state) => {
730
+ const draft = state.slotDraft
731
+ if (!draft || !draft.allDay) return null
732
+ return { start: draft.start, end: draft.end }
733
+ },
734
+ {
735
+ isEqual: (a, b) =>
736
+ a === b ||
737
+ (a !== null &&
738
+ b !== null &&
739
+ a.start.getTime() === b.start.getTime() &&
740
+ a.end.getTime() === b.end.getTime()),
741
+ }
742
+ )
743
+ // A single-day timed MOVE landing on THIS day: expose the proposed
744
+ // minute-of-day (+ color/validity) so the cell can render a drop placeholder
745
+ // at the correct time-sorted position, instead of the overlay marking a bar
746
+ // over the first chip. Skipped for bars (they keep the overlay ghost) and for
747
+ // a no-op move back onto the event's own day (the dimmed source already marks
748
+ // the spot).
749
+ const inlineDrop = useEventCalendarSelector<
750
+ unknown,
751
+ { min: number; valid: boolean; color?: string } | null
752
+ >(
753
+ (state) => {
754
+ const drag = state.drag
755
+ if (!drag || drag.kind !== "move" || drag.proposedAllDay) return null
756
+ if (drag.proposedEnd.getTime() - drag.proposedStart.getTime() > 86400000)
757
+ return null
758
+ const dropDayMs = zonedStartOfDay(
759
+ drag.proposedStart,
760
+ settings.timeZone
761
+ ).getTime()
762
+ if (dropDayMs !== dayStart.getTime()) return null
763
+ if (
764
+ zonedStartOfDay(drag.occurrence.start, settings.timeZone).getTime() ===
765
+ dayStart.getTime()
766
+ )
767
+ return null
768
+ return {
769
+ min: (drag.proposedStart.getTime() - dropDayMs) / 60000,
770
+ valid: drag.valid,
771
+ color: drag.occurrence.event.color,
772
+ }
773
+ },
774
+ {
775
+ isEqual: (a, b) =>
776
+ a === b ||
777
+ (a !== null &&
778
+ b !== null &&
779
+ a.min === b.min &&
780
+ a.valid === b.valid &&
781
+ a.color === b.color),
782
+ }
783
+ )
784
+
785
+ // Bars (allDay/multi-day) are drawn by the week-row overlay; the cell renders
786
+ // only single-day timed events, below the reserved bar lanes.
787
+ const extraHidden = hiddenBarKeys.size
788
+ const hiddenBarSegs = segments.allDay.filter((s) =>
789
+ hiddenBarKeys.has(s.occurrence.key)
790
+ )
791
+ const m = segments.timed.length
792
+ const timedSlots = Math.max(0, cap - reservedLanes)
793
+
794
+ // Static (at-rest) split: what the cell shows with no drag, and - crucially -
795
+ // what the "+N more" popover lists. The popover carries ONLY the hidden events
796
+ // (hidden bars + timed past the cap), never the chips already visible in the
797
+ // cell, so it never duplicates them. autoFit gives up one timed row to the
798
+ // "+N more" indicator so the visible chips fit the clipped cell height.
799
+ const staticOverflow = extraHidden > 0 || m > timedSlots
800
+ const staticShown =
801
+ autoFit && staticOverflow ? Math.max(0, timedSlots - 1) : timedSlots
802
+ const overflowSegments = [
803
+ ...hiddenBarSegs,
804
+ ...segments.timed.slice(staticShown),
805
+ ]
806
+
807
+ // Live split: while a timed chip is dragged onto this day, render the day
808
+ // exactly as it will look AFTER the drop. The dragged chip is a phantom in the
809
+ // time-sorted order and is shown as the placeholder - inline where it lands,
810
+ // or ON the "+N more" indicator when it lands in the overflow bucket (so the
811
+ // user sees the drop will push it into "more"). The "+N more" count always
812
+ // reflects the post-drop hidden total.
813
+ let visibleTimed: EventCalendarSegment[]
814
+ let overflowCount: number
815
+ let placeholderIndex: number
816
+ let placeholderAtMore: boolean
817
+ if (!inlineDrop) {
818
+ visibleTimed = segments.timed.slice(0, staticShown)
819
+ overflowCount = overflowSegments.length
820
+ placeholderIndex = -1
821
+ placeholderAtMore = false
822
+ } else {
823
+ // rank of the dragged chip in the resulting time-sorted list (chips are
824
+ // time-ordered, so this is the count starting at or before its time)
825
+ const insertRank = segments.timed.filter(
826
+ (s) => (s.startMin ?? 0) <= inlineDrop.min
827
+ ).length
828
+ const dropOverflow = extraHidden > 0 || m + 1 > timedSlots
829
+ // rows for timed items INCLUDING the phantom, before the "+N more" row
830
+ const vis = !dropOverflow
831
+ ? m + 1
832
+ : autoFit
833
+ ? Math.max(0, timedSlots - 1)
834
+ : timedSlots
835
+ placeholderAtMore = insertRank >= vis
836
+ visibleTimed = placeholderAtMore
837
+ ? segments.timed.slice(0, vis)
838
+ : segments.timed.slice(0, Math.max(0, vis - 1))
839
+ placeholderIndex = placeholderAtMore ? -1 : insertRank
840
+ overflowCount =
841
+ extraHidden + (m - visibleTimed.length) + (placeholderAtMore ? 1 : 0)
842
+ }
843
+
844
+ // No dep array: the replacement for a chip that lost focus to a commit can
845
+ // appear on any re-render of any cell, and a cell with nothing to restore
846
+ // bails after two comparisons.
847
+ const rootRef = useRef<HTMLDivElement | null>(null)
848
+ useIsoLayoutEffect(() => {
849
+ restoreChipFocus(rootRef.current, visibleTimed)
850
+ })
851
+
852
+ // Faint dashed drop placeholder, tinted to the dragged event's color, echoing
853
+ // the move ghost (EVENT_CALENDAR_GHOST.move); one chip-height tall so chips
854
+ // shift by exactly one row when it is inserted.
855
+ const dropPlaceholder = inlineDrop ? (
856
+ <div
857
+ key="ec-drop-placeholder"
858
+ aria-hidden
859
+ data-slot="event-calendar-drop-placeholder"
860
+ data-drop-invalid={!inlineDrop.valid || undefined}
861
+ className={cn(
862
+ "shrink-0 rounded-sm border border-dashed",
863
+ inlineDrop.valid
864
+ ? "border-(--ec-event-color)/50 bg-(--ec-event-color)/8"
865
+ : "border-destructive/70 bg-destructive/10",
866
+ viewConfig.classNames?.dragGhost
867
+ )}
868
+ style={
869
+ {
870
+ "--ec-event-color": inlineDrop.color ?? "var(--color-primary)",
871
+ height: "calc(var(--ec-month-bar-h, 1.75rem) - 0.125rem)",
872
+ } as CSSProperties
873
+ }
874
+ />
875
+ ) : null
876
+
877
+ const defaultContent = (
878
+ <>
879
+ <div
880
+ ref={contentRef}
881
+ className={cn(
882
+ // gap-0.5 pairs with the 0.125rem subtraction in the lane spacer
883
+ // below; changing the gap requires renderMonthCell
884
+ // px-1 matches the all-day bar wrapper inset so single-day chips and
885
+ // multi-day bars line up on the same left/right edge in a cell
886
+ "flex min-h-0 flex-1 flex-col gap-0.5 overflow-hidden px-1 pt-1.5",
887
+ viewConfig.classNames?.monthCellContent
888
+ )}
889
+ >
890
+ {reservedLanes > 0 && (
891
+ <div
892
+ aria-hidden
893
+ className="shrink-0"
894
+ style={{
895
+ // lane height already carries the 2px inter-lane gap; subtract
896
+ // it so spacer + the column's own gap-0.5 = the SAME 2px rhythm
897
+ // between the last bar and the first timed chip
898
+ height: `calc(${reservedLanes} * var(--ec-month-bar-h, 1.75rem) - 0.125rem)`,
899
+ }}
900
+ />
901
+ )}
902
+ {visibleTimed.flatMap((segment, i) => {
903
+ const chip = (
904
+ <EventCalendarEvent
905
+ key={segment.occurrence.key}
906
+ segment={segment}
907
+ // Remember the chip holding focus so a commit that re-keys it can
908
+ // hand focus back to the remounted one (see restoreChipFocus)
909
+ onFocus={(e) => {
910
+ focusedChip = {
911
+ node: e.currentTarget,
912
+ eventId: segment.occurrence.eventId,
913
+ recurrenceIndex: segment.occurrence.recurrenceIndex,
914
+ }
915
+ }}
916
+ // A chip still in the tree lost focus on its own, so there is
917
+ // nothing to restore; only a blur from the remount is kept.
918
+ onBlur={(e) => {
919
+ if (e.currentTarget.isConnected) focusedChip = null
920
+ }}
921
+ // Hold a fixed height like the all-day lane above; without this
922
+ // the chip flex-shrinks to whatever room the cell has left, so
923
+ // cells with a reserved bar lane or a second chip render shorter
924
+ // chips.
925
+ className="shrink-0"
926
+ />
927
+ )
928
+ return i === placeholderIndex ? [dropPlaceholder, chip] : [chip]
929
+ })}
930
+ {placeholderIndex >= 0 &&
931
+ placeholderIndex >= visibleTimed.length &&
932
+ dropPlaceholder}
933
+ {overflowCount > 0 && (
934
+ <EventCalendarMoreIndicator
935
+ day={day}
936
+ count={overflowCount}
937
+ segments={overflowSegments}
938
+ dropInto={
939
+ placeholderAtMore && inlineDrop
940
+ ? { color: inlineDrop.color, valid: inlineDrop.valid }
941
+ : undefined
942
+ }
943
+ />
944
+ )}
945
+ </div>
946
+ {/* Day number + add affordance, bottom-right (Notion-style) */}
947
+ <div
948
+ className={cn(
949
+ "flex items-center justify-end gap-1 px-2 pb-1.5",
950
+ viewConfig.classNames?.monthCellFooter
951
+ )}
952
+ >
953
+ {viewConfig.showDayAddButton && !isInteracting && (
954
+ <button
955
+ type="button"
956
+ data-slot="event-calendar-day-add"
957
+ aria-label={settings.i18n.labels.addEvent}
958
+ // a different icon/markup is a renderMonthCell job
959
+ className={cn(
960
+ "bg-primary text-primary-foreground flex size-5 cursor-pointer items-center justify-center rounded-sm opacity-0 transition-opacity group-hover/ec-cell:opacity-100 focus-visible:opacity-100",
961
+ viewConfig.classNames?.dayAddButton
962
+ )}
963
+ onClick={(e) => {
964
+ e.stopPropagation()
965
+ settings.onSlotClick?.(
966
+ { date: day, allDay: true, view: "month" },
967
+ e
968
+ )
969
+ }}
970
+ >
971
+ <PlusIcon className="size-3.5" aria-hidden="true" />
972
+ </button>
973
+ )}
974
+ <span
975
+ data-slot="event-calendar-month-day-number"
976
+ className={cn(
977
+ "flex size-5 items-center justify-center rounded-full",
978
+ isOutside && "text-muted-foreground",
979
+ // the filled circle already marks today; keep the number the same
980
+ // weight/size as the other days so it does not read as larger
981
+ // Same font-size as every other day; a lighter weight cancels the
982
+ // way white digits on the filled circle read bolder/larger than the
983
+ // dark-on-light numbers around them.
984
+ isToday && "bg-primary text-primary-foreground font-light",
985
+ viewConfig.classNames?.monthDayNumber
986
+ )}
987
+ >
988
+ {format(
989
+ toZoned(day, settings.timeZone),
990
+ settings.i18n.formats.monthCellDay,
991
+ { locale: settings.locale }
992
+ )}
993
+ </span>
994
+ </div>
995
+ </>
996
+ )
997
+
998
+ const content =
999
+ viewConfig.renderMonthCell?.({
1000
+ day,
1001
+ segments,
1002
+ isToday,
1003
+ isOutside,
1004
+ overflowCount,
1005
+ defaultContent,
1006
+ }) ?? defaultContent
1007
+
1008
+ return (
1009
+ <div
1010
+ ref={rootRef}
1011
+ role="gridcell"
1012
+ data-slot="event-calendar-month-cell"
1013
+ data-today={isToday || undefined}
1014
+ data-outside={isOutside || undefined}
1015
+ data-weekend={
1016
+ settings.weekendDays.includes(
1017
+ toZoned(day, settings.timeZone).getDay()
1018
+ ) || undefined
1019
+ }
1020
+ data-ec-day={dayStart.getTime()}
1021
+ data-drop-target={isDropTarget ?? undefined}
1022
+ data-off={isOff || undefined}
1023
+ data-draft={inDraft ? "" : undefined}
1024
+ aria-label={format(
1025
+ toZoned(day, settings.timeZone),
1026
+ settings.i18n.formats.monthCellAriaLabel,
1027
+ { locale: settings.locale }
1028
+ )}
1029
+ className={cn(
1030
+ "group/ec-cell relative flex min-h-0 min-w-0 flex-col overflow-hidden",
1031
+ !isLast && "border-e",
1032
+ isOutside && !settings.showOutsideDays && "invisible",
1033
+ isOff && offClassName,
1034
+ isToday &&
1035
+ cn(
1036
+ "bg-primary/3 border-b-primary/40 relative border-b-2",
1037
+ viewConfig.todayClassName
1038
+ ),
1039
+ viewConfig.dayClassName?.(day),
1040
+ // No drop-target bg fill on move/resize - the dragged bar + not-allowed
1041
+ // cursor carry the feedback; a cell-wide color wash is too distracting.
1042
+ // data-drop-target stays as an opt-in styling hook.
1043
+ inDraft && EVENT_CALENDAR_SLOT_DRAFT.surface,
1044
+ viewConfig.classNames?.monthCell
1045
+ )}
1046
+ onPointerDown={(e) => {
1047
+ const target = e.target as HTMLElement
1048
+ if (target.closest("[data-slot=event-calendar-event]")) return
1049
+ if (target.closest("[data-slot=event-calendar-more]")) return
1050
+ gestures.beginCreate(e, day, true)
1051
+ }}
1052
+ onClick={(e) => {
1053
+ if (wasRecentDrag() || wasRecentChipPress()) return
1054
+ settings.onSlotClick?.({ date: day, allDay: true, view: "month" }, e)
1055
+ }}
1056
+ >
1057
+ {inDraft && (
1058
+ <span
1059
+ aria-hidden
1060
+ data-slot="event-calendar-slot-draft"
1061
+ className={cn(
1062
+ "pointer-events-none absolute inset-0 z-10",
1063
+ EVENT_CALENDAR_SLOT_DRAFT.segment,
1064
+ inDraft.isStart && EVENT_CALENDAR_SLOT_DRAFT.segmentStart,
1065
+ inDraft.isEnd && EVENT_CALENDAR_SLOT_DRAFT.segmentEnd,
1066
+ viewConfig.classNames?.slotDraft
1067
+ )}
1068
+ >
1069
+ {/* Date-range readout, the month-view counterpart of the time-grid
1070
+ one. Rendered only on the FIRST cell of the selection, otherwise
1071
+ a three-day drag would repeat the same label three times. Pinned
1072
+ to the bottom because the day number owns the top of the cell,
1073
+ and given its own surface so it stays legible over event chips. */}
1074
+ {inDraft.isStart && draftRange && (
1075
+ <span
1076
+ className={cn(
1077
+ "absolute inset-x-1 bottom-1",
1078
+ EVENT_CALENDAR_SLOT_DRAFT.label
1079
+ )}
1080
+ >
1081
+ {settings.i18n.functions.formatDayRange(
1082
+ {
1083
+ start: toZoned(draftRange.start, settings.timeZone),
1084
+ end: toZoned(draftRange.end, settings.timeZone),
1085
+ },
1086
+ { locale: settings.locale }
1087
+ )}
1088
+ </span>
1089
+ )}
1090
+ </span>
1091
+ )}
1092
+ {content}
1093
+ </div>
1094
+ )
1095
+ }
1096
+
1097
+ interface EventCalendarMoreIndicatorProps {
1098
+ day: Date
1099
+ count: number
1100
+ /** The OVERFLOW (hidden) segments for this day - bars first, then timed. The
1101
+ * popover lists only these, never the chips already visible in the cell. */
1102
+ segments: EventCalendarSegment[]
1103
+ /** Set while a timed chip is dragged and will land in THIS overflow bucket:
1104
+ * the indicator itself becomes the drop placeholder (dashed, event-tinted) so
1105
+ * it reads as "the chip joins the +N more list". */
1106
+ dropInto?: { color?: string; valid: boolean }
1107
+ }
1108
+
1109
+ /**
1110
+ * "+N more" trigger opening a popover with the day's full event list.
1111
+ * onMoreClick returning false suppresses the built-in popover.
1112
+ */
1113
+ function EventCalendarMoreIndicator({
1114
+ day,
1115
+ count,
1116
+ segments,
1117
+ dropInto,
1118
+ }: EventCalendarMoreIndicatorProps) {
1119
+ const settings = useEventCalendarSettings()
1120
+ const viewConfig = useEventCalendarViewConfig()
1121
+ const [open, setOpen] = useState(false)
1122
+ const headerId = useId()
1123
+
1124
+ // Grabbing a chip from this list starts a drag; close the popover so it does
1125
+ // not sit over the drop target while the event is carried to another day.
1126
+ const isDragging = useEventCalendarSelector<unknown, boolean>(
1127
+ (state) => state.drag !== null
1128
+ )
1129
+ useEffect(() => {
1130
+ if (isDragging) setOpen(false)
1131
+ }, [isDragging])
1132
+
1133
+ return (
1134
+ <Popover open={open} onOpenChange={setOpen}>
1135
+ <PopoverTrigger
1136
+ data-slot="event-calendar-more"
1137
+ data-drop-into={dropInto ? "" : undefined}
1138
+ data-drop-invalid={dropInto && !dropInto.valid ? "" : undefined}
1139
+ className={cn(
1140
+ "text-muted-foreground hover:text-foreground cursor-pointer truncate rounded-sm px-1.5 text-start",
1141
+ // While a dragged chip will land in this overflow bucket, the "+N more"
1142
+ // link itself becomes the drop placeholder: dashed, event-tinted, one
1143
+ // chip tall, so the drop target is unmistakable.
1144
+ dropInto &&
1145
+ cn(
1146
+ "flex shrink-0 items-center border border-dashed",
1147
+ dropInto.valid
1148
+ ? "text-foreground border-(--ec-event-color)/50 bg-(--ec-event-color)/8"
1149
+ : "border-destructive/70 bg-destructive/10 text-destructive"
1150
+ ),
1151
+ viewConfig.classNames?.moreIndicator
1152
+ )}
1153
+ style={
1154
+ dropInto
1155
+ ? ({
1156
+ "--ec-event-color": dropInto.color ?? "var(--color-primary)",
1157
+ height: "calc(var(--ec-month-bar-h, 1.75rem) - 0.125rem)",
1158
+ } as CSSProperties)
1159
+ : undefined
1160
+ }
1161
+ onClick={(e: React.MouseEvent) => {
1162
+ e.stopPropagation()
1163
+ const verdict = settings.onMoreClick?.(
1164
+ day,
1165
+ segments.map((segment) => segment.occurrence),
1166
+ e
1167
+ )
1168
+ if (verdict === false) {
1169
+ e.preventDefault()
1170
+ setOpen(false)
1171
+ }
1172
+ }}
1173
+ >
1174
+ {viewConfig.renderMoreIndicator?.({ day, count, segments }) ??
1175
+ settings.i18n.labels.more(count)}
1176
+ </PopoverTrigger>
1177
+ <PopoverContent
1178
+ data-slot="event-calendar-more-popover"
1179
+ align={viewConfig.morePopoverAlign}
1180
+ // The popover is a dialog, so it needs a name. The built-in body already
1181
+ // renders the day header this list belongs to, so point at that; a
1182
+ // consumer body has no header to point at and gets the same formatted
1183
+ // day as a label instead.
1184
+ aria-labelledby={viewConfig.renderMoreContent ? undefined : headerId}
1185
+ aria-label={
1186
+ viewConfig.renderMoreContent
1187
+ ? format(
1188
+ toZoned(day, settings.timeZone),
1189
+ settings.i18n.formats.moreDayHeader,
1190
+ { locale: settings.locale }
1191
+ )
1192
+ : undefined
1193
+ }
1194
+ // PopoverContent is unlayered (flex-col gap-4 p-4); override with !.
1195
+ // text-xs re-establishes the calendar's base type here because this
1196
+ // content is portaled out of the root subtree and cannot inherit it.
1197
+ className={cn(
1198
+ "w-64 gap-1! p-2! text-xs",
1199
+ viewConfig.classNames?.morePopover
1200
+ )}
1201
+ onClick={(e: React.MouseEvent) => e.stopPropagation()}
1202
+ >
1203
+ {viewConfig.renderMoreContent ? (
1204
+ viewConfig.renderMoreContent({
1205
+ day,
1206
+ segments,
1207
+ close: () => setOpen(false),
1208
+ })
1209
+ ) : (
1210
+ <EventCalendarMoreDefaultContent
1211
+ day={day}
1212
+ segments={segments}
1213
+ headerId={headerId}
1214
+ />
1215
+ )}
1216
+ </PopoverContent>
1217
+ </Popover>
1218
+ )
1219
+ }
1220
+
1221
+ /** Built-in "+N more" popover body: day header + the day's chips. */
1222
+ function EventCalendarMoreDefaultContent({
1223
+ day,
1224
+ segments,
1225
+ headerId,
1226
+ }: {
1227
+ day: Date
1228
+ segments: EventCalendarSegment[]
1229
+ /** Names the popover dialog: the header IS the list's accessible name. */
1230
+ headerId?: string
1231
+ }) {
1232
+ const settings = useEventCalendarSettings()
1233
+ const viewConfig = useEventCalendarViewConfig()
1234
+ return (
1235
+ <>
1236
+ <div
1237
+ id={headerId}
1238
+ className={cn(
1239
+ "text-muted-foreground px-1 py-1 text-xs font-medium",
1240
+ viewConfig.classNames?.morePopoverHeader
1241
+ )}
1242
+ >
1243
+ {format(
1244
+ toZoned(day, settings.timeZone),
1245
+ settings.i18n.formats.moreDayHeader,
1246
+ { locale: settings.locale }
1247
+ )}
1248
+ </div>
1249
+ {/* The scroll region breaks out of the popover's right padding (-me-2)
1250
+ so the scrollbar sits flush in the gutter; the list then pads itself
1251
+ back (ps-1 aligns with the header, pe-4 clears the ~10px bar with a
1252
+ gap) and adds py-1 so the first/last focus ring is not clipped by
1253
+ the overflow. Layout is identical with or without a scrollbar. */}
1254
+ {viewConfig.scrollbars === "native" ? (
1255
+ <div
1256
+ data-ec-native-scroll=""
1257
+ className="-me-2 max-h-(--ec-more-max-height,16rem) min-h-0 overflow-y-auto"
1258
+ >
1259
+ <div className="flex flex-col gap-1 py-1 ps-1 pe-4">
1260
+ {segments.map((segment) => (
1261
+ <EventCalendarEvent
1262
+ key={segment.occurrence.key}
1263
+ segment={segment}
1264
+ className="py-0.5"
1265
+ />
1266
+ ))}
1267
+ </div>
1268
+ </div>
1269
+ ) : (
1270
+ <ScrollArea className="-me-2 min-h-0 **:data-[slot=scroll-area-viewport]:max-h-(--ec-more-max-height,16rem)">
1271
+ <div className="flex flex-col gap-1 py-1 ps-1 pe-4">
1272
+ {segments.map((segment) => (
1273
+ <EventCalendarEvent
1274
+ key={segment.occurrence.key}
1275
+ segment={segment}
1276
+ className="py-0.5"
1277
+ />
1278
+ ))}
1279
+ </div>
1280
+ </ScrollArea>
1281
+ )}
1282
+ </>
1283
+ )
1284
+ }
1285
+
1286
+ export { EventCalendarMonthView, EventCalendarMoreIndicator }
1287
+ export type { EventCalendarMonthViewProps, EventCalendarMoreIndicatorProps }