@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,1366 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ type CSSProperties,
7
+ type ReactNode,
8
+ } from "react"
9
+ import {
10
+ EventCalendarViewContext,
11
+ useEventCalendar,
12
+ useEventCalendarDay,
13
+ useEventCalendarSelector,
14
+ useEventCalendarSettings,
15
+ useEventCalendarViewConfig,
16
+ useEventCalendarViewContext,
17
+ useEventCalendarViewSettings,
18
+ } from "@/components/reui/event-calendar/event-calendar"
19
+ import {
20
+ useEventCalendarGestures,
21
+ wasRecentChipPress,
22
+ wasRecentDrag,
23
+ } from "@/components/reui/event-calendar/event-calendar-dnd"
24
+ import {
25
+ EVENT_CALENDAR_GHOST,
26
+ EVENT_CALENDAR_SLOT_DRAFT,
27
+ EventCalendarEvent,
28
+ } from "@/components/reui/event-calendar/event-calendar-event"
29
+ import {
30
+ getDayKey,
31
+ getDayTotalMinutes,
32
+ getRangeKey,
33
+ packTimedSegments,
34
+ resolveOffDay,
35
+ snapMinutes,
36
+ toZoned,
37
+ zonedStartOfDay,
38
+ } from "@/components/reui/event-calendar/event-calendar-lib"
39
+ import type {
40
+ CalendarView,
41
+ EventCalendarDateRange,
42
+ EventCalendarDragState,
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, addMinutes, differenceInMinutes, format } from "date-fns"
48
+
49
+ import { cn } from "cn"
50
+ import { ScrollArea } from "@/components/ui/scroll-area"
51
+
52
+ /** Current time, refreshed on an interval and on tab focus. */
53
+ function useNow(intervalMs = 30_000): Date {
54
+ const [now, setNow] = useState(() => new Date())
55
+ useEffect(() => {
56
+ const tick = () => setNow(new Date())
57
+ const id = setInterval(tick, intervalMs)
58
+ document.addEventListener("visibilitychange", tick)
59
+ window.addEventListener("focus", tick)
60
+ return () => {
61
+ clearInterval(id)
62
+ document.removeEventListener("visibilitychange", tick)
63
+ window.removeEventListener("focus", tick)
64
+ }
65
+ }, [intervalMs])
66
+ return now
67
+ }
68
+
69
+ const EMPTY_ALL_DAY_SEGMENTS: EventCalendarSegment[] = []
70
+
71
+ interface EventCalendarTimeGridProps extends useRender.ComponentProps<"div"> {
72
+ view: Extract<CalendarView, "week" | "day" | "days">
73
+ dayStartHour?: number
74
+ dayEndHour?: number
75
+ showAllDay?: boolean
76
+ /** Gutter/gridline interval in minutes; defaults to the interval view config. */
77
+ interval?: number
78
+ }
79
+
80
+ function EventCalendarTimeGrid({
81
+ view,
82
+ className,
83
+ render,
84
+ dayStartHour,
85
+ dayEndHour,
86
+ showAllDay = true,
87
+ interval: intervalProp,
88
+ ...props
89
+ }: EventCalendarTimeGridProps) {
90
+ const instance = useEventCalendar()
91
+ const settings = useEventCalendarSettings()
92
+ const viewConfig = useEventCalendarViewConfig()
93
+ const visibleRange = useEventCalendarSelector<
94
+ unknown,
95
+ EventCalendarDateRange
96
+ >((state) => state.visibleRange, {
97
+ isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),
98
+ })
99
+
100
+ const startHour = dayStartHour ?? settings.dayStartHour
101
+ const endHour = dayEndHour ?? settings.dayEndHour
102
+ const interval = Math.min(
103
+ Math.max(intervalProp ?? viewConfig.interval, 5),
104
+ 240
105
+ )
106
+ const contained = viewConfig.scrollMode !== "page"
107
+
108
+ const { effective } = useEventCalendarViewSettings()
109
+ const days = useMemo(() => {
110
+ const result: Date[] = []
111
+ let cursor = zonedStartOfDay(visibleRange.start, settings.timeZone)
112
+ while (cursor < visibleRange.end) {
113
+ result.push(cursor)
114
+ cursor = zonedStartOfDay(
115
+ addDays(toZoned(cursor, settings.timeZone), 1),
116
+ settings.timeZone
117
+ )
118
+ }
119
+ if (effective.weekends || view === "day") return result
120
+ // Same weekend definition the month view filters on, so the toggle cannot
121
+ // hide one set of days here and another one there.
122
+ const filtered = result.filter(
123
+ (day) =>
124
+ !settings.weekendDays.includes(toZoned(day, settings.timeZone).getDay())
125
+ )
126
+ // A short N-days window landing entirely on the weekend would otherwise
127
+ // filter to nothing and emit an invalid repeat(0, ...) track.
128
+ return filtered.length ? filtered : result
129
+ }, [
130
+ visibleRange,
131
+ settings.timeZone,
132
+ settings.weekendDays,
133
+ effective.weekends,
134
+ view,
135
+ ])
136
+
137
+ // Initial scroll to scrollToHour + api.scrollToTime registration (contained)
138
+ const scrollRef = useRef<HTMLDivElement | null>(null)
139
+ useEffect(() => {
140
+ if (!contained) return
141
+ const el = scrollRef.current
142
+ if (!el) return
143
+ const viewport = el.querySelector<HTMLElement>(
144
+ "[data-slot=scroll-area-viewport]"
145
+ )
146
+ // Measure a rendered slot row - the CSS var is in rem, rects are in px.
147
+ const slotRow = el.querySelector<HTMLElement>(
148
+ "[data-slot=event-calendar-time-gutter] > div"
149
+ )
150
+ const slotPx = slotRow?.getBoundingClientRect().height || 64
151
+ const pxPerMinute = slotPx / interval
152
+ const scrollTo = (minutes: number) => {
153
+ // keep the hour label above the target line visible (it hangs -top-2)
154
+ viewport?.scrollTo({
155
+ top: Math.max(0, (minutes - startHour * 60) * pxPerMinute - 12),
156
+ })
157
+ }
158
+ scrollTo(viewConfig.scrollToHour * 60)
159
+ instance.internals.registerScrollHandler((time) => {
160
+ const minutes =
161
+ typeof time === "number"
162
+ ? time
163
+ : toZoned(time, settings.timeZone).getHours() * 60 +
164
+ toZoned(time, settings.timeZone).getMinutes()
165
+ scrollTo(minutes)
166
+ })
167
+ // Classic (width-consuming) scrollbars squeeze the scrolling track while
168
+ // the header/all-day rows outside keep full width, drifting the column
169
+ // borders. Mirror the measured gutter onto those rows via a CSS var -
170
+ // 0px for overlay scrollbars and the custom ScrollArea, so both modes
171
+ // lay out identically.
172
+ const root = el.closest<HTMLElement>(
173
+ "[data-slot=event-calendar-time-grid], [data-slot=event-calendar-resource-view]"
174
+ )
175
+ const syncScrollbarGutter = () => {
176
+ root?.style.setProperty(
177
+ "--ec-scrollbar-w",
178
+ `${viewport ? viewport.offsetWidth - viewport.clientWidth : 0}px`
179
+ )
180
+ }
181
+ syncScrollbarGutter()
182
+ const gutterObserver = viewport
183
+ ? new ResizeObserver(syncScrollbarGutter)
184
+ : null
185
+ if (viewport) gutterObserver?.observe(viewport)
186
+ return () => {
187
+ instance.internals.registerScrollHandler(null)
188
+ gutterObserver?.disconnect()
189
+ }
190
+ }, [
191
+ contained,
192
+ instance,
193
+ settings.timeZone,
194
+ startHour,
195
+ interval,
196
+ viewConfig.scrollToHour,
197
+ // scrollbars custom<->native swaps the scroller DOM: re-bind the
198
+ // viewport, the scroll wiring, and the measured --ec-scrollbar-w
199
+ viewConfig.scrollbars,
200
+ ])
201
+
202
+ // Gutter slots in minutes from the zoned day start
203
+ const slots = useMemo(() => {
204
+ const result: number[] = []
205
+ for (let m = startHour * 60; m < endHour * 60; m += interval) {
206
+ result.push(m)
207
+ }
208
+ return result
209
+ }, [startHour, endHour, interval])
210
+
211
+ // Section-level all-day segments for renderAllDaySection - reads the same
212
+ // per-day index buckets the cells subscribe to; inert (stable empty array,
213
+ // so the subscription never re-renders) while the override is unset.
214
+ const allDaySegments = useEventCalendarSelector<
215
+ unknown,
216
+ EventCalendarSegment[]
217
+ >(
218
+ () => {
219
+ if (!viewConfig.renderAllDaySection) return EMPTY_ALL_DAY_SEGMENTS
220
+ const byDay = instance.internals.getIndex().byDay
221
+ const result = days.flatMap(
222
+ (day) => byDay.get(getDayKey(day, settings.timeZone))?.allDay ?? []
223
+ )
224
+ return result.length ? result : EMPTY_ALL_DAY_SEGMENTS
225
+ },
226
+ {
227
+ isEqual: (a, b) =>
228
+ a === b ||
229
+ (a.length === b.length && a.every((segment, i) => segment === b[i])),
230
+ }
231
+ )
232
+
233
+ const gridTemplateColumns = `repeat(${days.length}, minmax(var(--ec-day-col-min,0px), 1fr))`
234
+
235
+ const track = (
236
+ <div className="relative flex">
237
+ <EventCalendarTimeGutter
238
+ days={days}
239
+ slots={slots}
240
+ startHour={startHour}
241
+ interval={interval}
242
+ />
243
+ <div className="grid min-w-0 flex-1" style={{ gridTemplateColumns }}>
244
+ {days.map((day) => (
245
+ <EventCalendarDayColumn
246
+ key={day.getTime()}
247
+ day={day}
248
+ startHour={startHour}
249
+ endHour={endHour}
250
+ interval={interval}
251
+ />
252
+ ))}
253
+ </div>
254
+ {effective.nowIndicator && (
255
+ <EventCalendarNowIndicator
256
+ days={days}
257
+ startHour={startHour}
258
+ endHour={endHour}
259
+ />
260
+ )}
261
+ </div>
262
+ )
263
+
264
+ const defaultProps = {
265
+ "data-slot": "event-calendar-time-grid",
266
+ "data-view": view,
267
+ className: cn(
268
+ "flex flex-col border-t",
269
+ contained && "min-h-0 flex-1 overflow-hidden",
270
+ viewConfig.classNames?.timeGrid,
271
+ className
272
+ ),
273
+ style: { "--ec-hour-height": "4rem" } as CSSProperties,
274
+ children: (
275
+ <>
276
+ {/* Day-header row (sticky below the nav in page scroll mode) */}
277
+ <div
278
+ className={cn(
279
+ "flex border-b pe-(--ec-scrollbar-w,0px)",
280
+ !contained &&
281
+ "bg-background sticky top-(--ec-sticky-offset,0px) z-20",
282
+ viewConfig.classNames?.timeGridHeader
283
+ )}
284
+ >
285
+ <div className="w-(--ec-gutter-width,4.5rem) shrink-0 border-e" />
286
+ <div className="grid min-w-0 flex-1" style={{ gridTemplateColumns }}>
287
+ {days.map((day) => (
288
+ <EventCalendarDayHeader
289
+ key={day.getTime()}
290
+ day={day}
291
+ view={view}
292
+ />
293
+ ))}
294
+ </div>
295
+ </div>
296
+ {/* All-day row */}
297
+ {showAllDay && (
298
+ <div
299
+ data-slot="event-calendar-all-day-section"
300
+ className={cn(
301
+ "flex border-b pe-(--ec-scrollbar-w,0px)",
302
+ viewConfig.classNames?.allDaySection
303
+ )}
304
+ >
305
+ {viewConfig.renderAllDaySection?.({
306
+ days,
307
+ segments: allDaySegments,
308
+ }) ?? (
309
+ <>
310
+ <div
311
+ className={cn(
312
+ // pt-1.5 matches the bar overlay's top inset; the inner box
313
+ // is one bar-row tall and centers the label so it sits on
314
+ // the SAME baseline as the first all-day chip and stays top-
315
+ // aligned when the chips wrap onto more lanes
316
+ "text-muted-foreground w-(--ec-gutter-width,4.5rem) shrink-0 border-e ps-2 pe-2.5 pt-1.5",
317
+ viewConfig.classNames?.allDayLabel
318
+ )}
319
+ >
320
+ <span className="flex h-[calc(var(--ec-month-bar-h,1.625rem)-0.125rem)] items-center justify-end">
321
+ {settings.i18n.labels.allDay}
322
+ </span>
323
+ </div>
324
+ <EventCalendarAllDayBars
325
+ days={days}
326
+ gridTemplateColumns={gridTemplateColumns}
327
+ />
328
+ </>
329
+ )}
330
+ </div>
331
+ )}
332
+ {/* Time track: internal scroll (contained) or document flow (page) */}
333
+ {contained ? (
334
+ <div ref={scrollRef} className="min-h-0 flex-1">
335
+ {viewConfig.scrollbars === "native" ? (
336
+ <div
337
+ data-slot="scroll-area-viewport"
338
+ data-ec-native-scroll=""
339
+ className="h-full overflow-y-auto"
340
+ >
341
+ {track}
342
+ </div>
343
+ ) : (
344
+ <ScrollArea className="h-full">{track}</ScrollArea>
345
+ )}
346
+ </div>
347
+ ) : (
348
+ track
349
+ )}
350
+ </>
351
+ ),
352
+ }
353
+
354
+ return (
355
+ <EventCalendarViewContext.Provider value={{ view }}>
356
+ {useRender({
357
+ defaultTagName: "div",
358
+ render,
359
+ props: mergeProps<"div">(defaultProps, props),
360
+ })}
361
+ </EventCalendarViewContext.Provider>
362
+ )
363
+ }
364
+
365
+ function EventCalendarDayHeader({
366
+ day,
367
+ view,
368
+ }: {
369
+ day: Date
370
+ view: CalendarView
371
+ }) {
372
+ const settings = useEventCalendarSettings()
373
+ const viewConfig = useEventCalendarViewConfig()
374
+ const { isToday } = useEventCalendarDay(day)
375
+ return (
376
+ <div
377
+ data-slot="event-calendar-day-header"
378
+ data-today={isToday || undefined}
379
+ className={cn(
380
+ "data-today:text-primary min-w-0 truncate border-e px-2 py-1.5 font-medium last:border-e-0",
381
+ isToday && viewConfig.todayClassName
382
+ )}
383
+ >
384
+ {viewConfig.renderDayHeader?.({ day, view, isToday }) ??
385
+ format(
386
+ toZoned(day, settings.timeZone),
387
+ settings.i18n.formats.timeGridDayHeader,
388
+ { locale: settings.locale }
389
+ )}
390
+ </div>
391
+ )
392
+ }
393
+
394
+ /**
395
+ * Continuous all-day bars for the week/N-days all-day row: consecutive-day
396
+ * segments of one occurrence merge into a single bar spanning its columns
397
+ * (same treatment as the month view), lane-packed. Returns CLONED segments -
398
+ * the shared per-day segments must stay pristine (see packWeekRowLanes).
399
+ */
400
+ function useEventCalendarAllDayBars(days: Date[]): {
401
+ bars: EventCalendarSegment[]
402
+ laneCount: number
403
+ } {
404
+ const instance = useEventCalendar()
405
+ const settings = useEventCalendarSettings()
406
+ return useEventCalendarSelector<
407
+ unknown,
408
+ { bars: EventCalendarSegment[]; laneCount: number }
409
+ >(
410
+ () => {
411
+ const byDay = instance.internals.getIndex().byDay
412
+ type Bar = {
413
+ seg: EventCalendarSegment
414
+ colStart: number
415
+ colEnd: number
416
+ isStart: boolean
417
+ isEnd: boolean
418
+ lane: number
419
+ }
420
+ const merged = new Map<string, Bar>()
421
+ days.forEach((day, col) => {
422
+ for (const seg of byDay.get(getDayKey(day, settings.timeZone))
423
+ ?.allDay ?? []) {
424
+ const key = seg.occurrence.key
425
+ const bar = merged.get(key)
426
+ if (bar) {
427
+ bar.colEnd = col
428
+ bar.isEnd = seg.isEnd
429
+ } else {
430
+ merged.set(key, {
431
+ seg,
432
+ colStart: col,
433
+ colEnd: col,
434
+ isStart: seg.isStart,
435
+ isEnd: seg.isEnd,
436
+ lane: 0,
437
+ })
438
+ }
439
+ }
440
+ })
441
+ const packed = Array.from(merged.values()).sort(
442
+ (a, b) =>
443
+ a.colStart - b.colStart ||
444
+ b.colEnd - b.colStart - (a.colEnd - a.colStart) ||
445
+ a.seg.occurrence.key.localeCompare(b.seg.occurrence.key)
446
+ )
447
+ const lanes: boolean[][] = []
448
+ for (const bar of packed) {
449
+ let lane = 0
450
+ for (;;) {
451
+ lanes[lane] ??= new Array(days.length).fill(false)
452
+ let free = true
453
+ for (let c = bar.colStart; c <= bar.colEnd; c++) {
454
+ if (lanes[lane][c]) {
455
+ free = false
456
+ break
457
+ }
458
+ }
459
+ if (free) break
460
+ lane++
461
+ }
462
+ for (let c = bar.colStart; c <= bar.colEnd; c++) lanes[lane][c] = true
463
+ bar.lane = lane
464
+ }
465
+ return {
466
+ bars: packed.map((bar) => ({
467
+ ...bar.seg,
468
+ isStart: bar.isStart,
469
+ isEnd: bar.isEnd,
470
+ continuesBefore: !bar.isStart,
471
+ continuesAfter: !bar.isEnd,
472
+ colStart: bar.colStart,
473
+ colSpan: bar.colEnd - bar.colStart + 1,
474
+ lane: bar.lane,
475
+ })),
476
+ laneCount: lanes.length,
477
+ }
478
+ },
479
+ {
480
+ calendar: instance,
481
+ isEqual: (a, b) =>
482
+ a.laneCount === b.laneCount &&
483
+ a.bars.length === b.bars.length &&
484
+ a.bars.every((s, i) => {
485
+ const o = b.bars[i]
486
+ // Compare the OCCURRENCE by identity, not by key: the key encodes
487
+ // id+start only, so a title/color/data edit compared equal and the
488
+ // all-day row kept its stale bars. Occurrences come from the
489
+ // memoized index (rebuilt exactly when events change), so identity
490
+ // also subsumes the end-time check it replaces. The positional
491
+ // fields stay because they are recomputed on every read.
492
+ return (
493
+ s.occurrence === o.occurrence &&
494
+ s.colStart === o.colStart &&
495
+ s.colSpan === o.colSpan &&
496
+ s.lane === o.lane &&
497
+ s.isStart === o.isStart &&
498
+ s.isEnd === o.isEnd
499
+ )
500
+ }),
501
+ }
502
+ )
503
+ }
504
+
505
+ /**
506
+ * The all-day row body: drop-target cells underneath, one continuous bar per
507
+ * occurrence in an overlay grid on top (month-view treatment), plus the
508
+ * standardized day-granular drag ghost for moves/resizes in this lane.
509
+ */
510
+ function EventCalendarAllDayBars({
511
+ days,
512
+ gridTemplateColumns,
513
+ }: {
514
+ days: Date[]
515
+ gridTemplateColumns: string
516
+ }) {
517
+ const settings = useEventCalendarSettings()
518
+ const viewConfig = useEventCalendarViewConfig()
519
+ const { bars, laneCount } = useEventCalendarAllDayBars(days)
520
+
521
+ const dragGhost = useEventCalendarSelector<
522
+ unknown,
523
+ | (Pick<
524
+ EventCalendarDragState,
525
+ "kind" | "valid" | "occurrence" | "proposedStart" | "proposedEnd"
526
+ > & {
527
+ colStart: number
528
+ colSpan: number
529
+ isStart: boolean
530
+ isEnd: boolean
531
+ color?: string
532
+ })
533
+ | null
534
+ >(
535
+ (state) => {
536
+ const drag = state.drag
537
+ if (!drag || !drag.proposedDayGranular) return null
538
+ const tz = settings.timeZone
539
+ const startMs = zonedStartOfDay(drag.proposedStart, tz).getTime()
540
+ const lastMs = zonedStartOfDay(
541
+ new Date(drag.proposedEnd.getTime() - 1),
542
+ tz
543
+ ).getTime()
544
+ let colStart = -1
545
+ let colEnd = -1
546
+ days.forEach((day, i) => {
547
+ const t = zonedStartOfDay(day, tz).getTime()
548
+ if (t >= startMs && t <= lastMs) {
549
+ if (colStart === -1) colStart = i
550
+ colEnd = i
551
+ }
552
+ })
553
+ if (colStart === -1) return null
554
+ return {
555
+ kind: drag.kind,
556
+ valid: drag.valid,
557
+ occurrence: drag.occurrence,
558
+ proposedStart: drag.proposedStart,
559
+ proposedEnd: drag.proposedEnd,
560
+ colStart,
561
+ colSpan: colEnd - colStart + 1,
562
+ isStart: zonedStartOfDay(days[colStart], tz).getTime() <= startMs,
563
+ isEnd: zonedStartOfDay(days[colEnd], tz).getTime() >= lastMs,
564
+ color: drag.occurrence.event.color,
565
+ }
566
+ },
567
+ {
568
+ isEqual: (a, b) =>
569
+ a === b ||
570
+ (a !== null &&
571
+ b !== null &&
572
+ a.colStart === b.colStart &&
573
+ a.colSpan === b.colSpan &&
574
+ a.valid === b.valid &&
575
+ a.kind === b.kind &&
576
+ a.proposedStart.getTime() === b.proposedStart.getTime() &&
577
+ a.proposedEnd.getTime() === b.proposedEnd.getTime()),
578
+ }
579
+ )
580
+ const ghostLane = dragGhost
581
+ ? (bars.find((s) => s.occurrence.key === dragGhost.occurrence.key)?.lane ??
582
+ laneCount)
583
+ : 0
584
+ const effectiveLanes = Math.max(laneCount, dragGhost ? ghostLane + 1 : 0)
585
+
586
+ return (
587
+ <div className="relative min-w-0 flex-1">
588
+ <div
589
+ className="grid h-full"
590
+ style={{
591
+ gridTemplateColumns,
592
+ // always reserve at least one bar row so the all-day row keeps the
593
+ // same height whether or not the day has all-day events (an empty
594
+ // row would otherwise collapse to the label height and misalign)
595
+ minHeight: `calc(${Math.max(effectiveLanes, 1)} * var(--ec-month-bar-h, 1.625rem) + 0.625rem)`,
596
+ }}
597
+ >
598
+ {days.map((day) => (
599
+ <EventCalendarAllDayCell key={day.getTime()} day={day} />
600
+ ))}
601
+ </div>
602
+ {(bars.length > 0 || dragGhost) && (
603
+ <div
604
+ data-slot="event-calendar-all-day-bar-overlay"
605
+ className="pointer-events-none absolute inset-x-0 top-0 grid pt-1.5"
606
+ style={{
607
+ gridTemplateColumns,
608
+ gridAutoRows: "var(--ec-month-bar-h, 1.625rem)",
609
+ }}
610
+ >
611
+ {bars.map((segment) => (
612
+ <div
613
+ key={segment.occurrence.key}
614
+ className={cn(
615
+ "pointer-events-auto min-w-0 px-1",
616
+ viewConfig.classNames?.monthBar
617
+ )}
618
+ style={{
619
+ gridColumn: `${(segment.colStart ?? 0) + 1} / span ${segment.colSpan ?? 1}`,
620
+ gridRow: (segment.lane ?? 0) + 1,
621
+ }}
622
+ >
623
+ <EventCalendarEvent
624
+ segment={segment}
625
+ className="h-[calc(var(--ec-month-bar-h,1.625rem)-0.125rem)]"
626
+ />
627
+ </div>
628
+ ))}
629
+ {dragGhost && (
630
+ <div
631
+ aria-hidden
632
+ className={cn("min-w-0 px-1", viewConfig.classNames?.monthBar)}
633
+ style={{
634
+ gridColumn: `${dragGhost.colStart + 1} / span ${dragGhost.colSpan}`,
635
+ gridRow: ghostLane + 1,
636
+ }}
637
+ >
638
+ <div
639
+ data-slot="event-calendar-drag-ghost"
640
+ data-kind={dragGhost.kind}
641
+ data-drop-invalid={!dragGhost.valid || undefined}
642
+ className={cn(
643
+ "h-[calc(var(--ec-month-bar-h,1.625rem)-0.125rem)]",
644
+ dragGhost.kind === "move"
645
+ ? cn(
646
+ EVENT_CALENDAR_GHOST.move,
647
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalid
648
+ )
649
+ : cn(
650
+ EVENT_CALENDAR_GHOST.resize,
651
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidResize
652
+ ),
653
+ viewConfig.classNames?.dragGhost
654
+ )}
655
+ style={
656
+ {
657
+ "--ec-event-color":
658
+ dragGhost.color ?? "var(--color-primary)",
659
+ } as CSSProperties
660
+ }
661
+ >
662
+ {dragGhost.kind !== "move" && (
663
+ <EventCalendarEvent
664
+ preview
665
+ segment={{
666
+ occurrence: {
667
+ ...dragGhost.occurrence,
668
+ start: dragGhost.proposedStart,
669
+ end: dragGhost.proposedEnd,
670
+ },
671
+ day: days[dragGhost.colStart] ?? days[0],
672
+ isStart: dragGhost.isStart,
673
+ isEnd: dragGhost.isEnd,
674
+ continuesBefore: !dragGhost.isStart,
675
+ continuesAfter: !dragGhost.isEnd,
676
+ }}
677
+ className={cn(
678
+ "h-full inset-ring-0",
679
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidContent
680
+ )}
681
+ />
682
+ )}
683
+ </div>
684
+ </div>
685
+ )}
686
+ </div>
687
+ )}
688
+ </div>
689
+ )
690
+ }
691
+
692
+ function EventCalendarAllDayCell({ day }: { day: Date }) {
693
+ const settings = useEventCalendarSettings()
694
+ const viewConfig = useEventCalendarViewConfig()
695
+ const { view } = useEventCalendarViewContext()
696
+ const { effective } = useEventCalendarViewSettings()
697
+ const gestures = useEventCalendarGestures()
698
+ const dayStart = zonedStartOfDay(day, settings.timeZone)
699
+ const dayEnd = addDays(toZoned(dayStart, settings.timeZone), 1)
700
+ const isOff = resolveOffDay(
701
+ day,
702
+ settings.timeZone,
703
+ effective.offDays
704
+ ? typeof viewConfig.offDays === "object"
705
+ ? viewConfig.offDays
706
+ : true
707
+ : false,
708
+ settings.weekendDays
709
+ )
710
+ const offClassName =
711
+ (typeof viewConfig.offDays === "object" && viewConfig.offDays.className) ||
712
+ "bg-muted/25"
713
+
714
+ const isDropTarget = useEventCalendarSelector<
715
+ unknown,
716
+ "valid" | "invalid" | null
717
+ >((state) => {
718
+ const drag = state.drag
719
+ if (!drag || !drag.proposedDayGranular) return null
720
+ const covered = drag.proposedStart < dayEnd && drag.proposedEnd > dayStart
721
+ if (!covered) return null
722
+ return drag.valid ? "valid" : "invalid"
723
+ })
724
+ const inDraft = useEventCalendarSelector<
725
+ unknown,
726
+ { isStart: boolean; isEnd: boolean } | null
727
+ >(
728
+ (state) => {
729
+ const draft = state.slotDraft
730
+ if (!draft || !draft.allDay) return null
731
+ if (draft.start >= dayEnd || draft.end <= dayStart) return null
732
+ return {
733
+ isStart: draft.start >= dayStart,
734
+ isEnd: draft.end <= dayEnd,
735
+ }
736
+ },
737
+ {
738
+ isEqual: (a, b) =>
739
+ a === b ||
740
+ (a !== null &&
741
+ b !== null &&
742
+ a.isStart === b.isStart &&
743
+ a.isEnd === b.isEnd),
744
+ }
745
+ )
746
+
747
+ // All-day drags select whole DAYS, so this row reads a date range, matching
748
+ // the month grid. Without it, dragging the all-day strip in week view would
749
+ // be the one create gesture in the calendar with no readout at all.
750
+ const allDayDraftRange = useEventCalendarSelector<
751
+ unknown,
752
+ { start: Date; end: Date } | null
753
+ >(
754
+ (state) => {
755
+ const draft = state.slotDraft
756
+ if (!draft || !draft.allDay) return null
757
+ return { start: draft.start, end: draft.end }
758
+ },
759
+ {
760
+ isEqual: (a, b) =>
761
+ a === b ||
762
+ (a !== null &&
763
+ b !== null &&
764
+ a.start.getTime() === b.start.getTime() &&
765
+ a.end.getTime() === b.end.getTime()),
766
+ }
767
+ )
768
+
769
+ return (
770
+ <div
771
+ data-slot="event-calendar-all-day-cell"
772
+ data-ec-day={dayStart.getTime()}
773
+ data-drop-target={isDropTarget ?? undefined}
774
+ data-off={isOff || undefined}
775
+ className={cn(
776
+ "relative flex min-w-0 flex-col gap-0.5 border-e px-1 py-1.5 last:border-e-0",
777
+ isOff && offClassName,
778
+ viewConfig.dayClassName?.(day),
779
+ // No drop-target bg fill on move/resize (see month view) - a subtle
780
+ // dashed inset outline below marks the target instead.
781
+ inDraft &&
782
+ cn(
783
+ EVENT_CALENDAR_SLOT_DRAFT.surface,
784
+ EVENT_CALENDAR_SLOT_DRAFT.segment,
785
+ inDraft.isStart && EVENT_CALENDAR_SLOT_DRAFT.segmentStart,
786
+ inDraft.isEnd && EVENT_CALENDAR_SLOT_DRAFT.segmentEnd,
787
+ viewConfig.classNames?.slotDraft
788
+ ),
789
+ viewConfig.classNames?.allDayCell
790
+ )}
791
+ onPointerDown={(e) => {
792
+ if (e.target === e.currentTarget) gestures.beginCreate(e, day, true)
793
+ }}
794
+ onClick={(e) => {
795
+ if (
796
+ e.target === e.currentTarget &&
797
+ !wasRecentDrag() &&
798
+ !wasRecentChipPress()
799
+ ) {
800
+ settings.onSlotClick?.({ date: dayStart, allDay: true, view }, e)
801
+ }
802
+ }}
803
+ >
804
+ {/* Only on the first cell of the selection, same rule as the month grid,
805
+ so a three-day all-day drag labels itself once rather than three
806
+ times. pointer-events-none keeps the cell's own pointerdown guard
807
+ (`e.target === e.currentTarget`) intact. */}
808
+ {inDraft?.isStart && allDayDraftRange && (
809
+ <span
810
+ aria-hidden
811
+ className={cn(
812
+ "pointer-events-none absolute inset-x-1 top-1/2 -translate-y-1/2",
813
+ EVENT_CALENDAR_SLOT_DRAFT.label
814
+ )}
815
+ >
816
+ {settings.i18n.functions.formatDayRange(
817
+ {
818
+ start: toZoned(allDayDraftRange.start, settings.timeZone),
819
+ end: toZoned(allDayDraftRange.end, settings.timeZone),
820
+ },
821
+ { locale: settings.locale }
822
+ )}
823
+ </span>
824
+ )}
825
+ </div>
826
+ )
827
+ }
828
+
829
+ function EventCalendarTimeGutter({
830
+ days,
831
+ slots,
832
+ startHour,
833
+ interval,
834
+ }: {
835
+ days: Date[]
836
+ slots: number[]
837
+ startHour: number
838
+ interval: number
839
+ }) {
840
+ const settings = useEventCalendarSettings()
841
+ const viewConfig = useEventCalendarViewConfig()
842
+ const referenceDay = days[0] ?? new Date()
843
+ const labelFormat =
844
+ interval % 60 === 0
845
+ ? settings.i18n.formats.timeGutter
846
+ : settings.i18n.formats.timeGutterMinute
847
+ return (
848
+ <div
849
+ data-slot="event-calendar-time-gutter"
850
+ className={cn(
851
+ "relative w-(--ec-gutter-width,4.5rem) shrink-0 border-e",
852
+ viewConfig.classNames?.timeGutter
853
+ )}
854
+ >
855
+ {slots.map((minutes) => {
856
+ const time = addMinutes(
857
+ zonedStartOfDay(referenceDay, settings.timeZone),
858
+ minutes
859
+ )
860
+ // Always consult renderTimeGutterSlot (a consumer may label the first
861
+ // slot); only the DEFAULT label is suppressed at the day-start edge.
862
+ const label =
863
+ viewConfig.renderTimeGutterSlot?.({
864
+ time,
865
+ hour: Math.floor(minutes / 60),
866
+ minute: minutes % 60,
867
+ }) ??
868
+ (minutes > startHour * 60
869
+ ? format(time, labelFormat, { locale: settings.locale })
870
+ : null)
871
+ return (
872
+ <div
873
+ key={minutes}
874
+ className="relative"
875
+ style={{ height: `calc(var(--ec-hour-height) * ${interval / 60})` }}
876
+ >
877
+ {label != null && (
878
+ <span
879
+ className={cn(
880
+ "text-muted-foreground absolute end-2.5 -top-2",
881
+ viewConfig.classNames?.timeGutterLabel
882
+ )}
883
+ >
884
+ {label}
885
+ </span>
886
+ )}
887
+ </div>
888
+ )
889
+ })}
890
+ </div>
891
+ )
892
+ }
893
+
894
+ /**
895
+ * Inline size of the timed-block track. It stops short of the day column's end
896
+ * edge so the bare strip beside a full-width block is still the column itself,
897
+ * and a press there reaches the column's own create handler - the empty space
898
+ * Google Calendar and Notion Calendar both reserve for "new event next to this
899
+ * one". Consumers opt out with classNames.dayColumn: "[--ec-event-gutter:0px]".
900
+ */
901
+ const EVENT_TRACK_WIDTH = "(100% - var(--ec-event-gutter, 0.75rem))"
902
+
903
+ /** Absolute overlay block positioned by minutes (ghosts + drafts). */
904
+ function minuteBlockStyle(
905
+ startMin: number,
906
+ endMin: number,
907
+ boundsStartMin: number
908
+ ): CSSProperties {
909
+ const top = (startMin - boundsStartMin) / 60
910
+ const height = Math.max((endMin - startMin) / 60, 0.25)
911
+ return {
912
+ top: `calc(var(--ec-hour-height) * ${top})`,
913
+ height: `calc(var(--ec-hour-height) * ${height})`,
914
+ }
915
+ }
916
+
917
+ function EventCalendarDayColumn({
918
+ day,
919
+ startHour,
920
+ endHour,
921
+ interval,
922
+ }: {
923
+ day: Date
924
+ startHour: number
925
+ endHour: number
926
+ interval: number
927
+ }) {
928
+ const settings = useEventCalendarSettings()
929
+ const viewConfig = useEventCalendarViewConfig()
930
+ const { effective } = useEventCalendarViewSettings()
931
+ const { view } = useEventCalendarViewContext()
932
+ const gestures = useEventCalendarGestures()
933
+ const { segments, isToday } = useEventCalendarDay(day)
934
+ const isOff = resolveOffDay(
935
+ day,
936
+ settings.timeZone,
937
+ effective.offDays
938
+ ? typeof viewConfig.offDays === "object"
939
+ ? viewConfig.offDays
940
+ : true
941
+ : false,
942
+ settings.weekendDays
943
+ )
944
+ const offClassName =
945
+ (typeof viewConfig.offDays === "object" && viewConfig.offDays.className) ||
946
+ "bg-muted/25"
947
+
948
+ const timeZone = settings.timeZone
949
+ const dayStart = zonedStartOfDay(day, timeZone)
950
+ const dayEnd = addDays(toZoned(dayStart, timeZone), 1)
951
+ const totalMinutes = getDayTotalMinutes(day, timeZone)
952
+ const boundsStartMin = startHour * 60
953
+ const boundsEndMin = Math.min(endHour * 60, totalMinutes)
954
+ const boundsMinutes = Math.max(60, boundsEndMin - boundsStartMin)
955
+
956
+ // Minute window of a proposal intersecting THIS day, or null
957
+ const windowFor = (start: Date, end: Date): [number, number] | null => {
958
+ if (start >= dayEnd || end <= dayStart) return null
959
+ const from = Math.max(
960
+ differenceInMinutes(start > dayStart ? start : dayStart, dayStart),
961
+ boundsStartMin
962
+ )
963
+ const to = Math.min(
964
+ differenceInMinutes(end < dayEnd ? end : dayEnd, dayStart),
965
+ boundsEndMin
966
+ )
967
+ return to > from ? [from, to] : null
968
+ }
969
+
970
+ const dragGhost = useEventCalendarSelector<
971
+ unknown,
972
+ | (Pick<
973
+ EventCalendarDragState,
974
+ "valid" | "kind" | "occurrence" | "proposedStart" | "proposedEnd"
975
+ > & {
976
+ window: [number, number]
977
+ color?: string
978
+ title: string
979
+ })
980
+ | null
981
+ >(
982
+ (state) => {
983
+ const drag = state.drag
984
+ if (!drag || drag.proposedDayGranular) return null
985
+ const window = windowFor(drag.proposedStart, drag.proposedEnd)
986
+ if (!window) return null
987
+ return {
988
+ valid: drag.valid,
989
+ kind: drag.kind,
990
+ occurrence: drag.occurrence,
991
+ proposedStart: drag.proposedStart,
992
+ proposedEnd: drag.proposedEnd,
993
+ window,
994
+ color: drag.occurrence.event.color,
995
+ title: drag.occurrence.event.title,
996
+ }
997
+ },
998
+ {
999
+ isEqual: (a, b) =>
1000
+ a === b ||
1001
+ (a !== null &&
1002
+ b !== null &&
1003
+ a.window[0] === b.window[0] &&
1004
+ a.window[1] === b.window[1] &&
1005
+ a.valid === b.valid &&
1006
+ a.kind === b.kind &&
1007
+ a.proposedStart.getTime() === b.proposedStart.getTime() &&
1008
+ a.proposedEnd.getTime() === b.proposedEnd.getTime()),
1009
+ }
1010
+ )
1011
+
1012
+ const draftWindow = useEventCalendarSelector<
1013
+ unknown,
1014
+ [number, number] | null
1015
+ >(
1016
+ (state) => {
1017
+ const draft = state.slotDraft
1018
+ if (!draft || draft.allDay) return null
1019
+ return windowFor(draft.start, draft.end)
1020
+ },
1021
+ {
1022
+ isEqual: (a, b) =>
1023
+ a === b || (a !== null && b !== null && a[0] === b[0] && a[1] === b[1]),
1024
+ }
1025
+ )
1026
+
1027
+ // The draft's true instants, kept separate from `draftWindow` above because
1028
+ // that one is clipped to this column's day bounds - the readout has to show
1029
+ // what the user actually drew, not the clipped remainder.
1030
+ const draftRange = useEventCalendarSelector<
1031
+ unknown,
1032
+ { start: Date; end: Date } | null
1033
+ >(
1034
+ (state) => {
1035
+ const draft = state.slotDraft
1036
+ if (!draft || draft.allDay) return null
1037
+ return { start: draft.start, end: draft.end }
1038
+ },
1039
+ {
1040
+ isEqual: (a, b) =>
1041
+ a === b ||
1042
+ (a !== null &&
1043
+ b !== null &&
1044
+ a.start.getTime() === b.start.getTime() &&
1045
+ a.end.getTime() === b.end.getTime()),
1046
+ }
1047
+ )
1048
+
1049
+ // Segments the day bounds clip away still occupy a column in the shared
1050
+ // index's packing, leaving a phantom empty half beside the first in-bounds
1051
+ // chip. Repack the visible subset; clones keep the index cache untouched.
1052
+ const packedTimed = useMemo(() => {
1053
+ const visible = segments.timed.filter((segment) => {
1054
+ const startMin = Math.max(segment.startMin ?? 0, boundsStartMin)
1055
+ const endMin = Math.min(segment.endMin ?? startMin, boundsEndMin)
1056
+ return endMin > boundsStartMin && startMin < boundsEndMin
1057
+ })
1058
+ if (visible.length === segments.timed.length) return segments.timed
1059
+ const clones = visible.map(
1060
+ (segment) => ({ ...segment }) as EventCalendarSegment
1061
+ )
1062
+ packTimedSegments(clones)
1063
+ return clones
1064
+ }, [segments.timed, boundsStartMin, boundsEndMin])
1065
+
1066
+ const slotFromPointer = (e: React.MouseEvent): { date: Date; end: Date } => {
1067
+ const rect = e.currentTarget.getBoundingClientRect()
1068
+ const pxPerMinute = rect.height / boundsMinutes
1069
+ const minutes = snapMinutes(
1070
+ boundsStartMin + (e.clientY - rect.top) / pxPerMinute,
1071
+ settings.snapDuration
1072
+ )
1073
+ const clamped = Math.min(
1074
+ Math.max(minutes, boundsStartMin),
1075
+ boundsEndMin - settings.slotDuration
1076
+ )
1077
+ return {
1078
+ date: addMinutes(dayStart, clamped),
1079
+ end: addMinutes(dayStart, clamped + settings.slotDuration),
1080
+ }
1081
+ }
1082
+
1083
+ return (
1084
+ <div
1085
+ data-slot="event-calendar-day-column"
1086
+ data-today={isToday || undefined}
1087
+ data-off={isOff || undefined}
1088
+ data-ec-day={dayStart.getTime()}
1089
+ data-ec-bounds-start={boundsStartMin}
1090
+ data-ec-bounds-end={boundsEndMin}
1091
+ role="group"
1092
+ aria-label={format(
1093
+ toZoned(day, timeZone),
1094
+ settings.i18n.formats.dayAria,
1095
+ { locale: settings.locale }
1096
+ )}
1097
+ className={cn(
1098
+ "relative min-w-0 border-e last:border-e-0",
1099
+ isOff && offClassName,
1100
+ // time grid keeps today's column on the normal background (the header
1101
+ // marks today); only a consumer todayClassName can tint it
1102
+ isToday && viewConfig.todayClassName,
1103
+ viewConfig.dayClassName?.(day),
1104
+ viewConfig.classNames?.dayColumn
1105
+ )}
1106
+ style={{
1107
+ height: `calc(var(--ec-hour-height) * ${boundsMinutes / 60})`,
1108
+ backgroundImage: `repeating-linear-gradient(to bottom, transparent, transparent calc(var(--ec-hour-height) * ${interval / 60} - var(--ec-slot-line-width, 1px)), var(--ec-slot-line-color, var(--color-border)) calc(var(--ec-hour-height) * ${interval / 60} - var(--ec-slot-line-width, 1px)), var(--ec-slot-line-color, var(--color-border)) calc(var(--ec-hour-height) * ${interval / 60}))`,
1109
+ }}
1110
+ onPointerDown={(e) => {
1111
+ if (e.target === e.currentTarget) gestures.beginCreate(e, day, false)
1112
+ }}
1113
+ onClick={(e) => {
1114
+ if (
1115
+ e.target !== e.currentTarget ||
1116
+ wasRecentDrag() ||
1117
+ wasRecentChipPress()
1118
+ )
1119
+ return
1120
+ const slot = slotFromPointer(e)
1121
+ settings.onSlotClick?.({ ...slot, allDay: false, view }, e)
1122
+ }}
1123
+ >
1124
+ {viewConfig.renderDayColumnBackground && (
1125
+ <div className="pointer-events-none absolute inset-0">
1126
+ {viewConfig.renderDayColumnBackground({
1127
+ day,
1128
+ boundsStartMin,
1129
+ boundsEndMin,
1130
+ totalMinutes,
1131
+ })}
1132
+ </div>
1133
+ )}
1134
+ {packedTimed.map((segment) => {
1135
+ const startMin = Math.max(segment.startMin ?? 0, boundsStartMin)
1136
+ const endMin = Math.min(segment.endMin ?? startMin, boundsEndMin)
1137
+ if (endMin <= boundsStartMin || startMin >= boundsEndMin) return null
1138
+ const columnCount = segment.columnCount ?? 1
1139
+ const column = segment.column ?? 0
1140
+ const span = segment.columnSpan ?? 1
1141
+ const zIndex = segment.occurrence.event.zIndex ?? 10 + column
1142
+ // Strict side-by-side columns - no cascade overlap (fade-truncate +
1143
+ // hover reveal carry the legibility); the ring separates neighbors.
1144
+ return (
1145
+ <div
1146
+ key={segment.occurrence.key}
1147
+ // min-h keeps 15-min chips readable (Google-style: the block may
1148
+ // slightly outgrow its true window); hover raises a squeezed
1149
+ // cascade chip above its overlapping neighbors
1150
+ className="absolute z-(--ec-z) min-h-(--ec-event-min-h,1.5rem) px-0.5 hover:z-40"
1151
+ style={
1152
+ {
1153
+ ...minuteBlockStyle(startMin, endMin, boundsStartMin),
1154
+ left: `calc(${EVENT_TRACK_WIDTH} * ${column / columnCount})`,
1155
+ width: `calc(${EVENT_TRACK_WIDTH} * ${span / columnCount})`,
1156
+ "--ec-z": zIndex,
1157
+ } as CSSProperties
1158
+ }
1159
+ >
1160
+ <EventCalendarEvent
1161
+ segment={segment}
1162
+ className={cn(
1163
+ columnCount > 1 && "ring-background ring-1",
1164
+ // short chips: single centered row, exact-fit line height so
1165
+ // the title never slices mid-glyph
1166
+ endMin - startMin < viewConfig.compactEventMinutes
1167
+ ? "h-full gap-1 py-0 leading-4"
1168
+ : "h-full flex-col items-start justify-start gap-0 py-1",
1169
+ viewConfig.classNames?.timedChip
1170
+ )}
1171
+ />
1172
+ </div>
1173
+ )
1174
+ })}
1175
+ {/* Drag ghost, standardized (EVENT_CALENDAR_GHOST). Move: a faint
1176
+ dashed placeholder at the snapped slot - the cursor-attached carry
1177
+ clone owns the visual. Resize: the chip clone with a dashed boundary
1178
+ at the proposed extent. Invalid: destructive marking. */}
1179
+ {dragGhost && (
1180
+ <div
1181
+ data-slot="event-calendar-drag-ghost"
1182
+ data-kind={dragGhost.kind}
1183
+ data-drop-invalid={!dragGhost.valid || undefined}
1184
+ className={cn(
1185
+ "pointer-events-none absolute inset-x-0.5 z-50 min-h-(--ec-event-min-h,1.5rem)",
1186
+ dragGhost.kind === "move"
1187
+ ? cn(
1188
+ EVENT_CALENDAR_GHOST.move,
1189
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalid
1190
+ )
1191
+ : cn(
1192
+ EVENT_CALENDAR_GHOST.resize,
1193
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidResize
1194
+ ),
1195
+ viewConfig.classNames?.dragGhost
1196
+ )}
1197
+ style={
1198
+ {
1199
+ ...minuteBlockStyle(
1200
+ dragGhost.window[0],
1201
+ dragGhost.window[1],
1202
+ boundsStartMin
1203
+ ),
1204
+ "--ec-event-color": dragGhost.color ?? "var(--color-primary)",
1205
+ } as CSSProperties
1206
+ }
1207
+ >
1208
+ {dragGhost.kind !== "move" && (
1209
+ <EventCalendarEvent
1210
+ preview
1211
+ segment={{
1212
+ occurrence: {
1213
+ ...dragGhost.occurrence,
1214
+ start: dragGhost.proposedStart,
1215
+ end: dragGhost.proposedEnd,
1216
+ allDay: false,
1217
+ },
1218
+ day,
1219
+ isStart: true,
1220
+ isEnd: true,
1221
+ continuesBefore: false,
1222
+ continuesAfter: false,
1223
+ startMin: dragGhost.window[0],
1224
+ endMin: dragGhost.window[1],
1225
+ }}
1226
+ className={cn(
1227
+ dragGhost.window[1] - dragGhost.window[0] <
1228
+ viewConfig.compactEventMinutes
1229
+ ? "h-full gap-1 py-0 leading-4"
1230
+ : "h-full flex-col items-start justify-start gap-0 py-1",
1231
+ viewConfig.classNames?.timedChip,
1232
+ "inset-ring-0",
1233
+ !dragGhost.valid && EVENT_CALENDAR_GHOST.invalidContent
1234
+ )}
1235
+ />
1236
+ )}
1237
+ </div>
1238
+ )}
1239
+ {/* Drag-create draft */}
1240
+ {draftWindow && (
1241
+ <div
1242
+ data-slot="event-calendar-slot-draft"
1243
+ className={cn(
1244
+ EVENT_CALENDAR_SLOT_DRAFT.box,
1245
+ "pointer-events-none absolute inset-x-0.5 z-40 overflow-hidden",
1246
+ viewConfig.classNames?.slotDraft
1247
+ )}
1248
+ style={minuteBlockStyle(
1249
+ draftWindow[0],
1250
+ draftWindow[1],
1251
+ boundsStartMin
1252
+ )}
1253
+ >
1254
+ {/* Live range readout while dragging, the way Outlook and Google
1255
+ Calendar do it: the entire point of the gesture is to pick a
1256
+ time, so show the time being picked instead of an empty box.
1257
+ Reuses `formatEventTime`, the same formatter the chips use, so
1258
+ locale, time zone and 12/24-hour all match the rest of the
1259
+ calendar for free. `truncate` plus the wrapper's overflow-hidden
1260
+ keeps a 15-minute draft from spilling past its own border. */}
1261
+ {draftRange && (
1262
+ <span className={cn("block", EVENT_CALENDAR_SLOT_DRAFT.label)}>
1263
+ {settings.i18n.functions.formatEventTime(
1264
+ toZoned(draftRange.start, timeZone),
1265
+ toZoned(draftRange.end, timeZone),
1266
+ false,
1267
+ { locale: settings.locale }
1268
+ )}
1269
+ </span>
1270
+ )}
1271
+ </div>
1272
+ )}
1273
+ </div>
1274
+ )
1275
+ }
1276
+
1277
+ function EventCalendarNowIndicator({
1278
+ days,
1279
+ startHour,
1280
+ endHour,
1281
+ }: {
1282
+ days: Date[]
1283
+ startHour: number
1284
+ endHour: number
1285
+ }): ReactNode {
1286
+ const settings = useEventCalendarSettings()
1287
+ const viewConfig = useEventCalendarViewConfig()
1288
+ const now = useNow(viewConfig.nowIndicatorInterval)
1289
+
1290
+ const timeZone = settings.timeZone
1291
+ const todayKey = getDayKey(now, timeZone)
1292
+ const todayIndex = days.findIndex(
1293
+ (day) => getDayKey(day, timeZone) === todayKey
1294
+ )
1295
+ if (todayIndex === -1) return null
1296
+
1297
+ const dayStart = zonedStartOfDay(now, timeZone)
1298
+ const minutes = differenceInMinutes(now, dayStart)
1299
+ if (minutes < startHour * 60 || minutes > endHour * 60) return null
1300
+ const top = minutes / 60 - startHour
1301
+
1302
+ if (viewConfig.renderNowIndicator) {
1303
+ return (
1304
+ <div
1305
+ className="pointer-events-none absolute inset-x-0 z-40"
1306
+ style={{ top: `calc(var(--ec-hour-height) * ${top})` }}
1307
+ >
1308
+ {viewConfig.renderNowIndicator({ time: now })}
1309
+ </div>
1310
+ )
1311
+ }
1312
+
1313
+ const columnWidthPct = 100 / days.length
1314
+
1315
+ return (
1316
+ <div
1317
+ data-slot="event-calendar-now-indicator"
1318
+ className="pointer-events-none absolute inset-x-0 z-40"
1319
+ style={{ top: `calc(var(--ec-hour-height) * ${top})` }}
1320
+ >
1321
+ {/* hairline across the content columns only (clear of the time gutter) */}
1322
+ <div className="bg-destructive/40 absolute start-(--ec-gutter-width,4.5rem) end-0 h-px" />
1323
+ {/* stronger segment + dot over today's column */}
1324
+ <div
1325
+ className="absolute h-px"
1326
+ style={{
1327
+ left: `calc(var(--ec-gutter-width, 4.5rem) + (100% - var(--ec-gutter-width, 4.5rem)) * ${(todayIndex * columnWidthPct) / 100})`,
1328
+ width: `calc((100% - var(--ec-gutter-width, 4.5rem)) * ${columnWidthPct / 100})`,
1329
+ }}
1330
+ >
1331
+ <div className="bg-destructive absolute inset-x-0 top-0 h-px" />
1332
+ {/* dot leads the line at today's column-start border: pulled 1px left of
1333
+ center (-start-1 = -4px vs the 6px/size-1.5 circle) so it reads as a
1334
+ distinct bullet instead of merging into the line to its right */}
1335
+ <div className="bg-destructive absolute -start-1 top-0 size-1.5 -translate-y-1/2 rounded-full" />
1336
+ </div>
1337
+ </div>
1338
+ )
1339
+ }
1340
+
1341
+ type TimeGridViewProps = Omit<EventCalendarTimeGridProps, "view">
1342
+
1343
+ function EventCalendarWeekView(props: TimeGridViewProps) {
1344
+ return <EventCalendarTimeGrid view="week" {...props} />
1345
+ }
1346
+
1347
+ function EventCalendarDayView(props: TimeGridViewProps) {
1348
+ return <EventCalendarTimeGrid view="day" {...props} />
1349
+ }
1350
+
1351
+ function EventCalendarDaysView(props: TimeGridViewProps) {
1352
+ return <EventCalendarTimeGrid view="days" {...props} />
1353
+ }
1354
+
1355
+ export {
1356
+ EVENT_TRACK_WIDTH,
1357
+ EventCalendarDayView,
1358
+ EventCalendarDaysView,
1359
+ EventCalendarNowIndicator,
1360
+ EventCalendarTimeGrid,
1361
+ EventCalendarTimeGutter,
1362
+ EventCalendarWeekView,
1363
+ minuteBlockStyle,
1364
+ useNow,
1365
+ }
1366
+ export type { EventCalendarTimeGridProps }