@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,3 +1,4 @@
1
+ import { DRAFT_CARD, marginCards } from "../margin-layout.js";
1
2
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
2
3
  import { cn } from "../../../react/ui/utils.js";
3
4
  import { Icon } from "../../../react/Icon.js";
@@ -7,285 +8,118 @@ import { CommentComposer } from "./CommentComposer.js";
7
8
  import { CommentAvatar } from "./CommentAvatar.js";
8
9
  import { CommentThreadView } from "./CommentThreadView.js";
9
10
  import { useComments } from "./use-comments.js";
10
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { useMarginLayout } from "./use-margin-layout.js";
12
+ import { useMemo, useState } from "react";
11
13
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
14
  //#region src/features/comments/react/CommentsMargin.tsx
13
- const GAP = 8;
14
- const DRAFT = "__draft";
15
15
  const excerpt = (text) => text.length > 60 ? `${text.slice(0, 60)}…` : text;
16
- function sameTops(a, b) {
17
- const keys = Object.keys(a);
18
- return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
19
- }
20
- /**
21
- * Comments margin („Randspalte“): a column the host places inside its scroll container right of the editor, so it
22
- * scrolls with the text. Cards sit at the height of their text, the active thread is expanded; shown only while `open`
23
- * and the user's comments layout is `margin`.
24
- */
25
16
  function CommentsMargin({ editor, container, open = true }) {
26
17
  const { settings } = useEditorSettings();
27
18
  const { t } = useEditorI18n();
28
19
  const enabled = settings.editing.commentsLayout === "margin";
29
20
  const comments = useComments(enabled ? editor : void 0);
30
- /** Draft and open threads with a text range, in document order. */
31
- const positioned = useMemo(() => {
32
- const items = [];
33
- if (comments.draft) items.push({
34
- key: DRAFT,
35
- from: comments.draft.from,
36
- thread: null
37
- });
38
- for (const thread of comments.open) {
39
- const range = comments.ranges.get(thread.id);
40
- if (range) items.push({
41
- key: thread.id,
42
- from: range.from,
43
- thread
44
- });
45
- }
46
- return items.sort((a, b) => a.from - b.from);
47
- }, [
21
+ /** Draft and open threads in document order, each at its text (an orphan where its text was). */
22
+ const cards = useMemo(() => marginCards({
23
+ draft: comments.draft,
24
+ open: comments.open,
25
+ ranges: comments.ranges,
26
+ orphanAt: comments.orphanAt
27
+ }), [
48
28
  comments.draft,
49
29
  comments.open,
50
- comments.ranges
30
+ comments.ranges,
31
+ comments.orphanAt
51
32
  ]);
52
- const orphans = useMemo(() => comments.open.filter((thread) => !comments.ranges.has(thread.id)), [comments.open, comments.ranges]);
53
- const [showResolved, setShowResolved] = useState(false);
54
- const [tops, setTops] = useState({});
55
- const [resolvedTop, setResolvedTop] = useState(0);
56
- const [minHeight, setMinHeight] = useState(0);
57
- const [layout] = useState(() => ({
58
- observer: null,
59
- observed: /* @__PURE__ */ new WeakSet(),
60
- cards: /* @__PURE__ */ new Map(),
61
- frame: 0,
62
- column: null,
63
- resolvedSection: null
64
- }));
65
- const latest = useRef({
33
+ /** The card that owns its line: the draft composer, else the active thread. */
34
+ const activeId = comments.draft ? DRAFT_CARD : comments.active;
35
+ const { state: layout, setColumn, setResolvedSection, setCard } = useMarginLayout({
66
36
  editor,
67
- positioned,
68
- orphans
37
+ container,
38
+ cards,
39
+ activeId
69
40
  });
70
- latest.current = {
71
- editor,
72
- positioned,
73
- orphans
74
- };
75
- const observe = useCallback((el) => {
76
- if (!layout.observer || layout.observed.has(el)) return;
77
- layout.observed.add(el);
78
- layout.observer.observe(el);
79
- }, [layout]);
80
- const unobserve = useCallback((el) => {
81
- layout.observer?.unobserve(el);
82
- layout.observed.delete(el);
83
- }, [layout]);
84
- const relayout = useCallback(() => {
85
- layout.frame = 0;
86
- const { editor: instance, positioned: items, orphans: lost } = latest.current;
87
- const col = layout.column;
88
- if (!instance || instance.isDestroyed || !col || !col.getClientRects().length) return;
89
- const live = /* @__PURE__ */ new Set([...items.map((i) => i.key), ...lost.map((t) => t.id)]);
90
- for (const [key, el] of layout.cards) {
91
- if (live.has(key)) continue;
92
- unobserve(el);
93
- layout.cards.delete(key);
94
- }
95
- const base = col.getBoundingClientRect().top;
96
- const size = instance.state.doc.content.size;
97
- const next = {};
98
- let bottom = -8;
99
- for (const item of items) {
100
- let desired = bottom + GAP;
101
- try {
102
- desired = instance.view.coordsAtPos(Math.min(item.from, size)).top - base;
103
- } catch {}
104
- const top = Math.max(desired, bottom + GAP, 0);
105
- next[item.key] = top;
106
- bottom = top + (layout.cards.get(item.key)?.offsetHeight ?? 0);
107
- }
108
- for (const thread of lost) {
109
- const top = bottom + GAP;
110
- next[thread.id] = top;
111
- bottom = top + (layout.cards.get(thread.id)?.offsetHeight ?? 0);
112
- }
113
- const nextResolvedTop = bottom < 0 ? 0 : bottom + 16;
114
- setTops((previous) => sameTops(previous, next) ? previous : next);
115
- setResolvedTop(nextResolvedTop);
116
- setMinHeight(nextResolvedTop + (layout.resolvedSection?.offsetHeight ?? 0));
117
- }, [layout, unobserve]);
118
- const schedule = useCallback(() => {
119
- if (layout.frame || typeof requestAnimationFrame === "undefined") return;
120
- layout.frame = requestAnimationFrame(relayout);
121
- }, [layout, relayout]);
122
- useEffect(() => {
123
- if (typeof ResizeObserver !== "undefined") {
124
- layout.observer = new ResizeObserver(() => schedule());
125
- layout.observed = /* @__PURE__ */ new WeakSet();
126
- for (const el of layout.cards.values()) observe(el);
127
- if (layout.column) observe(layout.column);
128
- if (layout.resolvedSection) observe(layout.resolvedSection);
129
- }
130
- window.addEventListener("resize", schedule);
131
- schedule();
132
- return () => {
133
- window.removeEventListener("resize", schedule);
134
- layout.observer?.disconnect();
135
- layout.observer = null;
136
- if (layout.frame) cancelAnimationFrame(layout.frame);
137
- layout.frame = 0;
138
- };
139
- }, [
140
- layout,
141
- observe,
142
- schedule
143
- ]);
144
- useEffect(() => {
145
- if (!editor) return;
146
- editor.on("transaction", schedule);
147
- schedule();
148
- return () => {
149
- editor.off("transaction", schedule);
150
- };
151
- }, [editor, schedule]);
152
- useEffect(() => {
153
- if (!container || !layout.observer) return;
154
- layout.observer.observe(container);
155
- return () => layout.observer?.unobserve(container);
156
- }, [container, layout]);
157
- useEffect(() => {
158
- schedule();
159
- }, [
160
- positioned,
161
- orphans,
162
- comments.active,
163
- comments.resolved.length,
164
- showResolved,
165
- enabled,
166
- schedule
167
- ]);
168
- const setColumn = useCallback((el) => {
169
- if (layout.column) unobserve(layout.column);
170
- layout.column = el;
171
- if (el) observe(el);
172
- schedule();
173
- }, [
174
- layout,
175
- observe,
176
- unobserve,
177
- schedule
178
- ]);
179
- const setResolvedSection = useCallback((el) => {
180
- if (layout.resolvedSection) unobserve(layout.resolvedSection);
181
- layout.resolvedSection = el;
182
- if (el) observe(el);
183
- schedule();
184
- }, [
185
- layout,
186
- observe,
187
- unobserve,
188
- schedule
189
- ]);
190
- const setCard = (key, el) => {
191
- if (!el) return;
192
- layout.cards.set(key, el);
193
- observe(el);
194
- };
41
+ const [showResolved, setShowResolved] = useState(false);
195
42
  /** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */
196
43
  function select(id) {
197
44
  if (comments.active !== id) comments.activate(id, { reveal: true });
198
45
  }
199
- const cardStyle = (key) => ({ top: `${tops[key] ?? 0}px` });
200
46
  return /* @__PURE__ */ jsx("div", {
201
47
  ref: setColumn,
202
48
  className: "relative w-[300px] shrink-0",
203
49
  style: {
204
- minHeight: `${minHeight}px`,
50
+ minHeight: `${layout.minHeight}px`,
205
51
  ...open && enabled ? {} : { display: "none" }
206
52
  },
207
53
  "aria-label": t("comments.title"),
208
54
  children: !comments.available ? /* @__PURE__ */ jsx("p", {
209
55
  className: "p-3 text-sm text-muted-foreground",
210
56
  children: t("comments.unavailable")
211
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
212
- positioned.map((item) => /* @__PURE__ */ jsx("div", {
213
- ref: (el) => setCard(item.key, el),
214
- className: cn("absolute inset-x-0 transition-[top] duration-100", tops[item.key] === void 0 && "invisible"),
215
- style: cardStyle(item.key),
216
- children: !item.thread ? /* @__PURE__ */ jsx(CommentComposer, {
217
- quote: comments.draftQuote,
218
- className: "shadow-md",
219
- onSubmit: (text) => {
220
- comments.add(text);
221
- },
222
- onCancel: () => comments.cancelDraft()
223
- }) : comments.active === item.thread.id ? /* @__PURE__ */ jsx(CommentThreadView, {
224
- thread: item.thread,
225
- user: comments.user,
226
- active: true,
227
- orphan: false,
228
- comments,
229
- className: "shadow-md",
230
- onClick: () => select(item.thread.id)
231
- }) : /* @__PURE__ */ jsxs("button", {
232
- type: "button",
233
- className: "flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted",
234
- onClick: () => select(item.thread.id),
235
- children: [
236
- /* @__PURE__ */ jsx(CommentAvatar, {
237
- name: item.thread.author.name,
238
- color: item.thread.author.color,
239
- size: "2xs"
240
- }),
241
- /* @__PURE__ */ jsx("span", {
242
- className: "shrink-0 font-medium",
243
- children: item.thread.author.name
244
- }),
245
- /* @__PURE__ */ jsx("span", {
246
- className: "min-w-0 flex-1 truncate text-muted-foreground",
247
- children: excerpt(item.thread.text)
248
- }),
249
- item.thread.replies.length > 0 && /* @__PURE__ */ jsx("span", {
250
- className: "shrink-0 text-xs text-muted-foreground/70",
251
- children: item.thread.replies.length
252
- })
253
- ]
254
- })
255
- }, item.key)),
256
- orphans.map((thread) => /* @__PURE__ */ jsx("div", {
257
- ref: (el) => setCard(thread.id, el),
258
- className: cn("absolute inset-x-0", tops[thread.id] === void 0 && "invisible"),
259
- style: cardStyle(thread.id),
260
- children: /* @__PURE__ */ jsx(CommentThreadView, {
261
- thread,
262
- user: comments.user,
263
- active: comments.active === thread.id,
264
- orphan: true,
265
- comments,
266
- onClick: () => select(thread.id)
267
- })
268
- }, thread.id)),
269
- comments.resolved.length > 0 && /* @__PURE__ */ jsxs("div", {
270
- ref: setResolvedSection,
271
- className: "absolute inset-x-0 flex flex-col gap-2",
272
- style: { top: `${resolvedTop}px` },
273
- children: [/* @__PURE__ */ jsxs(Button, {
274
- variant: "ghost",
275
- size: "sm",
276
- className: "self-start",
277
- onClick: () => setShowResolved((value) => !value),
278
- children: [t("comments.resolvedCount", { n: comments.resolved.length }), /* @__PURE__ */ jsx(Icon, { name: showResolved ? "i-lucide-chevron-up" : "i-lucide-chevron-down" })]
279
- }), showResolved && comments.resolved.map((thread) => /* @__PURE__ */ jsx(CommentThreadView, {
280
- thread,
281
- user: comments.user,
282
- active: comments.active === thread.id,
283
- orphan: comments.isOrphan(thread.id),
284
- comments,
285
- onClick: () => select(thread.id)
286
- }, thread.id))]
57
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [cards.map((card) => /* @__PURE__ */ jsx("div", {
58
+ ref: (el) => setCard(card.id, el),
59
+ className: cn("absolute inset-x-0 transition-[top] duration-100", !layout.tops.has(card.id) && "invisible"),
60
+ style: { top: `${layout.tops.get(card.id) ?? 0}px` },
61
+ children: !card.thread ? /* @__PURE__ */ jsx(CommentComposer, {
62
+ quote: comments.draftQuote,
63
+ className: "shadow-md",
64
+ onSubmit: (text) => {
65
+ comments.add(text);
66
+ },
67
+ onCancel: () => comments.cancelDraft()
68
+ }) : activeId === card.id ? /* @__PURE__ */ jsx(CommentThreadView, {
69
+ thread: card.thread,
70
+ user: comments.user,
71
+ active: true,
72
+ orphan: card.orphan,
73
+ comments,
74
+ className: "shadow-md",
75
+ onClick: () => select(card.id)
76
+ }) : /* @__PURE__ */ jsxs("button", {
77
+ type: "button",
78
+ className: cn("flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted", card.orphan && "opacity-70"),
79
+ onClick: () => select(card.id),
80
+ children: [
81
+ /* @__PURE__ */ jsx(CommentAvatar, {
82
+ name: card.thread.author.name,
83
+ color: card.thread.author.color,
84
+ size: "2xs"
85
+ }),
86
+ /* @__PURE__ */ jsx("span", {
87
+ className: "shrink-0 font-medium",
88
+ children: card.thread.author.name
89
+ }),
90
+ /* @__PURE__ */ jsx("span", {
91
+ className: "min-w-0 flex-1 truncate text-muted-foreground",
92
+ children: excerpt(card.thread.text)
93
+ }),
94
+ card.orphan && /* @__PURE__ */ jsx("span", {
95
+ className: "shrink-0 text-xs text-muted-foreground/70 italic",
96
+ children: t("comments.orphan")
97
+ }),
98
+ card.thread.replies.length > 0 && /* @__PURE__ */ jsx("span", {
99
+ className: "shrink-0 text-xs text-muted-foreground/70",
100
+ children: card.thread.replies.length
101
+ })
102
+ ]
287
103
  })
288
- ] })
104
+ }, card.id)), comments.resolved.length > 0 && /* @__PURE__ */ jsxs("div", {
105
+ ref: setResolvedSection,
106
+ className: "absolute inset-x-0 flex flex-col gap-2",
107
+ style: { top: `${layout.resolvedTop}px` },
108
+ children: [/* @__PURE__ */ jsxs(Button, {
109
+ variant: "ghost",
110
+ size: "sm",
111
+ className: "self-start",
112
+ onClick: () => setShowResolved((value) => !value),
113
+ children: [t("comments.resolvedCount", { n: comments.resolved.length }), /* @__PURE__ */ jsx(Icon, { name: showResolved ? "i-lucide-chevron-up" : "i-lucide-chevron-down" })]
114
+ }), showResolved && comments.resolved.map((thread) => /* @__PURE__ */ jsx(CommentThreadView, {
115
+ thread,
116
+ user: comments.user,
117
+ active: comments.active === thread.id,
118
+ orphan: comments.isOrphan(thread.id),
119
+ comments,
120
+ onClick: () => select(thread.id)
121
+ }, thread.id))]
122
+ })] })
289
123
  });
290
124
  }
291
125
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"CommentsMargin.js","names":[],"sources":["../../../../src/features/comments/react/CommentsMargin.tsx"],"sourcesContent":["/**\n * Comments margin („Randspalte“): a 300px column the host places inside its scroll container right of the editor,\n * so it scrolls with the text. Cards sit absolutely at the height of their text (top-down in document order, pushed\n * down so they never overlap); the active thread is expanded, the others collapse to one line. The draft composer\n * takes part in the layout, orphaned threads stack after the positioned cards and resolved threads („Erledigt“)\n * sit at the bottom behind a toggle. Shown only while `open` (the host binds the surface's `commentsOpen` model) and the\n * user's comments layout is `margin`; the layout is recomputed whenever the column becomes visible again.\n */\nimport type { Editor } from '@tiptap/core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport type { CommentThread as Thread } from '../schema'\nimport { CommentAvatar } from './CommentAvatar'\nimport { CommentComposer } from './CommentComposer'\nimport { CommentThreadView } from './CommentThreadView'\nimport { useComments } from './use-comments'\n\nconst GAP = 8\nconst DRAFT = '__draft'\n\ninterface Item { key: string, from: number, thread: Thread | null }\n\nconst excerpt = (text: string) => text.length > 60 ? `${text.slice(0, 60)}…` : text\n\nfunction sameTops(a: Record<string, number>, b: Record<string, number>): boolean {\n const keys = Object.keys(a)\n return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key])\n}\n\n/**\n * Comments margin („Randspalte“): a column the host places inside its scroll container right of the editor, so it\n * scrolls with the text. Cards sit at the height of their text, the active thread is expanded; shown only while `open`\n * and the user's comments layout is `margin`.\n */\nexport function CommentsMargin({ editor, container, open = true }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** Whether the column is shown (the surface's `commentsOpen` model). */\n open?: boolean\n}) {\n const { settings } = useEditorSettings()\n const { t } = useEditorI18n()\n const enabled = settings.editing.commentsLayout === 'margin'\n // Idle (no Y.Doc observer) while another layout shows the comments.\n const comments = useComments(enabled ? editor : undefined)\n\n /** Draft and open threads with a text range, in document order. */\n const positioned = useMemo<Item[]>(() => {\n const items: Item[] = []\n if (comments.draft) items.push({ key: DRAFT, from: comments.draft.from, thread: null })\n for (const thread of comments.open) {\n const range = comments.ranges.get(thread.id)\n if (range) items.push({ key: thread.id, from: range.from, thread })\n }\n return items.sort((a, b) => a.from - b.from)\n }, [comments.draft, comments.open, comments.ranges])\n const orphans = useMemo(() => comments.open.filter(thread => !comments.ranges.has(thread.id)), [comments.open, comments.ranges])\n\n const [showResolved, setShowResolved] = useState(false)\n const [tops, setTops] = useState<Record<string, number>>({})\n const [resolvedTop, setResolvedTop] = useState(0)\n const [minHeight, setMinHeight] = useState(0)\n\n // Layout machinery that lives outside rendering: the observer, the pending frame, the cards by key.\n const [layout] = useState(() => ({\n observer: null as ResizeObserver | null,\n observed: new WeakSet<Element>(),\n cards: new Map<string, HTMLElement>(),\n frame: 0,\n column: null as HTMLElement | null,\n resolvedSection: null as HTMLElement | null,\n }))\n // What `relayout` reads: the latest render's values.\n const latest = useRef({ editor, positioned, orphans })\n latest.current = { editor, positioned, orphans }\n\n const observe = useCallback((el: Element) => {\n if (!layout.observer || layout.observed.has(el)) return\n layout.observed.add(el)\n layout.observer.observe(el)\n }, [layout])\n\n const unobserve = useCallback((el: Element) => {\n layout.observer?.unobserve(el)\n layout.observed.delete(el)\n }, [layout])\n\n const relayout = useCallback(() => {\n layout.frame = 0\n const { editor: instance, positioned: items, orphans: lost } = latest.current\n const col = layout.column\n // Hidden columns have no layout box; the column's own ResizeObserver re-runs this once shown.\n if (!instance || instance.isDestroyed || !col || !col.getClientRects().length) return\n const live = new Set([...items.map(i => i.key), ...lost.map(t => t.id)])\n for (const [key, el] of layout.cards) {\n if (live.has(key)) continue\n unobserve(el)\n layout.cards.delete(key)\n }\n\n const base = col.getBoundingClientRect().top\n const size = instance.state.doc.content.size\n const next: Record<string, number> = {}\n let bottom = -GAP\n for (const item of items) {\n let desired = bottom + GAP\n try {\n desired = instance.view.coordsAtPos(Math.min(item.from, size)).top - base\n }\n catch {\n // Position not rendered (yet): stack it below the previous card.\n }\n const top = Math.max(desired, bottom + GAP, 0)\n next[item.key] = top\n bottom = top + (layout.cards.get(item.key)?.offsetHeight ?? 0)\n }\n for (const thread of lost) {\n const top = bottom + GAP\n next[thread.id] = top\n bottom = top + (layout.cards.get(thread.id)?.offsetHeight ?? 0)\n }\n const nextResolvedTop = bottom < 0 ? 0 : bottom + 2 * GAP\n setTops(previous => sameTops(previous, next) ? previous : next)\n setResolvedTop(nextResolvedTop)\n setMinHeight(nextResolvedTop + (layout.resolvedSection?.offsetHeight ?? 0))\n }, [layout, unobserve])\n\n const schedule = useCallback(() => {\n if (layout.frame || typeof requestAnimationFrame === 'undefined') return\n layout.frame = requestAnimationFrame(relayout)\n }, [layout, relayout])\n\n // The observer and the window listener live while mounted; the elements already rendered are observed on creation.\n useEffect(() => {\n if (typeof ResizeObserver !== 'undefined') {\n layout.observer = new ResizeObserver(() => schedule())\n layout.observed = new WeakSet()\n for (const el of layout.cards.values()) observe(el)\n if (layout.column) observe(layout.column)\n if (layout.resolvedSection) observe(layout.resolvedSection)\n }\n window.addEventListener('resize', schedule)\n schedule()\n return () => {\n window.removeEventListener('resize', schedule)\n layout.observer?.disconnect()\n layout.observer = null\n if (layout.frame) cancelAnimationFrame(layout.frame)\n layout.frame = 0\n }\n }, [layout, observe, schedule])\n\n useEffect(() => {\n if (!editor) return\n editor.on('transaction', schedule)\n schedule()\n return () => { editor.off('transaction', schedule) }\n }, [editor, schedule])\n\n useEffect(() => {\n if (!container || !layout.observer) return\n layout.observer.observe(container)\n return () => layout.observer?.unobserve(container)\n }, [container, layout])\n\n // Cards appearing/disappearing or expanding: lay out once the DOM has them.\n useEffect(() => { schedule() }, [positioned, orphans, comments.active, comments.resolved.length, showResolved, enabled, schedule])\n\n // The column itself: its size flips between zero and real when the host toggles it.\n const setColumn = useCallback((el: HTMLElement | null) => {\n if (layout.column) unobserve(layout.column)\n layout.column = el\n if (el) observe(el)\n schedule()\n }, [layout, observe, unobserve, schedule])\n\n const setResolvedSection = useCallback((el: HTMLElement | null) => {\n if (layout.resolvedSection) unobserve(layout.resolvedSection)\n layout.resolvedSection = el\n if (el) observe(el)\n schedule()\n }, [layout, observe, unobserve, schedule])\n\n const setCard = (key: string, el: HTMLElement | null) => {\n if (!el) return\n layout.cards.set(key, el)\n observe(el)\n }\n\n /** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */\n function select(id: string) {\n if (comments.active !== id) comments.activate(id, { reveal: true })\n }\n\n const cardStyle = (key: string) => ({ top: `${tops[key] ?? 0}px` })\n\n return (\n <div\n ref={setColumn}\n className=\"relative w-[300px] shrink-0\"\n style={{ minHeight: `${minHeight}px`, ...(open && enabled ? {} : { display: 'none' }) }}\n aria-label={t('comments.title')}\n >\n {!comments.available\n ? <p className=\"p-3 text-sm text-muted-foreground\">{t('comments.unavailable')}</p>\n : (\n <>\n {positioned.map(item => (\n <div\n key={item.key}\n ref={el => setCard(item.key, el)}\n className={cn('absolute inset-x-0 transition-[top] duration-100', tops[item.key] === undefined && 'invisible')}\n style={cardStyle(item.key)}\n >\n {!item.thread\n ? (\n <CommentComposer\n quote={comments.draftQuote}\n className=\"shadow-md\"\n onSubmit={(text) => { comments.add(text) }}\n onCancel={() => comments.cancelDraft()}\n />\n )\n : comments.active === item.thread.id\n ? (\n <CommentThreadView\n thread={item.thread}\n user={comments.user}\n active\n orphan={false}\n comments={comments}\n className=\"shadow-md\"\n onClick={() => select(item.thread!.id)}\n />\n )\n : (\n <button\n type=\"button\"\n className=\"flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted\"\n onClick={() => select(item.thread!.id)}\n >\n <CommentAvatar name={item.thread.author.name} color={item.thread.author.color} size=\"2xs\" />\n <span className=\"shrink-0 font-medium\">{item.thread.author.name}</span>\n <span className=\"min-w-0 flex-1 truncate text-muted-foreground\">{excerpt(item.thread.text)}</span>\n {item.thread.replies.length > 0 && <span className=\"shrink-0 text-xs text-muted-foreground/70\">{item.thread.replies.length}</span>}\n </button>\n )}\n </div>\n ))}\n\n {orphans.map(thread => (\n <div\n key={thread.id}\n ref={el => setCard(thread.id, el)}\n className={cn('absolute inset-x-0', tops[thread.id] === undefined && 'invisible')}\n style={cardStyle(thread.id)}\n >\n <CommentThreadView\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan\n comments={comments}\n onClick={() => select(thread.id)}\n />\n </div>\n ))}\n\n {comments.resolved.length > 0 && (\n <div ref={setResolvedSection} className=\"absolute inset-x-0 flex flex-col gap-2\" style={{ top: `${resolvedTop}px` }}>\n <Button variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={() => setShowResolved(value => !value)}>\n {t('comments.resolvedCount', { n: comments.resolved.length })}\n <Icon name={showResolved ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'} />\n </Button>\n {showResolved && comments.resolved.map(thread => (\n <CommentThreadView\n key={thread.id}\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan={comments.isOrphan(thread.id)}\n comments={comments}\n onClick={() => select(thread.id)}\n />\n ))}\n </div>\n )}\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CommentsMargin.js","names":[],"sources":["../../../../src/features/comments/react/CommentsMargin.tsx"],"sourcesContent":["/**\n * Comments margin („Randspalte“): a 300px column the host places inside its scroll container right of the editor,\n * so it scrolls with the text. Layout „Aktiver Faden“ (`layoutMargin`): the active thread's card sits exactly at the\n * top of its text's line and expands, the others collapse to one line and give way up and down; without an active\n * thread the cards stack top-down at their lines. The draft composer takes part like an active card. An orphaned thread\n * (its text was deleted) is greyed out and sits where the text was; resolved threads („Erledigt“) sit at the bottom\n * behind a toggle. Shown only while `open` (the host binds the surface's `commentsOpen` model) and the user's comments\n * layout is `margin`; the layout is recomputed whenever the column becomes visible again.\n */\nimport type { Editor } from '@tiptap/core'\nimport { useMemo, useState } from 'react'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { DRAFT_CARD, marginCards } from '../margin-layout'\nimport { CommentAvatar } from './CommentAvatar'\nimport { CommentComposer } from './CommentComposer'\nimport { CommentThreadView } from './CommentThreadView'\nimport { useComments } from './use-comments'\nimport { useMarginLayout } from './use-margin-layout'\n\nconst excerpt = (text: string) => text.length > 60 ? `${text.slice(0, 60)}…` : text\n\nexport function CommentsMargin({ editor, container, open = true }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** Whether the column is shown (the surface's `commentsOpen` model). */\n open?: boolean\n}) {\n const { settings } = useEditorSettings()\n const { t } = useEditorI18n()\n const enabled = settings.editing.commentsLayout === 'margin'\n // Idle (no Y.Doc observer) while another layout shows the comments.\n const comments = useComments(enabled ? editor : undefined)\n\n /** Draft and open threads in document order, each at its text (an orphan where its text was). */\n const cards = useMemo(\n () => marginCards({ draft: comments.draft, open: comments.open, ranges: comments.ranges, orphanAt: comments.orphanAt }),\n [comments.draft, comments.open, comments.ranges, comments.orphanAt],\n )\n /** The card that owns its line: the draft composer, else the active thread. */\n const activeId = comments.draft ? DRAFT_CARD : comments.active\n\n const { state: layout, setColumn, setResolvedSection, setCard } = useMarginLayout({ editor, container, cards, activeId })\n const [showResolved, setShowResolved] = useState(false)\n\n /** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */\n function select(id: string) {\n if (comments.active !== id) comments.activate(id, { reveal: true })\n }\n\n return (\n <div\n ref={setColumn}\n className=\"relative w-[300px] shrink-0\"\n style={{ minHeight: `${layout.minHeight}px`, ...(open && enabled ? {} : { display: 'none' }) }}\n aria-label={t('comments.title')}\n >\n {!comments.available\n ? <p className=\"p-3 text-sm text-muted-foreground\">{t('comments.unavailable')}</p>\n : (\n <>\n {cards.map(card => (\n <div\n key={card.id}\n ref={el => setCard(card.id, el)}\n className={cn('absolute inset-x-0 transition-[top] duration-100', !layout.tops.has(card.id) && 'invisible')}\n style={{ top: `${layout.tops.get(card.id) ?? 0}px` }}\n >\n {!card.thread\n ? (\n <CommentComposer\n quote={comments.draftQuote}\n className=\"shadow-md\"\n onSubmit={(text) => { comments.add(text) }}\n onCancel={() => comments.cancelDraft()}\n />\n )\n : activeId === card.id\n ? (\n <CommentThreadView\n thread={card.thread}\n user={comments.user}\n active\n orphan={card.orphan}\n comments={comments}\n className=\"shadow-md\"\n onClick={() => select(card.id)}\n />\n )\n : (\n <button\n type=\"button\"\n className={cn('flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted', card.orphan && 'opacity-70')}\n onClick={() => select(card.id)}\n >\n <CommentAvatar name={card.thread.author.name} color={card.thread.author.color} size=\"2xs\" />\n <span className=\"shrink-0 font-medium\">{card.thread.author.name}</span>\n <span className=\"min-w-0 flex-1 truncate text-muted-foreground\">{excerpt(card.thread.text)}</span>\n {card.orphan && <span className=\"shrink-0 text-xs text-muted-foreground/70 italic\">{t('comments.orphan')}</span>}\n {card.thread.replies.length > 0 && <span className=\"shrink-0 text-xs text-muted-foreground/70\">{card.thread.replies.length}</span>}\n </button>\n )}\n </div>\n ))}\n\n {comments.resolved.length > 0 && (\n <div ref={setResolvedSection} className=\"absolute inset-x-0 flex flex-col gap-2\" style={{ top: `${layout.resolvedTop}px` }}>\n <Button variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={() => setShowResolved(value => !value)}>\n {t('comments.resolvedCount', { n: comments.resolved.length })}\n <Icon name={showResolved ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'} />\n </Button>\n {showResolved && comments.resolved.map(thread => (\n <CommentThreadView\n key={thread.id}\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan={comments.isOrphan(thread.id)}\n comments={comments}\n onClick={() => select(thread.id)}\n />\n ))}\n </div>\n )}\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
+ import { cancelCommentDraft, commentsKey, setActiveComment } from "../comments-plugin.js";
1
2
  import { useEditorSettings } from "../../../react/settings.js";
2
3
  import { useEditorUi } from "../../../react/editor-ui.js";
3
- import { cancelCommentDraft, commentsKey, setActiveComment } from "../comments-plugin.js";
4
4
  import { CommentBubble } from "./CommentBubble.js";
5
5
  import { CommentsPopover } from "./CommentsPopover.js";
6
6
  import { useEffect, useRef } from "react";
@@ -1,5 +1,5 @@
1
- import { useStore } from "../../../react/use-store.js";
2
1
  import { createEditorCommentsModel } from "../model.js";
2
+ import { useStore } from "../../../react/use-store.js";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  //#region src/features/comments/react/use-comments.ts
5
5
  /**
@@ -0,0 +1,45 @@
1
+ import { createMarginController } from "../margin-controller.js";
2
+ import { useStore } from "../../../react/use-store.js";
3
+ import { useEffect, useRef, useState } from "react";
4
+ //#region src/features/comments/react/use-margin-layout.ts
5
+ /**
6
+ * The layout of the comments margin for React: a thin hook over `createMarginController` (see there for the
7
+ * measuring). Hand the column, the card and the resolved section elements in through the returned `set*` functions
8
+ * (as ref callbacks); `state` holds the measured tops. Re-measures when `cards` or `activeId` change. It starts on
9
+ * mount and stops on unmount.
10
+ */
11
+ function useMarginLayout({ editor, container, cards, activeId }) {
12
+ const latest = useRef({
13
+ cards,
14
+ activeId
15
+ });
16
+ latest.current = {
17
+ cards,
18
+ activeId
19
+ };
20
+ const [controller] = useState(() => createMarginController({
21
+ cards: () => latest.current.cards,
22
+ activeId: () => latest.current.activeId
23
+ }));
24
+ useEffect(() => controller.setEditor(editor), [controller, editor]);
25
+ useEffect(() => controller.setContainer(container), [controller, container]);
26
+ useEffect(() => {
27
+ controller.connect();
28
+ return controller.disconnect;
29
+ }, [controller]);
30
+ useEffect(() => controller.schedule(), [
31
+ controller,
32
+ cards,
33
+ activeId
34
+ ]);
35
+ return {
36
+ state: useStore(controller.state),
37
+ setColumn: controller.setColumn,
38
+ setResolvedSection: controller.setResolvedSection,
39
+ setCard: controller.setCard
40
+ };
41
+ }
42
+ //#endregion
43
+ export { useMarginLayout };
44
+
45
+ //# sourceMappingURL=use-margin-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-margin-layout.js","names":[],"sources":["../../../../src/features/comments/react/use-margin-layout.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useRef, useState } from 'react'\nimport { useStore } from '../../../react/use-store'\nimport { createMarginController, type MarginState } from '../margin-controller'\nimport type { MarginCard } from '../margin-layout'\n\n/**\n * The layout of the comments margin for React: a thin hook over `createMarginController` (see there for the\n * measuring). Hand the column, the card and the resolved section elements in through the returned `set*` functions\n * (as ref callbacks); `state` holds the measured tops. Re-measures when `cards` or `activeId` change. It starts on\n * mount and stops on unmount.\n */\nexport function useMarginLayout({ editor, container, cards, activeId }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** The cards in document order, and the one that owns its line. */\n cards: readonly MarginCard[]\n activeId: string | null\n}) {\n // What the controller reads at each measurement: the latest render's values.\n const latest = useRef({ cards, activeId })\n latest.current = { cards, activeId }\n const [controller] = useState(() => createMarginController({ cards: () => latest.current.cards, activeId: () => latest.current.activeId }))\n\n useEffect(() => controller.setEditor(editor), [controller, editor])\n useEffect(() => controller.setContainer(container), [controller, container])\n // Paired start and stop, so StrictMode's mount, cleanup, mount leaves one observer.\n useEffect(() => {\n controller.connect()\n return controller.disconnect\n }, [controller])\n // Cards appearing, disappearing or expanding: measure once the DOM has them.\n useEffect(() => controller.schedule(), [controller, cards, activeId])\n\n const state: MarginState = useStore(controller.state)\n return { state, setColumn: controller.setColumn, setResolvedSection: controller.setResolvedSection, setCard: controller.setCard }\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,gBAAgB,EAAE,QAAQ,WAAW,OAAO,YAOzD;CAED,MAAM,SAAS,OAAO;EAAE;EAAO;CAAS,CAAC;CACzC,OAAO,UAAU;EAAE;EAAO;CAAS;CACnC,MAAM,CAAC,cAAc,eAAe,uBAAuB;EAAE,aAAa,OAAO,QAAQ;EAAO,gBAAgB,OAAO,QAAQ;CAAS,CAAC,CAAC;CAE1I,gBAAgB,WAAW,UAAU,MAAM,GAAG,CAAC,YAAY,MAAM,CAAC;CAClE,gBAAgB,WAAW,aAAa,SAAS,GAAG,CAAC,YAAY,SAAS,CAAC;CAE3E,gBAAgB;EACd,WAAW,QAAQ;EACnB,OAAO,WAAW;CACpB,GAAG,CAAC,UAAU,CAAC;CAEf,gBAAgB,WAAW,SAAS,GAAG;EAAC;EAAY;EAAO;CAAQ,CAAC;CAGpE,OAAO;EAAE,OADkB,SAAS,WAAW,KAClC;EAAG,WAAW,WAAW;EAAW,oBAAoB,WAAW;EAAoB,SAAS,WAAW;CAAQ;AAClI"}
@@ -1,6 +1,6 @@
1
+ import { CommentsFeatureOptions } from "./feature.js";
1
2
  import { ReactFeature } from "../../react/EditorSurface.js";
2
3
  import "../../react/index.js";
3
- import { CommentsFeatureOptions } from "./feature.js";
4
4
  import { CommentBubble } from "./react/CommentBubble.js";
5
5
  import { CommentComposer } from "./react/CommentComposer.js";
6
6
  import { CommentsListMenu } from "./react/CommentsListMenu.js";
@@ -20,6 +20,23 @@ export interface CommentReply {
20
20
  createdAt: string;
21
21
  editedAt?: string;
22
22
  }
23
+ /**
24
+ * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the
25
+ * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text
26
+ * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.
27
+ */
28
+ export interface CommentAnchor {
29
+ type?: {
30
+ client: number;
31
+ clock: number;
32
+ };
33
+ tname?: string;
34
+ item?: {
35
+ client: number;
36
+ clock: number;
37
+ };
38
+ assoc?: number;
39
+ }
23
40
  /**
24
41
  * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`
25
42
  * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the
@@ -35,6 +52,12 @@ export interface CommentThread {
35
52
  quote: string;
36
53
  /** Resolved threads are kept, without highlight, until reopened. */
37
54
  resolved: boolean;
55
+ /**
56
+ * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it
57
+ * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads
58
+ * not yet seen with a range, and when the position could not be captured.
59
+ */
60
+ anchor?: CommentAnchor;
38
61
  replies: CommentReply[];
39
62
  }
40
63
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/comments/schema.ts"],"sourcesContent":["import { type AnyExtension, Mark, mergeAttributes } from '@tiptap/core'\nimport type { AuthorIdentity } from '../../core/feature'\nimport '../../core/schema/mark-flags'\n\n// Vue-free and server-importable (`@ecsplico/editor/comments/schema`): the comment mark that anchors a thread to\n// text, the thread data model and the limits. The Y.Map store, the highlights and the UI live in the feature.\n\nexport { COMMENTS_LAYOUTS, type CommentsLayout } from '../../settings/values'\n\n/** Name of the `Y.Map` holding the comment threads in the Y.Doc, beside the document's 'default' XmlFragment. */\nexport const COMMENTS_FIELD = 'comments'\n\n/** Maximum lengths of a comment's or reply's text and of the quoted text a thread keeps of its range. */\nexport const COMMENT_LIMITS = { text: 5000, quote: 200 } as const\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n replies: CommentReply[]\n}\n\n/**\n * Anchors a comment thread to text (attribute `id` = the thread's id). Storage only: the visible highlight is a\n * client-side decoration driven by the thread's state (`Comments` extension). `excludes: ''` lets the marks of\n * overlapping threads coexist on the same text; `inclusive: false` keeps text typed at the end of a range outside it.\n * No Markdown form (`markdownInvisible`).\n */\nexport const CommentMark = Mark.create({\n name: 'comment',\n excludes: '',\n inclusive: false,\n markdownInvisible: true,\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: el => el.getAttribute('data-comment-id'),\n renderHTML: attrs => ({ 'data-comment-id': attrs.id }),\n },\n }\n },\n parseHTML: () => [{ tag: 'span[data-comment-id]' }],\n renderHTML: ({ HTMLAttributes }) => ['span', mergeAttributes(HTMLAttributes, { class: 'comment' }), 0],\n})\n\n/** The comment mark of the document schema. */\nexport const commentsSchemaExtensions = (): AnyExtension[] => [CommentMark]\n"],"mappings":";;;;AAUA,MAAa,iBAAiB;;AAG9B,MAAa,iBAAiB;CAAE,MAAM;CAAM,OAAO;AAAI;;;;;;;AAsCvD,MAAa,cAAc,KAAK,OAAO;CACrC,MAAM;CACN,UAAU;CACV,WAAW;CACX,mBAAmB;CACnB,gBAAgB;EACd,OAAO,EACL,IAAI;GACF,SAAS;GACT,YAAW,OAAM,GAAG,aAAa,iBAAiB;GAClD,aAAY,WAAU,EAAE,mBAAmB,MAAM,GAAG;EACtD,EACF;CACF;CACA,iBAAiB,CAAC,EAAE,KAAK,wBAAwB,CAAC;CAClD,aAAa,EAAE,qBAAqB;EAAC;EAAQ,gBAAgB,gBAAgB,EAAE,OAAO,UAAU,CAAC;EAAG;CAAC;AACvG,CAAC;;AAGD,MAAa,iCAAiD,CAAC,WAAW"}
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/comments/schema.ts"],"sourcesContent":["import { type AnyExtension, Mark, mergeAttributes } from '@tiptap/core'\nimport type { AuthorIdentity } from '../../core/feature'\nimport '../../core/schema/mark-flags'\n\n// Vue-free and server-importable (`@ecsplico/editor/comments/schema`): the comment mark that anchors a thread to\n// text, the thread data model and the limits. The Y.Map store, the highlights and the UI live in the feature.\n\nexport { COMMENTS_LAYOUTS, type CommentsLayout } from '../../settings/values'\n\n/** Name of the `Y.Map` holding the comment threads in the Y.Doc, beside the document's 'default' XmlFragment. */\nexport const COMMENTS_FIELD = 'comments'\n\n/** Maximum lengths of a comment's or reply's text and of the quoted text a thread keeps of its range. */\nexport const COMMENT_LIMITS = { text: 5000, quote: 200 } as const\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/**\n * Anchors a comment thread to text (attribute `id` = the thread's id). Storage only: the visible highlight is a\n * client-side decoration driven by the thread's state (`Comments` extension). `excludes: ''` lets the marks of\n * overlapping threads coexist on the same text; `inclusive: false` keeps text typed at the end of a range outside it.\n * No Markdown form (`markdownInvisible`).\n */\nexport const CommentMark = Mark.create({\n name: 'comment',\n excludes: '',\n inclusive: false,\n markdownInvisible: true,\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: el => el.getAttribute('data-comment-id'),\n renderHTML: attrs => ({ 'data-comment-id': attrs.id }),\n },\n }\n },\n parseHTML: () => [{ tag: 'span[data-comment-id]' }],\n renderHTML: ({ HTMLAttributes }) => ['span', mergeAttributes(HTMLAttributes, { class: 'comment' }), 0],\n})\n\n/** The comment mark of the document schema. */\nexport const commentsSchemaExtensions = (): AnyExtension[] => [CommentMark]\n"],"mappings":";;;;AAUA,MAAa,iBAAiB;;AAG9B,MAAa,iBAAiB;CAAE,MAAM;CAAM,OAAO;AAAI;;;;;;;AAwDvD,MAAa,cAAc,KAAK,OAAO;CACrC,MAAM;CACN,UAAU;CACV,WAAW;CACX,mBAAmB;CACnB,gBAAgB;EACd,OAAO,EACL,IAAI;GACF,SAAS;GACT,YAAW,OAAM,GAAG,aAAa,iBAAiB;GAClD,aAAY,WAAU,EAAE,mBAAmB,MAAM,GAAG;EACtD,EACF;CACF;CACA,iBAAiB,CAAC,EAAE,KAAK,wBAAwB,CAAC;CAClD,aAAa,EAAE,qBAAqB;EAAC;EAAQ,gBAAgB,gBAAgB,EAAE,OAAO,UAAU,CAAC;EAAG;CAAC;AACvG,CAAC;;AAGD,MAAa,iCAAiD,CAAC,WAAW"}
@@ -0,0 +1,40 @@
1
+ //#region src/features/comments/snapshot-equality.ts
2
+ const NO_RANGES = /* @__PURE__ */ new Map();
3
+ const NO_THREADS = [];
4
+ const NO_POSITIONS = /* @__PURE__ */ new Map();
5
+ /** The snapshot of an editor that has no comments (yet). */
6
+ const EMPTY_SNAPSHOT = {
7
+ available: false,
8
+ canComment: false,
9
+ user: null,
10
+ threads: NO_THREADS,
11
+ ranges: NO_RANGES,
12
+ orphanAt: NO_POSITIONS,
13
+ active: null,
14
+ draft: null,
15
+ draftQuote: "",
16
+ open: NO_THREADS,
17
+ resolved: NO_THREADS
18
+ };
19
+ function sameRange(a, b) {
20
+ return a === b || !!a && !!b && a.from === b.from && a.to === b.to;
21
+ }
22
+ function sameRanges(a, b) {
23
+ if (a === b) return true;
24
+ if (a.size !== b.size) return false;
25
+ for (const [id, range] of a) if (!sameRange(range, b.get(id) ?? null)) return false;
26
+ return true;
27
+ }
28
+ function sameOrphanAt(a, b) {
29
+ if (a === b) return true;
30
+ if (a.size !== b.size) return false;
31
+ for (const [id, position] of a) if (b.get(id) !== position) return false;
32
+ return true;
33
+ }
34
+ function sameUser(a, b) {
35
+ return a === b || !!a && !!b && a.id === b.id && a.name === b.name && a.color === b.color;
36
+ }
37
+ //#endregion
38
+ export { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser };
39
+
40
+ //# sourceMappingURL=snapshot-equality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-equality.js","names":[],"sources":["../../../src/features/comments/snapshot-equality.ts"],"sourcesContent":["import type { AuthorIdentity } from '../../core'\nimport type { CommentsSnapshot, DocRange } from './model'\nimport type { CommentThread } from './schema'\n\n// What both comments models (the Y.Doc one in `./model` and the REST one in `adapters/comments/client`) need to hand\n// out a snapshot that keeps its identity while nothing in it changed. Type-only imports: this module stays free of `yjs`.\n\nexport const NO_RANGES: ReadonlyMap<string, DocRange> = new Map()\nexport const NO_THREADS: readonly CommentThread[] = []\nexport const NO_POSITIONS: ReadonlyMap<string, number> = new Map()\n\n/** The snapshot of an editor that has no comments (yet). */\nexport const EMPTY_SNAPSHOT: CommentsSnapshot = {\n available: false,\n canComment: false,\n user: null,\n threads: NO_THREADS,\n ranges: NO_RANGES,\n orphanAt: NO_POSITIONS,\n active: null,\n draft: null,\n draftQuote: '',\n open: NO_THREADS,\n resolved: NO_THREADS,\n}\n\nexport function sameRange(a: DocRange | null, b: DocRange | null): boolean {\n return a === b || (!!a && !!b && a.from === b.from && a.to === b.to)\n}\n\nexport function sameRanges(a: ReadonlyMap<string, DocRange>, b: ReadonlyMap<string, DocRange>): boolean {\n if (a === b) return true\n if (a.size !== b.size) return false\n for (const [id, range] of a) {\n if (!sameRange(range, b.get(id) ?? null)) return false\n }\n return true\n}\n\nexport function sameOrphanAt(a: ReadonlyMap<string, number>, b: ReadonlyMap<string, number>): boolean {\n if (a === b) return true\n if (a.size !== b.size) return false\n for (const [id, position] of a) {\n if (b.get(id) !== position) return false\n }\n return true\n}\n\nexport function sameUser(a: AuthorIdentity | null, b: AuthorIdentity | null): boolean {\n return a === b || (!!a && !!b && a.id === b.id && a.name === b.name && a.color === b.color)\n}\n"],"mappings":";AAOA,MAAa,4BAA2C,IAAI,IAAI;AAChE,MAAa,aAAuC,CAAC;AACrD,MAAa,+BAA4C,IAAI,IAAI;;AAGjE,MAAa,iBAAmC;CAC9C,WAAW;CACX,YAAY;CACZ,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,OAAO;CACP,YAAY;CACZ,MAAM;CACN,UAAU;AACZ;AAEA,SAAgB,UAAU,GAAoB,GAA6B;CACzE,OAAO,MAAM,KAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE;AACnE;AAEA,SAAgB,WAAW,GAAkC,GAA2C;CACtG,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,SAAS,EAAE,MAAM,OAAO;CAC9B,KAAK,MAAM,CAAC,IAAI,UAAU,GACxB,IAAI,CAAC,UAAU,OAAO,EAAE,IAAI,EAAE,KAAK,IAAI,GAAG,OAAO;CAEnD,OAAO;AACT;AAEA,SAAgB,aAAa,GAAgC,GAAyC;CACpG,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,SAAS,EAAE,MAAM,OAAO;CAC9B,KAAK,MAAM,CAAC,IAAI,aAAa,GAC3B,IAAI,EAAE,IAAI,EAAE,MAAM,UAAU,OAAO;CAErC,OAAO;AACT;AAEA,SAAgB,SAAS,GAA0B,GAAmC;CACpF,OAAO,MAAM,KAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE;AACvF"}