@ecsplico/editor 2.0.0 → 2.1.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 (278) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -3
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/surface-session.js +139 -0
  57. package/dist/core/surface-session.js.map +1 -0
  58. package/dist/document/DocumentSurface.d.ts +1 -1
  59. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  60. package/dist/features/ai/agent-op-review.d.ts +2 -2
  61. package/dist/features/ai/agent-op-review.js.map +1 -1
  62. package/dist/features/ai/agent-ops.d.ts +1 -1
  63. package/dist/features/ai/agent-ops.js +9 -6
  64. package/dist/features/ai/agent-ops.js.map +1 -1
  65. package/dist/features/ai/ai-apply.d.ts +1 -1
  66. package/dist/features/ai/ai-review.js +10 -1
  67. package/dist/features/ai/ai-review.js.map +1 -1
  68. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  69. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  71. package/dist/features/ai/components/AiSidebar.js +2 -2
  72. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  73. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  74. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  75. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  76. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  77. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  78. package/dist/features/ai/document-chat.d.ts +3 -2
  79. package/dist/features/ai/document-chat.js +16 -19
  80. package/dist/features/ai/document-chat.js.map +1 -1
  81. package/dist/features/ai/index.d.ts +5 -3
  82. package/dist/features/ai/index.js +4 -2
  83. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  84. package/dist/features/ai/protocol/agent-ops.js +65 -39
  85. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  86. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  87. package/dist/features/ai/protocol/block-lines.js +177 -0
  88. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  89. package/dist/features/ai/protocol/chat.js +10 -10
  90. package/dist/features/ai/protocol/chat.js.map +1 -1
  91. package/dist/features/ai/protocol/index.d.ts +3 -2
  92. package/dist/features/ai/protocol/index.js +3 -2
  93. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  94. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  95. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  96. package/dist/features/ai/react/AiSidebar.js +2 -2
  97. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  98. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  99. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  100. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  101. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  102. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  103. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  104. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  105. package/dist/features/ai/selection-flow.d.ts +133 -0
  106. package/dist/features/ai/selection-flow.js +283 -0
  107. package/dist/features/ai/selection-flow.js.map +1 -0
  108. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  109. package/dist/features/ai/use-document-chat.d.ts +1 -1
  110. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  111. package/dist/features/authors/feature.js +1 -1
  112. package/dist/features/authors/index.js +1 -1
  113. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  114. package/dist/features/comments/components/CommentBubble.js +1 -1
  115. package/dist/features/comments/components/CommentComposer.js +1 -1
  116. package/dist/features/comments/components/CommentThreadView.js +5 -2
  117. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  118. package/dist/features/comments/components/CommentsMargin.js +108 -230
  119. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  120. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  121. package/dist/features/comments/feature.js +1 -1
  122. package/dist/features/comments/index.d.ts +4 -2
  123. package/dist/features/comments/index.js +4 -2
  124. package/dist/features/comments/margin-controller.d.ts +44 -0
  125. package/dist/features/comments/margin-controller.js +132 -0
  126. package/dist/features/comments/margin-controller.js.map +1 -0
  127. package/dist/features/comments/margin-layout.d.ts +55 -0
  128. package/dist/features/comments/margin-layout.js +79 -0
  129. package/dist/features/comments/margin-layout.js.map +1 -0
  130. package/dist/features/comments/model.d.ts +8 -0
  131. package/dist/features/comments/model.js +60 -34
  132. package/dist/features/comments/model.js.map +1 -1
  133. package/dist/features/comments/react/CommentBubble.js +1 -1
  134. package/dist/features/comments/react/CommentComposer.js +1 -1
  135. package/dist/features/comments/react/CommentThreadView.js +2 -2
  136. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  137. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  138. package/dist/features/comments/react/CommentsMargin.js +85 -251
  139. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  140. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  141. package/dist/features/comments/react/use-comments.js +1 -1
  142. package/dist/features/comments/react/use-margin-layout.js +45 -0
  143. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  144. package/dist/features/comments/react.d.ts +1 -1
  145. package/dist/features/comments/schema.d.ts +23 -0
  146. package/dist/features/comments/schema.js.map +1 -1
  147. package/dist/features/comments/snapshot-equality.js +40 -0
  148. package/dist/features/comments/snapshot-equality.js.map +1 -0
  149. package/dist/features/comments/thread-anchor.js +33 -0
  150. package/dist/features/comments/thread-anchor.js.map +1 -0
  151. package/dist/features/comments/use-comments.d.ts +3 -1
  152. package/dist/features/comments/use-comments.js +1 -0
  153. package/dist/features/comments/use-comments.js.map +1 -1
  154. package/dist/features/comments/use-margin-layout.js +29 -0
  155. package/dist/features/comments/use-margin-layout.js.map +1 -0
  156. package/dist/features/comments/vue.d.ts +1 -1
  157. package/dist/features/composites/accepted-children.d.ts +21 -0
  158. package/dist/features/composites/accepted-children.js +126 -0
  159. package/dist/features/composites/accepted-children.js.map +1 -0
  160. package/dist/features/composites/components/CompositeDetails.js +1 -1
  161. package/dist/features/composites/components/CompositeInspector.js +38 -164
  162. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  163. package/dist/features/composites/feature.d.ts +2 -1
  164. package/dist/features/composites/feature.js +8 -2
  165. package/dist/features/composites/feature.js.map +1 -1
  166. package/dist/features/composites/index.d.ts +5 -3
  167. package/dist/features/composites/index.js +8 -6
  168. package/dist/features/composites/inspector-model.d.ts +97 -0
  169. package/dist/features/composites/inspector-model.js +255 -0
  170. package/dist/features/composites/inspector-model.js.map +1 -0
  171. package/dist/features/composites/kinds.d.ts +11 -0
  172. package/dist/features/composites/kinds.js +17 -2
  173. package/dist/features/composites/kinds.js.map +1 -1
  174. package/dist/features/composites/outline.d.ts +18 -5
  175. package/dist/features/composites/outline.js +44 -12
  176. package/dist/features/composites/outline.js.map +1 -1
  177. package/dist/features/composites/react/CompositeDetails.js +1 -1
  178. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  179. package/dist/features/composites/react/CompositeInspector.js +21 -180
  180. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  181. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  182. package/dist/features/composites/react/CompositeView.js +1 -1
  183. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  184. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  185. package/dist/features/composites/schema.d.ts +2 -2
  186. package/dist/features/composites/schema.js +2 -2
  187. package/dist/features/composites/schema.js.map +1 -1
  188. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  189. package/dist/features/composites/use-composite-inspector.js +41 -0
  190. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  191. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  192. package/dist/features/media/index.d.ts +1 -1
  193. package/dist/features/media/vue.d.ts +1 -1
  194. package/dist/features/mentions/index.d.ts +3 -3
  195. package/dist/features/mentions/index.js +3 -3
  196. package/dist/features/mentions/schema.d.ts +1 -1
  197. package/dist/features/mentions/schema.js +1 -1
  198. package/dist/features/themes/index.d.ts +1 -1
  199. package/dist/features/themes/index.js +1 -1
  200. package/dist/features/transcript/feature.js +1 -1
  201. package/dist/features/transcript/index.js +2 -2
  202. package/dist/i18n/de/ai.js +2 -0
  203. package/dist/i18n/de/ai.js.map +1 -1
  204. package/dist/i18n/de/composites.js +8 -1
  205. package/dist/i18n/de/composites.js.map +1 -1
  206. package/dist/i18n/de/tree.js +14 -1
  207. package/dist/i18n/de/tree.js.map +1 -1
  208. package/dist/i18n/de.d.ts +22 -0
  209. package/dist/i18n/en/ai.js +2 -0
  210. package/dist/i18n/en/ai.js.map +1 -1
  211. package/dist/i18n/en/composites.js +8 -1
  212. package/dist/i18n/en/composites.js.map +1 -1
  213. package/dist/i18n/en/tree.js +14 -1
  214. package/dist/i18n/en/tree.js.map +1 -1
  215. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  216. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  217. package/dist/integrations/composite-ai/index.js +2 -2
  218. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  219. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  220. package/dist/markdown/MarkdownSurface.js +2 -2
  221. package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
  222. package/dist/markdown/index.d.ts +2 -2
  223. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  224. package/dist/react/EditorSurface.d.ts +1 -1
  225. package/dist/react/EditorSurface.js +43 -149
  226. package/dist/react/EditorSurface.js.map +1 -1
  227. package/dist/react/EditorToolbar.js +3 -40
  228. package/dist/react/EditorToolbar.js.map +1 -1
  229. package/dist/schema/advanced.js +2 -2
  230. package/dist/server/adapter.d.ts +2 -2
  231. package/dist/server/collab.js +1 -1
  232. package/dist/server/handlers/ai.d.ts +4 -2
  233. package/dist/server/handlers/ai.js +11 -9
  234. package/dist/server/handlers/ai.js.map +1 -1
  235. package/dist/server/handlers/context.d.ts +1 -1
  236. package/dist/server/handlers/editor.d.ts +1 -1
  237. package/dist/server/handlers/editor.js +2 -2
  238. package/dist/server/host.d.ts +1 -1
  239. package/dist/server/index.d.ts +2 -2
  240. package/dist/settings/index.d.ts +1 -1
  241. package/dist/styles/tokens.css +1 -1
  242. package/dist/tree/DocumentTree.js +159 -387
  243. package/dist/tree/DocumentTree.js.map +1 -1
  244. package/dist/tree/drop-rules.js +37 -0
  245. package/dist/tree/drop-rules.js.map +1 -0
  246. package/dist/tree/react/DocumentTree.d.ts +2 -2
  247. package/dist/tree/react/DocumentTree.js +135 -397
  248. package/dist/tree/react/DocumentTree.js.map +1 -1
  249. package/dist/tree/react/use-tree-model.js +129 -0
  250. package/dist/tree/react/use-tree-model.js.map +1 -0
  251. package/dist/tree/react.js +1 -1
  252. package/dist/tree/tree-host.js +100 -0
  253. package/dist/tree/tree-host.js.map +1 -0
  254. package/dist/tree/tree-items.js +22 -43
  255. package/dist/tree/tree-items.js.map +1 -1
  256. package/dist/tree/tree-model.js +580 -0
  257. package/dist/tree/tree-model.js.map +1 -0
  258. package/dist/tree/tree-rows.js +77 -0
  259. package/dist/tree/tree-rows.js.map +1 -0
  260. package/dist/tree/use-tree-model.js +94 -0
  261. package/dist/tree/use-tree-model.js.map +1 -0
  262. package/dist/tree/vue.js +1 -1
  263. package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
  264. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  265. package/dist/vue/components/EditorSurface.js +27 -87
  266. package/dist/vue/components/EditorSurface.js.map +1 -1
  267. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  268. package/dist/vue/components/EditorToolbar.js +7 -44
  269. package/dist/vue/components/EditorToolbar.js.map +1 -1
  270. package/dist/vue/context.d.ts +1 -1
  271. package/dist/vue/context.js +1 -1
  272. package/dist/vue/context.js.map +1 -1
  273. package/dist/vue/use-editor-ui.d.ts +1 -1
  274. package/dist/vue/use-notifier.d.ts +1 -1
  275. package/docs/integration.md +97 -4
  276. package/docs/llms.md +9 -1
  277. package/docs/usage.md +10 -5
  278. package/package.json +67 -5
@@ -1,6 +1,8 @@
1
1
  import { COMMENTS_LAYOUTS } from "../../settings/values.js";
2
- import { Comments, cancelCommentDraft, commentsAvailable, commentsCanComment, commentsDoc, commentsKey, commentsState, commentsUser, setActiveComment, startCommentDraft } from "./comments-plugin.js";
3
2
  import { COMMENTS_FIELD, COMMENT_LIMITS, CommentMark, commentsSchemaExtensions } from "./schema.js";
3
+ import { Comments, cancelCommentDraft, commentsAvailable, commentsCanComment, commentsDoc, commentsKey, commentsState, commentsUser, setActiveComment, startCommentDraft } from "./comments-plugin.js";
4
4
  import { commentsModelOf, createCommentsModel, createEditorCommentsModel } from "./model.js";
5
5
  import { commentsFeatureCore } from "./feature.js";
6
- export { COMMENTS_FIELD, COMMENTS_LAYOUTS, COMMENT_LIMITS, CommentMark, Comments, cancelCommentDraft, commentsAvailable, commentsCanComment, commentsDoc, commentsFeatureCore, commentsKey, commentsModelOf, commentsSchemaExtensions, commentsState, commentsUser, createCommentsModel, createEditorCommentsModel, setActiveComment, startCommentDraft };
6
+ import { DRAFT_CARD, MARGIN_GAP, layoutMargin, marginCards } from "./margin-layout.js";
7
+ import { createMarginController } from "./margin-controller.js";
8
+ export { COMMENTS_FIELD, COMMENTS_LAYOUTS, COMMENT_LIMITS, CommentMark, Comments, DRAFT_CARD, MARGIN_GAP, cancelCommentDraft, commentsAvailable, commentsCanComment, commentsDoc, commentsFeatureCore, commentsKey, commentsModelOf, commentsSchemaExtensions, commentsState, commentsUser, createCommentsModel, createEditorCommentsModel, createMarginController, layoutMargin, marginCards, setActiveComment, startCommentDraft };
@@ -0,0 +1,44 @@
1
+ import { ReadonlyStore } from "../../core/store.js";
2
+ import { MarginCard } from "./margin-layout.js";
3
+ import { Editor } from "@tiptap/core";
4
+ //#region src/features/comments/margin-controller.d.ts
5
+ /** What the margin renders from the measurements. */
6
+ export interface MarginState {
7
+ /** Top of every card by id, relative to the column; empty until the first measurement (cards stay invisible till then). */
8
+ tops: ReadonlyMap<string, number>;
9
+ /** Top of the section below the cards (the resolved threads) and the column's minimum height. */
10
+ resolvedTop: number;
11
+ minHeight: number;
12
+ }
13
+ /** What the controller lays out, read afresh at every measurement. */
14
+ export interface MarginSource {
15
+ /** The cards in document order. */
16
+ cards: () => readonly MarginCard[];
17
+ /** The card that owns its line (the draft composer or the active thread), if any. */
18
+ activeId: () => string | null;
19
+ }
20
+ /**
21
+ * The measuring and scheduling glue of the comments margin, for both frameworks: where each card's line is
22
+ * (`coordsAtPos`), how tall each card is, one `layoutMargin` per animation frame, re-run after transactions, window and
23
+ * element resizes. Created inert; `connect()` starts it (call it on mount) and `disconnect()` stops it, and both may
24
+ * follow each other again (StrictMode mounts twice). Elements are handed in with the `set*` functions (framework
25
+ * refs), the editor and container with `setEditor` / `setContainer`; none of them needs to exist before `connect()`.
26
+ */
27
+ export interface MarginController {
28
+ readonly state: ReadonlyStore<MarginState>;
29
+ setEditor: (editor: Editor | undefined) => void;
30
+ /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */
31
+ setContainer: (container: HTMLElement | null) => void;
32
+ /** The column (its size flips between zero and real when the host hides it) and the section of resolved threads; `null` or anything but an element clears. */
33
+ setColumn: (el: unknown) => void;
34
+ setResolvedSection: (el: unknown) => void;
35
+ /** A card's element by id (`null` is ignored: cards that are gone are dropped at the next measurement). */
36
+ setCard: (id: string, el: unknown) => void;
37
+ /** Measures again on the next animation frame; call when the cards or the active card changed. */
38
+ schedule: () => void;
39
+ connect: () => void;
40
+ disconnect: () => void;
41
+ }
42
+ export declare function createMarginController(source: MarginSource): MarginController;
43
+ //#endregion
44
+ //# sourceMappingURL=margin-controller.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { createStore } from "../../core/store.js";
2
+ import { layoutMargin } from "./margin-layout.js";
3
+ //#region src/features/comments/margin-controller.ts
4
+ const INITIAL = {
5
+ tops: /* @__PURE__ */ new Map(),
6
+ resolvedTop: 0,
7
+ minHeight: 0
8
+ };
9
+ function sameState(a, b) {
10
+ if (a.resolvedTop !== b.resolvedTop || a.minHeight !== b.minHeight || a.tops.size !== b.tops.size) return false;
11
+ for (const [id, top] of a.tops) if (b.tops.get(id) !== top) return false;
12
+ return true;
13
+ }
14
+ const asElement = (el) => el instanceof HTMLElement ? el : null;
15
+ function createMarginController(source) {
16
+ const state = createStore(INITIAL);
17
+ const cards = /* @__PURE__ */ new Map();
18
+ let editor;
19
+ let container = null;
20
+ let column = null;
21
+ let resolvedSection = null;
22
+ let observer = null;
23
+ let connected = false;
24
+ let frame = 0;
25
+ function schedule() {
26
+ if (!connected || frame || typeof requestAnimationFrame === "undefined") return;
27
+ frame = requestAnimationFrame(measure);
28
+ }
29
+ /** Top of the line at document position `pos`, relative to the column; null where the position is not rendered (yet). */
30
+ function lineTop(instance, pos, base) {
31
+ try {
32
+ return instance.view.coordsAtPos(Math.max(0, Math.min(pos, instance.state.doc.content.size))).top - base;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function measure() {
38
+ frame = 0;
39
+ const instance = editor;
40
+ if (!instance || instance.isDestroyed || !column || !column.getClientRects().length) return;
41
+ const list = source.cards();
42
+ const live = new Set(list.map((card) => card.id));
43
+ for (const [id, el] of cards) {
44
+ if (live.has(id)) continue;
45
+ observer?.unobserve(el);
46
+ cards.delete(id);
47
+ }
48
+ const base = column.getBoundingClientRect().top;
49
+ const entries = list.map((card) => ({
50
+ id: card.id,
51
+ top: card.pos === null ? null : lineTop(instance, card.pos, base),
52
+ height: cards.get(card.id)?.offsetHeight ?? 0
53
+ }));
54
+ const { tops, bottom } = layoutMargin(entries, source.activeId());
55
+ const resolvedTop = entries.length ? bottom + 16 : 0;
56
+ const next = {
57
+ tops,
58
+ resolvedTop,
59
+ minHeight: resolvedTop + (resolvedSection?.offsetHeight ?? 0)
60
+ };
61
+ if (!sameState(state.get(), next)) state.set(next);
62
+ }
63
+ /** Replaces the element held in a slot, moving the resize observation along. */
64
+ function swap(previous, next) {
65
+ if (previous === next) return previous;
66
+ if (previous) observer?.unobserve(previous);
67
+ if (next) observer?.observe(next);
68
+ schedule();
69
+ return next;
70
+ }
71
+ const onTransaction = () => schedule();
72
+ function connect() {
73
+ if (connected) return;
74
+ connected = true;
75
+ if (typeof ResizeObserver !== "undefined") {
76
+ observer = new ResizeObserver(schedule);
77
+ for (const el of [
78
+ column,
79
+ resolvedSection,
80
+ container,
81
+ ...cards.values()
82
+ ]) if (el) observer.observe(el);
83
+ }
84
+ editor?.on("transaction", onTransaction);
85
+ window.addEventListener("resize", schedule);
86
+ schedule();
87
+ }
88
+ function disconnect() {
89
+ if (!connected) return;
90
+ connected = false;
91
+ editor?.off("transaction", onTransaction);
92
+ window.removeEventListener("resize", schedule);
93
+ observer?.disconnect();
94
+ observer = null;
95
+ if (frame) cancelAnimationFrame(frame);
96
+ frame = 0;
97
+ }
98
+ return {
99
+ state,
100
+ setEditor(next) {
101
+ if (next === editor) return;
102
+ if (connected) editor?.off("transaction", onTransaction);
103
+ editor = next;
104
+ if (connected) next?.on("transaction", onTransaction);
105
+ schedule();
106
+ },
107
+ setContainer(next) {
108
+ container = swap(container, next);
109
+ },
110
+ setColumn(el) {
111
+ column = swap(column, asElement(el));
112
+ },
113
+ setResolvedSection(el) {
114
+ resolvedSection = swap(resolvedSection, asElement(el));
115
+ },
116
+ setCard(id, el) {
117
+ const next = asElement(el);
118
+ if (!next || cards.get(id) === next) return;
119
+ const previous = cards.get(id);
120
+ if (previous) observer?.unobserve(previous);
121
+ cards.set(id, next);
122
+ observer?.observe(next);
123
+ },
124
+ schedule,
125
+ connect,
126
+ disconnect
127
+ };
128
+ }
129
+ //#endregion
130
+ export { createMarginController };
131
+
132
+ //# sourceMappingURL=margin-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"margin-controller.js","names":[],"sources":["../../../src/features/comments/margin-controller.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport { layoutMargin, type MarginCard, type MarginEntry, MARGIN_GAP } from './margin-layout'\n\n/** What the margin renders from the measurements. */\nexport interface MarginState {\n /** Top of every card by id, relative to the column; empty until the first measurement (cards stay invisible till then). */\n tops: ReadonlyMap<string, number>\n /** Top of the section below the cards (the resolved threads) and the column's minimum height. */\n resolvedTop: number\n minHeight: number\n}\n\n/** What the controller lays out, read afresh at every measurement. */\nexport interface MarginSource {\n /** The cards in document order. */\n cards: () => readonly MarginCard[]\n /** The card that owns its line (the draft composer or the active thread), if any. */\n activeId: () => string | null\n}\n\n/**\n * The measuring and scheduling glue of the comments margin, for both frameworks: where each card's line is\n * (`coordsAtPos`), how tall each card is, one `layoutMargin` per animation frame, re-run after transactions, window and\n * element resizes. Created inert; `connect()` starts it (call it on mount) and `disconnect()` stops it, and both may\n * follow each other again (StrictMode mounts twice). Elements are handed in with the `set*` functions (framework\n * refs), the editor and container with `setEditor` / `setContainer`; none of them needs to exist before `connect()`.\n */\nexport interface MarginController {\n readonly state: ReadonlyStore<MarginState>\n setEditor: (editor: Editor | undefined) => void\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n setContainer: (container: HTMLElement | null) => void\n /** The column (its size flips between zero and real when the host hides it) and the section of resolved threads; `null` or anything but an element clears. */\n setColumn: (el: unknown) => void\n setResolvedSection: (el: unknown) => void\n /** A card's element by id (`null` is ignored: cards that are gone are dropped at the next measurement). */\n setCard: (id: string, el: unknown) => void\n /** Measures again on the next animation frame; call when the cards or the active card changed. */\n schedule: () => void\n connect: () => void\n disconnect: () => void\n}\n\nconst INITIAL: MarginState = { tops: new Map(), resolvedTop: 0, minHeight: 0 }\n\nfunction sameState(a: MarginState, b: MarginState): boolean {\n if (a.resolvedTop !== b.resolvedTop || a.minHeight !== b.minHeight || a.tops.size !== b.tops.size) return false\n for (const [id, top] of a.tops) {\n if (b.tops.get(id) !== top) return false\n }\n return true\n}\n\nconst asElement = (el: unknown): HTMLElement | null => el instanceof HTMLElement ? el : null\n\nexport function createMarginController(source: MarginSource): MarginController {\n const state = createStore<MarginState>(INITIAL)\n const cards = new Map<string, HTMLElement>()\n let editor: Editor | undefined\n let container: HTMLElement | null = null\n let column: HTMLElement | null = null\n let resolvedSection: HTMLElement | null = null\n let observer: ResizeObserver | null = null\n let connected = false\n let frame = 0\n\n function schedule() {\n if (!connected || frame || typeof requestAnimationFrame === 'undefined') return\n frame = requestAnimationFrame(measure)\n }\n\n /** Top of the line at document position `pos`, relative to the column; null where the position is not rendered (yet). */\n function lineTop(instance: Editor, pos: number, base: number): number | null {\n try {\n return instance.view.coordsAtPos(Math.max(0, Math.min(pos, instance.state.doc.content.size))).top - base\n }\n catch {\n return null\n }\n }\n\n function measure() {\n frame = 0\n const instance = editor\n // Hidden columns have no layout box; the column's own ResizeObserver re-runs this once shown.\n if (!instance || instance.isDestroyed || !column || !column.getClientRects().length) return\n const list = source.cards()\n const live = new Set(list.map(card => card.id))\n for (const [id, el] of cards) {\n if (live.has(id)) continue\n observer?.unobserve(el)\n cards.delete(id)\n }\n const base = column.getBoundingClientRect().top\n const entries: MarginEntry[] = list.map(card => ({\n id: card.id,\n top: card.pos === null ? null : lineTop(instance, card.pos, base),\n height: cards.get(card.id)?.offsetHeight ?? 0,\n }))\n const { tops, bottom } = layoutMargin(entries, source.activeId())\n const resolvedTop = entries.length ? bottom + 2 * MARGIN_GAP : 0\n const next: MarginState = { tops, resolvedTop, minHeight: resolvedTop + (resolvedSection?.offsetHeight ?? 0) }\n if (!sameState(state.get(), next)) state.set(next)\n }\n\n /** Replaces the element held in a slot, moving the resize observation along. */\n function swap(previous: HTMLElement | null, next: HTMLElement | null): HTMLElement | null {\n if (previous === next) return previous\n if (previous) observer?.unobserve(previous)\n if (next) observer?.observe(next)\n schedule()\n return next\n }\n\n const onTransaction = () => schedule()\n\n function connect() {\n if (connected) return\n connected = true\n if (typeof ResizeObserver !== 'undefined') {\n observer = new ResizeObserver(schedule)\n for (const el of [column, resolvedSection, container, ...cards.values()]) {\n if (el) observer.observe(el)\n }\n }\n editor?.on('transaction', onTransaction)\n window.addEventListener('resize', schedule)\n schedule()\n }\n\n function disconnect() {\n if (!connected) return\n connected = false\n editor?.off('transaction', onTransaction)\n window.removeEventListener('resize', schedule)\n observer?.disconnect()\n observer = null\n if (frame) cancelAnimationFrame(frame)\n frame = 0\n }\n\n return {\n state,\n setEditor(next) {\n if (next === editor) return\n if (connected) editor?.off('transaction', onTransaction)\n editor = next\n if (connected) next?.on('transaction', onTransaction)\n schedule()\n },\n setContainer(next) {\n container = swap(container, next)\n },\n setColumn(el) {\n column = swap(column, asElement(el))\n },\n setResolvedSection(el) {\n resolvedSection = swap(resolvedSection, asElement(el))\n },\n setCard(id, el) {\n const next = asElement(el)\n if (!next || cards.get(id) === next) return\n const previous = cards.get(id)\n if (previous) observer?.unobserve(previous)\n cards.set(id, next)\n observer?.observe(next)\n },\n schedule,\n connect,\n disconnect,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { CommentThread } from "./schema.js";
2
+ import { DocRange } from "./model.js";
3
+ //#region src/features/comments/margin-layout.d.ts
4
+ /** Vertical gap between two cards of the comments margin, in px. */
5
+ export declare const MARGIN_GAP = 8;
6
+ /** The id of the draft composer's card (a thread id never looks like this). */
7
+ export declare const DRAFT_CARD = "__draft";
8
+ /** One card of the comments margin as the layout rule sees it. */
9
+ export interface MarginEntry {
10
+ id: string;
11
+ /** Top of the line the card belongs to, relative to the column; `null`: the card has no place and follows the others. */
12
+ top: number | null;
13
+ /** Measured height of the card. */
14
+ height: number;
15
+ }
16
+ export interface MarginLayout {
17
+ /** Top of every entry, relative to the column. */
18
+ tops: ReadonlyMap<string, number>;
19
+ /** Bottom edge of the lowest card (`minTop` without entries). */
20
+ bottom: number;
21
+ }
22
+ /**
23
+ * The layout rule of the comments margin („Aktiver Faden“). `entries` come in document order. The active card sits
24
+ * exactly at the top of its line; the cards above it are pushed up, the ones below pushed down, each as close to its own
25
+ * line as its neighbour allows, and none overlap (`gap` apart). When pushing up would leave the column (above `minTop`,
26
+ * the column's top offset), the whole stack shifts down just enough. Without an active card (`activeId` null or not among
27
+ * the entries) the cards stack top-down: each at its line, or right below the previous card when that is taken. Entries
28
+ * without a line (`top: null`) come last, stacked below the others, and are never pinned.
29
+ */
30
+ export declare function layoutMargin(entries: readonly MarginEntry[], activeId: string | null, { gap, minTop }?: {
31
+ gap?: number;
32
+ minTop?: number;
33
+ }): MarginLayout;
34
+ /** A card of the comments margin: the draft composer, or a thread with the document position its line sits at. */
35
+ export interface MarginCard {
36
+ id: string;
37
+ /** Document position of the card's line: the text of an attached thread (its first character), the draft's start, or where an orphan's text was. `null`: unknown, the card goes to the end of the margin. */
38
+ pos: number | null;
39
+ /** The thread; `null` for the draft composer. */
40
+ thread: CommentThread | null;
41
+ /** The thread's text was deleted. */
42
+ orphan: boolean;
43
+ }
44
+ /**
45
+ * The cards of the margin in document order: the draft composer, then the open threads, each at its text. An orphaned
46
+ * thread sits at its deletion point (`orphanAt`), one without a known deletion point after all the others.
47
+ */
48
+ export declare function marginCards(source: {
49
+ draft: DocRange | null;
50
+ open: readonly CommentThread[];
51
+ ranges: ReadonlyMap<string, DocRange>;
52
+ orphanAt: ReadonlyMap<string, number>;
53
+ }): MarginCard[];
54
+ //#endregion
55
+ //# sourceMappingURL=margin-layout.d.ts.map
@@ -0,0 +1,79 @@
1
+ //#region src/features/comments/margin-layout.ts
2
+ /** Vertical gap between two cards of the comments margin, in px. */
3
+ const MARGIN_GAP = 8;
4
+ /** The id of the draft composer's card (a thread id never looks like this). */
5
+ const DRAFT_CARD = "__draft";
6
+ /**
7
+ * The layout rule of the comments margin („Aktiver Faden“). `entries` come in document order. The active card sits
8
+ * exactly at the top of its line; the cards above it are pushed up, the ones below pushed down, each as close to its own
9
+ * line as its neighbour allows, and none overlap (`gap` apart). When pushing up would leave the column (above `minTop`,
10
+ * the column's top offset), the whole stack shifts down just enough. Without an active card (`activeId` null or not among
11
+ * the entries) the cards stack top-down: each at its line, or right below the previous card when that is taken. Entries
12
+ * without a line (`top: null`) come last, stacked below the others, and are never pinned.
13
+ */
14
+ function layoutMargin(entries, activeId, { gap = 8, minTop = 0 } = {}) {
15
+ const placed = entries.flatMap((e) => e.top === null ? [] : [{
16
+ id: e.id,
17
+ want: e.top,
18
+ height: e.height
19
+ }]);
20
+ const floating = entries.filter((e) => e.top === null);
21
+ const pinned = activeId === null ? -1 : placed.findIndex((e) => e.id === activeId);
22
+ let tops = [];
23
+ if (pinned < 0) {
24
+ let bottom = minTop - gap;
25
+ for (const entry of placed) {
26
+ const top = Math.max(entry.want, bottom + gap);
27
+ tops.push(top);
28
+ bottom = top + entry.height;
29
+ }
30
+ } else {
31
+ tops[pinned] = placed[pinned].want;
32
+ for (let i = pinned - 1; i >= 0; i--) tops[i] = Math.min(placed[i].want, tops[i + 1] - gap - placed[i].height);
33
+ for (let i = pinned + 1; i < placed.length; i++) tops[i] = Math.max(placed[i].want, tops[i - 1] + placed[i - 1].height + gap);
34
+ const overflow = minTop - Math.min(...tops);
35
+ if (overflow > 0) tops = tops.map((top) => top + overflow);
36
+ }
37
+ const result = /* @__PURE__ */ new Map();
38
+ let bottom = minTop - gap;
39
+ placed.forEach((entry, i) => {
40
+ result.set(entry.id, tops[i]);
41
+ bottom = Math.max(bottom, tops[i] + entry.height);
42
+ });
43
+ for (const entry of floating) {
44
+ const top = bottom + gap;
45
+ result.set(entry.id, top);
46
+ bottom = top + entry.height;
47
+ }
48
+ return {
49
+ tops: result,
50
+ bottom: Math.max(bottom, minTop)
51
+ };
52
+ }
53
+ /**
54
+ * The cards of the margin in document order: the draft composer, then the open threads, each at its text. An orphaned
55
+ * thread sits at its deletion point (`orphanAt`), one without a known deletion point after all the others.
56
+ */
57
+ function marginCards(source) {
58
+ const cards = [];
59
+ if (source.draft) cards.push({
60
+ id: DRAFT_CARD,
61
+ pos: source.draft.from,
62
+ thread: null,
63
+ orphan: false
64
+ });
65
+ for (const thread of source.open) {
66
+ const range = source.ranges.get(thread.id);
67
+ cards.push({
68
+ id: thread.id,
69
+ pos: range ? range.from : source.orphanAt.get(thread.id) ?? null,
70
+ thread,
71
+ orphan: !range
72
+ });
73
+ }
74
+ return cards.sort((a, b) => a.pos === b.pos ? 0 : a.pos === null ? 1 : b.pos === null ? -1 : a.pos - b.pos);
75
+ }
76
+ //#endregion
77
+ export { DRAFT_CARD, MARGIN_GAP, layoutMargin, marginCards };
78
+
79
+ //# sourceMappingURL=margin-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"margin-layout.js","names":[],"sources":["../../../src/features/comments/margin-layout.ts"],"sourcesContent":["import type { DocRange } from './model'\nimport type { CommentThread } from './schema'\n\n/** Vertical gap between two cards of the comments margin, in px. */\nexport const MARGIN_GAP = 8\n\n/** The id of the draft composer's card (a thread id never looks like this). */\nexport const DRAFT_CARD = '__draft'\n\n/** One card of the comments margin as the layout rule sees it. */\nexport interface MarginEntry {\n id: string\n /** Top of the line the card belongs to, relative to the column; `null`: the card has no place and follows the others. */\n top: number | null\n /** Measured height of the card. */\n height: number\n}\n\nexport interface MarginLayout {\n /** Top of every entry, relative to the column. */\n tops: ReadonlyMap<string, number>\n /** Bottom edge of the lowest card (`minTop` without entries). */\n bottom: number\n}\n\n/**\n * The layout rule of the comments margin („Aktiver Faden“). `entries` come in document order. The active card sits\n * exactly at the top of its line; the cards above it are pushed up, the ones below pushed down, each as close to its own\n * line as its neighbour allows, and none overlap (`gap` apart). When pushing up would leave the column (above `minTop`,\n * the column's top offset), the whole stack shifts down just enough. Without an active card (`activeId` null or not among\n * the entries) the cards stack top-down: each at its line, or right below the previous card when that is taken. Entries\n * without a line (`top: null`) come last, stacked below the others, and are never pinned.\n */\nexport function layoutMargin(\n entries: readonly MarginEntry[],\n activeId: string | null,\n { gap = MARGIN_GAP, minTop = 0 }: { gap?: number, minTop?: number } = {},\n): MarginLayout {\n const placed = entries.flatMap(e => e.top === null ? [] : [{ id: e.id, want: e.top, height: e.height }])\n const floating = entries.filter(e => e.top === null)\n const pinned = activeId === null ? -1 : placed.findIndex(e => e.id === activeId)\n\n let tops: number[] = []\n if (pinned < 0) {\n let bottom = minTop - gap\n for (const entry of placed) {\n const top = Math.max(entry.want, bottom + gap)\n tops.push(top)\n bottom = top + entry.height\n }\n }\n else {\n tops[pinned] = placed[pinned]!.want\n for (let i = pinned - 1; i >= 0; i--) tops[i] = Math.min(placed[i]!.want, tops[i + 1]! - gap - placed[i]!.height)\n for (let i = pinned + 1; i < placed.length; i++) tops[i] = Math.max(placed[i]!.want, tops[i - 1]! + placed[i - 1]!.height + gap)\n const overflow = minTop - Math.min(...tops)\n if (overflow > 0) tops = tops.map(top => top + overflow)\n }\n\n const result = new Map<string, number>()\n let bottom = minTop - gap\n placed.forEach((entry, i) => {\n result.set(entry.id, tops[i]!)\n bottom = Math.max(bottom, tops[i]! + entry.height)\n })\n for (const entry of floating) {\n const top = bottom + gap\n result.set(entry.id, top)\n bottom = top + entry.height\n }\n return { tops: result, bottom: Math.max(bottom, minTop) }\n}\n\n/** A card of the comments margin: the draft composer, or a thread with the document position its line sits at. */\nexport interface MarginCard {\n id: string\n /** Document position of the card's line: the text of an attached thread (its first character), the draft's start, or where an orphan's text was. `null`: unknown, the card goes to the end of the margin. */\n pos: number | null\n /** The thread; `null` for the draft composer. */\n thread: CommentThread | null\n /** The thread's text was deleted. */\n orphan: boolean\n}\n\n/**\n * The cards of the margin in document order: the draft composer, then the open threads, each at its text. An orphaned\n * thread sits at its deletion point (`orphanAt`), one without a known deletion point after all the others.\n */\nexport function marginCards(source: {\n draft: DocRange | null\n open: readonly CommentThread[]\n ranges: ReadonlyMap<string, DocRange>\n orphanAt: ReadonlyMap<string, number>\n}): MarginCard[] {\n const cards: MarginCard[] = []\n if (source.draft) cards.push({ id: DRAFT_CARD, pos: source.draft.from, thread: null, orphan: false })\n for (const thread of source.open) {\n const range = source.ranges.get(thread.id)\n cards.push({ id: thread.id, pos: range ? range.from : source.orphanAt.get(thread.id) ?? null, thread, orphan: !range })\n }\n // Stable: cards at the same place keep their order (draft first, then the threads' order).\n return cards.sort((a, b) => a.pos === b.pos ? 0 : a.pos === null ? 1 : b.pos === null ? -1 : a.pos - b.pos)\n}\n"],"mappings":";;AAIA,MAAa,aAAa;;AAG1B,MAAa,aAAa;;;;;;;;;AA0B1B,SAAgB,aACd,SACA,UACA,EAAE,MAAA,GAAkB,SAAS,MAAyC,CAAC,GACzD;CACd,MAAM,SAAS,QAAQ,SAAQ,MAAK,EAAE,QAAQ,OAAO,CAAC,IAAI,CAAC;EAAE,IAAI,EAAE;EAAI,MAAM,EAAE;EAAK,QAAQ,EAAE;CAAO,CAAC,CAAC;CACvG,MAAM,WAAW,QAAQ,QAAO,MAAK,EAAE,QAAQ,IAAI;CACnD,MAAM,SAAS,aAAa,OAAO,KAAK,OAAO,WAAU,MAAK,EAAE,OAAO,QAAQ;CAE/E,IAAI,OAAiB,CAAC;CACtB,IAAI,SAAS,GAAG;EACd,IAAI,SAAS,SAAS;EACtB,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,MAAM,KAAK,IAAI,MAAM,MAAM,SAAS,GAAG;GAC7C,KAAK,KAAK,GAAG;GACb,SAAS,MAAM,MAAM;EACvB;CACF,OACK;EACH,KAAK,UAAU,OAAO,OAAO,CAAE;EAC/B,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK,KAAK,KAAK,KAAK,IAAI,OAAO,EAAE,CAAE,MAAM,KAAK,IAAI,KAAM,MAAM,OAAO,EAAE,CAAE,MAAM;EAChH,KAAK,IAAI,IAAI,SAAS,GAAG,IAAI,OAAO,QAAQ,KAAK,KAAK,KAAK,KAAK,IAAI,OAAO,EAAE,CAAE,MAAM,KAAK,IAAI,KAAM,OAAO,IAAI,EAAE,CAAE,SAAS,GAAG;EAC/H,MAAM,WAAW,SAAS,KAAK,IAAI,GAAG,IAAI;EAC1C,IAAI,WAAW,GAAG,OAAO,KAAK,KAAI,QAAO,MAAM,QAAQ;CACzD;CAEA,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,SAAS,SAAS;CACtB,OAAO,SAAS,OAAO,MAAM;EAC3B,OAAO,IAAI,MAAM,IAAI,KAAK,EAAG;EAC7B,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAM,MAAM,MAAM;CACnD,CAAC;CACD,KAAK,MAAM,SAAS,UAAU;EAC5B,MAAM,MAAM,SAAS;EACrB,OAAO,IAAI,MAAM,IAAI,GAAG;EACxB,SAAS,MAAM,MAAM;CACvB;CACA,OAAO;EAAE,MAAM;EAAQ,QAAQ,KAAK,IAAI,QAAQ,MAAM;CAAE;AAC1D;;;;;AAiBA,SAAgB,YAAY,QAKX;CACf,MAAM,QAAsB,CAAC;CAC7B,IAAI,OAAO,OAAO,MAAM,KAAK;EAAE,IAAI;EAAY,KAAK,OAAO,MAAM;EAAM,QAAQ;EAAM,QAAQ;CAAM,CAAC;CACpG,KAAK,MAAM,UAAU,OAAO,MAAM;EAChC,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,EAAE;EACzC,MAAM,KAAK;GAAE,IAAI,OAAO;GAAI,KAAK,QAAQ,MAAM,OAAO,OAAO,SAAS,IAAI,OAAO,EAAE,KAAK;GAAM;GAAQ,QAAQ,CAAC;EAAM,CAAC;CACxH;CAEA,OAAO,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,QAAQ,OAAO,IAAI,EAAE,QAAQ,OAAO,KAAK,EAAE,MAAM,EAAE,GAAG;AAC5G"}
@@ -22,6 +22,12 @@ export interface CommentsSnapshot {
22
22
  threads: readonly CommentThread[];
23
23
  /** Current text range per thread id (threads without one are orphans). */
24
24
  ranges: ReadonlyMap<string, DocRange>;
25
+ /**
26
+ * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the
27
+ * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans
28
+ * without a usable anchor (threads from before anchors, or whose block is gone for good).
29
+ */
30
+ orphanAt: ReadonlyMap<string, number>;
25
31
  active: string | null;
26
32
  /** Current range of the draft a new comment is being written for. */
27
33
  draft: DocRange | null;
@@ -69,6 +75,8 @@ export interface CommentsModel extends CommentsActions {
69
75
  * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and
70
76
  * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).
71
77
  * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).
78
+ * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves
79
+ * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.
72
80
  */
73
81
  export declare function createCommentsModel(getEditor: () => Editor | undefined): CommentsModel;
74
82
  /** Creates the comments model of one editor, as a host supplies it to `commentsFeatureCore`. */
@@ -1,25 +1,13 @@
1
1
  import { newBlockId } from "../../core/schema/block-ids.js";
2
2
  import { createStore } from "../../core/store.js";
3
- import { releaseRanges, resolveRange } from "../../core/doc-anchors.js";
3
+ import { anchorResolverOf, releaseRanges, resolveRange } from "../../core/doc-anchors.js";
4
4
  import { editorEntryOf } from "../../core/editor-ui.js";
5
- import { cancelCommentDraft, commentsDoc, commentsKey, commentsUser, setActiveComment, startCommentDraft } from "./comments-plugin.js";
6
5
  import { COMMENTS_FIELD, COMMENT_LIMITS } from "./schema.js";
6
+ import { cancelCommentDraft, commentsDoc, commentsKey, commentsUser, setActiveComment, startCommentDraft } from "./comments-plugin.js";
7
+ import { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from "./snapshot-equality.js";
8
+ import { captureThreadAnchor, resolveThreadAnchor } from "./thread-anchor.js";
7
9
  import * as Y from "yjs";
8
10
  //#region src/features/comments/model.ts
9
- const NO_RANGES = /* @__PURE__ */ new Map();
10
- const NO_THREADS = [];
11
- const EMPTY = {
12
- available: false,
13
- canComment: false,
14
- user: null,
15
- threads: NO_THREADS,
16
- ranges: NO_RANGES,
17
- active: null,
18
- draft: null,
19
- draftQuote: "",
20
- open: NO_THREADS,
21
- resolved: NO_THREADS
22
- };
23
11
  /** A thread's `Y.Map` as plain data; `replies` converted explicitly so it is always a plain array. */
24
12
  function snapshotOf(thread) {
25
13
  const replies = thread.get("replies");
@@ -28,18 +16,6 @@ function snapshotOf(thread) {
28
16
  replies: replies instanceof Y.Array ? replies.toArray() : []
29
17
  };
30
18
  }
31
- function sameRange(a, b) {
32
- return a === b || !!a && !!b && a.from === b.from && a.to === b.to;
33
- }
34
- function sameRanges(a, b) {
35
- if (a === b) return true;
36
- if (a.size !== b.size) return false;
37
- for (const [id, range] of a) if (!sameRange(range, b.get(id) ?? null)) return false;
38
- return true;
39
- }
40
- function sameUser(a, b) {
41
- return a === b || !!a && !!b && a.id === b.id && a.name === b.name && a.color === b.color;
42
- }
43
19
  /**
44
20
  * The comment threads of a collaborative editor, stored in its Y.Doc (`COMMENTS_FIELD`, one nested `Y.Map` per thread,
45
21
  * replies in a nested `Y.Array`), so they sync live with the document and persist with its Yjs state. Keeps the
@@ -47,9 +23,11 @@ function sameUser(a, b) {
47
23
  * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and
48
24
  * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).
49
25
  * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).
26
+ * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves
27
+ * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.
50
28
  */
51
29
  function createCommentsModel(getEditor) {
52
- const store = createStore(EMPTY);
30
+ const store = createStore(EMPTY_SNAPSHOT);
53
31
  const current = () => {
54
32
  const instance = getEditor();
55
33
  return instance && !instance.isDestroyed ? instance : void 0;
@@ -59,6 +37,8 @@ function createCommentsModel(getEditor) {
59
37
  let threads = NO_THREADS;
60
38
  let syncQueued = false;
61
39
  let told = NO_THREADS;
40
+ const anchorTried = /* @__PURE__ */ new Set();
41
+ let healQueued = false;
62
42
  function unobserve() {
63
43
  observed?.map.unobserveDeep(onThreadsChanged);
64
44
  observed = void 0;
@@ -81,7 +61,7 @@ function createCommentsModel(getEditor) {
81
61
  function recompute() {
82
62
  const instance = current();
83
63
  if (!instance) {
84
- store.set(EMPTY);
64
+ store.set(EMPTY_SNAPSHOT);
85
65
  return;
86
66
  }
87
67
  const previous = store.get();
@@ -89,6 +69,7 @@ function createCommentsModel(getEditor) {
89
69
  const user = commentsUser(instance);
90
70
  const plugin = commentsKey.getState(instance.state);
91
71
  const ranges = plugin?.ranges ?? NO_RANGES;
72
+ const orphanAt = orphanPositions(instance, ranges);
92
73
  const active = plugin?.active ?? null;
93
74
  const anchor = plugin?.draft;
94
75
  const draft = anchor ? resolveRange(instance.state, anchor) : null;
@@ -97,7 +78,8 @@ function createCommentsModel(getEditor) {
97
78
  const canComment = available && instance.isEditable;
98
79
  const sameThreads = threads === previous.threads;
99
80
  const sameRangeMap = sameRanges(ranges, previous.ranges);
100
- if (available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote) return;
81
+ const sameOrphanMap = sameOrphanAt(orphanAt, previous.orphanAt);
82
+ if (available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && sameOrphanMap && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote) return;
101
83
  const open = sameThreads && sameRangeMap ? previous.open : threads.filter((t) => !t.resolved).sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt));
102
84
  const resolved = sameThreads ? previous.resolved : threads.filter((t) => t.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
103
85
  store.set({
@@ -106,12 +88,51 @@ function createCommentsModel(getEditor) {
106
88
  user,
107
89
  threads,
108
90
  ranges: sameRangeMap ? previous.ranges : ranges,
91
+ orphanAt: sameOrphanMap ? previous.orphanAt : orphanAt,
109
92
  active,
110
93
  draft: sameRange(draft, previous.draft) ? previous.draft : draft,
111
94
  draftQuote,
112
95
  open,
113
96
  resolved
114
97
  });
98
+ if (available) queueHeal();
99
+ }
100
+ /** Where each unresolved thread without a text range was, from its anchor; empty without a Yjs session. */
101
+ function orphanPositions(instance, ranges) {
102
+ const resolver = anchorResolverOf(instance);
103
+ if (!resolver) return NO_POSITIONS;
104
+ const size = instance.state.doc.content.size;
105
+ let found;
106
+ for (const thread of threads) {
107
+ if (thread.resolved || ranges.has(thread.id) || !thread.anchor) continue;
108
+ const position = resolveThreadAnchor(instance.state, resolver, thread.anchor);
109
+ if (position !== null && position <= size) (found ??= /* @__PURE__ */ new Map()).set(thread.id, position);
110
+ }
111
+ return found ?? NO_POSITIONS;
112
+ }
113
+ function queueHeal() {
114
+ if (healQueued || !threads.some((t) => !t.anchor && !anchorTried.has(t.id))) return;
115
+ healQueued = true;
116
+ queueMicrotask(() => {
117
+ healQueued = false;
118
+ healAnchors();
119
+ });
120
+ }
121
+ function healAnchors() {
122
+ const ctx = writable();
123
+ const resolver = ctx && anchorResolverOf(ctx.editor);
124
+ if (!ctx || !resolver || ctx.editor !== bound) return;
125
+ const { ranges } = store.get();
126
+ const written = [];
127
+ for (const { id, anchor } of threads) {
128
+ const range = ranges.get(id);
129
+ if (anchor || anchorTried.has(id) || !range) continue;
130
+ anchorTried.add(id);
131
+ const thread = threadMap(ctx.map, id);
132
+ const captured = thread && captureThreadAnchor(ctx.editor.state, resolver, range);
133
+ if (thread && captured) written.push([thread, captured]);
134
+ }
135
+ if (written.length) ctx.ydoc.transact(() => written.forEach(([thread, captured]) => thread.set("anchor", captured)));
115
136
  }
116
137
  function queueSync() {
117
138
  if (syncQueued) return;
@@ -146,6 +167,7 @@ function createCommentsModel(getEditor) {
146
167
  bound = instance;
147
168
  threads = NO_THREADS;
148
169
  told = NO_THREADS;
170
+ anchorTried.clear();
149
171
  instance?.on("transaction", onTransaction);
150
172
  instance?.on("update", onTransaction);
151
173
  queueSync();
@@ -171,7 +193,8 @@ function createCommentsModel(getEditor) {
171
193
  unobserve();
172
194
  threads = NO_THREADS;
173
195
  told = NO_THREADS;
174
- store.set(EMPTY);
196
+ anchorTried.clear();
197
+ store.set(EMPTY_SNAPSHOT);
175
198
  }
176
199
  /** Editor, thread map and user while comments are available; null otherwise. */
177
200
  function writable() {
@@ -209,6 +232,8 @@ function createCommentsModel(getEditor) {
209
232
  if (!ctx || !anchor || !range || range.from >= range.to || !body || !type) return null;
210
233
  const id = newBlockId();
211
234
  const { state } = ctx.editor;
235
+ const resolver = anchorResolverOf(ctx.editor);
236
+ const threadAnchor = resolver ? captureThreadAnchor(state, resolver, range) : null;
212
237
  const quote = state.doc.textBetween(range.from, range.to, " ").slice(0, COMMENT_LIMITS.quote);
213
238
  const tr = state.tr.addMark(range.from, range.to, type.create({ id }));
214
239
  if (!tr.docChanged) return null;
@@ -225,6 +250,7 @@ function createCommentsModel(getEditor) {
225
250
  thread.set("createdAt", (/* @__PURE__ */ new Date()).toISOString());
226
251
  thread.set("quote", quote);
227
252
  thread.set("resolved", false);
253
+ if (threadAnchor) thread.set("anchor", threadAnchor);
228
254
  thread.set("replies", new Y.Array());
229
255
  ctx.map.set(id, thread);
230
256
  return id;
@@ -343,7 +369,7 @@ function commentsModelOf(ctx) {
343
369
  * consumer of this function, so a host model that must share state across them keeps it outside the factory.
344
370
  */
345
371
  function createEditorCommentsModel(getEditor) {
346
- const store = createStore(EMPTY);
372
+ const store = createStore(EMPTY_SNAPSHOT);
347
373
  let inner;
348
374
  function release() {
349
375
  inner?.stop();
@@ -371,7 +397,7 @@ function createEditorCommentsModel(getEditor) {
371
397
  refresh: () => current().refresh(),
372
398
  dispose: () => {
373
399
  release();
374
- store.set(EMPTY);
400
+ store.set(EMPTY_SNAPSHOT);
375
401
  },
376
402
  add: (text) => current().add(text),
377
403
  reply: (threadId, text) => current().reply(threadId, text),