@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,1916 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ useSyncExternalStore,
10
+ type ComponentType,
11
+ type ReactNode,
12
+ type RefObject,
13
+ } from "react"
14
+ import {
15
+ mergeEventCalendarI18n,
16
+ type EventCalendarI18nConfig,
17
+ type EventCalendarI18nOverrides,
18
+ } from "@/components/reui/event-calendar/event-calendar-i18n"
19
+ import {
20
+ buildEventIndex,
21
+ defaultEventOrder,
22
+ eventsOverlap,
23
+ getDayKey,
24
+ getRangeKey,
25
+ getViewDateRange,
26
+ stepDate,
27
+ toZoned,
28
+ zonedStartOfDay,
29
+ type EventCalendarDayBucket,
30
+ type EventCalendarIndex,
31
+ type WeekStartsOn,
32
+ } from "@/components/reui/event-calendar/event-calendar-lib"
33
+ import type {
34
+ CalendarEvent,
35
+ CalendarView,
36
+ EventCalendarDateRange,
37
+ EventCalendarDragState,
38
+ EventCalendarEventId,
39
+ EventCalendarInteractions,
40
+ EventCalendarOccurrence,
41
+ EventCalendarOffDaysConfig,
42
+ EventCalendarProposedUpdate,
43
+ EventCalendarRangeInfo,
44
+ EventCalendarResource,
45
+ EventCalendarSegment,
46
+ EventCalendarSelection,
47
+ EventCalendarSlotDraft,
48
+ EventCalendarSlotInfo,
49
+ EventCalendarState,
50
+ EventCalendarUpdateResult,
51
+ EventCalendarViewSettings,
52
+ } from "@/components/reui/event-calendar/event-calendar-types"
53
+ import { mergeProps } from "@base-ui/react/merge-props"
54
+ import { useRender } from "@base-ui/react/use-render"
55
+ import { addDays, type Locale } from "date-fns"
56
+
57
+ import { cn } from "cn"
58
+
59
+ const BASE_VIEWS: CalendarView[] = ["month", "week", "day", "days", "agenda"]
60
+ const ALL_VIEWS: CalendarView[] = [...BASE_VIEWS, "resource"]
61
+
62
+ const DEFAULT_INTERACTIONS: EventCalendarInteractions = {
63
+ drag: true,
64
+ resize: true,
65
+ selectSlot: true,
66
+ }
67
+
68
+ const EMPTY_SELECTION: EventCalendarSelection = { eventKeys: [], slot: null }
69
+
70
+ // resolveSettings runs on every render, so a freshly allocated default would
71
+ // bust the view memos keyed on it (the month grid rebuilds its 42 zoned day
72
+ // starts whenever weekendDays changes identity). Shared, never mutated.
73
+ const DEFAULT_WEEKEND_DAYS: number[] = [0, 6]
74
+ const EMPTY_RESOURCES: EventCalendarResource[] = []
75
+ const DEFAULT_EVENT_PRIORITY = (event: CalendarEvent<never>) =>
76
+ event.priority ?? 0
77
+
78
+ // Priority-aware default order, cached on the resolver identity: eventOrder is
79
+ // part of the index cache key, so a fresh closure per render would rebuild the
80
+ // whole index on every render.
81
+ const priorityOrderCache = new WeakMap<object, unknown>()
82
+ type EventOrder<TData> = (
83
+ a: EventCalendarOccurrence<TData>,
84
+ b: EventCalendarOccurrence<TData>
85
+ ) => number
86
+ function priorityEventOrder<TData>(
87
+ getEventPriority: (event: CalendarEvent<TData>) => number
88
+ ): EventOrder<TData> {
89
+ const cached = priorityOrderCache.get(getEventPriority)
90
+ if (cached) return cached as EventOrder<TData>
91
+ // higher priority packs and orders first; ties fall through to the
92
+ // start/duration/key default
93
+ const order: EventOrder<TData> = (a, b) =>
94
+ getEventPriority(b.event) - getEventPriority(a.event) ||
95
+ defaultEventOrder(a, b)
96
+ priorityOrderCache.set(getEventPriority, order)
97
+ return order
98
+ }
99
+
100
+ interface EventCalendarCallbacks<TData = unknown> {
101
+ onEventClick?: (
102
+ occurrence: EventCalendarOccurrence<TData>,
103
+ e: React.MouseEvent
104
+ ) => void
105
+ onEventDoubleClick?: (
106
+ occurrence: EventCalendarOccurrence<TData>,
107
+ e: React.MouseEvent
108
+ ) => void
109
+ onEventUpdate?: (
110
+ update: EventCalendarProposedUpdate<TData>
111
+ ) => EventCalendarUpdateResult
112
+ canDropEvent?: (update: EventCalendarProposedUpdate<TData>) => boolean
113
+ /**
114
+ * A move/resize was attempted on an event that cannot be dragged (readOnly,
115
+ * per-event draggable/resizable false, or the interaction is off). Fires once
116
+ * per gesture when the pointer crosses the activation threshold; the calendar
117
+ * shows a not-allowed cursor for the duration but never picks the message -
118
+ * broadcast it here so the consumer can surface a custom one.
119
+ */
120
+ onDragBlocked?: (
121
+ occurrence: EventCalendarOccurrence<TData>,
122
+ info: {
123
+ gesture: "move" | "resize"
124
+ reason: "readOnly" | "disabled" | "interactions-off"
125
+ }
126
+ ) => void
127
+ onSlotClick?: (slot: EventCalendarSlotInfo, e: React.MouseEvent) => void
128
+ onSelectSlot?: (slot: EventCalendarSlotDraft) => void
129
+ canSelectSlot?: (slot: EventCalendarSlotDraft) => boolean
130
+ onRangeChange?: (info: EventCalendarRangeInfo) => void
131
+ onViewChange?: (view: CalendarView) => void
132
+ onDateChange?: (date: Date) => void
133
+ onDayCountChange?: (count: number) => void
134
+ onSelectionChange?: (selection: EventCalendarSelection) => void
135
+ onInteractionsChange?: (interactions: EventCalendarInteractions) => void
136
+ onViewSettingsChange?: (viewSettings: EventCalendarViewSettings) => void
137
+ onEventsChange?: (events: CalendarEvent<TData>[]) => void
138
+ onMoreClick?: (
139
+ day: Date,
140
+ segments: EventCalendarOccurrence<TData>[],
141
+ e: React.MouseEvent
142
+ ) => void | false
143
+ }
144
+
145
+ interface UseEventCalendarStateOptions<
146
+ TData = unknown,
147
+ > extends EventCalendarCallbacks<TData> {
148
+ events?: CalendarEvent<TData>[]
149
+ defaultEvents?: CalendarEvent<TData>[]
150
+ view?: CalendarView
151
+ defaultView?: CalendarView
152
+ date?: Date
153
+ defaultDate?: Date
154
+ dayCount?: number
155
+ defaultDayCount?: number
156
+ selection?: EventCalendarSelection
157
+ defaultSelection?: EventCalendarSelection
158
+ interactions?: Partial<EventCalendarInteractions>
159
+ defaultInteractions?: Partial<EventCalendarInteractions>
160
+ viewSettings?: EventCalendarViewSettings
161
+ defaultViewSettings?: EventCalendarViewSettings
162
+ loading?: boolean
163
+ views?: CalendarView[]
164
+ timeZone?: string
165
+ locale?: Locale
166
+ weekStartsOn?: WeekStartsOn
167
+ dayStartHour?: number
168
+ dayEndHour?: number
169
+ slotDuration?: number
170
+ snapDuration?: number
171
+ agendaDayCount?: number
172
+ fixedWeeks?: boolean
173
+ showOutsideDays?: boolean
174
+ i18n?: EventCalendarI18nOverrides
175
+ /** Bookable resources for the resource view. */
176
+ resources?: EventCalendarResource[]
177
+ getEventPriority?: (event: CalendarEvent<TData>) => number
178
+ eventOrder?: (
179
+ a: EventCalendarOccurrence<TData>,
180
+ b: EventCalendarOccurrence<TData>
181
+ ) => number
182
+ getOccurrences?: (
183
+ event: CalendarEvent<TData>,
184
+ range: EventCalendarDateRange,
185
+ ctx: { timeZone: string }
186
+ ) => Array<{ start: Date; end: Date }> | null
187
+ /**
188
+ * Weekday numbers (0 = Sunday) treated as the weekend by the "weekends"
189
+ * view toggle. @default [0, 6]
190
+ */
191
+ weekendDays?: number[]
192
+ /** Pointer-gesture tuning (activation distances, touch delay, autoscroll). */
193
+ activation?: Partial<EventCalendarActivationConfig>
194
+ }
195
+
196
+ interface EventCalendarActivationConfig {
197
+ moveDistancePx: number
198
+ createDistancePx: number
199
+ touchDelayMs: number
200
+ touchTolerancePx: number
201
+ autoScrollEdgePx: number
202
+ autoScrollMaxStepPx: number
203
+ }
204
+
205
+ /**
206
+ * Resolved configuration: every UseEventCalendarStateOptions field except the
207
+ * controlled/uncontrolled state pairs, with defaults applied and i18n merged.
208
+ * Read via ref semantics - callback identity changes never re-render the grid.
209
+ */
210
+ interface EventCalendarSettings<
211
+ TData = unknown,
212
+ > extends EventCalendarCallbacks<TData> {
213
+ timeZone: string
214
+ locale?: Locale
215
+ weekStartsOn: WeekStartsOn
216
+ views: CalendarView[]
217
+ dayStartHour: number
218
+ dayEndHour: number
219
+ slotDuration: number
220
+ snapDuration: number
221
+ agendaDayCount: number
222
+ fixedWeeks: boolean
223
+ showOutsideDays: boolean
224
+ i18n: EventCalendarI18nConfig
225
+ resources: EventCalendarResource[]
226
+ weekendDays: number[]
227
+ activation?: Partial<EventCalendarActivationConfig>
228
+ getEventPriority: (event: CalendarEvent<TData>) => number
229
+ eventOrder: (
230
+ a: EventCalendarOccurrence<TData>,
231
+ b: EventCalendarOccurrence<TData>
232
+ ) => number
233
+ getOccurrences?: (
234
+ event: CalendarEvent<TData>,
235
+ range: EventCalendarDateRange,
236
+ ctx: { timeZone: string }
237
+ ) => Array<{ start: Date; end: Date }> | null
238
+ }
239
+
240
+ interface EventCalendarApi<TData = unknown> {
241
+ next(): void
242
+ prev(): void
243
+ today(): void
244
+ goTo(date: Date): void
245
+ setView(view: CalendarView, opts?: { dayCount?: number }): void
246
+ setDayCount(count: number): void
247
+ getEvents(): CalendarEvent<TData>[]
248
+ getEvent(id: EventCalendarEventId): CalendarEvent<TData> | undefined
249
+ setEvents(events: CalendarEvent<TData>[]): void
250
+ addEvent(event: CalendarEvent<TData>): void
251
+ updateEvent(
252
+ id: EventCalendarEventId,
253
+ patch: Partial<CalendarEvent<TData>>
254
+ ): void
255
+ removeEvent(id: EventCalendarEventId): void
256
+ getOccurrences(
257
+ range?: EventCalendarDateRange
258
+ ): EventCalendarOccurrence<TData>[]
259
+ getOccurrencesForDay(day: Date): EventCalendarOccurrence<TData>[]
260
+ findOverlapping(candidate: {
261
+ start: Date
262
+ end: Date
263
+ excludeEventId?: string
264
+ }): EventCalendarOccurrence<TData>[]
265
+ select(selection: Partial<EventCalendarSelection>): void
266
+ selectEvent(key: string, opts?: { additive?: boolean }): void
267
+ clearSelection(): void
268
+ setInteractions(patch: Partial<EventCalendarInteractions>): void
269
+ setViewSettings(patch: EventCalendarViewSettings): void
270
+ getVisibleRange(): EventCalendarDateRange
271
+ getActiveRange(): EventCalendarDateRange
272
+ /** TZDate in the calendar's display time zone. */
273
+ toZoned(date: Date): Date
274
+ /** number = minutes from the zoned day start; no-op outside time-grid views. */
275
+ scrollToTime(time: Date | number): void
276
+ }
277
+
278
+ /** Cross-file plumbing for sibling view/interaction modules; not public API. */
279
+ interface EventCalendarInternals<TData = unknown> {
280
+ getIndex(): EventCalendarIndex<TData>
281
+ setDrag(drag: EventCalendarDragState<TData> | null): void
282
+ setSlotDraft(draft: EventCalendarSlotDraft | null): void
283
+ registerScrollHandler(handler: ((time: Date | number) => void) | null): void
284
+ applyProposedUpdate(
285
+ update: EventCalendarProposedUpdate<TData>,
286
+ extraPatch?: Partial<CalendarEvent<TData>>
287
+ ): boolean
288
+ getSettingsVersion(): number
289
+ /** The rendered calendar root element, or null before mount. */
290
+ getRootEl(): HTMLElement | null
291
+ setRootEl(el: HTMLElement | null): void
292
+ }
293
+
294
+ interface EventCalendarInstance<TData = unknown> {
295
+ getState(): EventCalendarState<TData>
296
+ subscribe(listener: () => void): () => void
297
+ api: EventCalendarApi<TData>
298
+ settings: EventCalendarSettings<TData>
299
+ internals: EventCalendarInternals<TData>
300
+ }
301
+
302
+ function resolveSettings<TData>(
303
+ options: UseEventCalendarStateOptions<TData>
304
+ ): EventCalendarSettings<TData> {
305
+ const {
306
+ // strip state pairs; the rest flows into settings
307
+ events: _e,
308
+ defaultEvents: _de,
309
+ view: _v,
310
+ defaultView: _dv,
311
+ date: _d,
312
+ defaultDate: _dd,
313
+ dayCount: _dc,
314
+ defaultDayCount: _ddc,
315
+ selection: _s,
316
+ defaultSelection: _ds,
317
+ interactions: _i,
318
+ defaultInteractions: _di,
319
+ viewSettings: _vs,
320
+ defaultViewSettings: _dvs,
321
+ loading: _l,
322
+ ...rest
323
+ } = options
324
+ const getEventPriority =
325
+ options.getEventPriority ??
326
+ (DEFAULT_EVENT_PRIORITY as (event: CalendarEvent<TData>) => number)
327
+ return {
328
+ ...rest,
329
+ timeZone:
330
+ options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
331
+ locale: options.locale,
332
+ // locale-first default: a de/fr locale gets Monday weeks without also
333
+ // having to set weekStartsOn; an explicit weekStartsOn always wins
334
+ weekStartsOn:
335
+ options.weekStartsOn ?? options.locale?.options?.weekStartsOn ?? 0,
336
+ // the resource view only makes sense with resources configured
337
+ views:
338
+ options.views ?? (options.resources?.length ? ALL_VIEWS : BASE_VIEWS),
339
+ dayStartHour: options.dayStartHour ?? 0,
340
+ dayEndHour: options.dayEndHour ?? 24,
341
+ slotDuration: options.slotDuration ?? 30,
342
+ snapDuration: options.snapDuration ?? 15,
343
+ agendaDayCount: options.agendaDayCount ?? 30,
344
+ fixedWeeks: options.fixedWeeks ?? true,
345
+ showOutsideDays: options.showOutsideDays ?? true,
346
+ i18n: mergeEventCalendarI18n(options.i18n),
347
+ resources: options.resources ?? EMPTY_RESOURCES,
348
+ getEventPriority,
349
+ eventOrder: options.eventOrder ?? priorityEventOrder(getEventPriority),
350
+ getOccurrences: options.getOccurrences,
351
+ weekendDays: options.weekendDays ?? DEFAULT_WEEKEND_DAYS,
352
+ activation: options.activation,
353
+ }
354
+ }
355
+
356
+ const warned = new Set<string>()
357
+ function warnOnce(key: string, message: string) {
358
+ if (process.env.NODE_ENV !== "production" && !warned.has(key)) {
359
+ warned.add(key)
360
+ console.warn(`[event-calendar] ${message}`)
361
+ }
362
+ }
363
+
364
+ interface EventCalendarStore<TData> {
365
+ instance: EventCalendarInstance<TData>
366
+ setOptions(next: UseEventCalendarStateOptions<TData>): boolean
367
+ notify(): void
368
+ emitRangeIfChanged(): void
369
+ }
370
+
371
+ function createEventCalendarStore<TData>(
372
+ initial: UseEventCalendarStateOptions<TData>
373
+ ): EventCalendarStore<TData> {
374
+ let options = initial
375
+ let settings = resolveSettings(initial)
376
+ let settingsVersion = 0
377
+
378
+ const listeners = new Set<() => void>()
379
+
380
+ const resolveView = (view: CalendarView): CalendarView => {
381
+ if (settings.views.includes(view)) return view
382
+ const fallback = settings.views[0] ?? "month"
383
+ warnOnce(
384
+ `view-${view}`,
385
+ `view "${view}" is not in views [${settings.views.join(", ")}]; falling back to "${fallback}".`
386
+ )
387
+ return fallback
388
+ }
389
+
390
+ const internal = {
391
+ view: resolveView(initial.defaultView ?? "month"),
392
+ date: initial.defaultDate ?? new Date(),
393
+ dayCount: initial.defaultDayCount ?? 3,
394
+ events: initial.defaultEvents ?? [],
395
+ selection: initial.defaultSelection ?? EMPTY_SELECTION,
396
+ interactions: { ...DEFAULT_INTERACTIONS, ...initial.defaultInteractions },
397
+ viewSettings: initial.defaultViewSettings ?? {},
398
+ drag: null as EventCalendarDragState<TData> | null,
399
+ slotDraft: null as EventCalendarSlotDraft | null,
400
+ }
401
+
402
+ let snapshot: EventCalendarState<TData> | null = null
403
+ let indexCache: {
404
+ events: CalendarEvent<TData>[]
405
+ rangeKey: string
406
+ timeZone: string
407
+ weekStartsOn: WeekStartsOn
408
+ eventOrder: EventCalendarSettings<TData>["eventOrder"]
409
+ getOccurrences: EventCalendarSettings<TData>["getOccurrences"]
410
+ index: EventCalendarIndex<TData>
411
+ } | null = null
412
+ // Second single-entry cache, for api.getOccurrences(range) outside the
413
+ // visible range: that branch builds a throwaway index, so without it every
414
+ // call hands back brand new occurrence OBJECTS and the identity-based
415
+ // isEqual in useEventCalendarOccurrences can never settle - an unstable
416
+ // getSnapshot is a hard render loop under useSyncExternalStore, not a slow
417
+ // render.
418
+ let rangeCache: {
419
+ events: CalendarEvent<TData>[]
420
+ rangeKey: string
421
+ timeZone: string
422
+ weekStartsOn: WeekStartsOn
423
+ eventOrder: EventCalendarSettings<TData>["eventOrder"]
424
+ getOccurrences: EventCalendarSettings<TData>["getOccurrences"]
425
+ occurrences: EventCalendarOccurrence<TData>[]
426
+ } | null = null
427
+ let scrollHandler: ((time: Date | number) => void) | null = null
428
+ // The rendered calendar root element, registered by the <EventCalendar> host.
429
+ // The drag engine falls back to it to find day cells when a gesture starts
430
+ // from a portaled surface (e.g. a chip inside the "+N more" popover), whose
431
+ // DOM ancestors do not include the calendar.
432
+ let rootEl: HTMLElement | null = null
433
+ let lastEmittedRangeKey: string | null = null
434
+
435
+ // Controlled interactions merge, cached on input identity: rebuilding the
436
+ // merged object per snapshot would break Object.is for selector hooks.
437
+ let interactionsCache: {
438
+ input: Partial<EventCalendarInteractions>
439
+ merged: EventCalendarInteractions
440
+ } | null = null
441
+ const mergedInteractions = (
442
+ input: Partial<EventCalendarInteractions>
443
+ ): EventCalendarInteractions => {
444
+ if (interactionsCache?.input !== input) {
445
+ interactionsCache = {
446
+ input,
447
+ merged: { ...DEFAULT_INTERACTIONS, ...input },
448
+ }
449
+ }
450
+ return interactionsCache.merged
451
+ }
452
+
453
+ const invalidate = () => {
454
+ snapshot = null
455
+ }
456
+
457
+ const notify = () => {
458
+ listeners.forEach((listener) => listener())
459
+ emitRangeIfChanged()
460
+ }
461
+
462
+ const getState = (): EventCalendarState<TData> => {
463
+ if (snapshot) return snapshot
464
+ const view = resolveView(options.view ?? internal.view)
465
+ const date = options.date ?? internal.date
466
+ const dayCount = Math.max(1, options.dayCount ?? internal.dayCount)
467
+ const { visibleRange, activeRange } = getViewDateRange(view, date, {
468
+ timeZone: settings.timeZone,
469
+ weekStartsOn: settings.weekStartsOn,
470
+ dayCount,
471
+ agendaDayCount: settings.agendaDayCount,
472
+ fixedWeeks: settings.fixedWeeks,
473
+ })
474
+ snapshot = {
475
+ view,
476
+ date,
477
+ dayCount,
478
+ visibleRange,
479
+ activeRange,
480
+ events: options.events ?? internal.events,
481
+ selection: options.selection ?? internal.selection,
482
+ interactions: options.interactions
483
+ ? mergedInteractions(options.interactions)
484
+ : internal.interactions,
485
+ loading: options.loading ?? false,
486
+ drag: internal.drag,
487
+ slotDraft: internal.slotDraft,
488
+ viewSettings: options.viewSettings ?? internal.viewSettings,
489
+ }
490
+ return snapshot
491
+ }
492
+
493
+ const emitRangeIfChanged = () => {
494
+ if (!settings.onRangeChange) return
495
+ const state = getState()
496
+ const key = `${state.view}:${getRangeKey(state.visibleRange)}:${settings.timeZone}`
497
+ if (key === lastEmittedRangeKey) return
498
+ lastEmittedRangeKey = key
499
+ settings.onRangeChange({
500
+ range: state.visibleRange,
501
+ activeRange: state.activeRange,
502
+ view: state.view,
503
+ date: state.date,
504
+ timeZone: settings.timeZone,
505
+ })
506
+ }
507
+
508
+ type ControlledKey =
509
+ | "view"
510
+ | "date"
511
+ | "dayCount"
512
+ | "events"
513
+ | "selection"
514
+ | "interactions"
515
+ | "viewSettings"
516
+
517
+ const setField = <K extends ControlledKey>(
518
+ key: K,
519
+ value: EventCalendarState<TData>[K extends "events" ? "events" : K]
520
+ ) => {
521
+ const controlled = options[key] !== undefined
522
+ if (!controlled) {
523
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
524
+ ;(internal as any)[key] = value
525
+ invalidate()
526
+ }
527
+ const callbacks: Record<ControlledKey, ((v: never) => void) | undefined> = {
528
+ view: settings.onViewChange as never,
529
+ date: settings.onDateChange as never,
530
+ dayCount: settings.onDayCountChange as never,
531
+ events: settings.onEventsChange as never,
532
+ selection: settings.onSelectionChange as never,
533
+ interactions: settings.onInteractionsChange as never,
534
+ viewSettings: settings.onViewSettingsChange as never,
535
+ }
536
+ callbacks[key]?.(value as never)
537
+ if (!controlled) notify()
538
+ }
539
+
540
+ // An occurrence key encodes the start instant (id::startISO), so committing a
541
+ // move re-keys the occurrence and a selection holding the old key would point
542
+ // at nothing. Remapped in the same commit and emitted BEFORE the events write
543
+ // so a controlled consumer applies the two in a consistent order.
544
+ const remapSelectionKey = (
545
+ id: EventCalendarEventId,
546
+ oldKey: string,
547
+ nextStart: Date
548
+ ) => {
549
+ const newKey = `${id}::${nextStart.toISOString()}`
550
+ if (newKey === oldKey) return
551
+ const selection = getState().selection
552
+ if (!selection.eventKeys.includes(oldKey)) return
553
+ setField("selection", {
554
+ ...selection,
555
+ eventKeys: selection.eventKeys.map((key) =>
556
+ key === oldKey ? newKey : key
557
+ ),
558
+ })
559
+ }
560
+
561
+ // extraPatch: non-timing fields committed in the SAME write. Two sequential
562
+ // setField("events") calls break controlled mode - the second one re-reads
563
+ // the still-stale controlled array and its onEventsChange payload silently
564
+ // reverts the timing change the first one emitted.
565
+ const applyProposedUpdate = (
566
+ update: EventCalendarProposedUpdate<TData>,
567
+ extraPatch?: Partial<CalendarEvent<TData>>
568
+ ): boolean => {
569
+ const result = settings.onEventUpdate?.(update)
570
+ if (result === false) return false
571
+ const adjusted: Partial<CalendarEvent<TData>> =
572
+ result && typeof result === "object"
573
+ ? {
574
+ start: result.start ?? update.start,
575
+ end: result.end ?? update.end,
576
+ allDay: result.allDay ?? update.allDay,
577
+ }
578
+ : { start: update.start, end: update.end, allDay: update.allDay }
579
+ if (update.resourceId !== undefined) adjusted.resourceId = update.resourceId
580
+ // the STORED event holds the pre-commit start, which is what the live
581
+ // occurrence key was built from (update.event already carries the proposal
582
+ // when the call comes from api.updateEvent)
583
+ const stored = getState().events.find(
584
+ (event) => event.id === update.event.id
585
+ )
586
+ const oldKey =
587
+ update.occurrence?.key ??
588
+ (stored ? `${stored.id}::${stored.start.toISOString()}` : null)
589
+ if (oldKey) {
590
+ remapSelectionKey(update.event.id, oldKey, adjusted.start ?? update.start)
591
+ }
592
+ const events = getState().events
593
+ const next = events.map((event) =>
594
+ event.id === update.event.id
595
+ ? { ...event, ...extraPatch, ...adjusted }
596
+ : event
597
+ )
598
+ setField("events", next)
599
+ return true
600
+ }
601
+
602
+ const getIndex = (): EventCalendarIndex<TData> => {
603
+ const state = getState()
604
+ const rangeKey = getRangeKey(state.visibleRange)
605
+ if (
606
+ indexCache &&
607
+ indexCache.events === state.events &&
608
+ indexCache.rangeKey === rangeKey &&
609
+ indexCache.timeZone === settings.timeZone &&
610
+ indexCache.weekStartsOn === settings.weekStartsOn &&
611
+ indexCache.eventOrder === settings.eventOrder &&
612
+ indexCache.getOccurrences === settings.getOccurrences
613
+ ) {
614
+ return indexCache.index
615
+ }
616
+ const index = buildEventIndex(state.events, state.visibleRange, {
617
+ timeZone: settings.timeZone,
618
+ weekStartsOn: settings.weekStartsOn,
619
+ eventOrder: settings.eventOrder,
620
+ getOccurrences: settings.getOccurrences,
621
+ })
622
+ indexCache = {
623
+ events: state.events,
624
+ rangeKey,
625
+ timeZone: settings.timeZone,
626
+ weekStartsOn: settings.weekStartsOn,
627
+ eventOrder: settings.eventOrder,
628
+ getOccurrences: settings.getOccurrences,
629
+ index,
630
+ }
631
+ return index
632
+ }
633
+
634
+ const api: EventCalendarApi<TData> = {
635
+ next() {
636
+ const state = getState()
637
+ setField(
638
+ "date",
639
+ stepDate(state.view, state.date, 1, {
640
+ timeZone: settings.timeZone,
641
+ dayCount: state.dayCount,
642
+ agendaDayCount: settings.agendaDayCount,
643
+ })
644
+ )
645
+ },
646
+ prev() {
647
+ const state = getState()
648
+ setField(
649
+ "date",
650
+ stepDate(state.view, state.date, -1, {
651
+ timeZone: settings.timeZone,
652
+ dayCount: state.dayCount,
653
+ agendaDayCount: settings.agendaDayCount,
654
+ })
655
+ )
656
+ },
657
+ today() {
658
+ setField("date", new Date())
659
+ },
660
+ goTo(date) {
661
+ setField("date", date)
662
+ },
663
+ setView(view, opts) {
664
+ if (opts?.dayCount !== undefined) {
665
+ setField("dayCount", Math.max(1, opts.dayCount))
666
+ }
667
+ setField("view", resolveView(view))
668
+ },
669
+ setDayCount(count) {
670
+ setField("dayCount", Math.max(1, count))
671
+ },
672
+ getEvents() {
673
+ return getState().events
674
+ },
675
+ getEvent(id) {
676
+ return getState().events.find((event) => event.id === id)
677
+ },
678
+ setEvents(events) {
679
+ setField("events", events)
680
+ },
681
+ addEvent(event) {
682
+ setField("events", [...getState().events, event])
683
+ },
684
+ updateEvent(id, patch) {
685
+ const event = api.getEvent(id)
686
+ if (!event) return
687
+ const merged = { ...event, ...patch }
688
+ const timingChanged =
689
+ patch.start !== undefined ||
690
+ patch.end !== undefined ||
691
+ patch.allDay !== undefined
692
+ if (timingChanged && settings.onEventUpdate) {
693
+ // single write: the non-timing rest rides along as extraPatch so
694
+ // controlled mode sees one consistent onEventsChange payload
695
+ const rest = { ...patch }
696
+ delete rest.start
697
+ delete rest.end
698
+ delete rest.allDay
699
+ applyProposedUpdate(
700
+ {
701
+ event: merged,
702
+ occurrence: null,
703
+ start: merged.start,
704
+ end: merged.end,
705
+ allDay: merged.allDay ?? false,
706
+ source: "api",
707
+ },
708
+ rest
709
+ )
710
+ return
711
+ }
712
+ if (timingChanged) {
713
+ remapSelectionKey(
714
+ id,
715
+ `${id}::${event.start.toISOString()}`,
716
+ merged.start
717
+ )
718
+ }
719
+ setField(
720
+ "events",
721
+ getState().events.map((e) => (e.id === id ? merged : e))
722
+ )
723
+ },
724
+ removeEvent(id) {
725
+ setField(
726
+ "events",
727
+ getState().events.filter((event) => event.id !== id)
728
+ )
729
+ },
730
+ getOccurrences(range) {
731
+ if (!range) return getIndex().occurrences
732
+ const state = getState()
733
+ const within =
734
+ range.start >= state.visibleRange.start &&
735
+ range.end <= state.visibleRange.end
736
+ if (within) {
737
+ return getIndex().occurrences.filter((occ) => eventsOverlap(occ, range))
738
+ }
739
+ const rangeKey = getRangeKey(range)
740
+ if (
741
+ rangeCache &&
742
+ rangeCache.events === state.events &&
743
+ rangeCache.rangeKey === rangeKey &&
744
+ rangeCache.timeZone === settings.timeZone &&
745
+ rangeCache.weekStartsOn === settings.weekStartsOn &&
746
+ rangeCache.eventOrder === settings.eventOrder &&
747
+ rangeCache.getOccurrences === settings.getOccurrences
748
+ ) {
749
+ return rangeCache.occurrences
750
+ }
751
+ const { occurrences } = buildEventIndex(state.events, range, {
752
+ timeZone: settings.timeZone,
753
+ weekStartsOn: settings.weekStartsOn,
754
+ eventOrder: settings.eventOrder,
755
+ getOccurrences: settings.getOccurrences,
756
+ })
757
+ rangeCache = {
758
+ events: state.events,
759
+ rangeKey,
760
+ timeZone: settings.timeZone,
761
+ weekStartsOn: settings.weekStartsOn,
762
+ eventOrder: settings.eventOrder,
763
+ getOccurrences: settings.getOccurrences,
764
+ occurrences,
765
+ }
766
+ return occurrences
767
+ },
768
+ getOccurrencesForDay(day) {
769
+ const bucket = getIndex().byDay.get(getDayKey(day, settings.timeZone))
770
+ if (!bucket) return []
771
+ const seen = new Set<string>()
772
+ const result: EventCalendarOccurrence<TData>[] = []
773
+ for (const seg of [...bucket.allDay, ...bucket.timed]) {
774
+ if (seen.has(seg.occurrence.key)) continue
775
+ seen.add(seg.occurrence.key)
776
+ result.push(seg.occurrence)
777
+ }
778
+ return result
779
+ },
780
+ findOverlapping({ start, end, excludeEventId }) {
781
+ return api
782
+ .getOccurrences({ start, end })
783
+ .filter((occ) => occ.eventId !== excludeEventId)
784
+ },
785
+ select(partial) {
786
+ const current = getState().selection
787
+ setField("selection", {
788
+ eventKeys: partial.eventKeys ?? current.eventKeys,
789
+ slot: partial.slot !== undefined ? partial.slot : current.slot,
790
+ })
791
+ },
792
+ selectEvent(key, opts) {
793
+ const current = getState().selection
794
+ const eventKeys = opts?.additive
795
+ ? current.eventKeys.includes(key)
796
+ ? current.eventKeys.filter((k) => k !== key)
797
+ : [...current.eventKeys, key]
798
+ : [key]
799
+ setField("selection", { ...current, eventKeys })
800
+ },
801
+ clearSelection() {
802
+ setField("selection", EMPTY_SELECTION)
803
+ },
804
+ setInteractions(patch) {
805
+ setField("interactions", { ...getState().interactions, ...patch })
806
+ },
807
+ setViewSettings(patch) {
808
+ setField("viewSettings", { ...getState().viewSettings, ...patch })
809
+ },
810
+ getVisibleRange() {
811
+ return getState().visibleRange
812
+ },
813
+ getActiveRange() {
814
+ return getState().activeRange
815
+ },
816
+ toZoned(date) {
817
+ return toZoned(date, settings.timeZone)
818
+ },
819
+ scrollToTime(time) {
820
+ scrollHandler?.(time)
821
+ },
822
+ }
823
+
824
+ const internals: EventCalendarInternals<TData> = {
825
+ getIndex,
826
+ setDrag(drag) {
827
+ internal.drag = drag
828
+ invalidate()
829
+ notify()
830
+ },
831
+ setSlotDraft(draft) {
832
+ internal.slotDraft = draft
833
+ invalidate()
834
+ notify()
835
+ },
836
+ registerScrollHandler(handler) {
837
+ scrollHandler = handler
838
+ },
839
+ applyProposedUpdate,
840
+ getSettingsVersion() {
841
+ return settingsVersion
842
+ },
843
+ getRootEl() {
844
+ return rootEl
845
+ },
846
+ setRootEl(el) {
847
+ rootEl = el
848
+ },
849
+ }
850
+
851
+ const instance: EventCalendarInstance<TData> = {
852
+ getState,
853
+ subscribe(listener) {
854
+ listeners.add(listener)
855
+ return () => listeners.delete(listener)
856
+ },
857
+ api,
858
+ get settings() {
859
+ return settings
860
+ },
861
+ internals,
862
+ }
863
+
864
+ const STATE_KEYS = [
865
+ "events",
866
+ "view",
867
+ "date",
868
+ "dayCount",
869
+ "selection",
870
+ "interactions",
871
+ "viewSettings",
872
+ "loading",
873
+ ] as const
874
+ const SETTINGS_KEYS = [
875
+ "timeZone",
876
+ "locale",
877
+ "weekStartsOn",
878
+ "views",
879
+ "dayStartHour",
880
+ "dayEndHour",
881
+ "slotDuration",
882
+ "snapDuration",
883
+ "agendaDayCount",
884
+ "fixedWeeks",
885
+ "showOutsideDays",
886
+ "i18n",
887
+ "resources",
888
+ "getEventPriority",
889
+ "eventOrder",
890
+ "getOccurrences",
891
+ "weekendDays",
892
+ "activation",
893
+ ] as const
894
+
895
+ return {
896
+ instance,
897
+ setOptions(next) {
898
+ const prev = options
899
+ options = next
900
+ let changed = false
901
+ for (const key of STATE_KEYS) {
902
+ if (prev[key] !== next[key]) {
903
+ changed = true
904
+ break
905
+ }
906
+ }
907
+ let settingsChanged = false
908
+ for (const key of SETTINGS_KEYS) {
909
+ if (prev[key] !== next[key]) {
910
+ settingsChanged = true
911
+ break
912
+ }
913
+ }
914
+ settings = resolveSettings(next)
915
+ if (settingsChanged) {
916
+ settingsVersion++
917
+ changed = true
918
+ }
919
+ if (changed) invalidate()
920
+ return changed
921
+ },
922
+ notify,
923
+ emitRangeIfChanged,
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Headless root hook - the full calendar engine without any markup.
929
+ * Pass the returned instance to <EventCalendar calendar={instance}> or drive
930
+ * fully custom UI from instance.getState()/subscribe/api.
931
+ */
932
+ function useEventCalendarState<TData = unknown>(
933
+ options: UseEventCalendarStateOptions<TData> = {}
934
+ ): EventCalendarInstance<TData> {
935
+ const [store] = useState(() => createEventCalendarStore<TData>(options))
936
+ const changed = store.setOptions(options)
937
+ const changedRef = useRef(false)
938
+ if (changed) changedRef.current = true
939
+ useLayoutEffect(() => {
940
+ if (changedRef.current) {
941
+ changedRef.current = false
942
+ store.notify()
943
+ }
944
+ })
945
+ useEffect(() => {
946
+ store.emitRangeIfChanged()
947
+ // mount-only: onRangeChange fires once for the initial range
948
+ }, [])
949
+ return store.instance
950
+ }
951
+
952
+ const EventCalendarContext =
953
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
954
+ createContext<EventCalendarInstance<any> | null>(null)
955
+
956
+ /** The stable calendar instance; throws outside <EventCalendar>. */
957
+ function useEventCalendar<TData = unknown>(): EventCalendarInstance<TData> {
958
+ const instance = useContext(EventCalendarContext)
959
+ if (!instance) {
960
+ throw new Error("useEventCalendar must be used within <EventCalendar>")
961
+ }
962
+ return instance as EventCalendarInstance<TData>
963
+ }
964
+
965
+ interface UseEventCalendarSelectorOptions<TData, TSelected> {
966
+ calendar?: EventCalendarInstance<TData>
967
+ isEqual?: (a: TSelected, b: TSelected) => boolean
968
+ }
969
+
970
+ /** Fine-grained subscription with equality memoization (Object.is default). */
971
+ function useEventCalendarSelector<TData = unknown, TSelected = unknown>(
972
+ selector: (state: EventCalendarState<TData>) => TSelected,
973
+ options?: UseEventCalendarSelectorOptions<TData, TSelected>
974
+ ): TSelected {
975
+ const contextInstance = useContext(EventCalendarContext)
976
+ const instance = options?.calendar ?? contextInstance
977
+ if (!instance) {
978
+ throw new Error(
979
+ "useEventCalendarSelector needs an <EventCalendar> ancestor or an explicit `calendar` option"
980
+ )
981
+ }
982
+ const isEqual = options?.isEqual ?? Object.is
983
+ const lastRef = useRef<{ value: TSelected } | null>(null)
984
+ const selectorRef = useRef(selector)
985
+ selectorRef.current = selector
986
+
987
+ const getSnapshot = () => {
988
+ const next = selectorRef.current(
989
+ instance.getState() as EventCalendarState<TData>
990
+ )
991
+ if (lastRef.current && isEqual(lastRef.current.value, next)) {
992
+ return lastRef.current.value
993
+ }
994
+ lastRef.current = { value: next }
995
+ return next
996
+ }
997
+
998
+ return useSyncExternalStore(instance.subscribe, getSnapshot, getSnapshot)
999
+ }
1000
+
1001
+ function useEventCalendarView(): {
1002
+ view: CalendarView
1003
+ dayCount: number
1004
+ /** The resolved `views` option from settings. */
1005
+ availableViews: CalendarView[]
1006
+ setView: (view: CalendarView, opts?: { dayCount?: number }) => void
1007
+ } {
1008
+ const instance = useEventCalendar()
1009
+ const view = useEventCalendarSelector((state) => state.view)
1010
+ const dayCount = useEventCalendarSelector((state) => state.dayCount)
1011
+ useEventCalendarSettingsVersion(instance)
1012
+ return {
1013
+ view,
1014
+ dayCount,
1015
+ availableViews: instance.settings.views,
1016
+ setView: instance.api.setView,
1017
+ }
1018
+ }
1019
+
1020
+ /**
1021
+ * isToday is a wall-clock read, so no store write ever invalidates it and a
1022
+ * calendar left open overnight keeps highlighting yesterday. One timer per
1023
+ * display zone, armed for the next zoned midnight and re-armed on fire, wakes
1024
+ * every day-scoped hook exactly when the answer changes; an interval would
1025
+ * tick thousands of times a day to catch one transition.
1026
+ */
1027
+ const midnightTicker = (() => {
1028
+ const zones = new Map<
1029
+ string,
1030
+ { listeners: Set<() => void>; timer: ReturnType<typeof setTimeout> | null }
1031
+ >()
1032
+
1033
+ const arm = (timeZone: string) => {
1034
+ const entry = zones.get(timeZone)
1035
+ if (!entry) return
1036
+ const now = new Date()
1037
+ const next = zonedStartOfDay(
1038
+ addDays(toZoned(now, timeZone), 1),
1039
+ timeZone
1040
+ ).getTime()
1041
+ entry.timer = setTimeout(
1042
+ () => {
1043
+ entry.listeners.forEach((listener) => listener())
1044
+ arm(timeZone)
1045
+ },
1046
+ Math.max(1000, next - now.getTime())
1047
+ )
1048
+ }
1049
+
1050
+ return {
1051
+ subscribe(timeZone: string, listener: () => void) {
1052
+ let entry = zones.get(timeZone)
1053
+ if (!entry) {
1054
+ entry = { listeners: new Set(), timer: null }
1055
+ zones.set(timeZone, entry)
1056
+ entry.listeners.add(listener)
1057
+ arm(timeZone)
1058
+ } else {
1059
+ entry.listeners.add(listener)
1060
+ }
1061
+ const current = entry
1062
+ return () => {
1063
+ current.listeners.delete(listener)
1064
+ if (current.listeners.size > 0) return
1065
+ if (current.timer) clearTimeout(current.timer)
1066
+ zones.delete(timeZone)
1067
+ }
1068
+ },
1069
+ }
1070
+ })()
1071
+
1072
+ /**
1073
+ * Today's day key in `timeZone`, re-rendering the caller at the next midnight.
1074
+ * The clock is read in the snapshot, never in render: React Compiler caches a
1075
+ * render-time `new Date()`, so the wake-up re-rendered yesterday's answer.
1076
+ */
1077
+ function useTodayKey(timeZone: string): string {
1078
+ const subscribe = useCallback(
1079
+ (listener: () => void) => midnightTicker.subscribe(timeZone, listener),
1080
+ [timeZone]
1081
+ )
1082
+ const getSnapshot = useCallback(
1083
+ () => getDayKey(new Date(), timeZone),
1084
+ [timeZone]
1085
+ )
1086
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
1087
+ }
1088
+
1089
+ function useEventCalendarNavigation(): {
1090
+ date: Date
1091
+ /** i18n.functions.formatTitle output for the current view. */
1092
+ title: string
1093
+ visibleRange: EventCalendarDateRange
1094
+ activeRange: EventCalendarDateRange
1095
+ next: () => void
1096
+ prev: () => void
1097
+ today: () => void
1098
+ goTo: (date: Date) => void
1099
+ /** True when the anchor period contains now in the display time zone. */
1100
+ isToday: boolean
1101
+ } {
1102
+ const instance = useEventCalendar()
1103
+ const { settings } = instance
1104
+ const slice = useEventCalendarSelector(
1105
+ (state) => ({
1106
+ date: state.date,
1107
+ view: state.view,
1108
+ visibleRange: state.visibleRange,
1109
+ activeRange: state.activeRange,
1110
+ }),
1111
+ {
1112
+ isEqual: (a, b) =>
1113
+ a.date.getTime() === b.date.getTime() &&
1114
+ a.view === b.view &&
1115
+ getRangeKey(a.visibleRange) === getRangeKey(b.visibleRange),
1116
+ }
1117
+ )
1118
+ useEventCalendarSettingsVersion(instance)
1119
+ // The key only wakes this hook at midnight. The range test reads the clock
1120
+ // in a selector, which runs on every render whether compiled or not.
1121
+ useTodayKey(settings.timeZone)
1122
+ const isToday = useEventCalendarSelector((state) => {
1123
+ const now = new Date()
1124
+ return now >= state.activeRange.start && now < state.activeRange.end
1125
+ })
1126
+ return {
1127
+ date: slice.date,
1128
+ title: settings.i18n.functions.formatTitle(slice.view, {
1129
+ date: toZoned(slice.date, settings.timeZone),
1130
+ activeRange: slice.activeRange,
1131
+ visibleRange: slice.visibleRange,
1132
+ locale: settings.locale,
1133
+ }),
1134
+ visibleRange: slice.visibleRange,
1135
+ activeRange: slice.activeRange,
1136
+ next: instance.api.next,
1137
+ prev: instance.api.prev,
1138
+ today: instance.api.today,
1139
+ goTo: instance.api.goTo,
1140
+ isToday,
1141
+ }
1142
+ }
1143
+
1144
+ function useEventCalendarSelection(): {
1145
+ selection: EventCalendarSelection
1146
+ select: (selection: Partial<EventCalendarSelection>) => void
1147
+ selectEvent: (key: string, opts?: { additive?: boolean }) => void
1148
+ clearSelection: () => void
1149
+ } {
1150
+ const instance = useEventCalendar()
1151
+ const selection = useEventCalendarSelector((state) => state.selection)
1152
+ return {
1153
+ selection,
1154
+ select: instance.api.select,
1155
+ selectEvent: instance.api.selectEvent,
1156
+ clearSelection: instance.api.clearSelection,
1157
+ }
1158
+ }
1159
+
1160
+ function useEventCalendarInteractions(): {
1161
+ interactions: EventCalendarInteractions
1162
+ setInteractions: (patch: Partial<EventCalendarInteractions>) => void
1163
+ } {
1164
+ const instance = useEventCalendar()
1165
+ const interactions = useEventCalendarSelector((state) => state.interactions)
1166
+ return { interactions, setInteractions: instance.api.setInteractions }
1167
+ }
1168
+
1169
+ /** Expanded, sorted occurrences; defaults to the visible range. */
1170
+ function useEventCalendarOccurrences<TData = unknown>(
1171
+ range?: EventCalendarDateRange
1172
+ ): EventCalendarOccurrence<TData>[] {
1173
+ const instance = useEventCalendar<TData>()
1174
+ return useEventCalendarSelector<TData, EventCalendarOccurrence<TData>[]>(
1175
+ () => instance.api.getOccurrences(range),
1176
+ {
1177
+ calendar: instance,
1178
+ // element identity, not keys: keys encode id+start only, so an end-only
1179
+ // resize or a title/color/data edit would never invalidate a key-based
1180
+ // memo. The index rebuilds occurrence objects precisely when events
1181
+ // change, so identity is the correct (and cheapest) change signal.
1182
+ isEqual: (a, b) =>
1183
+ a === b || (a.length === b.length && a.every((occ, i) => occ === b[i])),
1184
+ }
1185
+ )
1186
+ }
1187
+
1188
+ const EMPTY_BUCKET: EventCalendarDayBucket = { allDay: [], timed: [] }
1189
+
1190
+ /** Per-cell subscription: only cells whose segments changed re-render. */
1191
+ function useEventCalendarDay<TData = unknown>(
1192
+ day: Date
1193
+ ): {
1194
+ segments: EventCalendarDayBucket<TData>
1195
+ isToday: boolean
1196
+ isOutside: boolean
1197
+ } {
1198
+ const instance = useEventCalendar<TData>()
1199
+ const { timeZone } = instance.settings
1200
+ const dayKey = getDayKey(day, timeZone)
1201
+
1202
+ const bucket = useEventCalendarSelector<TData, EventCalendarDayBucket<TData>>(
1203
+ () =>
1204
+ instance.internals.getIndex().byDay.get(dayKey) ??
1205
+ (EMPTY_BUCKET as EventCalendarDayBucket<TData>),
1206
+ {
1207
+ calendar: instance,
1208
+ // Element identity, not a content key: an occurrence key encodes
1209
+ // id+start only, so a title/color/data edit produced an identical key
1210
+ // and this cell kept serving its STALE segments. Buckets come straight
1211
+ // out of the memoized index, which rebuilds precisely when events
1212
+ // change, so identity is both correct and the cheapest signal.
1213
+ isEqual: (a, b) =>
1214
+ a === b ||
1215
+ (a.allDay.length === b.allDay.length &&
1216
+ a.timed.length === b.timed.length &&
1217
+ a.allDay.every((segment, i) => segment === b.allDay[i]) &&
1218
+ a.timed.every((segment, i) => segment === b.timed[i])),
1219
+ }
1220
+ )
1221
+ const activeRange = useEventCalendarSelector<TData, EventCalendarDateRange>(
1222
+ (state) => state.activeRange,
1223
+ {
1224
+ calendar: instance,
1225
+ isEqual: (a, b) => getRangeKey(a) === getRangeKey(b),
1226
+ }
1227
+ )
1228
+ const todayKey = useTodayKey(timeZone)
1229
+ const dayStart = zonedStartOfDay(day, timeZone)
1230
+ return {
1231
+ segments: bucket,
1232
+ isToday: todayKey === dayKey,
1233
+ isOutside: dayStart < activeRange.start || dayStart >= activeRange.end,
1234
+ }
1235
+ }
1236
+
1237
+ const EMPTY_BARS: EventCalendarSegment[] = []
1238
+
1239
+ /**
1240
+ * Per-week-row subscription for the month view: the laned multi-day/all-day
1241
+ * bar segments (one per occurrence per row, colStart/colSpan/lane set) that
1242
+ * render as continuous cross-day bars. `laneCount` is the row's bar height.
1243
+ * Matched by CONTAINMENT - any day inside the row resolves it - so a
1244
+ * weekends-hidden month (first visible day Monday) still finds its row;
1245
+ * `rowStart` returns the row's TRUE start for colStart/colSpan day math.
1246
+ */
1247
+ function useEventCalendarWeek<TData = unknown>(
1248
+ day: Date
1249
+ ): {
1250
+ bars: EventCalendarSegment<TData>[]
1251
+ laneCount: number
1252
+ rowStart: Date | null
1253
+ } {
1254
+ const instance = useEventCalendar<TData>()
1255
+ const { timeZone } = instance.settings
1256
+ const dayStartMs = zonedStartOfDay(day, timeZone).getTime()
1257
+
1258
+ const row = useEventCalendarSelector<
1259
+ TData,
1260
+ { bars: EventCalendarSegment<TData>[]; rowStart: Date | null }
1261
+ >(
1262
+ () => {
1263
+ const index = instance.internals.getIndex()
1264
+ const match = index.weekRows.find((r) => {
1265
+ const startMs = zonedStartOfDay(r.rowStart, timeZone).getTime()
1266
+ // calendar-aware row end: a fixed 168h window would let the first
1267
+ // day AFTER a spring-forward week (167h long) match the wrong row
1268
+ const endMs = zonedStartOfDay(
1269
+ addDays(toZoned(r.rowStart, timeZone), 7),
1270
+ timeZone
1271
+ ).getTime()
1272
+ return dayStartMs >= startMs && dayStartMs < endMs
1273
+ })
1274
+ return {
1275
+ bars: match?.bars ?? (EMPTY_BARS as EventCalendarSegment<TData>[]),
1276
+ rowStart: match?.rowStart ?? null,
1277
+ }
1278
+ },
1279
+ {
1280
+ calendar: instance,
1281
+ // Same reasoning as the day bucket above: `bars` is the array held by
1282
+ // the memoized index, so element identity catches content edits that a
1283
+ // key built from id+start could never see.
1284
+ isEqual: (a, b) =>
1285
+ (a.rowStart?.getTime() ?? 0) === (b.rowStart?.getTime() ?? 0) &&
1286
+ (a.bars === b.bars ||
1287
+ (a.bars.length === b.bars.length &&
1288
+ a.bars.every((segment, i) => segment === b.bars[i]))),
1289
+ }
1290
+ )
1291
+ const laneCount = row.bars.reduce((m, s) => Math.max(m, (s.lane ?? 0) + 1), 0)
1292
+ return { bars: row.bars, laneCount, rowStart: row.rowStart }
1293
+ }
1294
+
1295
+ /**
1296
+ * User view settings (weekends, week numbers, now line, off days, schedule
1297
+ * hint) + the effective values after falling back to the root view-config
1298
+ * props. Drives the nav submenu; fully controllable from outside via
1299
+ * `viewSettings`/`onViewSettingsChange` or api.setViewSettings.
1300
+ */
1301
+ function useEventCalendarViewSettings(): {
1302
+ viewSettings: EventCalendarViewSettings
1303
+ setViewSettings: (patch: EventCalendarViewSettings) => void
1304
+ effective: Required<EventCalendarViewSettings>
1305
+ } {
1306
+ const instance = useEventCalendar()
1307
+ const viewConfig = useEventCalendarViewConfig()
1308
+ const viewSettings = useEventCalendarSelector((state) => state.viewSettings)
1309
+ return {
1310
+ viewSettings,
1311
+ setViewSettings: instance.api.setViewSettings,
1312
+ effective: {
1313
+ weekends: viewSettings.weekends ?? true,
1314
+ weekNumbers: viewSettings.weekNumbers ?? viewConfig.showWeekNumbers,
1315
+ nowIndicator: viewSettings.nowIndicator ?? viewConfig.nowIndicator,
1316
+ offDays:
1317
+ viewSettings.offDays ??
1318
+ (viewConfig.offDays !== undefined && viewConfig.offDays !== false),
1319
+ },
1320
+ }
1321
+ }
1322
+
1323
+ /** Subscribes to settings changes only (version counter, not state). */
1324
+ function useEventCalendarSettingsVersion<TData>(
1325
+ instance: EventCalendarInstance<TData>
1326
+ ): number {
1327
+ return useSyncExternalStore(
1328
+ instance.subscribe,
1329
+ instance.internals.getSettingsVersion,
1330
+ instance.internals.getSettingsVersion
1331
+ )
1332
+ }
1333
+
1334
+ /** Resolved settings incl. merged i18n; re-renders only when settings change. */
1335
+ function useEventCalendarSettings<
1336
+ TData = unknown,
1337
+ >(): EventCalendarSettings<TData> {
1338
+ const instance = useEventCalendar<TData>()
1339
+ useEventCalendarSettingsVersion(instance)
1340
+ return instance.settings
1341
+ }
1342
+
1343
+ const EventCalendarViewContext = createContext<{ view: CalendarView } | null>(
1344
+ null
1345
+ )
1346
+
1347
+ /**
1348
+ * Per-element class hooks, cn()-merged AFTER the built-in classes (so tailwind
1349
+ * variants and ! overrides win). Metric CSS variables can ride on any parent
1350
+ * key, e.g. classNames.timeGrid: "[--ec-gutter-width:4.5rem]".
1351
+ */
1352
+ interface EventCalendarClassNames {
1353
+ nav?: string
1354
+ toolbar?: string
1355
+ content?: string
1356
+ monthView?: string
1357
+ monthCell?: string
1358
+ timeGrid?: string
1359
+ timeGutter?: string
1360
+ dayColumn?: string
1361
+ allDaySection?: string
1362
+ agendaView?: string
1363
+ event?: string
1364
+ /** The styled hover tooltip popup (viewConfig.eventTooltip). */
1365
+ eventTooltip?: string
1366
+ moreIndicator?: string
1367
+ /** The "+N more" popover panel. Control the on-demand scroll cap through
1368
+ * the CSS variable, e.g. "[--ec-more-max-height:20rem]". */
1369
+ morePopover?: string
1370
+ /** "+N more" popover day header row. */
1371
+ morePopoverHeader?: string
1372
+ // nav family (reachable without recomposing the default nav)
1373
+ navButton?: string
1374
+ title?: string
1375
+ navTooltip?: string
1376
+ viewSwitcherContent?: string
1377
+ viewSwitcherLabel?: string
1378
+ viewShortcut?: string
1379
+ datePickerContent?: string
1380
+ // month view
1381
+ monthHeader?: string
1382
+ monthDayHeader?: string
1383
+ monthBody?: string
1384
+ monthRow?: string
1385
+ weekNumber?: string
1386
+ monthBarOverlay?: string
1387
+ monthBar?: string
1388
+ monthCellContent?: string
1389
+ monthCellFooter?: string
1390
+ monthDayNumber?: string
1391
+ dayAddButton?: string
1392
+ // time grid / resource
1393
+ timeGridHeader?: string
1394
+ timeGutterLabel?: string
1395
+ allDayLabel?: string
1396
+ allDayCell?: string
1397
+ timedChip?: string
1398
+ resourceHeader?: string
1399
+ // interaction surfaces (shared by every view)
1400
+ dragGhost?: string
1401
+ dragCarry?: string
1402
+ dragCarryInvalid?: string
1403
+ dropHint?: string
1404
+ dropIndicator?: string
1405
+ slotDraft?: string
1406
+ resizeHandle?: string
1407
+ resizeGrip?: string
1408
+ // agenda
1409
+ noEvents?: string
1410
+ agendaDay?: string
1411
+ agendaDayHeader?: string
1412
+ agendaDayGutter?: string
1413
+ agendaDate?: string
1414
+ agendaDayToggle?: string
1415
+ agendaDayContent?: string
1416
+ agendaItem?: string
1417
+ agendaItemSurface?: string
1418
+ agendaItemToggle?: string
1419
+ agendaDaySummary?: string
1420
+ agendaSummaryDot?: string
1421
+ }
1422
+
1423
+ interface EventCalendarRenderEventProps<TData = unknown> {
1424
+ occurrence: EventCalendarOccurrence<TData>
1425
+ segment: EventCalendarSegment<TData>
1426
+ view: CalendarView
1427
+ isDragging: boolean
1428
+ isSelected: boolean
1429
+ }
1430
+
1431
+ /**
1432
+ * View-layer configuration: display props and render overrides. These live on
1433
+ * <EventCalendar> (and per-view components), never in the headless options.
1434
+ */
1435
+ interface EventCalendarViewConfig<TData = unknown> {
1436
+ scrollToHour: number
1437
+ nowIndicator: boolean
1438
+ /**
1439
+ * Grid interval in minutes for the time-based views (day, week, N-days,
1440
+ * time grid): gutter slots and gridlines follow it. Also accepted as a
1441
+ * prop on each view component.
1442
+ */
1443
+ interval: number
1444
+ maxEventsPerCell: number | "auto"
1445
+ showWeekNumbers: boolean
1446
+ enableShortcuts: boolean
1447
+ shortcutsScope: "focus-within" | "global"
1448
+ /**
1449
+ * "contained" (default): the calendar fills its container and views scroll
1450
+ * internally. "page": content flows with the document, the page scrolls,
1451
+ * and day headers stick below `--ec-sticky-offset` (default 0px).
1452
+ */
1453
+ scrollMode: "contained" | "page"
1454
+ /** Stick the default nav to the top while the page scrolls. */
1455
+ stickyNav: boolean
1456
+ /**
1457
+ * Custom per-day indication (light background classes work in both themes,
1458
+ * e.g. "bg-amber-500/10"). Applied to month cells, time-grid day columns,
1459
+ * and all-day cells; content stays readable on top of it.
1460
+ */
1461
+ dayClassName?: (day: Date) => string | undefined
1462
+ /**
1463
+ * Extra classes for the CURRENT day, appended after the built-in highlight
1464
+ * (primary-tinted background + accent top border) on month cells, time-grid
1465
+ * day columns, and day headers.
1466
+ */
1467
+ todayClassName?: string
1468
+ /**
1469
+ * Show a hover "+" add affordance on month cells next to the day number.
1470
+ * It fires the same onSlotClick as clicking the day. Calendar-level config
1471
+ * (consistent affordance, wired to the create flow); use renderMonthCell
1472
+ * when a fully custom cell is needed instead.
1473
+ */
1474
+ showDayAddButton: boolean
1475
+ /**
1476
+ * Scroll implementation for every internally scrolling surface (time grid,
1477
+ * agenda, time-grid resources, month "+N more" popover):
1478
+ * "custom" (default, shadcn ScrollArea) or "native" (browser scrollbars
1479
+ * via overflow auto).
1480
+ */
1481
+ scrollbars: "custom" | "native"
1482
+ /** Nav button variant; all nav buttons follow it. Default "ghost". */
1483
+ navButtonVariant: "ghost" | "outline" | "secondary" | "default"
1484
+ /** Nav button size; icon buttons use the icon twin. Default "sm". */
1485
+ navButtonSize: "sm" | "default"
1486
+ /**
1487
+ * Off-day (non-working day) marking. true = weekends with a muted
1488
+ * background; a config object customizes weekdays, explicit dates, a
1489
+ * predicate, and the marker class. Marked cells carry data-off.
1490
+ */
1491
+ offDays?: boolean | EventCalendarOffDaysConfig
1492
+ classNames?: EventCalendarClassNames
1493
+ components?: Partial<Record<CalendarView, ComponentType>>
1494
+ renderEvent?: (props: EventCalendarRenderEventProps<TData>) => ReactNode
1495
+ renderAgendaEvent?: (props: EventCalendarRenderEventProps<TData>) => ReactNode
1496
+ /**
1497
+ * Content for the styled hover tooltip (viewConfig.eventTooltip). Return a
1498
+ * falsy value (null / undefined, or the `false` a `cond && <node>` yields)
1499
+ * to fall back to the default label (title + time); `label` itself is
1500
+ * undefined when a consumer i18n.formatEventLabel opts out.
1501
+ */
1502
+ renderEventTooltip?: (props: {
1503
+ occurrence: EventCalendarOccurrence<TData>
1504
+ segment: EventCalendarSegment<TData>
1505
+ view: CalendarView
1506
+ label: string | undefined
1507
+ }) => ReactNode
1508
+ renderDragPreview?: (props: {
1509
+ drag: EventCalendarDragState<TData>
1510
+ }) => ReactNode
1511
+ renderMonthCell?: (props: {
1512
+ day: Date
1513
+ segments: EventCalendarDayBucket<TData>
1514
+ isToday: boolean
1515
+ isOutside: boolean
1516
+ overflowCount: number
1517
+ defaultContent: ReactNode
1518
+ }) => ReactNode
1519
+ /**
1520
+ * Time-grid business-logic layer, rendered pointer-events-none BEHIND event
1521
+ * segments in each day column. Position overlays with
1522
+ * top/height: calc(var(--ec-hour-height) * minutes / 60).
1523
+ */
1524
+ renderDayColumnBackground?: (props: {
1525
+ day: Date
1526
+ boundsStartMin: number
1527
+ boundsEndMin: number
1528
+ totalMinutes: number
1529
+ }) => ReactNode
1530
+ renderDayHeader?: (props: {
1531
+ day: Date
1532
+ view: CalendarView
1533
+ isToday: boolean
1534
+ }) => ReactNode
1535
+ renderTimeGutterSlot?: (props: {
1536
+ time: Date
1537
+ hour: number
1538
+ minute: number
1539
+ }) => ReactNode
1540
+ renderAllDaySection?: (props: {
1541
+ days: Date[]
1542
+ segments: EventCalendarSegment<TData>[]
1543
+ }) => ReactNode
1544
+ renderMoreIndicator?: (props: {
1545
+ day: Date
1546
+ count: number
1547
+ segments: EventCalendarSegment<TData>[]
1548
+ }) => ReactNode
1549
+ /**
1550
+ * Replaces the ENTIRE body of the built-in "+N more" popover (header +
1551
+ * chip list) while keeping its trigger and positioning; `close` dismisses
1552
+ * it. For a fully custom surface, return false from onMoreClick instead
1553
+ * and open your own UI.
1554
+ */
1555
+ renderMoreContent?: (props: {
1556
+ day: Date
1557
+ segments: EventCalendarSegment<TData>[]
1558
+ close: () => void
1559
+ }) => ReactNode
1560
+ /**
1561
+ * Agenda-only expandable details. When it returns a node for an occurrence,
1562
+ * the agenda row gains an expand/collapse toggle revealing the details
1563
+ * below the chip (the calendar itself never knows the consumer's fields).
1564
+ */
1565
+ renderAgendaEventDetails?: (
1566
+ occurrence: EventCalendarOccurrence<TData>
1567
+ ) => ReactNode
1568
+ renderNowIndicator?: (props: { time: Date }) => ReactNode
1569
+ renderNoEvents?: () => ReactNode
1570
+ /** Resource column header cell content; default is resource.title. */
1571
+ renderResourceHeader?: (props: {
1572
+ resource: EventCalendarResource
1573
+ }) => ReactNode
1574
+ /** Agenda date gutter (day badge + weekday + collapse toggle). */
1575
+ renderAgendaDayHeader?: (props: {
1576
+ day: Date
1577
+ collapsed: boolean
1578
+ count: number
1579
+ toggle: () => void
1580
+ defaultContent: ReactNode
1581
+ }) => ReactNode
1582
+ /** Collapsed agenda day summary row content. */
1583
+ renderAgendaDaySummary?: (props: {
1584
+ day: Date
1585
+ occurrences: EventCalendarOccurrence<TData>[]
1586
+ count: number
1587
+ expand: () => void
1588
+ defaultContent: ReactNode
1589
+ }) => ReactNode
1590
+ /**
1591
+ * N-day presets offered by the view switcher when the "days" view is
1592
+ * enabled. @default [5]
1593
+ */
1594
+ dayCountPresets: number[]
1595
+ /**
1596
+ * Nav tooltips: false disables them all; an object tunes placement and
1597
+ * timings. @default { side: "bottom", delay: 600, closeDelay: 0, timeout: 300 }
1598
+ */
1599
+ navTooltips?:
1600
+ | false
1601
+ | {
1602
+ side?: "top" | "bottom" | "left" | "right"
1603
+ delay?: number
1604
+ closeDelay?: number
1605
+ timeout?: number
1606
+ }
1607
+ /**
1608
+ * Styled tooltip on event hover / keyboard focus. `false` (default) keeps
1609
+ * only the native title attribute; `true` shows the standard Tooltip with
1610
+ * the event label; an object also tunes the side and open delay. Content is
1611
+ * overridable with renderEventTooltip. @default false
1612
+ */
1613
+ eventTooltip?:
1614
+ | boolean
1615
+ | {
1616
+ side?: "top" | "bottom" | "left" | "right"
1617
+ delay?: number
1618
+ }
1619
+ /**
1620
+ * Timed events shorter than this render the compact single-row chip layout.
1621
+ * @default 45
1622
+ */
1623
+ compactEventMinutes: number
1624
+ /** "+N more" popover alignment against its trigger. @default "start" */
1625
+ morePopoverAlign: "start" | "center" | "end"
1626
+ /** Now-indicator refresh cadence in milliseconds. @default 30000 */
1627
+ nowIndicatorInterval: number
1628
+ /** Max color dots in a collapsed agenda day summary. @default 6 */
1629
+ agendaSummaryMaxDots: number
1630
+ }
1631
+
1632
+ const DEFAULT_VIEW_CONFIG: EventCalendarViewConfig = {
1633
+ scrollToHour: 7,
1634
+ nowIndicator: true,
1635
+ interval: 60,
1636
+ maxEventsPerCell: "auto",
1637
+ showWeekNumbers: false,
1638
+ enableShortcuts: true,
1639
+ shortcutsScope: "focus-within",
1640
+ scrollMode: "contained",
1641
+ stickyNav: false,
1642
+ showDayAddButton: false,
1643
+ scrollbars: "custom",
1644
+ navButtonVariant: "ghost",
1645
+ navButtonSize: "sm",
1646
+ dayCountPresets: [5],
1647
+ eventTooltip: false,
1648
+ compactEventMinutes: 45,
1649
+ morePopoverAlign: "start",
1650
+ nowIndicatorInterval: 30_000,
1651
+ agendaSummaryMaxDots: 6,
1652
+ }
1653
+
1654
+ const EventCalendarViewConfigContext = createContext<
1655
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1656
+ EventCalendarViewConfig<any>
1657
+ >(DEFAULT_VIEW_CONFIG)
1658
+
1659
+ /** Root-level display props + render overrides, for view components. */
1660
+ function useEventCalendarViewConfig<
1661
+ TData = unknown,
1662
+ >(): EventCalendarViewConfig<TData> {
1663
+ return useContext(EventCalendarViewConfigContext)
1664
+ }
1665
+
1666
+ const VIEW_CONFIG_KEYS: Array<keyof EventCalendarViewConfig> = [
1667
+ "scrollToHour",
1668
+ "nowIndicator",
1669
+ "interval",
1670
+ "maxEventsPerCell",
1671
+ "showWeekNumbers",
1672
+ "enableShortcuts",
1673
+ "shortcutsScope",
1674
+ "scrollMode",
1675
+ "stickyNav",
1676
+ "dayClassName",
1677
+ "todayClassName",
1678
+ "showDayAddButton",
1679
+ "scrollbars",
1680
+ "navButtonVariant",
1681
+ "navButtonSize",
1682
+ "offDays",
1683
+ "classNames",
1684
+ "components",
1685
+ "renderEvent",
1686
+ "renderAgendaEvent",
1687
+ "renderEventTooltip",
1688
+ "renderDragPreview",
1689
+ "renderMonthCell",
1690
+ "renderDayColumnBackground",
1691
+ "renderDayHeader",
1692
+ "renderTimeGutterSlot",
1693
+ "renderAllDaySection",
1694
+ "renderMoreIndicator",
1695
+ "renderMoreContent",
1696
+ "renderNowIndicator",
1697
+ "renderNoEvents",
1698
+ "renderAgendaEventDetails",
1699
+ "renderResourceHeader",
1700
+ "renderAgendaDayHeader",
1701
+ "renderAgendaDaySummary",
1702
+ "dayCountPresets",
1703
+ "navTooltips",
1704
+ "eventTooltip",
1705
+ "compactEventMinutes",
1706
+ "morePopoverAlign",
1707
+ "nowIndicatorInterval",
1708
+ "agendaSummaryMaxDots",
1709
+ ]
1710
+
1711
+ /** The rendering view of the nearest view component ("month", "week", ...). */
1712
+ function useEventCalendarViewContext(): { view: CalendarView } {
1713
+ const ctx = useContext(EventCalendarViewContext)
1714
+ if (!ctx) {
1715
+ throw new Error(
1716
+ "useEventCalendarViewContext must be used inside a calendar view"
1717
+ )
1718
+ }
1719
+ return ctx
1720
+ }
1721
+
1722
+ interface EventCalendarProps<TData = unknown>
1723
+ extends
1724
+ UseEventCalendarStateOptions<TData>,
1725
+ Partial<EventCalendarViewConfig<TData>>,
1726
+ Omit<useRender.ComponentProps<"div">, "children" | "defaultValue"> {
1727
+ /** Adopt a hoisted useEventCalendarState instance; option props are then ignored. */
1728
+ calendar?: EventCalendarInstance<TData>
1729
+ /** Imperative escape hatch usable from outside the tree. */
1730
+ apiRef?: RefObject<EventCalendarApi<TData> | null>
1731
+ children?: ReactNode
1732
+ }
1733
+
1734
+ const OPTION_KEYS: Array<keyof UseEventCalendarStateOptions> = [
1735
+ "events",
1736
+ "defaultEvents",
1737
+ "view",
1738
+ "defaultView",
1739
+ "date",
1740
+ "defaultDate",
1741
+ "dayCount",
1742
+ "defaultDayCount",
1743
+ "selection",
1744
+ "defaultSelection",
1745
+ "interactions",
1746
+ "defaultInteractions",
1747
+ "viewSettings",
1748
+ "defaultViewSettings",
1749
+ "loading",
1750
+ "views",
1751
+ "timeZone",
1752
+ "locale",
1753
+ "weekStartsOn",
1754
+ "dayStartHour",
1755
+ "dayEndHour",
1756
+ "slotDuration",
1757
+ "snapDuration",
1758
+ "agendaDayCount",
1759
+ "fixedWeeks",
1760
+ "showOutsideDays",
1761
+ "i18n",
1762
+ "resources",
1763
+ "getEventPriority",
1764
+ "eventOrder",
1765
+ "getOccurrences",
1766
+ "weekendDays",
1767
+ "activation",
1768
+ "onEventClick",
1769
+ "onEventDoubleClick",
1770
+ "onEventUpdate",
1771
+ "canDropEvent",
1772
+ "onDragBlocked",
1773
+ "onSlotClick",
1774
+ "onSelectSlot",
1775
+ "canSelectSlot",
1776
+ "onRangeChange",
1777
+ "onViewChange",
1778
+ "onDateChange",
1779
+ "onDayCountChange",
1780
+ "onSelectionChange",
1781
+ "onInteractionsChange",
1782
+ "onViewSettingsChange",
1783
+ "onEventsChange",
1784
+ "onMoreClick",
1785
+ ]
1786
+
1787
+ function splitOptions<TData>(props: Record<string, unknown>): {
1788
+ options: UseEventCalendarStateOptions<TData>
1789
+ viewConfig: EventCalendarViewConfig<TData>
1790
+ rest: Record<string, unknown>
1791
+ } {
1792
+ const options: Record<string, unknown> = {}
1793
+ const viewConfig: Record<string, unknown> = { ...DEFAULT_VIEW_CONFIG }
1794
+ const rest: Record<string, unknown> = {}
1795
+ for (const [key, value] of Object.entries(props)) {
1796
+ if ((OPTION_KEYS as string[]).includes(key)) options[key] = value
1797
+ else if ((VIEW_CONFIG_KEYS as string[]).includes(key)) {
1798
+ if (value !== undefined) viewConfig[key] = value
1799
+ } else rest[key] = value
1800
+ }
1801
+ return {
1802
+ options: options as UseEventCalendarStateOptions<TData>,
1803
+ viewConfig: viewConfig as unknown as EventCalendarViewConfig<TData>,
1804
+ rest,
1805
+ }
1806
+ }
1807
+
1808
+ /**
1809
+ * Root provider + container. Composition contract:
1810
+ * <EventCalendar><EventCalendarNav/><EventCalendarToolbar/><EventCalendarContent/></EventCalendar>
1811
+ */
1812
+ function EventCalendar<TData = unknown>({
1813
+ calendar,
1814
+ apiRef,
1815
+ className,
1816
+ render,
1817
+ children,
1818
+ ...props
1819
+ }: EventCalendarProps<TData>) {
1820
+ const { options, viewConfig, rest } = splitOptions<TData>(
1821
+ props as Record<string, unknown>
1822
+ )
1823
+
1824
+ if (calendar && Object.keys(options).length > 0) {
1825
+ warnOnce(
1826
+ "calendar-and-options",
1827
+ "both `calendar` and option props were passed; option props are ignored when adopting an instance."
1828
+ )
1829
+ }
1830
+
1831
+ const own = useEventCalendarState<TData>(calendar ? {} : options)
1832
+ const instance = calendar ?? own
1833
+
1834
+ useEffect(() => {
1835
+ if (apiRef) apiRef.current = instance.api
1836
+ }, [apiRef, instance])
1837
+
1838
+ // Register the root element so the drag engine can find day cells even when a
1839
+ // gesture starts from a portaled surface (the "+N more" popover).
1840
+ const registerRoot = useCallback(
1841
+ (el: HTMLElement | null) => instance.internals.setRootEl(el),
1842
+ [instance]
1843
+ )
1844
+
1845
+ const defaultProps = {
1846
+ "data-slot": "event-calendar",
1847
+ ref: registerRoot,
1848
+ // text-xs is the calendar-wide default type size; because it sits before
1849
+ // `className`, a consumer can override the whole scale with e.g.
1850
+ // <EventCalendar className="text-sm"> and every inheriting element follows.
1851
+ // Inner elements omit their own text-size so they cascade from here (the
1852
+ // few portaled surfaces - "+N more" popover, drag carry clone - pin the
1853
+ // size explicitly since DOM inheritance does not cross a portal).
1854
+ className: cn("flex min-h-0 min-w-0 flex-col text-xs", className),
1855
+ children: (
1856
+ <>
1857
+ {children}
1858
+ <div
1859
+ data-slot="event-calendar-announcer"
1860
+ aria-live="polite"
1861
+ className="sr-only"
1862
+ />
1863
+ </>
1864
+ ),
1865
+ }
1866
+
1867
+ return (
1868
+ <EventCalendarContext.Provider value={instance}>
1869
+ <EventCalendarViewConfigContext.Provider value={viewConfig}>
1870
+ {useRender({
1871
+ defaultTagName: "div",
1872
+ render,
1873
+ props: mergeProps<"div">(defaultProps, rest),
1874
+ })}
1875
+ </EventCalendarViewConfigContext.Provider>
1876
+ </EventCalendarContext.Provider>
1877
+ )
1878
+ }
1879
+
1880
+ export {
1881
+ ALL_VIEWS,
1882
+ BASE_VIEWS,
1883
+ DEFAULT_VIEW_CONFIG,
1884
+ EventCalendar,
1885
+ EventCalendarContext,
1886
+ EventCalendarViewConfigContext,
1887
+ EventCalendarViewContext,
1888
+ useEventCalendar,
1889
+ useEventCalendarDay,
1890
+ useEventCalendarWeek,
1891
+ useEventCalendarInteractions,
1892
+ useEventCalendarNavigation,
1893
+ useEventCalendarOccurrences,
1894
+ useEventCalendarSelection,
1895
+ useEventCalendarSelector,
1896
+ useEventCalendarSettings,
1897
+ useEventCalendarSettingsVersion,
1898
+ useEventCalendarState,
1899
+ useEventCalendarView,
1900
+ useEventCalendarViewConfig,
1901
+ useEventCalendarViewContext,
1902
+ useEventCalendarViewSettings,
1903
+ }
1904
+ export type {
1905
+ EventCalendarActivationConfig,
1906
+ EventCalendarApi,
1907
+ EventCalendarCallbacks,
1908
+ EventCalendarClassNames,
1909
+ EventCalendarInstance,
1910
+ EventCalendarInternals,
1911
+ EventCalendarProps,
1912
+ EventCalendarRenderEventProps,
1913
+ EventCalendarSettings,
1914
+ EventCalendarViewConfig,
1915
+ UseEventCalendarStateOptions,
1916
+ }