@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d

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 (338) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1152 -350
  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/{ui/avatar.js → canvas/canvas-events.js} +14 -22
  6. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  7. package/dist-cjs/canvas/cluster-badge.js +121 -0
  8. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  9. package/dist-cjs/canvas/cluster-fade.js +85 -0
  10. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  11. package/dist-cjs/canvas/cluster-input.js +53 -3
  12. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  13. package/dist-cjs/canvas/cluster-model.js +252 -0
  14. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  15. package/dist-cjs/canvas/comment-body.js +1 -1
  16. package/dist-cjs/canvas/comment-body.js.map +2 -2
  17. package/dist-cjs/canvas/comment-drafts.js +54 -0
  18. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  19. package/dist-cjs/canvas/comment-mutations.js +152 -0
  20. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  21. package/dist-cjs/canvas/comment-reactions.js +160 -0
  22. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  23. package/dist-cjs/canvas/comment-render.js +9 -6
  24. package/dist-cjs/canvas/comment-render.js.map +2 -2
  25. package/dist-cjs/canvas/comment-store.js +19 -9
  26. package/dist-cjs/canvas/comment-store.js.map +2 -2
  27. package/dist-cjs/canvas/comment-tool.js +63 -14
  28. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  29. package/dist-cjs/canvas/comments-filter-menu.js +17 -37
  30. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  31. package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
  32. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  33. package/dist-cjs/canvas/comments-overlay.js +151 -989
  34. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  35. package/dist-cjs/canvas/comments-sidebar.js +49 -41
  36. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  37. package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
  38. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  39. package/dist-cjs/canvas/context.js +17 -0
  40. package/dist-cjs/canvas/context.js.map +7 -0
  41. package/dist-cjs/canvas/hooks.js +28 -3
  42. package/dist-cjs/canvas/hooks.js.map +2 -2
  43. package/dist-cjs/canvas/license.js.map +1 -1
  44. package/dist-cjs/canvas/mobile-placement.js +91 -0
  45. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  46. package/dist-cjs/canvas/options.js +57 -2
  47. package/dist-cjs/canvas/options.js.map +2 -2
  48. package/dist-cjs/canvas/pending-composer.js +134 -0
  49. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  50. package/dist-cjs/canvas/pin-stacking.js +81 -0
  51. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  52. package/dist-cjs/canvas/region-box.js +113 -0
  53. package/dist-cjs/canvas/region-box.js.map +7 -0
  54. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  55. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  56. package/dist-cjs/canvas/state.js +28 -14
  57. package/dist-cjs/canvas/state.js.map +2 -2
  58. package/dist-cjs/canvas/thread-pin.js +304 -0
  59. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  60. package/dist-cjs/canvas/thread-preview.js +177 -0
  61. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  62. package/dist-cjs/canvas/thread-stack.js +199 -0
  63. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  64. package/dist-cjs/canvas/thread-state.js +82 -21
  65. package/dist-cjs/canvas/thread-state.js.map +2 -2
  66. package/dist-cjs/canvas/thread-view.js +450 -0
  67. package/dist-cjs/canvas/thread-view.js.map +7 -0
  68. package/dist-cjs/clustering/computeClusterTable.js +6 -6
  69. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  70. package/dist-cjs/clustering/replay.js +89 -22
  71. package/dist-cjs/clustering/replay.js.map +3 -3
  72. package/dist-cjs/clustering/runtime.js +126 -7
  73. package/dist-cjs/clustering/runtime.js.map +2 -2
  74. package/dist-cjs/clustering/types.js.map +1 -1
  75. package/dist-cjs/index.d.ts +904 -353
  76. package/dist-cjs/index.js +52 -28
  77. package/dist-cjs/index.js.map +2 -2
  78. package/dist-cjs/ui/byline.js +10 -6
  79. package/dist-cjs/ui/byline.js.map +2 -2
  80. package/dist-cjs/ui/comment-card.js +15 -8
  81. package/dist-cjs/ui/comment-card.js.map +2 -2
  82. package/dist-cjs/ui/comment-composer.js +124 -39
  83. package/dist-cjs/ui/comment-composer.js.map +2 -2
  84. package/dist-cjs/ui/comment-pin.js +11 -2
  85. package/dist-cjs/ui/comment-pin.js.map +2 -2
  86. package/dist-cjs/ui/comment-thread.js +9 -7
  87. package/dist-cjs/ui/comment-thread.js.map +2 -2
  88. package/dist-cjs/ui/comments-list.js +39 -40
  89. package/dist-cjs/ui/comments-list.js.map +2 -2
  90. package/dist-cjs/ui/count-badge.js +3 -2
  91. package/dist-cjs/ui/count-badge.js.map +2 -2
  92. package/dist-cjs/ui/emoji-picker.js +54 -0
  93. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  94. package/dist-cjs/ui/empty-state.js +3 -3
  95. package/dist-cjs/ui/empty-state.js.map +2 -2
  96. package/dist-cjs/ui/format-time.js +16 -1
  97. package/dist-cjs/ui/format-time.js.map +2 -2
  98. package/dist-cjs/ui/icons.js +115 -0
  99. package/dist-cjs/ui/icons.js.map +7 -0
  100. package/dist-cjs/ui/reaction-picker.js +71 -0
  101. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  102. package/dist-cjs/ui/reaction.js +58 -6
  103. package/dist-cjs/ui/reaction.js.map +2 -2
  104. package/dist-cjs/ui/reactions.js +20 -7
  105. package/dist-cjs/ui/reactions.js.map +2 -2
  106. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  107. package/dist-cjs/ui/reply-count.js.map +7 -0
  108. package/dist-cjs/ui/send-button.js +13 -1
  109. package/dist-cjs/ui/send-button.js.map +2 -2
  110. package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
  111. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  112. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  113. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  114. package/dist-esm/canvas/canvas-events.mjs +15 -0
  115. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  116. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  117. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  118. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  119. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  120. package/dist-esm/canvas/cluster-input.mjs +54 -4
  121. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  122. package/dist-esm/canvas/cluster-model.mjs +236 -0
  123. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  124. package/dist-esm/canvas/comment-body.mjs +1 -1
  125. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  126. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  127. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  128. package/dist-esm/canvas/comment-mutations.mjs +132 -0
  129. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  130. package/dist-esm/canvas/comment-reactions.mjs +145 -0
  131. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  132. package/dist-esm/canvas/comment-render.mjs +10 -7
  133. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  134. package/dist-esm/canvas/comment-store.mjs +19 -9
  135. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  136. package/dist-esm/canvas/comment-tool.mjs +66 -17
  137. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  138. package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
  139. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  140. package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
  141. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  142. package/dist-esm/canvas/comments-overlay.mjs +157 -1010
  143. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  144. package/dist-esm/canvas/comments-sidebar.mjs +53 -46
  145. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  146. package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
  147. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  148. package/dist-esm/canvas/context.mjs +1 -0
  149. package/dist-esm/canvas/context.mjs.map +7 -0
  150. package/dist-esm/canvas/hooks.mjs +35 -5
  151. package/dist-esm/canvas/hooks.mjs.map +2 -2
  152. package/dist-esm/canvas/license.mjs.map +1 -1
  153. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  154. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  155. package/dist-esm/canvas/options.mjs +59 -3
  156. package/dist-esm/canvas/options.mjs.map +2 -2
  157. package/dist-esm/canvas/pending-composer.mjs +126 -0
  158. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  159. package/dist-esm/canvas/pin-stacking.mjs +61 -0
  160. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  161. package/dist-esm/canvas/region-box.mjs +93 -0
  162. package/dist-esm/canvas/region-box.mjs.map +7 -0
  163. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  164. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  165. package/dist-esm/canvas/state.mjs +29 -19
  166. package/dist-esm/canvas/state.mjs.map +2 -2
  167. package/dist-esm/canvas/thread-pin.mjs +310 -0
  168. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  169. package/dist-esm/canvas/thread-preview.mjs +163 -0
  170. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  171. package/dist-esm/canvas/thread-stack.mjs +189 -0
  172. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  173. package/dist-esm/canvas/thread-state.mjs +86 -22
  174. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  175. package/dist-esm/canvas/thread-view.mjs +461 -0
  176. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  177. package/dist-esm/clustering/computeClusterTable.mjs +6 -6
  178. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  179. package/dist-esm/clustering/replay.mjs +89 -22
  180. package/dist-esm/clustering/replay.mjs.map +3 -3
  181. package/dist-esm/clustering/runtime.mjs +126 -7
  182. package/dist-esm/clustering/runtime.mjs.map +2 -2
  183. package/dist-esm/index.d.mts +904 -353
  184. package/dist-esm/index.mjs +102 -43
  185. package/dist-esm/index.mjs.map +2 -2
  186. package/dist-esm/ui/byline.mjs +11 -7
  187. package/dist-esm/ui/byline.mjs.map +2 -2
  188. package/dist-esm/ui/comment-card.mjs +15 -8
  189. package/dist-esm/ui/comment-card.mjs.map +2 -2
  190. package/dist-esm/ui/comment-composer.mjs +135 -39
  191. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  192. package/dist-esm/ui/comment-pin.mjs +11 -2
  193. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  194. package/dist-esm/ui/comment-thread.mjs +9 -7
  195. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  196. package/dist-esm/ui/comments-list.mjs +51 -50
  197. package/dist-esm/ui/comments-list.mjs.map +2 -2
  198. package/dist-esm/ui/count-badge.mjs +3 -2
  199. package/dist-esm/ui/count-badge.mjs.map +2 -2
  200. package/dist-esm/ui/emoji-picker.mjs +34 -0
  201. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  202. package/dist-esm/ui/empty-state.mjs +3 -3
  203. package/dist-esm/ui/empty-state.mjs.map +2 -2
  204. package/dist-esm/ui/format-time.mjs +16 -1
  205. package/dist-esm/ui/format-time.mjs.map +2 -2
  206. package/dist-esm/ui/icons.mjs +95 -0
  207. package/dist-esm/ui/icons.mjs.map +7 -0
  208. package/dist-esm/ui/reaction-picker.mjs +59 -0
  209. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  210. package/dist-esm/ui/reaction.mjs +59 -7
  211. package/dist-esm/ui/reaction.mjs.map +2 -2
  212. package/dist-esm/ui/reactions.mjs +21 -8
  213. package/dist-esm/ui/reactions.mjs.map +2 -2
  214. package/dist-esm/ui/reply-count.mjs +9 -0
  215. package/dist-esm/ui/reply-count.mjs.map +7 -0
  216. package/dist-esm/ui/send-button.mjs +13 -1
  217. package/dist-esm/ui/send-button.mjs.map +2 -2
  218. package/dist-esm/ui/visual-viewport.mjs +12 -0
  219. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  220. package/package.json +6 -6
  221. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  222. package/src/canvas/anchor-lifecycle.ts +191 -0
  223. package/src/canvas/canvas-events.ts +18 -0
  224. package/src/canvas/canvas.css +464 -55
  225. package/src/canvas/cluster-badge.tsx +133 -0
  226. package/src/canvas/cluster-fade.ts +102 -0
  227. package/src/canvas/cluster-input.test.ts +178 -36
  228. package/src/canvas/cluster-input.ts +95 -9
  229. package/src/canvas/cluster-model.ts +378 -0
  230. package/src/canvas/comment-body.tsx +1 -1
  231. package/src/canvas/comment-drafts.test.ts +55 -0
  232. package/src/canvas/comment-drafts.ts +46 -0
  233. package/src/canvas/comment-mutations.test.ts +452 -0
  234. package/src/canvas/comment-mutations.ts +299 -0
  235. package/src/canvas/comment-reactions.test.ts +154 -0
  236. package/src/canvas/comment-reactions.tsx +246 -0
  237. package/src/canvas/comment-render.test.ts +1 -1
  238. package/src/canvas/comment-render.ts +17 -13
  239. package/src/canvas/comment-store.ts +68 -33
  240. package/src/canvas/comment-tool.test.ts +162 -0
  241. package/src/canvas/comment-tool.tsx +103 -25
  242. package/src/canvas/comments-filter-menu.tsx +20 -36
  243. package/src/canvas/comments-overflow-menu.tsx +17 -19
  244. package/src/canvas/comments-overlay.tsx +252 -1369
  245. package/src/canvas/comments-sidebar.test.ts +45 -0
  246. package/src/canvas/comments-sidebar.tsx +112 -73
  247. package/src/canvas/comments-visibility-toggle.tsx +25 -0
  248. package/src/canvas/context.ts +48 -0
  249. package/src/canvas/hooks.test.ts +94 -0
  250. package/src/canvas/hooks.ts +66 -11
  251. package/src/canvas/license.ts +1 -1
  252. package/src/canvas/mobile-placement.ts +115 -0
  253. package/src/canvas/options.test.ts +176 -4
  254. package/src/canvas/options.ts +276 -18
  255. package/src/canvas/pending-composer.tsx +157 -0
  256. package/src/canvas/pin-stacking.test.ts +224 -0
  257. package/src/canvas/pin-stacking.ts +103 -0
  258. package/src/canvas/region-box.tsx +124 -0
  259. package/src/canvas/sidebar-filters.ts +4 -3
  260. package/src/canvas/state.ts +88 -39
  261. package/src/canvas/thread-pin.tsx +418 -0
  262. package/src/canvas/thread-preview.test.ts +91 -0
  263. package/src/canvas/thread-preview.tsx +294 -0
  264. package/src/canvas/thread-stack.tsx +242 -0
  265. package/src/canvas/thread-state.test.ts +260 -0
  266. package/src/canvas/thread-state.ts +171 -37
  267. package/src/canvas/thread-view.test.ts +72 -0
  268. package/src/canvas/thread-view.tsx +614 -0
  269. package/src/clustering/computeClusterTable.test.ts +24 -23
  270. package/src/clustering/computeClusterTable.ts +17 -8
  271. package/src/clustering/replay.test.ts +0 -7
  272. package/src/clustering/replay.ts +131 -32
  273. package/src/clustering/runtime.test.ts +267 -13
  274. package/src/clustering/runtime.ts +176 -19
  275. package/src/clustering/schedule.test.ts +0 -6
  276. package/src/clustering/screen-offsets.test.ts +171 -0
  277. package/src/clustering/types.ts +19 -9
  278. package/src/index.ts +91 -42
  279. package/src/ui/byline.tsx +21 -9
  280. package/src/ui/comment-card.tsx +20 -10
  281. package/src/ui/comment-composer.tsx +178 -56
  282. package/src/ui/comment-pin.tsx +13 -2
  283. package/src/ui/comment-thread.tsx +13 -6
  284. package/src/ui/comments-list.test.ts +31 -0
  285. package/src/ui/comments-list.tsx +82 -47
  286. package/src/ui/comments.css +577 -294
  287. package/src/ui/count-badge.tsx +9 -2
  288. package/src/ui/emoji-picker.test.ts +25 -0
  289. package/src/ui/emoji-picker.tsx +76 -0
  290. package/src/ui/empty-state.tsx +3 -3
  291. package/src/ui/format-time.test.ts +69 -0
  292. package/src/ui/format-time.ts +27 -2
  293. package/src/ui/icons.tsx +116 -0
  294. package/src/ui/reaction-picker.tsx +102 -0
  295. package/src/ui/reaction.tsx +125 -6
  296. package/src/ui/reactions.tsx +69 -10
  297. package/src/ui/reply-count.ts +16 -0
  298. package/src/ui/send-button.tsx +13 -3
  299. package/src/ui/visual-viewport.test.ts +36 -0
  300. package/src/ui/visual-viewport.ts +27 -0
  301. package/dist-cjs/canvas/region-options.js +0 -49
  302. package/dist-cjs/canvas/region-options.js.map +0 -7
  303. package/dist-cjs/ui/avatar.js.map +0 -7
  304. package/dist-cjs/ui/comment-mention.js +0 -42
  305. package/dist-cjs/ui/comment-mention.js.map +0 -7
  306. package/dist-cjs/ui/comment-text.js.map +0 -7
  307. package/dist-cjs/ui/mention-list.js +0 -70
  308. package/dist-cjs/ui/mention-list.js.map +0 -7
  309. package/dist-cjs/ui/mention-suggestion.js +0 -206
  310. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  311. package/dist-cjs/ui/mention.js.map +0 -7
  312. package/dist-cjs/ui/render-markdown.js +0 -63
  313. package/dist-cjs/ui/render-markdown.js.map +0 -7
  314. package/dist-esm/canvas/region-options.mjs +0 -29
  315. package/dist-esm/canvas/region-options.mjs.map +0 -7
  316. package/dist-esm/ui/avatar.mjs +0 -23
  317. package/dist-esm/ui/avatar.mjs.map +0 -7
  318. package/dist-esm/ui/comment-mention.mjs +0 -22
  319. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  320. package/dist-esm/ui/comment-text.mjs +0 -9
  321. package/dist-esm/ui/comment-text.mjs.map +0 -7
  322. package/dist-esm/ui/mention-list.mjs +0 -50
  323. package/dist-esm/ui/mention-list.mjs.map +0 -7
  324. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  325. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  326. package/dist-esm/ui/mention.mjs +0 -11
  327. package/dist-esm/ui/mention.mjs.map +0 -7
  328. package/dist-esm/ui/render-markdown.mjs +0 -45
  329. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  330. package/src/canvas/region-options.ts +0 -57
  331. package/src/ui/avatar.tsx +0 -31
  332. package/src/ui/comment-mention.ts +0 -47
  333. package/src/ui/comment-text.tsx +0 -12
  334. package/src/ui/mention-list.tsx +0 -106
  335. package/src/ui/mention-suggestion.test.ts +0 -18
  336. package/src/ui/mention-suggestion.tsx +0 -285
  337. package/src/ui/mention.tsx +0 -9
  338. package/src/ui/render-markdown.tsx +0 -72
@@ -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, ({ put }) =>
216
+ put([{ ...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,191 @@
1
+ import { WeakCache } from '@tldraw/utils'
2
+ import { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'
3
+ import { commitCommentMutation } 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. Owned by the editor, not the registration closure: the
12
+ * registering effect can re-run, and an undo can arrive long after the delete.
13
+ */
14
+ const convertedByShapeCache = new WeakCache<
15
+ Editor,
16
+ Map<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>
17
+ >()
18
+
19
+ function isAnchoredToShape(
20
+ thread: TLCommentThread,
21
+ shapeId: TLShapeId
22
+ ): thread is TLCommentThread & { anchor: ShapeAnchor } {
23
+ const anchor = thread.anchor
24
+ return anchor.type === 'shape' && anchor.shapeId === shapeId
25
+ }
26
+
27
+ /**
28
+ * Keep shape-anchored threads alive across their shape's lifecycle:
29
+ *
30
+ * - When the shape is deleted, the thread converts to a `point` anchor where its pin last sat, so
31
+ * the conversation outlives the shape instead of becoming invisible.
32
+ * - When the shape moves to another page, the thread follows: its `pageId` and each comment's
33
+ * denormalized `pageId` update, and the anchor keeps riding the shape.
34
+ * - When a deleted shape comes back, the thread re-attaches — unless its pin was manually moved in
35
+ * the meantime, in which case the manual placement wins.
36
+ *
37
+ * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each
38
+ * store write is its own operation, so "this shape is being moved" is never observable as a single
39
+ * event. The handlers therefore cooperate across operations: `beforeDelete` snapshots where each
40
+ * affected pin sits, the operation-complete pass converts threads whose shape is really gone, and
41
+ * `afterCreate` plus that same pass restore the anchor once the store has settled. Undo/redo of a
42
+ * move replays as a `parentId` update, so `afterChange` re-homes threads on cross-page reparents —
43
+ * including threads anchored to descendants, which move without change events of their own.
44
+ *
45
+ * Remote changes are ignored: the client that performed the operation runs this same maintenance
46
+ * and syncs the result. Writes honour the {@link CommentingOptions.history} option.
47
+ *
48
+ * Registered by `CanvasComments` on mount; parts-built consumers can call this directly. Returns a
49
+ * cleanup function that unregisters all handlers.
50
+ *
51
+ * @public
52
+ */
53
+ export function registerCommentAnchorLifecycle(editor: Editor): () => void {
54
+ // Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page
55
+ // point).
56
+ const pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()
57
+ const convertedByShape = convertedByShapeCache.get(editor, () => new Map())
58
+
59
+ function rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {
60
+ updates.push({ ...thread, pageId })
61
+ for (const comment of getComments(editor)) {
62
+ if (comment.threadId === thread.id) updates.push({ ...comment, pageId })
63
+ }
64
+ }
65
+
66
+ const disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(
67
+ 'shape',
68
+ (shape, source) => {
69
+ if (source === 'remote') return
70
+ for (const thread of getCommentThreads(editor)) {
71
+ if (!isAnchoredToShape(thread, shape.id)) continue
72
+ let snapshot = pendingByShape.get(shape.id)
73
+ if (!snapshot) {
74
+ snapshot = new Map()
75
+ pendingByShape.set(shape.id, snapshot)
76
+ }
77
+ // Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins render inset
78
+ // toward the shape's centre, so without baking the inset in at the current zoom the pin would jump.
79
+ let point = anchorPagePoint(editor, thread.anchor)
80
+ const inset = impreciseShapePinInset(editor, thread.anchor)
81
+ if (point && inset) {
82
+ const zoom = editor.getZoomLevel()
83
+ point = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }
84
+ }
85
+ snapshot.set(thread.id, point)
86
+ }
87
+ }
88
+ )
89
+
90
+ // Shape ids from `convertedByShape` re-created during the current operation. Settled at operation
91
+ // complete rather than in `afterCreate`: creation order within an operation is arbitrary, so a shape
92
+ // can appear before its parent and not resolve an ancestor page yet.
93
+ const returnedShapeIds = new Set<TLShapeId>()
94
+
95
+ const disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {
96
+ // Only user-sourced handlers populate the maps and only local operations should settle
97
+ // them; a remote operation completing leaves any (impossible-in-practice) leftovers for
98
+ // the next local settle rather than judging them against remote state.
99
+ if (source === 'remote') return
100
+ if (pendingByShape.size === 0 && returnedShapeIds.size === 0) return
101
+ const settled = [...pendingByShape.entries()]
102
+ pendingByShape.clear()
103
+ const returned = [...returnedShapeIds]
104
+ returnedShapeIds.clear()
105
+
106
+ const updates: TLCommentRecord[] = []
107
+ for (const [shapeId, threadPoints] of settled) {
108
+ if (editor.getShape(shapeId)) continue
109
+ for (const [threadId, point] of threadPoints) {
110
+ if (!point) continue
111
+ const thread = getCommentRecord(editor, threadId)
112
+ if (!thread || thread.typeName !== 'comment-thread') continue
113
+ if (!isAnchoredToShape(thread, shapeId)) continue
114
+ let converted = convertedByShape.get(shapeId)
115
+ if (!converted) {
116
+ converted = []
117
+ convertedByShape.set(shapeId, converted)
118
+ }
119
+ converted.push({ threadId, anchor: thread.anchor, point })
120
+ updates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })
121
+ }
122
+ }
123
+
124
+ for (const shapeId of returned) {
125
+ const shape = editor.getShape(shapeId)
126
+ if (!shape) continue
127
+ const converted = convertedByShape.get(shapeId)
128
+ if (!converted) continue
129
+ convertedByShape.delete(shapeId)
130
+
131
+ const pageId = editor.getAncestorPageId(shape)
132
+ for (const { threadId, anchor, point } of converted) {
133
+ const thread = getCommentRecord(editor, threadId)
134
+ if (!thread || thread.typeName !== 'comment-thread') continue
135
+ // Re-attach only threads still sitting exactly where the conversion left them — a
136
+ // pin moved since then was placed deliberately, and that placement wins.
137
+ const current = thread.anchor
138
+ if (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {
139
+ continue
140
+ }
141
+ if (!pageId || thread.pageId === pageId) {
142
+ updates.push({ ...thread, anchor })
143
+ } else {
144
+ rehomeThread({ ...thread, anchor }, pageId, updates)
145
+ }
146
+ }
147
+ }
148
+
149
+ if (updates.length > 0) {
150
+ commitCommentMutation(editor, ({ put }) => put(updates))
151
+ }
152
+ })
153
+
154
+ const disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(
155
+ 'shape',
156
+ (shape, source) => {
157
+ if (source === 'remote') return
158
+ if (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)
159
+ }
160
+ )
161
+
162
+ const disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(
163
+ 'shape',
164
+ (prev, next, source) => {
165
+ if (source === 'remote') return
166
+ if (prev.parentId === next.parentId) return
167
+ const pageId = editor.getAncestorPageId(next)
168
+ if (!pageId) return
169
+ // Descendants move with their parent without change events of their own.
170
+ const movedIds = editor.getShapeAndDescendantIds([next.id])
171
+ const updates: TLCommentRecord[] = []
172
+ for (const thread of getCommentThreads(editor)) {
173
+ const anchor = thread.anchor
174
+ if (anchor.type !== 'shape') continue
175
+ if (!movedIds.has(anchor.shapeId)) continue
176
+ if (thread.pageId === pageId) continue
177
+ rehomeThread(thread, pageId, updates)
178
+ }
179
+ if (updates.length > 0) {
180
+ commitCommentMutation(editor, ({ put }) => put(updates))
181
+ }
182
+ }
183
+ )
184
+
185
+ return () => {
186
+ disposeBeforeDelete()
187
+ disposeOperationComplete()
188
+ disposeAfterCreate()
189
+ disposeAfterChange()
190
+ }
191
+ }
@@ -0,0 +1,18 @@
1
+ import { type PointerEvent as ReactPointerEvent } from 'react'
2
+ import { Editor } from 'tldraw'
3
+
4
+ /** A pointer-down that belongs to the camera, not the comment UI: any non-primary button
5
+ * (middle/right-button pans), or a primary press with the spacebar pan key held. */
6
+ export function isCanvasPanGesture(editor: Editor, e: ReactPointerEvent) {
7
+ return e.button !== 0 || editor.inputs.keys.has('Space')
8
+ }
9
+
10
+ /** Hand a pointer event to the canvas beneath the comments layer, marked the same way the
11
+ * pass-through wheel/hover hooks mark their re-dispatched events. */
12
+ export function forwardPointerEventToCanvas(container: HTMLElement, e: ReactPointerEvent) {
13
+ const cvs = container.querySelector('.tl-canvas')
14
+ if (!cvs) return
15
+ const newEvent = new PointerEvent(e.type, e.nativeEvent as any)
16
+ ;(newEvent as any).isSpecialRedispatchedEvent = true
17
+ cvs.dispatchEvent(newEvent)
18
+ }