@tldraw/commenting 5.3.0-internal.c5c7f1d817d0 → 5.3.0-next.2fa9c61a8de6

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 (268) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1040 -230
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-drafts.js +54 -0
  8. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  9. package/dist-cjs/canvas/comment-mutations.js +123 -0
  10. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  11. package/dist-cjs/canvas/comment-reactions.js +163 -0
  12. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  13. package/dist-cjs/canvas/comment-render.js +5 -3
  14. package/dist-cjs/canvas/comment-render.js.map +2 -2
  15. package/dist-cjs/canvas/comment-store.js +19 -9
  16. package/dist-cjs/canvas/comment-store.js.map +2 -2
  17. package/dist-cjs/canvas/comment-tool.js +64 -14
  18. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  19. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  20. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  21. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  22. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overlay.js +377 -437
  24. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  25. package/dist-cjs/canvas/comments-sidebar.js +34 -31
  26. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  27. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  28. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  29. package/dist-cjs/canvas/context.js +17 -0
  30. package/dist-cjs/canvas/context.js.map +7 -0
  31. package/dist-cjs/canvas/hooks.js +22 -3
  32. package/dist-cjs/canvas/hooks.js.map +2 -2
  33. package/dist-cjs/canvas/options.js +19 -2
  34. package/dist-cjs/canvas/options.js.map +2 -2
  35. package/dist-cjs/canvas/pin-stacking.js +56 -0
  36. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  37. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  38. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  39. package/dist-cjs/canvas/state.js +28 -14
  40. package/dist-cjs/canvas/state.js.map +2 -2
  41. package/dist-cjs/canvas/thread-preview.js +185 -0
  42. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  43. package/dist-cjs/canvas/thread-stack.js +199 -0
  44. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  45. package/dist-cjs/canvas/thread-state.js +67 -21
  46. package/dist-cjs/canvas/thread-state.js.map +2 -2
  47. package/dist-cjs/canvas/thread-view.js +397 -0
  48. package/dist-cjs/canvas/thread-view.js.map +7 -0
  49. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  50. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  51. package/dist-cjs/clustering/runtime.js +9 -9
  52. package/dist-cjs/clustering/runtime.js.map +2 -2
  53. package/dist-cjs/clustering/types.js.map +1 -1
  54. package/dist-cjs/index.d.ts +763 -353
  55. package/dist-cjs/index.js +46 -28
  56. package/dist-cjs/index.js.map +2 -2
  57. package/dist-cjs/ui/byline.js +3 -1
  58. package/dist-cjs/ui/byline.js.map +2 -2
  59. package/dist-cjs/ui/comment-card.js +12 -5
  60. package/dist-cjs/ui/comment-card.js.map +2 -2
  61. package/dist-cjs/ui/comment-composer.js +112 -17
  62. package/dist-cjs/ui/comment-composer.js.map +2 -2
  63. package/dist-cjs/ui/comment-pin.js +20 -2
  64. package/dist-cjs/ui/comment-pin.js.map +2 -2
  65. package/dist-cjs/ui/comment-thread.js +4 -2
  66. package/dist-cjs/ui/comment-thread.js.map +2 -2
  67. package/dist-cjs/ui/comments-list.js +22 -10
  68. package/dist-cjs/ui/comments-list.js.map +2 -2
  69. package/dist-cjs/ui/count-badge.js +3 -2
  70. package/dist-cjs/ui/count-badge.js.map +2 -2
  71. package/dist-cjs/ui/emoji-picker.js +54 -0
  72. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  73. package/dist-cjs/ui/format-time.js +4 -1
  74. package/dist-cjs/ui/format-time.js.map +2 -2
  75. package/dist-cjs/ui/reaction-picker.js +73 -0
  76. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  77. package/dist-cjs/ui/reaction.js +50 -4
  78. package/dist-cjs/ui/reaction.js.map +2 -2
  79. package/dist-cjs/ui/reactions.js +20 -15
  80. package/dist-cjs/ui/reactions.js.map +2 -2
  81. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  82. package/dist-cjs/ui/reply-count.js.map +7 -0
  83. package/dist-cjs/ui/send-button.js +20 -1
  84. package/dist-cjs/ui/send-button.js.map +2 -2
  85. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  86. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  87. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  89. package/dist-esm/canvas/cluster-input.mjs +2 -2
  90. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  91. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  92. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  93. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  94. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  95. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  96. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  97. package/dist-esm/canvas/comment-render.mjs +5 -3
  98. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  99. package/dist-esm/canvas/comment-store.mjs +19 -9
  100. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  101. package/dist-esm/canvas/comment-tool.mjs +67 -17
  102. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  103. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  104. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  105. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  106. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  107. package/dist-esm/canvas/comments-overlay.mjs +398 -442
  108. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  109. package/dist-esm/canvas/comments-sidebar.mjs +37 -34
  110. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  112. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  113. package/dist-esm/canvas/context.mjs +1 -0
  114. package/dist-esm/canvas/context.mjs.map +7 -0
  115. package/dist-esm/canvas/hooks.mjs +28 -5
  116. package/dist-esm/canvas/hooks.mjs.map +2 -2
  117. package/dist-esm/canvas/options.mjs +21 -3
  118. package/dist-esm/canvas/options.mjs.map +2 -2
  119. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  120. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  121. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  122. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  123. package/dist-esm/canvas/state.mjs +29 -19
  124. package/dist-esm/canvas/state.mjs.map +2 -2
  125. package/dist-esm/canvas/thread-preview.mjs +170 -0
  126. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  127. package/dist-esm/canvas/thread-stack.mjs +190 -0
  128. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  129. package/dist-esm/canvas/thread-state.mjs +71 -22
  130. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-view.mjs +402 -0
  132. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  133. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  134. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  135. package/dist-esm/clustering/runtime.mjs +9 -9
  136. package/dist-esm/clustering/runtime.mjs.map +2 -2
  137. package/dist-esm/index.d.mts +763 -353
  138. package/dist-esm/index.mjs +87 -41
  139. package/dist-esm/index.mjs.map +2 -2
  140. package/dist-esm/ui/byline.mjs +3 -1
  141. package/dist-esm/ui/byline.mjs.map +2 -2
  142. package/dist-esm/ui/comment-card.mjs +12 -5
  143. package/dist-esm/ui/comment-card.mjs.map +2 -2
  144. package/dist-esm/ui/comment-composer.mjs +122 -16
  145. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  146. package/dist-esm/ui/comment-pin.mjs +20 -2
  147. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  148. package/dist-esm/ui/comment-thread.mjs +4 -2
  149. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  150. package/dist-esm/ui/comments-list.mjs +39 -25
  151. package/dist-esm/ui/comments-list.mjs.map +2 -2
  152. package/dist-esm/ui/count-badge.mjs +3 -2
  153. package/dist-esm/ui/count-badge.mjs.map +2 -2
  154. package/dist-esm/ui/emoji-picker.mjs +34 -0
  155. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  156. package/dist-esm/ui/format-time.mjs +4 -1
  157. package/dist-esm/ui/format-time.mjs.map +2 -2
  158. package/dist-esm/ui/reaction-picker.mjs +60 -0
  159. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  160. package/dist-esm/ui/reaction.mjs +51 -5
  161. package/dist-esm/ui/reaction.mjs.map +2 -2
  162. package/dist-esm/ui/reactions.mjs +21 -16
  163. package/dist-esm/ui/reactions.mjs.map +2 -2
  164. package/dist-esm/ui/reply-count.mjs +9 -0
  165. package/dist-esm/ui/reply-count.mjs.map +7 -0
  166. package/dist-esm/ui/send-button.mjs +20 -1
  167. package/dist-esm/ui/send-button.mjs.map +2 -2
  168. package/dist-esm/ui/tooltip-button.mjs +12 -0
  169. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  170. package/package.json +6 -6
  171. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  172. package/src/canvas/anchor-lifecycle.ts +205 -0
  173. package/src/canvas/canvas.css +439 -24
  174. package/src/canvas/cluster-input.test.ts +31 -23
  175. package/src/canvas/cluster-input.ts +3 -4
  176. package/src/canvas/comment-drafts.test.ts +55 -0
  177. package/src/canvas/comment-drafts.ts +46 -0
  178. package/src/canvas/comment-mutations.test.ts +410 -0
  179. package/src/canvas/comment-mutations.ts +288 -0
  180. package/src/canvas/comment-reactions.test.ts +132 -0
  181. package/src/canvas/comment-reactions.tsx +245 -0
  182. package/src/canvas/comment-render.ts +9 -5
  183. package/src/canvas/comment-store.ts +67 -27
  184. package/src/canvas/comment-tool.test.ts +162 -0
  185. package/src/canvas/comment-tool.tsx +105 -23
  186. package/src/canvas/comments-filter-menu.tsx +23 -32
  187. package/src/canvas/comments-overflow-menu.tsx +12 -19
  188. package/src/canvas/comments-overlay.tsx +602 -562
  189. package/src/canvas/comments-sidebar.test.ts +45 -0
  190. package/src/canvas/comments-sidebar.tsx +75 -57
  191. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  192. package/src/canvas/context.ts +47 -0
  193. package/src/canvas/hooks.ts +52 -11
  194. package/src/canvas/options.test.ts +52 -2
  195. package/src/canvas/options.ts +133 -9
  196. package/src/canvas/pin-stacking.test.ts +114 -0
  197. package/src/canvas/pin-stacking.ts +57 -0
  198. package/src/canvas/sidebar-filters.ts +4 -3
  199. package/src/canvas/state.ts +92 -35
  200. package/src/canvas/thread-preview.test.ts +91 -0
  201. package/src/canvas/thread-preview.tsx +304 -0
  202. package/src/canvas/thread-stack.tsx +252 -0
  203. package/src/canvas/thread-state.test.ts +209 -0
  204. package/src/canvas/thread-state.ts +137 -36
  205. package/src/canvas/thread-view.tsx +516 -0
  206. package/src/clustering/computeClusterTable.test.ts +24 -23
  207. package/src/clustering/computeClusterTable.ts +5 -5
  208. package/src/clustering/runtime.test.ts +34 -0
  209. package/src/clustering/runtime.ts +20 -15
  210. package/src/clustering/types.ts +9 -9
  211. package/src/index.ts +76 -40
  212. package/src/ui/byline.tsx +4 -2
  213. package/src/ui/comment-card.tsx +17 -7
  214. package/src/ui/comment-composer.tsx +160 -13
  215. package/src/ui/comment-pin.tsx +22 -2
  216. package/src/ui/comment-thread.tsx +6 -1
  217. package/src/ui/comments-list.test.ts +31 -0
  218. package/src/ui/comments-list.tsx +32 -12
  219. package/src/ui/comments.css +489 -204
  220. package/src/ui/count-badge.tsx +9 -2
  221. package/src/ui/emoji-picker.test.ts +25 -0
  222. package/src/ui/emoji-picker.tsx +76 -0
  223. package/src/ui/format-time.ts +7 -2
  224. package/src/ui/reaction-picker.tsx +109 -0
  225. package/src/ui/reaction.tsx +116 -4
  226. package/src/ui/reactions.tsx +68 -13
  227. package/src/ui/reply-count.ts +16 -0
  228. package/src/ui/send-button.tsx +18 -3
  229. package/src/ui/tooltip-button.tsx +20 -0
  230. package/dist-cjs/canvas/region-options.js +0 -49
  231. package/dist-cjs/canvas/region-options.js.map +0 -7
  232. package/dist-cjs/ui/avatar.js +0 -51
  233. package/dist-cjs/ui/avatar.js.map +0 -7
  234. package/dist-cjs/ui/comment-mention.js +0 -42
  235. package/dist-cjs/ui/comment-mention.js.map +0 -7
  236. package/dist-cjs/ui/comment-text.js.map +0 -7
  237. package/dist-cjs/ui/mention-list.js +0 -70
  238. package/dist-cjs/ui/mention-list.js.map +0 -7
  239. package/dist-cjs/ui/mention-suggestion.js +0 -206
  240. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  241. package/dist-cjs/ui/mention.js.map +0 -7
  242. package/dist-cjs/ui/render-markdown.js +0 -63
  243. package/dist-cjs/ui/render-markdown.js.map +0 -7
  244. package/dist-esm/canvas/region-options.mjs +0 -29
  245. package/dist-esm/canvas/region-options.mjs.map +0 -7
  246. package/dist-esm/ui/avatar.mjs +0 -31
  247. package/dist-esm/ui/avatar.mjs.map +0 -7
  248. package/dist-esm/ui/comment-mention.mjs +0 -22
  249. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  250. package/dist-esm/ui/comment-text.mjs +0 -9
  251. package/dist-esm/ui/comment-text.mjs.map +0 -7
  252. package/dist-esm/ui/mention-list.mjs +0 -50
  253. package/dist-esm/ui/mention-list.mjs.map +0 -7
  254. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  255. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  256. package/dist-esm/ui/mention.mjs +0 -11
  257. package/dist-esm/ui/mention.mjs.map +0 -7
  258. package/dist-esm/ui/render-markdown.mjs +0 -45
  259. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  260. package/src/canvas/region-options.ts +0 -57
  261. package/src/ui/avatar.tsx +0 -38
  262. package/src/ui/comment-mention.ts +0 -47
  263. package/src/ui/comment-text.tsx +0 -12
  264. package/src/ui/mention-list.tsx +0 -106
  265. package/src/ui/mention-suggestion.test.ts +0 -18
  266. package/src/ui/mention-suggestion.tsx +0 -285
  267. package/src/ui/mention.tsx +0 -9
  268. package/src/ui/render-markdown.tsx +0 -72
@@ -1,6 +1,25 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  function SendButton({ label, disabled, onClick }) {
3
- return /* @__PURE__ */ jsx("button", { className: "tlui-cmt-send", type: "button", disabled, onClick, children: label });
3
+ return /* @__PURE__ */ jsx(
4
+ "button",
5
+ {
6
+ className: "tlui-cmt-send",
7
+ type: "button",
8
+ disabled,
9
+ onClick,
10
+ "aria-label": label,
11
+ title: label,
12
+ children: /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
13
+ "path",
14
+ {
15
+ d: "M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z",
16
+ fill: "currentColor",
17
+ fillRule: "evenodd",
18
+ clipRule: "evenodd"
19
+ }
20
+ ) })
21
+ }
22
+ );
4
23
  }
5
24
  export {
6
25
  SendButton
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/send-button.tsx"],
4
- "sourcesContent": ["/** @public */\nexport interface SendButtonProps {\n\tlabel: string\n\tdisabled?: boolean\n\tonClick?(): void\n}\n\n/** The button that posts a comment. @public @react */\nexport function SendButton({ label, disabled, onClick }: SendButtonProps) {\n\treturn (\n\t\t<button className=\"tlui-cmt-send\" type=\"button\" disabled={disabled} onClick={onClick}>\n\t\t\t{label}\n\t\t</button>\n\t)\n}\n"],
5
- "mappings": "AAUE;AAFK,SAAS,WAAW,EAAE,OAAO,UAAU,QAAQ,GAAoB;AACzE,SACC,oBAAC,YAAO,WAAU,iBAAgB,MAAK,UAAS,UAAoB,SAClE,iBACF;AAEF;",
4
+ "sourcesContent": ["/** @public */\nexport interface SendButtonProps {\n\t/** The button's accessible name (e.g. \"Send\"). Shown as an up-arrow icon, so this is its label. */\n\tlabel: string\n\tdisabled?: boolean\n\tonClick?(): void\n}\n\n/** The button that posts a comment \u2014 an up arrow. @public @react */\nexport function SendButton({ label, disabled, onClick }: SendButtonProps) {\n\treturn (\n\t\t<button\n\t\t\tclassName=\"tlui-cmt-send\"\n\t\t\ttype=\"button\"\n\t\t\tdisabled={disabled}\n\t\t\tonClick={onClick}\n\t\t\taria-label={label}\n\t\t\ttitle={label}\n\t\t>\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z\"\n\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t/>\n\t\t\t</svg>\n\t\t</button>\n\t)\n}\n"],
5
+ "mappings": "AAoBI;AAXG,SAAS,WAAW,EAAE,OAAO,UAAU,QAAQ,GAAoB;AACzE,SACC;AAAA,IAAC;AAAA;AAAA,MACA,WAAU;AAAA,MACV,MAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,OAAO;AAAA,MAEP,8BAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,QAAC;AAAA;AAAA,UACA,GAAE;AAAA,UACF,MAAK;AAAA,UACL,UAAS;AAAA,UACT,UAAS;AAAA;AAAA,MACV,GACD;AAAA;AAAA,EACD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,12 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { TldrawUiTooltip } from "tldraw";
4
+ const TooltipButton = forwardRef(
5
+ function TooltipButton2({ tooltip, children, ...props }, ref) {
6
+ return /* @__PURE__ */ jsx(TldrawUiTooltip, { content: tooltip, children: /* @__PURE__ */ jsx("button", { ref, type: "button", "aria-label": tooltip, ...props, children }) });
7
+ }
8
+ );
9
+ export {
10
+ TooltipButton
11
+ };
12
+ //# sourceMappingURL=tooltip-button.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/tooltip-button.tsx"],
4
+ "sourcesContent": ["import { type ButtonHTMLAttributes, forwardRef } from 'react'\nimport { TldrawUiTooltip } from 'tldraw'\n\ninterface TooltipButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n\ttooltip: string\n}\n\n/** An icon button labelled by a styled tooltip. Forwards ref and props to the underlying button\n * so `asChild` triggers (dropdown, tooltip) both compose onto the real element. */\nexport const TooltipButton = forwardRef<HTMLButtonElement, TooltipButtonProps>(\n\tfunction TooltipButton({ tooltip, children, ...props }, ref) {\n\t\treturn (\n\t\t\t<TldrawUiTooltip content={tooltip}>\n\t\t\t\t<button ref={ref} type=\"button\" aria-label={tooltip} {...props}>\n\t\t\t\t\t{children}\n\t\t\t\t</button>\n\t\t\t</TldrawUiTooltip>\n\t\t)\n\t}\n)\n"],
5
+ "mappings": "AAaI;AAbJ,SAAoC,kBAAkB;AACtD,SAAS,uBAAuB;AAQzB,MAAM,gBAAgB;AAAA,EAC5B,SAASA,eAAc,EAAE,SAAS,UAAU,GAAG,MAAM,GAAG,KAAK;AAC5D,WACC,oBAAC,mBAAgB,SAAS,SACzB,8BAAC,YAAO,KAAU,MAAK,UAAS,cAAY,SAAU,GAAG,OACvD,UACF,GACD;AAAA,EAEF;AACD;",
6
+ "names": ["TooltipButton"]
7
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/commenting",
3
3
  "description": "tldraw commenting UI components.",
4
- "version": "5.3.0-internal.c5c7f1d817d0",
4
+ "version": "5.3.0-next.2fa9c61a8de6",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -28,7 +28,7 @@
28
28
  "test": "yarn run -T vitest --passWithNoTests",
29
29
  "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
30
30
  "predev": "node ./scripts/copy-css-files.mjs",
31
- "dev": "chokidar 'src/ui/comments.css' 'src/canvas/canvas.css' -c 'node ./scripts/copy-css-files.mjs'",
31
+ "dev": "chokidar '../mentions/mentions.css' 'src/ui/comments.css' 'src/canvas/canvas.css' -c 'node ./scripts/copy-css-files.mjs'",
32
32
  "prebuild": "node ./scripts/copy-css-files.mjs",
33
33
  "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
34
34
  "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
@@ -38,6 +38,7 @@
38
38
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
39
39
  },
40
40
  "devDependencies": {
41
+ "@tldraw/driver": "5.3.0-next.2fa9c61a8de6",
41
42
  "@types/react": "^19.2.7",
42
43
  "@types/react-dom": "^19.2.3",
43
44
  "react": "^19.2.1",
@@ -52,12 +53,11 @@
52
53
  },
53
54
  "dependencies": {
54
55
  "@tiptap/core": "^3.12.1",
55
- "@tiptap/extension-mention": "3.13.0",
56
56
  "@tiptap/pm": "^3.12.1",
57
57
  "@tiptap/react": "^3.12.1",
58
- "@tiptap/suggestion": "3.13.0",
59
- "@tldraw/utils": "5.3.0-internal.c5c7f1d817d0",
60
- "tldraw": "5.3.0-internal.c5c7f1d817d0"
58
+ "@tldraw/mentions": "5.3.0-next.2fa9c61a8de6",
59
+ "@tldraw/utils": "5.3.0-next.2fa9c61a8de6",
60
+ "tldraw": "5.3.0-next.2fa9c61a8de6"
61
61
  },
62
62
  "module": "dist-esm/index.mjs",
63
63
  "source": "src/index.ts",
@@ -0,0 +1,290 @@
1
+ import {
2
+ commentSchemaRecords,
3
+ createCommentThread,
4
+ createShapeId,
5
+ createTLSchema,
6
+ createTLStore,
7
+ defaultBindingUtils,
8
+ defaultShapeUtils,
9
+ defaultTools,
10
+ Editor,
11
+ toRichText,
12
+ createComment as tlCreateComment,
13
+ TLPageId,
14
+ TLShapeId,
15
+ } from 'tldraw'
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
17
+ import { registerCommentAnchorLifecycle } from './anchor-lifecycle'
18
+ import { commitCommentMutation, putCommentRecords } from './comment-mutations'
19
+ import { getCommentRecord, getComments } from './comment-store'
20
+ import { CommentTool } from './comment-tool'
21
+
22
+ /**
23
+ * These tests need a real editor: the behavior under test is the interplay between store
24
+ * side effects (the beforeDelete snapshot, operation-complete settlement) and real editor
25
+ * operations like `deleteShapes` and `moveShapesToPage` — exactly what a stub can't model.
26
+ */
27
+
28
+ let editor: Editor
29
+ let dispose: () => void
30
+
31
+ beforeEach(() => {
32
+ editor = new Editor({
33
+ store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
34
+ shapeUtils: defaultShapeUtils,
35
+ bindingUtils: defaultBindingUtils,
36
+ tools: defaultTools,
37
+ getContainer: () => document.body,
38
+ })
39
+ dispose = registerCommentAnchorLifecycle(editor)
40
+ })
41
+
42
+ afterEach(() => {
43
+ dispose()
44
+ editor.dispose()
45
+ })
46
+
47
+ function makeShape(x = 100, y = 100, w = 100, h = 50): TLShapeId {
48
+ const id = createShapeId()
49
+ editor.createShape({ id, type: 'geo', x, y, props: { w, h } })
50
+ return id
51
+ }
52
+
53
+ function makeThread(shapeId: TLShapeId, anchorX = 0.5, anchorY = 0.5, isPrecise = true) {
54
+ const thread = createCommentThread({
55
+ pageId: editor.getCurrentPageId(),
56
+ anchor: { type: 'shape', shapeId, x: anchorX, y: anchorY, isPrecise },
57
+ createdBy: 'me',
58
+ })
59
+ const comment = tlCreateComment({
60
+ threadId: thread.id,
61
+ pageId: thread.pageId,
62
+ authorId: 'me',
63
+ body: toRichText('hello'),
64
+ })
65
+ putCommentRecords(editor, [thread, comment])
66
+ return { thread, comment }
67
+ }
68
+
69
+ function makeSecondPage(): TLPageId {
70
+ editor.createPage({ name: 'page 2' })
71
+ return editor.getPages()[1].id
72
+ }
73
+
74
+ describe('shape deletion', () => {
75
+ it('converts a precise shape anchor to a point anchor where the pin sat', () => {
76
+ const shapeId = makeShape(100, 100, 100, 50)
77
+ const { thread } = makeThread(shapeId, 0.5, 0.5, true)
78
+
79
+ editor.deleteShape(shapeId)
80
+
81
+ const updated = getCommentRecord(editor, thread.id)!
82
+ expect(updated.typeName).toBe('comment-thread')
83
+ expect((updated as typeof thread).anchor).toEqual({ type: 'point', x: 150, y: 125 })
84
+ })
85
+
86
+ it('converts an imprecise shape anchor where the inset pin is drawn, not at the raw corner', () => {
87
+ const shapeId = makeShape(100, 100, 100, 50)
88
+ const { thread } = makeThread(shapeId, 0.2, 0.9, false)
89
+
90
+ editor.deleteShape(shapeId)
91
+
92
+ // Top-right corner (200, 100) plus the 20px screen inset toward the shape's centre.
93
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
94
+ expect(updated.anchor).toEqual({ type: 'point', x: 180, y: 120 })
95
+ })
96
+
97
+ it('resolves the imprecise spot from the commenting options when no override is passed', () => {
98
+ const configured = new Editor({
99
+ store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
100
+ shapeUtils: defaultShapeUtils,
101
+ bindingUtils: defaultBindingUtils,
102
+ tools: [...defaultTools, CommentTool.configure({ impreciseShapeAnchor: { x: 0, y: 1 } })],
103
+ getContainer: () => document.body,
104
+ })
105
+ const disposeConfigured = registerCommentAnchorLifecycle(configured)
106
+ try {
107
+ const shapeId = createShapeId()
108
+ configured.createShape({ id: shapeId, type: 'geo', x: 100, y: 100, props: { w: 100, h: 50 } })
109
+ const thread = createCommentThread({
110
+ pageId: configured.getCurrentPageId(),
111
+ anchor: { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: false },
112
+ createdBy: 'me',
113
+ })
114
+ putCommentRecords(configured, [thread])
115
+
116
+ configured.deleteShape(shapeId)
117
+
118
+ // Bottom-left corner (100, 150) plus the inset toward the shape's centre.
119
+ const updated = getCommentRecord(configured, thread.id) as typeof thread
120
+ expect(updated.anchor).toEqual({ type: 'point', x: 120, y: 130 })
121
+ } finally {
122
+ disposeConfigured()
123
+ configured.dispose()
124
+ }
125
+ })
126
+
127
+ it('converts anchors on children when their frame is deleted with them', () => {
128
+ const frameId = createShapeId()
129
+ editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
130
+ const childId = createShapeId()
131
+ editor.createShape({
132
+ id: childId,
133
+ type: 'geo',
134
+ parentId: frameId,
135
+ x: 50,
136
+ y: 50,
137
+ props: { w: 100, h: 100 },
138
+ })
139
+ const { thread } = makeThread(childId, 0.5, 0.5, true)
140
+
141
+ editor.deleteShape(frameId)
142
+
143
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
144
+ expect(updated.anchor).toEqual({ type: 'point', x: 100, y: 100 })
145
+ })
146
+
147
+ it('re-attaches the thread when the delete is undone', () => {
148
+ const shapeId = makeShape(100, 100, 100, 50)
149
+ const { thread } = makeThread(shapeId, 0.5, 0.5, true)
150
+ editor.markHistoryStoppingPoint()
151
+
152
+ editor.deleteShape(shapeId)
153
+ expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
154
+
155
+ editor.undo()
156
+
157
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
158
+ expect(updated.anchor).toEqual(thread.anchor)
159
+ })
160
+
161
+ it('keeps the restore when a returning shape cannot resolve its page yet', () => {
162
+ const frameId = createShapeId()
163
+ editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
164
+ const childId = createShapeId()
165
+ editor.createShape({
166
+ id: childId,
167
+ type: 'geo',
168
+ parentId: frameId,
169
+ x: 50,
170
+ y: 50,
171
+ props: { w: 100, h: 100 },
172
+ })
173
+ const { thread } = makeThread(childId, 0.5, 0.5, true)
174
+ const childRecord = editor.getShape(childId)!
175
+ const frameRecord = editor.getShape(frameId)!
176
+
177
+ editor.deleteShape(frameId)
178
+ expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
179
+
180
+ // The child returns in an earlier operation than its parent, so its ancestor page can't
181
+ // resolve when it first reappears.
182
+ editor.store.put([childRecord])
183
+ editor.store.put([frameRecord])
184
+
185
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
186
+ expect(updated.anchor).toEqual(thread.anchor)
187
+ })
188
+
189
+ it('re-attaches on undo even when the lifecycle was re-registered in between', () => {
190
+ const shapeId = makeShape(100, 100, 100, 50)
191
+ const { thread } = makeThread(shapeId, 0.5, 0.5, true)
192
+ editor.markHistoryStoppingPoint()
193
+
194
+ editor.deleteShape(shapeId)
195
+ expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
196
+
197
+ // The registering effect re-runs whenever its inputs change identity; conversion memory
198
+ // must not live and die with any one registration.
199
+ dispose()
200
+ dispose = registerCommentAnchorLifecycle(editor)
201
+
202
+ editor.undo()
203
+
204
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
205
+ expect(updated.anchor).toEqual(thread.anchor)
206
+ })
207
+
208
+ it('does not re-attach on undo when the pin was moved after the conversion', () => {
209
+ const shapeId = makeShape(100, 100, 100, 50)
210
+ const { thread } = makeThread(shapeId, 0.5, 0.5, true)
211
+ editor.markHistoryStoppingPoint()
212
+
213
+ editor.deleteShape(shapeId)
214
+ const converted = getCommentRecord(editor, thread.id) as typeof thread
215
+ commitCommentMutation(editor, () =>
216
+ putCommentRecords(editor, [{ ...converted, anchor: { type: 'point', x: 900, y: 900 } }])
217
+ )
218
+
219
+ editor.undo()
220
+
221
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
222
+ expect(updated.anchor).toEqual({ type: 'point', x: 900, y: 900 })
223
+ })
224
+
225
+ it('leaves threads on other shapes untouched', () => {
226
+ const shapeId = makeShape()
227
+ const otherId = makeShape(300, 300)
228
+ const { thread } = makeThread(otherId)
229
+
230
+ editor.deleteShape(shapeId)
231
+
232
+ const updated = getCommentRecord(editor, thread.id) as typeof thread
233
+ expect(updated.anchor.type).toBe('shape')
234
+ })
235
+ })
236
+
237
+ describe('moving a shape to another page', () => {
238
+ it('re-homes the thread and its comments to the new page, keeping the shape anchor', () => {
239
+ const shapeId = makeShape()
240
+ const { thread, comment } = makeThread(shapeId)
241
+ const page2 = makeSecondPage()
242
+
243
+ editor.moveShapesToPage([shapeId], page2)
244
+
245
+ const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
246
+ expect(updatedThread.pageId).toBe(page2)
247
+ expect(updatedThread.anchor).toEqual(thread.anchor)
248
+ const updatedComment = getComments(editor).find((c) => c.id === comment.id)!
249
+ expect(updatedComment.pageId).toBe(page2)
250
+ })
251
+
252
+ it('re-homes threads anchored to children of a moved frame', () => {
253
+ const frameId = createShapeId()
254
+ editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
255
+ const childId = createShapeId()
256
+ editor.createShape({
257
+ id: childId,
258
+ type: 'geo',
259
+ parentId: frameId,
260
+ x: 50,
261
+ y: 50,
262
+ props: { w: 100, h: 100 },
263
+ })
264
+ const { thread } = makeThread(childId)
265
+ const page2 = makeSecondPage()
266
+
267
+ editor.moveShapesToPage([frameId], page2)
268
+
269
+ const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
270
+ expect(updatedThread.pageId).toBe(page2)
271
+ expect(updatedThread.anchor.type).toBe('shape')
272
+ })
273
+
274
+ it('re-homes the thread back when the move is undone', () => {
275
+ const shapeId = makeShape()
276
+ const { thread } = makeThread(shapeId)
277
+ const page1 = editor.getCurrentPageId()
278
+ const page2 = makeSecondPage()
279
+ editor.markHistoryStoppingPoint()
280
+
281
+ editor.moveShapesToPage([shapeId], page2)
282
+ expect((getCommentRecord(editor, thread.id) as typeof thread).pageId).toBe(page2)
283
+
284
+ editor.undo()
285
+
286
+ const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
287
+ expect(updatedThread.pageId).toBe(page1)
288
+ expect(updatedThread.anchor.type).toBe('shape')
289
+ })
290
+ })
@@ -0,0 +1,205 @@
1
+ import { WeakCache } from '@tldraw/utils'
2
+ import { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'
3
+ import { commitCommentMutation, putRecordsInCommit } from './comment-mutations'
4
+ import { getCommentRecord, getComments, getCommentThreads, TLCommentRecord } from './comment-store'
5
+ import { anchorPagePoint, impreciseShapePinInset } from './thread-state'
6
+
7
+ type ShapeAnchor = Extract<TLCommentAnchor, { type: 'shape' }>
8
+
9
+ /**
10
+ * Threads converted to point anchors because their shape was deleted, kept so the anchor can be
11
+ * restored if the shape comes back (page move re-create, undo of the delete). Owned by the
12
+ * editor, not the registration closure: the registering effect can re-run (a prop identity
13
+ * change, a remount), and an undo can arrive arbitrarily long after the delete — the memory has
14
+ * to outlive any one registration.
15
+ */
16
+ const convertedByShapeCache = new WeakCache<
17
+ Editor,
18
+ Map<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>
19
+ >()
20
+
21
+ function isAnchoredToShape(
22
+ thread: TLCommentThread,
23
+ shapeId: TLShapeId
24
+ ): thread is TLCommentThread & { anchor: ShapeAnchor } {
25
+ const anchor = thread.anchor
26
+ return anchor.type === 'shape' && anchor.shapeId === shapeId
27
+ }
28
+
29
+ /**
30
+ * Keep shape-anchored threads alive across their shape's lifecycle:
31
+ *
32
+ * - When the shape is deleted, the thread converts to a `point` anchor at the spot its pin last
33
+ * occupied, so the conversation outlives the shape instead of becoming invisible (a missing
34
+ * shape has no bounds, which hides the pin).
35
+ * - When the shape moves to another page, the thread follows: its `pageId` and each comment's
36
+ * denormalized `pageId` update to the shape's new page, and the anchor keeps riding the shape.
37
+ * - When a deleted shape comes back (a page move re-creates it with its id preserved; undoing a
38
+ * delete restores it), the thread re-attaches — unless its pin was manually moved in the
39
+ * meantime, in which case the manual placement wins.
40
+ *
41
+ * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each
42
+ * store write is its own operation — so at no single moment does "this shape is being moved"
43
+ * exist as an observable event. The handlers therefore cooperate across operations:
44
+ * `beforeDelete` snapshots where each affected pin sits (every record is still in the store at
45
+ * that point, so page bounds resolve even for children of a deleted frame); the
46
+ * operation-complete pass converts threads whose shape is really gone, remembering the original
47
+ * anchor; `afterCreate` notes a reappeared shape id, and the operation-complete pass restores
48
+ * the anchor once the store has settled (creation order within an operation is arbitrary, so a
49
+ * fresh shape may not resolve an ancestor page mid-operation). Undo/redo of a move
50
+ * replays as a `parentId` update (remove + add of one id squash to an update in the history
51
+ * diff), so an `afterChange` handler re-homes threads on cross-page reparents — including
52
+ * threads anchored to descendants, which move with their parent without change events of their
53
+ * own.
54
+ *
55
+ * Remote changes are ignored: the client that performed the operation runs this same
56
+ * maintenance and syncs the resulting thread updates. Writes honour the
57
+ * {@link CommentingOptions.history} option, so the consumer decides whether they're undoable.
58
+ *
59
+ * Registered by `CanvasComments` on mount; parts-built consumers can call this directly.
60
+ * Returns a cleanup function that unregisters all handlers.
61
+ *
62
+ * @public
63
+ */
64
+ export function registerCommentAnchorLifecycle(editor: Editor): () => void {
65
+ // Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page
66
+ // point).
67
+ const pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()
68
+ const convertedByShape = convertedByShapeCache.get(editor, () => new Map())
69
+
70
+ function rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {
71
+ updates.push({ ...thread, pageId })
72
+ for (const comment of getComments(editor)) {
73
+ if (comment.threadId === thread.id) updates.push({ ...comment, pageId })
74
+ }
75
+ }
76
+
77
+ const disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(
78
+ 'shape',
79
+ (shape, source) => {
80
+ if (source === 'remote') return
81
+ for (const thread of getCommentThreads(editor)) {
82
+ if (!isAnchoredToShape(thread, shape.id)) continue
83
+ let snapshot = pendingByShape.get(shape.id)
84
+ if (!snapshot) {
85
+ snapshot = new Map()
86
+ pendingByShape.set(shape.id, snapshot)
87
+ }
88
+ // Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins
89
+ // render inset toward the shape's centre, and the converted point pin renders at
90
+ // its point exactly — without the inset the pin would jump on deletion. The inset
91
+ // is screen-fixed, so bake it in at the current zoom.
92
+ let point = anchorPagePoint(editor, thread.anchor)
93
+ const inset = impreciseShapePinInset(editor, thread.anchor)
94
+ if (point && inset) {
95
+ const zoom = editor.getZoomLevel()
96
+ point = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }
97
+ }
98
+ snapshot.set(thread.id, point)
99
+ }
100
+ }
101
+ )
102
+
103
+ // Shape ids from `convertedByShape` re-created during the current operation. Restores are
104
+ // settled at operation complete rather than in `afterCreate` itself: creation order within an
105
+ // operation is arbitrary, so a shape can appear before its parent and not resolve an ancestor
106
+ // page yet — and the conversion memory must outlive any such partial state.
107
+ const returnedShapeIds = new Set<TLShapeId>()
108
+
109
+ const disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {
110
+ // Only user-sourced handlers populate the maps and only local operations should settle
111
+ // them; a remote operation completing leaves any (impossible-in-practice) leftovers for
112
+ // the next local settle rather than judging them against remote state.
113
+ if (source === 'remote') return
114
+ if (pendingByShape.size === 0 && returnedShapeIds.size === 0) return
115
+ const settled = [...pendingByShape.entries()]
116
+ pendingByShape.clear()
117
+ const returned = [...returnedShapeIds]
118
+ returnedShapeIds.clear()
119
+
120
+ const updates: TLCommentRecord[] = []
121
+ for (const [shapeId, threadPoints] of settled) {
122
+ if (editor.getShape(shapeId)) continue
123
+ for (const [threadId, point] of threadPoints) {
124
+ if (!point) continue
125
+ const thread = getCommentRecord(editor, threadId)
126
+ if (!thread || thread.typeName !== 'comment-thread') continue
127
+ if (!isAnchoredToShape(thread, shapeId)) continue
128
+ let converted = convertedByShape.get(shapeId)
129
+ if (!converted) {
130
+ converted = []
131
+ convertedByShape.set(shapeId, converted)
132
+ }
133
+ converted.push({ threadId, anchor: thread.anchor, point })
134
+ updates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })
135
+ }
136
+ }
137
+
138
+ for (const shapeId of returned) {
139
+ const shape = editor.getShape(shapeId)
140
+ if (!shape) continue
141
+ const converted = convertedByShape.get(shapeId)
142
+ if (!converted) continue
143
+ convertedByShape.delete(shapeId)
144
+
145
+ const pageId = editor.getAncestorPageId(shape)
146
+ for (const { threadId, anchor, point } of converted) {
147
+ const thread = getCommentRecord(editor, threadId)
148
+ if (!thread || thread.typeName !== 'comment-thread') continue
149
+ // Re-attach only threads still sitting exactly where the conversion left them — a
150
+ // pin moved since then was placed deliberately, and that placement wins.
151
+ const current = thread.anchor
152
+ if (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {
153
+ continue
154
+ }
155
+ if (!pageId || thread.pageId === pageId) {
156
+ updates.push({ ...thread, anchor })
157
+ } else {
158
+ rehomeThread({ ...thread, anchor }, pageId, updates)
159
+ }
160
+ }
161
+ }
162
+
163
+ if (updates.length > 0) {
164
+ commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
165
+ }
166
+ })
167
+
168
+ const disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(
169
+ 'shape',
170
+ (shape, source) => {
171
+ if (source === 'remote') return
172
+ if (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)
173
+ }
174
+ )
175
+
176
+ const disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(
177
+ 'shape',
178
+ (prev, next, source) => {
179
+ if (source === 'remote') return
180
+ if (prev.parentId === next.parentId) return
181
+ const pageId = editor.getAncestorPageId(next)
182
+ if (!pageId) return
183
+ // Descendants move with their parent without change events of their own.
184
+ const movedIds = editor.getShapeAndDescendantIds([next.id])
185
+ const updates: TLCommentRecord[] = []
186
+ for (const thread of getCommentThreads(editor)) {
187
+ const anchor = thread.anchor
188
+ if (anchor.type !== 'shape') continue
189
+ if (!movedIds.has(anchor.shapeId)) continue
190
+ if (thread.pageId === pageId) continue
191
+ rehomeThread(thread, pageId, updates)
192
+ }
193
+ if (updates.length > 0) {
194
+ commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
195
+ }
196
+ }
197
+ )
198
+
199
+ return () => {
200
+ disposeBeforeDelete()
201
+ disposeOperationComplete()
202
+ disposeAfterCreate()
203
+ disposeAfterChange()
204
+ }
205
+ }