@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,106 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { memo, useMemo, useRef } from "react";
3
+ import {
4
+ useContainer,
5
+ usePassThroughWheelEvents,
6
+ useTranslation,
7
+ useValue
8
+ } from "tldraw";
9
+ import { CountBadge } from "../ui/count-badge.mjs";
10
+ import { forwardPointerEventToCanvas, isCanvasPanGesture } from "./canvas-events.mjs";
11
+ import { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from "./thread-preview.mjs";
12
+ import { impreciseShapePinInset, isInInflatedViewport } from "./thread-state.mjs";
13
+ const ClusterBadge = memo(function ClusterBadge2({
14
+ editor,
15
+ node,
16
+ onExpand,
17
+ onSelectThread,
18
+ threadsById,
19
+ ...props
20
+ }) {
21
+ const container = useContainer();
22
+ const msg = useTranslation();
23
+ const badgeRef = useRef(null);
24
+ const { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`);
25
+ usePassThroughWheelEvents(badgeRef);
26
+ const meanInset = useValue(
27
+ "cluster badge inset",
28
+ () => {
29
+ let x = 0;
30
+ let y = 0;
31
+ for (const id of node.members) {
32
+ const thread = threadsById.get(id);
33
+ if (!thread) continue;
34
+ const inset = impreciseShapePinInset(editor, thread.anchor);
35
+ if (inset) {
36
+ x += inset.x;
37
+ y += inset.y;
38
+ }
39
+ }
40
+ return { x: x / node.count, y: y / node.count };
41
+ },
42
+ [editor, node, threadsById]
43
+ );
44
+ const point = useValue(
45
+ "cluster badge point",
46
+ () => {
47
+ const pagePoint = editor.pageToViewport(node.centroid);
48
+ if (!isInInflatedViewport(editor, pagePoint)) return null;
49
+ return { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y };
50
+ },
51
+ [editor, node, meanInset]
52
+ );
53
+ const previewThreads = useMemo(() => {
54
+ if (!previewShown) return [];
55
+ const threads = [];
56
+ for (const id of node.members) {
57
+ const thread = threadsById.get(id);
58
+ if (thread) threads.push(thread);
59
+ }
60
+ return sortThreadsForPreview(threads);
61
+ }, [previewShown, node.members, threadsById]);
62
+ if (!point) return null;
63
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
64
+ /* @__PURE__ */ jsx(
65
+ "button",
66
+ {
67
+ ref: badgeRef,
68
+ type: "button",
69
+ className: "tlui-cmt-button tlui-cmt-canvas-cluster",
70
+ style: { left: point.x, top: point.y },
71
+ "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
72
+ onPointerDown: (e) => {
73
+ if (isCanvasPanGesture(editor, e)) {
74
+ forwardPointerEventToCanvas(container, e);
75
+ return;
76
+ }
77
+ e.stopPropagation();
78
+ },
79
+ onClick: (e) => {
80
+ e.stopPropagation();
81
+ onExpand(node);
82
+ },
83
+ ...previewHandlers,
84
+ onFocus: previewHandlers.onPointerEnter,
85
+ onBlur: previewHandlers.onPointerLeave,
86
+ children: /* @__PURE__ */ jsx(CountBadge, { count: node.count })
87
+ }
88
+ ),
89
+ previewShown && previewThreads.length > 0 && /* @__PURE__ */ jsx(
90
+ ThreadPreview,
91
+ {
92
+ editor,
93
+ threads: previewThreads,
94
+ variant: "list",
95
+ point,
96
+ onSelectThread,
97
+ ...previewHandlers,
98
+ ...props
99
+ }
100
+ )
101
+ ] });
102
+ });
103
+ export {
104
+ ClusterBadge
105
+ };
106
+ //# sourceMappingURL=cluster-badge.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/cluster-badge.tsx"],
4
+ "sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg('comments.cluster-label').replace('{count}', String(node.count))}\n\t\t\t\tonPointerDown={(e) => {\n\t\t\t\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\t\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t}}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t\tonExpand(node)\n\t\t\t\t}}\n\t\t\t\t{...previewHandlers}\n\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t>\n\t\t\t\t<CountBadge count={node.count} />\n\t\t\t</button>\n\t\t\t{previewShown && previewThreads.length > 0 && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\tonSelectThread={onSelectThread}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</>\n\t)\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["ClusterBadge"]
7
+ }
@@ -0,0 +1,65 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ const CLUSTER_FADE_MS = 150;
3
+ function useFadeVisibleNodes(nodes, resetKey) {
4
+ const resetKeyRef = useRef(resetKey);
5
+ const didReset = resetKeyRef.current !== resetKey;
6
+ if (didReset) {
7
+ resetKeyRef.current = resetKey;
8
+ }
9
+ const [fadeNodes, setFadeNodes] = useState(() => toPresentFadeNodes(nodes));
10
+ const renderedNodes = didReset ? toPresentFadeNodes(nodes) : fadeNodes;
11
+ useEffect(() => {
12
+ setFadeNodes(toPresentFadeNodes(nodes));
13
+ }, [resetKey]);
14
+ useEffect(() => {
15
+ if (didReset) return;
16
+ setFadeNodes((previous) => reconcileFadeNodes(previous, nodes));
17
+ }, [didReset, nodes]);
18
+ const hasEntering = renderedNodes.some((item) => item.phase === "entering");
19
+ useEffect(() => {
20
+ if (!hasEntering) return;
21
+ const frame = requestAnimationFrame(() => {
22
+ setFadeNodes(
23
+ (previous) => previous.map((item) => item.phase === "entering" ? { ...item, phase: "present" } : item)
24
+ );
25
+ });
26
+ return () => cancelAnimationFrame(frame);
27
+ }, [hasEntering, renderedNodes]);
28
+ const hasExiting = renderedNodes.some((item) => item.phase === "exiting");
29
+ useEffect(() => {
30
+ if (!hasExiting) return;
31
+ const timeout = window.setTimeout(() => {
32
+ setFadeNodes((previous) => previous.filter((item) => item.phase !== "exiting"));
33
+ }, CLUSTER_FADE_MS);
34
+ return () => window.clearTimeout(timeout);
35
+ }, [hasExiting, renderedNodes]);
36
+ return renderedNodes;
37
+ }
38
+ function toPresentFadeNodes(nodes) {
39
+ return nodes.map((node) => ({ node, phase: "present" }));
40
+ }
41
+ function reconcileFadeNodes(previous, nextNodes) {
42
+ const previousById = new Map(previous.map((item) => [item.node.id, item]));
43
+ const nextIds = new Set(nextNodes.map((node) => node.id));
44
+ const next = [];
45
+ for (const node of nextNodes) {
46
+ const previousItem = previousById.get(node.id);
47
+ next.push({
48
+ node,
49
+ phase: previousItem && previousItem.phase !== "exiting" ? previousItem.phase : previousItem ? "present" : "entering"
50
+ });
51
+ }
52
+ for (const item of previous) {
53
+ if (nextIds.has(item.node.id)) continue;
54
+ next.push(item.phase === "exiting" ? item : { ...item, phase: "exiting" });
55
+ }
56
+ return next;
57
+ }
58
+ function clusterFadeClassName(phase) {
59
+ return `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`;
60
+ }
61
+ export {
62
+ clusterFadeClassName,
63
+ useFadeVisibleNodes
64
+ };
65
+ //# sourceMappingURL=cluster-fade.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/cluster-fade.ts"],
4
+ "sourcesContent": ["import { useEffect, useRef, useState } from 'react'\nimport type { ClusterNode } from '../clustering/types'\n\nconst CLUSTER_FADE_MS = 150\n\nexport type ClusterFadePhase = 'entering' | 'present' | 'exiting'\n\nexport interface ClusterFadeNode {\n\tnode: ClusterNode\n\tphase: ClusterFadePhase\n}\n\n/**\n * Cross-fades the visible cluster nodes as the partition changes: a node that appears starts\n * `entering` and flips to `present` on the next frame, and one that leaves is kept as `exiting`\n * until the CSS transition has run. `resetKey` is compared by identity \u2014 a new cluster model\n * replaces the whole world, so its nodes snap to `present` instead of animating.\n */\nexport function useFadeVisibleNodes(\n\tnodes: readonly ClusterNode[],\n\tresetKey: object\n): ClusterFadeNode[] {\n\tconst resetKeyRef = useRef(resetKey)\n\tconst didReset = resetKeyRef.current !== resetKey\n\tif (didReset) {\n\t\tresetKeyRef.current = resetKey\n\t}\n\n\tconst [fadeNodes, setFadeNodes] = useState<ClusterFadeNode[]>(() => toPresentFadeNodes(nodes))\n\tconst renderedNodes = didReset ? toPresentFadeNodes(nodes) : fadeNodes\n\n\tuseEffect(() => {\n\t\tsetFadeNodes(toPresentFadeNodes(nodes))\n\t\t// Resets only on a new model (resetKey); node-list changes within the same model are\n\t\t// handled by the reconcile effect below, which fades entries in/out instead of snapping.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [resetKey])\n\n\tuseEffect(() => {\n\t\tif (didReset) return\n\t\tsetFadeNodes((previous) => reconcileFadeNodes(previous, nodes))\n\t}, [didReset, nodes])\n\n\tconst hasEntering = renderedNodes.some((item) => item.phase === 'entering')\n\tuseEffect(() => {\n\t\tif (!hasEntering) return\n\t\tconst frame = requestAnimationFrame(() => {\n\t\t\tsetFadeNodes((previous) =>\n\t\t\t\tprevious.map((item) => (item.phase === 'entering' ? { ...item, phase: 'present' } : item))\n\t\t\t)\n\t\t})\n\t\treturn () => cancelAnimationFrame(frame)\n\t}, [hasEntering, renderedNodes])\n\n\tconst hasExiting = renderedNodes.some((item) => item.phase === 'exiting')\n\tuseEffect(() => {\n\t\tif (!hasExiting) return\n\t\tconst timeout = window.setTimeout(() => {\n\t\t\tsetFadeNodes((previous) => previous.filter((item) => item.phase !== 'exiting'))\n\t\t}, CLUSTER_FADE_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [hasExiting, renderedNodes])\n\n\treturn renderedNodes\n}\n\nfunction toPresentFadeNodes(nodes: readonly ClusterNode[]): ClusterFadeNode[] {\n\treturn nodes.map((node) => ({ node, phase: 'present' }))\n}\n\nfunction reconcileFadeNodes(\n\tprevious: readonly ClusterFadeNode[],\n\tnextNodes: readonly ClusterNode[]\n): ClusterFadeNode[] {\n\tconst previousById = new Map(previous.map((item) => [item.node.id, item]))\n\tconst nextIds = new Set(nextNodes.map((node) => node.id))\n\tconst next: ClusterFadeNode[] = []\n\n\tfor (const node of nextNodes) {\n\t\tconst previousItem = previousById.get(node.id)\n\t\tnext.push({\n\t\t\tnode,\n\t\t\tphase:\n\t\t\t\tpreviousItem && previousItem.phase !== 'exiting'\n\t\t\t\t\t? previousItem.phase\n\t\t\t\t\t: previousItem\n\t\t\t\t\t\t? 'present'\n\t\t\t\t\t\t: 'entering',\n\t\t})\n\t}\n\n\tfor (const item of previous) {\n\t\tif (nextIds.has(item.node.id)) continue\n\t\tnext.push(item.phase === 'exiting' ? item : { ...item, phase: 'exiting' })\n\t}\n\n\treturn next\n}\n\nexport function clusterFadeClassName(phase: ClusterFadePhase): string {\n\treturn `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`\n}\n"],
5
+ "mappings": "AAAA,SAAS,WAAW,QAAQ,gBAAgB;AAG5C,MAAM,kBAAkB;AAejB,SAAS,oBACf,OACA,UACoB;AACpB,QAAM,cAAc,OAAO,QAAQ;AACnC,QAAM,WAAW,YAAY,YAAY;AACzC,MAAI,UAAU;AACb,gBAAY,UAAU;AAAA,EACvB;AAEA,QAAM,CAAC,WAAW,YAAY,IAAI,SAA4B,MAAM,mBAAmB,KAAK,CAAC;AAC7F,QAAM,gBAAgB,WAAW,mBAAmB,KAAK,IAAI;AAE7D,YAAU,MAAM;AACf,iBAAa,mBAAmB,KAAK,CAAC;AAAA,EAIvC,GAAG,CAAC,QAAQ,CAAC;AAEb,YAAU,MAAM;AACf,QAAI,SAAU;AACd,iBAAa,CAAC,aAAa,mBAAmB,UAAU,KAAK,CAAC;AAAA,EAC/D,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,QAAM,cAAc,cAAc,KAAK,CAAC,SAAS,KAAK,UAAU,UAAU;AAC1E,YAAU,MAAM;AACf,QAAI,CAAC,YAAa;AAClB,UAAM,QAAQ,sBAAsB,MAAM;AACzC;AAAA,QAAa,CAAC,aACb,SAAS,IAAI,CAAC,SAAU,KAAK,UAAU,aAAa,EAAE,GAAG,MAAM,OAAO,UAAU,IAAI,IAAK;AAAA,MAC1F;AAAA,IACD,CAAC;AACD,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACxC,GAAG,CAAC,aAAa,aAAa,CAAC;AAE/B,QAAM,aAAa,cAAc,KAAK,CAAC,SAAS,KAAK,UAAU,SAAS;AACxE,YAAU,MAAM;AACf,QAAI,CAAC,WAAY;AACjB,UAAM,UAAU,OAAO,WAAW,MAAM;AACvC,mBAAa,CAAC,aAAa,SAAS,OAAO,CAAC,SAAS,KAAK,UAAU,SAAS,CAAC;AAAA,IAC/E,GAAG,eAAe;AAClB,WAAO,MAAM,OAAO,aAAa,OAAO;AAAA,EACzC,GAAG,CAAC,YAAY,aAAa,CAAC;AAE9B,SAAO;AACR;AAEA,SAAS,mBAAmB,OAAkD;AAC7E,SAAO,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,OAAO,UAAU,EAAE;AACxD;AAEA,SAAS,mBACR,UACA,WACoB;AACpB,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC;AACzE,QAAM,UAAU,IAAI,IAAI,UAAU,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AACxD,QAAM,OAA0B,CAAC;AAEjC,aAAW,QAAQ,WAAW;AAC7B,UAAM,eAAe,aAAa,IAAI,KAAK,EAAE;AAC7C,SAAK,KAAK;AAAA,MACT;AAAA,MACA,OACC,gBAAgB,aAAa,UAAU,YACpC,aAAa,QACb,eACC,YACA;AAAA,IACN,CAAC;AAAA,EACF;AAEA,aAAW,QAAQ,UAAU;AAC5B,QAAI,QAAQ,IAAI,KAAK,KAAK,EAAE,EAAG;AAC/B,SAAK,KAAK,KAAK,UAAU,YAAY,OAAO,EAAE,GAAG,MAAM,OAAO,UAAU,CAAC;AAAA,EAC1E;AAEA,SAAO;AACR;AAEO,SAAS,qBAAqB,OAAiC;AACrE,SAAO,gDAAgD,KAAK;AAC7D;",
6
+ "names": []
7
+ }
@@ -1,20 +1,70 @@
1
- import { anchorPagePoint } from "./thread-state.mjs";
2
- function collectClusterLeaves(editor, threads, openThreadId, impreciseShapeAnchor) {
1
+ import { anchorPagePoint, impreciseShapePinInset } from "./thread-state.mjs";
2
+ function collectClusterLeaves(editor, threads, openThreadId) {
3
3
  const pageId = editor.getCurrentPageId();
4
4
  const leaves = [];
5
+ let screenOffsets;
5
6
  for (const thread of threads) {
6
7
  if (thread.id === openThreadId) continue;
7
8
  if (thread.pageId !== pageId) continue;
8
- const point = anchorPagePoint(editor, thread.anchor, impreciseShapeAnchor);
9
+ const point = anchorPagePoint(editor, thread.anchor);
9
10
  if (!point) continue;
10
11
  leaves.push({
11
12
  id: thread.id,
12
13
  point: { x: point.x, y: point.y }
13
14
  });
15
+ const inset = impreciseShapePinInset(editor, thread.anchor);
16
+ if (inset) (screenOffsets ??= /* @__PURE__ */ new Map()).set(thread.id, inset);
14
17
  }
15
- return leaves;
18
+ return { leaves, screenOffsets };
19
+ }
20
+ function clusterInputEqual(a, b) {
21
+ if (a === b) return true;
22
+ return clusterLeavesEqual(a.leaves, b.leaves) && screenOffsetsEqual(a.screenOffsets, b.screenOffsets);
23
+ }
24
+ function clusterInputIdsEqual(a, b) {
25
+ if (a === b) return true;
26
+ return clusterLeafIdsEqual(a.leaves, b.leaves) && screenOffsetIdsEqual(a.screenOffsets, b.screenOffsets);
27
+ }
28
+ function screenOffsetsEqual(a, b) {
29
+ if (a === b) return true;
30
+ if (!a || !b) return false;
31
+ if (a.size !== b.size) return false;
32
+ for (const [id, offset] of b) {
33
+ const prev = a.get(id);
34
+ if (!prev || prev.x !== offset.x || prev.y !== offset.y) return false;
35
+ }
36
+ return true;
37
+ }
38
+ function screenOffsetIdsEqual(a, b) {
39
+ if (a === b) return true;
40
+ if (!a || !b) return false;
41
+ if (a.size !== b.size) return false;
42
+ for (const id of b.keys()) {
43
+ if (!a.has(id)) return false;
44
+ }
45
+ return true;
46
+ }
47
+ function clusterLeavesEqual(a, b) {
48
+ if (a === b) return true;
49
+ if (a.length !== b.length) return false;
50
+ for (let i = 0; i < a.length; i++) {
51
+ const la = a[i];
52
+ const lb = b[i];
53
+ if (la.id !== lb.id || la.point.x !== lb.point.x || la.point.y !== lb.point.y) return false;
54
+ }
55
+ return true;
56
+ }
57
+ function clusterLeafIdsEqual(a, b) {
58
+ if (a === b) return true;
59
+ if (a.length !== b.length) return false;
60
+ for (let i = 0; i < a.length; i++) {
61
+ if (a[i].id !== b[i].id) return false;
62
+ }
63
+ return true;
16
64
  }
17
65
  export {
66
+ clusterInputEqual,
67
+ clusterInputIdsEqual,
18
68
  collectClusterLeaves
19
69
  };
20
70
  //# sourceMappingURL=cluster-input.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/cluster-input.ts"],
4
- "sourcesContent": ["import type { Editor, TLCommentThread } from 'tldraw'\nimport type { LeafInput } from '../clustering/types'\nimport { anchorPagePoint } from './thread-state'\n\n/** @public */\nexport function collectClusterLeaves(\n\teditor: Editor,\n\tthreads: readonly TLCommentThread[],\n\topenThreadId: string | null,\n\timpreciseShapeAnchor?: { x: number; y: number }\n): LeafInput[] {\n\tconst pageId = editor.getCurrentPageId()\n\tconst leaves: LeafInput[] = []\n\n\tfor (const thread of threads) {\n\t\tif (thread.id === openThreadId) continue\n\t\tif (thread.pageId !== pageId) continue\n\n\t\tconst point = anchorPagePoint(editor, thread.anchor, impreciseShapeAnchor)\n\t\tif (!point) continue\n\n\t\tleaves.push({\n\t\t\tid: thread.id,\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\t}\n\n\treturn leaves\n}\n"],
5
- "mappings": "AAEA,SAAS,uBAAuB;AAGzB,SAAS,qBACf,QACA,SACA,cACA,sBACc;AACd,QAAM,SAAS,OAAO,iBAAiB;AACvC,QAAM,SAAsB,CAAC;AAE7B,aAAW,UAAU,SAAS;AAC7B,QAAI,OAAO,OAAO,aAAc;AAChC,QAAI,OAAO,WAAW,OAAQ;AAE9B,UAAM,QAAQ,gBAAgB,QAAQ,OAAO,QAAQ,oBAAoB;AACzE,QAAI,CAAC,MAAO;AAEZ,WAAO,KAAK;AAAA,MACX,IAAI,OAAO;AAAA,MACX,OAAO,EAAE,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE;AAAA,IACjC,CAAC;AAAA,EACF;AAEA,SAAO;AACR;",
4
+ "sourcesContent": ["import type { Editor, TLCommentThread, VecLike } from 'tldraw'\nimport type { LeafInput, LeafScreenOffsets } from '../clustering/types'\nimport { anchorPagePoint, impreciseShapePinInset } from './thread-state'\n\n/** The clustering input for the current page's pinned threads. @internal */\nexport interface ClusterInput {\n\t/** One leaf per pinned thread, at its anchor page point. */\n\tleaves: LeafInput[]\n\t/**\n\t * The render offsets of the imprecise pins among the leaves \u2014 those draw tucked into their\n\t * shape by a constant screen-px inset rather than on their anchor, and the clustering prices\n\t * their merges at the visual positions. Undefined when every pin renders on its anchor,\n\t * which guarantees a byte-identical offset-unaware clustering run.\n\t */\n\tscreenOffsets: LeafScreenOffsets | undefined\n}\n\n/** The cluster leaves for the threads pinned on the current page. @internal */\nexport function collectClusterLeaves(\n\teditor: Editor,\n\tthreads: readonly TLCommentThread[],\n\topenThreadId: string | null\n): ClusterInput {\n\tconst pageId = editor.getCurrentPageId()\n\tconst leaves: LeafInput[] = []\n\tlet screenOffsets: Map<string, VecLike> | undefined\n\n\tfor (const thread of threads) {\n\t\tif (thread.id === openThreadId) continue\n\t\tif (thread.pageId !== pageId) continue\n\n\t\tconst point = anchorPagePoint(editor, thread.anchor)\n\t\tif (!point) continue\n\n\t\tleaves.push({\n\t\t\tid: thread.id,\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\n\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\tif (inset) (screenOffsets ??= new Map()).set(thread.id, inset)\n\t}\n\n\treturn { leaves, screenOffsets }\n}\n\n/**\n * Value equality over everything `computeClusterTable` reads. Keeps the input's identity stable\n * across recomputes that changed nothing, so the O(N\u00B2) rebuild only runs when it has to.\n * @internal\n */\nexport function clusterInputEqual(a: ClusterInput, b: ClusterInput): boolean {\n\tif (a === b) return true\n\treturn (\n\t\tclusterLeavesEqual(a.leaves, b.leaves) && screenOffsetsEqual(a.screenOffsets, b.screenOffsets)\n\t)\n}\n\n/**\n * The same, ignoring everything positional \u2014 anchor points and offset vectors alike. The mid-drag\n * gate: a move defers, but a thread added, removed, or changed precision still rebuilds.\n * @internal\n */\nexport function clusterInputIdsEqual(a: ClusterInput, b: ClusterInput): boolean {\n\tif (a === b) return true\n\treturn (\n\t\tclusterLeafIdsEqual(a.leaves, b.leaves) &&\n\t\tscreenOffsetIdsEqual(a.screenOffsets, b.screenOffsets)\n\t)\n}\n\nfunction screenOffsetsEqual(a: LeafScreenOffsets | undefined, b: LeafScreenOffsets | undefined) {\n\tif (a === b) return true\n\tif (!a || !b) return false\n\tif (a.size !== b.size) return false\n\tfor (const [id, offset] of b) {\n\t\tconst prev = a.get(id)\n\t\tif (!prev || prev.x !== offset.x || prev.y !== offset.y) return false\n\t}\n\treturn true\n}\n\n/** Which leaves carry an offset, ignoring the vectors: a leaf gains or loses one by changing\n * precision, which is a record change rather than a gesture. */\nfunction screenOffsetIdsEqual(a: LeafScreenOffsets | undefined, b: LeafScreenOffsets | undefined) {\n\tif (a === b) return true\n\tif (!a || !b) return false\n\tif (a.size !== b.size) return false\n\tfor (const id of b.keys()) {\n\t\tif (!a.has(id)) return false\n\t}\n\treturn true\n}\n\n/** Whether two leaf lists have the same threads at the same positions, in the same order. */\nfunction clusterLeavesEqual(a: readonly LeafInput[], b: readonly LeafInput[]): boolean {\n\tif (a === b) return true\n\tif (a.length !== b.length) return false\n\tfor (let i = 0; i < a.length; i++) {\n\t\tconst la = a[i]\n\t\tconst lb = b[i]\n\t\tif (la.id !== lb.id || la.point.x !== lb.point.x || la.point.y !== lb.point.y) return false\n\t}\n\treturn true\n}\n\n/** Whether two leaf lists contain the same thread ids in the same order, ignoring positions. */\nfunction clusterLeafIdsEqual(a: readonly LeafInput[], b: readonly LeafInput[]): boolean {\n\tif (a === b) return true\n\tif (a.length !== b.length) return false\n\tfor (let i = 0; i < a.length; i++) {\n\t\tif (a[i].id !== b[i].id) return false\n\t}\n\treturn true\n}\n"],
5
+ "mappings": "AAEA,SAAS,iBAAiB,8BAA8B;AAgBjD,SAAS,qBACf,QACA,SACA,cACe;AACf,QAAM,SAAS,OAAO,iBAAiB;AACvC,QAAM,SAAsB,CAAC;AAC7B,MAAI;AAEJ,aAAW,UAAU,SAAS;AAC7B,QAAI,OAAO,OAAO,aAAc;AAChC,QAAI,OAAO,WAAW,OAAQ;AAE9B,UAAM,QAAQ,gBAAgB,QAAQ,OAAO,MAAM;AACnD,QAAI,CAAC,MAAO;AAEZ,WAAO,KAAK;AAAA,MACX,IAAI,OAAO;AAAA,MACX,OAAO,EAAE,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE;AAAA,IACjC,CAAC;AAED,UAAM,QAAQ,uBAAuB,QAAQ,OAAO,MAAM;AAC1D,QAAI,MAAO,EAAC,kBAAkB,oBAAI,IAAI,GAAG,IAAI,OAAO,IAAI,KAAK;AAAA,EAC9D;AAEA,SAAO,EAAE,QAAQ,cAAc;AAChC;AAOO,SAAS,kBAAkB,GAAiB,GAA0B;AAC5E,MAAI,MAAM,EAAG,QAAO;AACpB,SACC,mBAAmB,EAAE,QAAQ,EAAE,MAAM,KAAK,mBAAmB,EAAE,eAAe,EAAE,aAAa;AAE/F;AAOO,SAAS,qBAAqB,GAAiB,GAA0B;AAC/E,MAAI,MAAM,EAAG,QAAO;AACpB,SACC,oBAAoB,EAAE,QAAQ,EAAE,MAAM,KACtC,qBAAqB,EAAE,eAAe,EAAE,aAAa;AAEvD;AAEA,SAAS,mBAAmB,GAAkC,GAAkC;AAC/F,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,CAAC,KAAK,CAAC,EAAG,QAAO;AACrB,MAAI,EAAE,SAAS,EAAE,KAAM,QAAO;AAC9B,aAAW,CAAC,IAAI,MAAM,KAAK,GAAG;AAC7B,UAAM,OAAO,EAAE,IAAI,EAAE;AACrB,QAAI,CAAC,QAAQ,KAAK,MAAM,OAAO,KAAK,KAAK,MAAM,OAAO,EAAG,QAAO;AAAA,EACjE;AACA,SAAO;AACR;AAIA,SAAS,qBAAqB,GAAkC,GAAkC;AACjG,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,CAAC,KAAK,CAAC,EAAG,QAAO;AACrB,MAAI,EAAE,SAAS,EAAE,KAAM,QAAO;AAC9B,aAAW,MAAM,EAAE,KAAK,GAAG;AAC1B,QAAI,CAAC,EAAE,IAAI,EAAE,EAAG,QAAO;AAAA,EACxB;AACA,SAAO;AACR;AAGA,SAAS,mBAAmB,GAAyB,GAAkC;AACtF,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAClC,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,CAAC;AACd,QAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM,GAAG,MAAM,EAAG,QAAO;AAAA,EACvF;AACA,SAAO;AACR;AAGA,SAAS,oBAAoB,GAAyB,GAAkC;AACvF,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAClC,QAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAI,QAAO;AAAA,EACjC;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,236 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { react, useValue } from "tldraw";
3
+ import { computeClusterTable } from "../clustering/computeClusterTable.mjs";
4
+ import { createClusterRuntime } from "../clustering/runtime.mjs";
5
+ import { useFadeVisibleNodes } from "./cluster-fade.mjs";
6
+ import {
7
+ clusterInputEqual,
8
+ clusterInputIdsEqual,
9
+ collectClusterLeaves
10
+ } from "./cluster-input.mjs";
11
+ import { openThreadId } from "./state.mjs";
12
+ import { anchorPagePoint, commentCenterScreenOffset } from "./thread-state.mjs";
13
+ const CLUSTER_EXPAND_ZOOM_MS = 450;
14
+ const CLUSTER_SPLIT_ZOOM_FACTOR = 1.05;
15
+ const EMPTY_SET = /* @__PURE__ */ new Set();
16
+ const MOVED_LEAF_EPSILON = 1e-6;
17
+ const SHAPE_DRAG_STATE_PATHS = [
18
+ "select.translating",
19
+ "select.resizing",
20
+ "select.rotating",
21
+ "select.dragging_handle",
22
+ "select.crop.cropping"
23
+ ];
24
+ function isShapeDragInProgress(editor) {
25
+ return editor.isInAny(...SHAPE_DRAG_STATE_PATHS);
26
+ }
27
+ function useClusterModel(editor, threads, openId) {
28
+ const [heldThreadIds, setHeldThreadIds] = useState(EMPTY_SET);
29
+ const adoptOnRebuild = useRef(false);
30
+ const clusterInputRef = useRef({ leaves: [], screenOffsets: void 0 });
31
+ const clusterInput = useValue(
32
+ "comment cluster leaves",
33
+ () => {
34
+ const next = collectClusterLeaves(
35
+ editor,
36
+ threads.filter((thread) => !heldThreadIds.has(thread.id)),
37
+ openThreadId.get(editor)
38
+ );
39
+ const prev = clusterInputRef.current;
40
+ if (isShapeDragInProgress(editor) && clusterInputIdsEqual(prev, next)) return prev;
41
+ if (clusterInputEqual(prev, next)) return prev;
42
+ clusterInputRef.current = next;
43
+ return next;
44
+ },
45
+ [editor, threads, heldThreadIds]
46
+ );
47
+ const clusterZoomBounds = useValue(
48
+ "comment cluster zoom bounds",
49
+ () => getClusterZoomBounds(editor),
50
+ [editor]
51
+ );
52
+ const latestModel = useMemo(() => {
53
+ const table = computeClusterTable(
54
+ clusterInput.leaves,
55
+ clusterZoomBounds,
56
+ clusterInput.screenOffsets
57
+ );
58
+ const runtime = createClusterRuntime(table);
59
+ runtime.seed(editor.getZoomLevel());
60
+ return { runtime, table };
61
+ }, [clusterInput, clusterZoomBounds, editor]);
62
+ const [renderedModel, setRenderedModel] = useState(latestModel);
63
+ let clusterModel = renderedModel;
64
+ const pageId = useValue("comment cluster page", () => editor.getCurrentPageId(), [editor]);
65
+ const pageRef = useRef(pageId);
66
+ if (pageRef.current !== pageId) {
67
+ pageRef.current = pageId;
68
+ adoptOnRebuild.current = false;
69
+ latestModel.runtime.seed(editor.getZoomLevel());
70
+ if (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET);
71
+ setRenderedModel(latestModel);
72
+ clusterModel = latestModel;
73
+ }
74
+ const rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current;
75
+ if (renderedModel !== latestModel && rejoinPending) {
76
+ adoptOnRebuild.current = false;
77
+ latestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible());
78
+ setRenderedModel(latestModel);
79
+ clusterModel = latestModel;
80
+ } else if (heldThreadIds.size === 0 && renderedModel === latestModel) {
81
+ adoptOnRebuild.current = false;
82
+ }
83
+ const newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel);
84
+ if (newlyMovedIds.length > 0) {
85
+ const next = new Set(heldThreadIds);
86
+ for (const id of newlyMovedIds) next.add(id);
87
+ setHeldThreadIds(next);
88
+ }
89
+ if (clusterModel !== latestModel) {
90
+ const latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
91
+ const removedLeafIds = [];
92
+ for (const leaf of clusterModel.table.leaves) {
93
+ if (!latestLeafIds.has(leaf.id)) {
94
+ removedLeafIds.push(leaf.id);
95
+ }
96
+ }
97
+ if (removedLeafIds.length > 0) clusterModel.runtime.detachLeaves(removedLeafIds);
98
+ }
99
+ useEffect(() => {
100
+ if (heldThreadIds.size === 0) return;
101
+ let lastZoom = editor.getZoomLevel();
102
+ return react("rejoin moved comment pins on zoom out", () => {
103
+ const zoom = editor.getZoomLevel();
104
+ const prevZoom = lastZoom;
105
+ lastZoom = zoom;
106
+ if (zoom >= prevZoom) return;
107
+ adoptOnRebuild.current = true;
108
+ setHeldThreadIds(EMPTY_SET);
109
+ });
110
+ }, [heldThreadIds, editor]);
111
+ useEffect(() => {
112
+ if (clusterModel === latestModel) return;
113
+ let lastZoom = editor.getZoomLevel();
114
+ return react("adopt pending cluster model on zoom out", () => {
115
+ const zoom = editor.getZoomLevel();
116
+ const prevZoom = lastZoom;
117
+ lastZoom = zoom;
118
+ if (zoom >= prevZoom) return;
119
+ latestModel.runtime.seedFrom(zoom, clusterModel.runtime.getVisible());
120
+ setRenderedModel(latestModel);
121
+ });
122
+ }, [clusterModel, latestModel, editor]);
123
+ const partitionVersion = clusterModel.runtime.version;
124
+ const orphanThreads = useMemo(() => {
125
+ if (clusterModel === latestModel) return [];
126
+ const displayed = /* @__PURE__ */ new Set();
127
+ for (const node of clusterModel.runtime.getVisible().values()) {
128
+ for (const member of node.members) displayed.add(member);
129
+ }
130
+ const latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
131
+ return threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id));
132
+ }, [clusterModel, latestModel, threads, partitionVersion]);
133
+ const heldThreads = useMemo(
134
+ () => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),
135
+ [threads, heldThreadIds, openId]
136
+ );
137
+ useValue(
138
+ "comment cluster version",
139
+ () => {
140
+ clusterModel.runtime.onCamera(editor.getZoomLevel());
141
+ return clusterModel.runtime.version;
142
+ },
143
+ [clusterModel, editor]
144
+ );
145
+ const visibleNodes = useMemo(() => {
146
+ return Array.from(clusterModel.runtime.getVisible().values());
147
+ }, [clusterModel, partitionVersion]);
148
+ const fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel);
149
+ return {
150
+ model: clusterModel,
151
+ zoomBounds: clusterZoomBounds,
152
+ fadeNodes,
153
+ orphanThreads,
154
+ heldThreads
155
+ };
156
+ }
157
+ function findMovedClusteredLeafIds(rendered, latest) {
158
+ if (rendered.table === latest.table) return [];
159
+ const visible = rendered.runtime.getVisible();
160
+ const latestById = new Map(latest.table.leaves.map((leaf) => [leaf.id, leaf]));
161
+ const moved = [];
162
+ for (const leaf of rendered.table.leaves) {
163
+ if (visible.has(leaf.id)) continue;
164
+ const current = latestById.get(leaf.id);
165
+ if (!current) continue;
166
+ if (Math.abs(current.centroid.x - leaf.centroid.x) > MOVED_LEAF_EPSILON || Math.abs(current.centroid.y - leaf.centroid.y) > MOVED_LEAF_EPSILON) {
167
+ moved.push(leaf.id);
168
+ }
169
+ }
170
+ return moved;
171
+ }
172
+ function getClusterZoomBounds(editor) {
173
+ const cameraOptions = editor.getCameraOptions();
174
+ const baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1;
175
+ const zoomSteps = cameraOptions.zoomSteps;
176
+ return {
177
+ minZoom: zoomSteps[0] * baseZoom,
178
+ maxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom
179
+ };
180
+ }
181
+ function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
182
+ if (thread.pageId !== editor.getCurrentPageId()) {
183
+ editor.setCurrentPage(thread.pageId);
184
+ }
185
+ const point = anchorPagePoint(editor, thread.anchor);
186
+ if (!point) return;
187
+ if (options.enableClustering) {
188
+ const parentEvent = findDirectParentEvent(table, thread.id);
189
+ if (parentEvent && Number.isFinite(parentEvent.zSplit) && parentEvent.zSplit <= zoomBounds.maxZoom) {
190
+ const zoom = clamp(
191
+ parentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
192
+ zoomBounds.minZoom,
193
+ zoomBounds.maxZoom
194
+ );
195
+ centerOnPointAtZoom(editor, point, zoom, duration);
196
+ return;
197
+ }
198
+ }
199
+ const offset = commentCenterScreenOffset(editor) / editor.getZoomLevel();
200
+ editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } });
201
+ }
202
+ function zoomToClusterSplit(editor, table, zoomBounds, node) {
203
+ const event = table.events.find((e) => e.result.id === node.id);
204
+ if (!event || !Number.isFinite(event.zSplit)) return;
205
+ const zoom = clamp(
206
+ event.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
207
+ zoomBounds.minZoom,
208
+ zoomBounds.maxZoom
209
+ );
210
+ centerOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS);
211
+ }
212
+ function findDirectParentEvent(table, threadId) {
213
+ return table.events.find((event) => event.children.some((child) => child.id === threadId));
214
+ }
215
+ function centerOnPointAtZoom(editor, point, zoom, duration = 200) {
216
+ const viewport = editor.getViewportScreenBounds();
217
+ const offset = commentCenterScreenOffset(editor);
218
+ editor.setCamera(
219
+ {
220
+ x: (viewport.w / 2 - offset) / zoom - point.x,
221
+ y: viewport.h / (2 * zoom) - point.y,
222
+ z: zoom
223
+ },
224
+ { animation: { duration } }
225
+ );
226
+ }
227
+ function clamp(value, min, max) {
228
+ return Math.max(min, Math.min(max, value));
229
+ }
230
+ export {
231
+ CLUSTER_EXPAND_ZOOM_MS,
232
+ revealThreadPin,
233
+ useClusterModel,
234
+ zoomToClusterSplit
235
+ };
236
+ //# sourceMappingURL=cluster-model.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/cluster-model.ts"],
4
+ "sourcesContent": ["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Editor, react, TLCommentThread, useValue } from 'tldraw'\nimport { computeClusterTable } from '../clustering/computeClusterTable'\nimport { type ClusterRuntime, createClusterRuntime } from '../clustering/runtime'\nimport type { ClusterNode, ClusterTable, MergeEvent } from '../clustering/types'\nimport { type ClusterFadeNode, useFadeVisibleNodes } from './cluster-fade'\nimport {\n\ttype ClusterInput,\n\tclusterInputEqual,\n\tclusterInputIdsEqual,\n\tcollectClusterLeaves,\n} from './cluster-input'\nimport { type CommentingOptions } from './options'\nimport { openThreadId } from './state'\nimport { anchorPagePoint, commentCenterScreenOffset } from './thread-state'\n\n/** Duration of the click-a-badge zoom-to-split animation. */\nexport const CLUSTER_EXPAND_ZOOM_MS = 450\n/** How far past a cluster's split zoom to land when expanding it \u2014 a 5% overshoot, so the badge\n * lands clear of the threshold it just crossed rather than flickering on it. */\nconst CLUSTER_SPLIT_ZOOM_FACTOR = 1.05\n\nconst EMPTY_SET: ReadonlySet<string> = new Set()\nconst MOVED_LEAF_EPSILON = 1e-6\n\n/** Default select-tool states that move shapes continuously, one store write per pointermove. A\n * custom tool in their place just never matches, falling back to a rebuild per frame. */\nconst SHAPE_DRAG_STATE_PATHS = [\n\t'select.translating',\n\t'select.resizing',\n\t'select.rotating',\n\t'select.dragging_handle',\n\t'select.crop.cropping',\n] as const\n\n/** Reactive: `isInAny` reads the tool state path, so a computed reading this re-evaluates when the\n * gesture starts or settles. */\nfunction isShapeDragInProgress(editor: Editor): boolean {\n\treturn editor.isInAny(...SHAPE_DRAG_STATE_PATHS)\n}\n\n/** The clustering table for the current scene, plus the runtime walking its merge events. */\nexport interface ClusterModel {\n\truntime: ClusterRuntime\n\ttable: ClusterTable\n}\n\nexport interface ClusterZoomBounds {\n\tminZoom: number\n\tmaxZoom: number\n}\n\nexport interface ClusterModelState {\n\t/** The partition actually on screen. See {@link useClusterModel} for why it can lag the input. */\n\tmodel: ClusterModel\n\tzoomBounds: ClusterZoomBounds\n\t/** The displayed nodes, each tagged with its cross-fade phase. */\n\tfadeNodes: ClusterFadeNode[]\n\t/** Threads in the input that the displayed partition doesn't show \u2014 render them as plain pins. */\n\torphanThreads: TLCommentThread[]\n\t/** Threads held out of clustering because their anchor moved while folded into a badge. */\n\theldThreads: TLCommentThread[]\n}\n\n/**\n * The clustering state machine behind the comments layer.\n *\n * The core invariant: the only thing that re-flows clustering doc-wide is zoom. Every rebuild is\n * computed immediately as `latestModel`, but the on-screen partition is `renderedModel`, which only\n * changes via (a) the cursor walking on zoom, (b) adoption of the pending rebuild on zoom-out, or\n * (c) LOCAL detach patches, where a leaf that left the input is detached from its own badge in\n * place and nothing else on the canvas moves.\n *\n * Threads the displayed partition can't represent are returned separately (`orphanThreads`,\n * `heldThreads`) for the layer to draw as ordinary pins.\n */\nexport function useClusterModel(\n\teditor: Editor,\n\tthreads: readonly TLCommentThread[],\n\topenId: string | null\n): ClusterModelState {\n\t// Threads held out of clustering because their anchor moved while folded inside a badge\n\t// (drag, nudge, align, undo, a collaborator \u2014 detected by position, not gesture). They render\n\t// as live pins riding their anchor and rejoin clustering on the next zoom-out.\n\tconst [heldThreadIds, setHeldThreadIds] = useState<ReadonlySet<string>>(EMPTY_SET)\n\tconst adoptOnRebuild = useRef(false)\n\t// This input's identity keys the O(N\u00B2) table rebuild below, so it's gated on value equality: a\n\t// reply, a reaction, a resolve \u2014 anything that touches comment records without moving a pin \u2014\n\t// returns the previous input and rebuilds nothing.\n\t//\n\t// Mid-drag the gate widens to ignore positions too, and that costs something real: it freezes\n\t// `latestModel`, which is where `findMovedClusteredLeafIds` reads live positions from, so a pin\n\t// folded into a badge stops popping out to ride its anchor and corrects on release instead. The\n\t// per-frame rebuild it buys back was paid by every drag on the board, comment-anchored or not.\n\t// An added or deleted thread changes the id set, so it still rebuilds promptly.\n\tconst clusterInputRef = useRef<ClusterInput>({ leaves: [], screenOffsets: undefined })\n\tconst clusterInput = useValue(\n\t\t'comment cluster leaves',\n\t\t() => {\n\t\t\tconst next = collectClusterLeaves(\n\t\t\t\teditor,\n\t\t\t\tthreads.filter((thread) => !heldThreadIds.has(thread.id)),\n\t\t\t\topenThreadId.get(editor)\n\t\t\t)\n\t\t\tconst prev = clusterInputRef.current\n\t\t\tif (isShapeDragInProgress(editor) && clusterInputIdsEqual(prev, next)) return prev\n\t\t\tif (clusterInputEqual(prev, next)) return prev\n\t\t\tclusterInputRef.current = next\n\t\t\treturn next\n\t\t},\n\t\t[editor, threads, heldThreadIds]\n\t)\n\tconst clusterZoomBounds = useValue(\n\t\t'comment cluster zoom bounds',\n\t\t() => getClusterZoomBounds(editor),\n\t\t[editor]\n\t)\n\tconst latestModel = useMemo(() => {\n\t\tconst table = computeClusterTable(\n\t\t\tclusterInput.leaves,\n\t\t\tclusterZoomBounds,\n\t\t\tclusterInput.screenOffsets\n\t\t)\n\t\tconst runtime = createClusterRuntime(table)\n\t\truntime.seed(editor.getZoomLevel())\n\t\treturn { runtime, table }\n\t}, [clusterInput, clusterZoomBounds, editor])\n\tconst [renderedModel, setRenderedModel] = useState(latestModel)\n\tlet clusterModel = renderedModel\n\t// A page switch replaces the whole scene: hard-reset rather than detach the world.\n\tconst pageId = useValue('comment cluster page', () => editor.getCurrentPageId(), [editor])\n\tconst pageRef = useRef(pageId)\n\tif (pageRef.current !== pageId) {\n\t\tpageRef.current = pageId\n\t\tadoptOnRebuild.current = false\n\t\tlatestModel.runtime.seed(editor.getZoomLevel())\n\t\tif (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET)\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t}\n\t// adoptOnRebuild is set outside React's render cycle, paired with clearing heldThreadIds. Only trust\n\t// it once that pairing is visible here, or an unrelated re-render can land in the gap.\n\tconst rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current\n\tif (renderedModel !== latestModel && rejoinPending) {\n\t\tadoptOnRebuild.current = false\n\t\t// Carryover seed: band events inherit the outgoing partition's merged/unmerged state, so\n\t\t// nothing changes state because of the swap alone. Idempotent, so safe during render.\n\t\tlatestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible())\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t} else if (heldThreadIds.size === 0 && renderedModel === latestModel) {\n\t\t// Nothing pending and nothing to adopt: clear any leftover force-adopt intent so it can't\n\t\t// survive to force-adopt a later, unrelated rebuild.\n\t\tadoptOnRebuild.current = false\n\t}\n\t// Pop-out detection: a leaf folded inside a badge can't follow its anchor (the badge position\n\t// is baked into the model), so when its live position drifts from the baked one, hold it out.\n\t// It renders as a live pin riding the anchor; the detach loop below shrinks its badge locally.\n\tconst newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel)\n\tif (newlyMovedIds.length > 0) {\n\t\tconst next = new Set(heldThreadIds)\n\t\tfor (const id of newlyMovedIds) next.add(id)\n\t\tsetHeldThreadIds(next)\n\t}\n\t// Local partition maintenance \u2014 the only non-zoom visual change. Any displayed leaf that has left the\n\t// cluster input is detached from its badge in place; the corrected rebuild already sits in latestModel\n\t// awaiting the next zoom-out. When the two models match, the leaf sets are identical, so skip the scan.\n\tif (clusterModel !== latestModel) {\n\t\tconst latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\tconst removedLeafIds: string[] = []\n\t\tfor (const leaf of clusterModel.table.leaves) {\n\t\t\tif (!latestLeafIds.has(leaf.id)) {\n\t\t\t\tremovedLeafIds.push(leaf.id)\n\t\t\t}\n\t\t}\n\t\t// Batched: one patch rebuild and one version bump for the whole set.\n\t\tif (removedLeafIds.length > 0) clusterModel.runtime.detachLeaves(removedLeafIds)\n\t}\n\t// Moved pins rejoin clustering on the next zoom-out motion: clear the set (so the rebuild\n\t// includes them again) and adopt that rebuild immediately instead of deferring it. Zooming in\n\t// never folds pins into clusters \u2014 merging is a zoom-out-only move, matching the runtime.\n\tuseEffect(() => {\n\t\tif (heldThreadIds.size === 0) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('rejoin moved comment pins on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tadoptOnRebuild.current = true\n\t\t\tsetHeldThreadIds(EMPTY_SET)\n\t\t})\n\t}, [heldThreadIds, editor])\n\t// Adopt a pending rebuild only on zoom-out motion: folding deferred additions into clusters is\n\t// a merge, and merging only happens while zooming out. While zooming in, the stale table still\n\t// splits correctly on its own (split thresholds are direction-safe by the hysteresis invariant).\n\tuseEffect(() => {\n\t\tif (clusterModel === latestModel) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('adopt pending cluster model on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tlatestModel.runtime.seedFrom(zoom, clusterModel.runtime.getVisible())\n\t\t\tsetRenderedModel(latestModel)\n\t\t})\n\t}, [clusterModel, latestModel, editor])\n\t// Threads the displayed partition doesn't show anywhere (new comments, reopened threads, undone\n\t// deletions) render as plain pins until the next zoom-out folds them in. Judged against the\n\t// displayed partition, so a detached-then-restored leaf reappears.\n\tconst partitionVersion = clusterModel.runtime.version\n\tconst orphanThreads = useMemo(() => {\n\t\tif (clusterModel === latestModel) return []\n\t\tconst displayed = new Set<string>()\n\t\tfor (const node of clusterModel.runtime.getVisible().values()) {\n\t\t\tfor (const member of node.members) displayed.add(member)\n\t\t}\n\t\tconst latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\treturn threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id))\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, latestModel, threads, partitionVersion])\n\tconst heldThreads = useMemo(\n\t\t() => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),\n\t\t[threads, heldThreadIds, openId]\n\t)\n\t// Subscribe to the runtime's partition version, not the raw zoom, so this only re-renders on cluster\n\t// changes rather than every camera frame. The memo below re-reads the version inline because\n\t// render-time detaches bump it after the subscription's computed already evaluated.\n\tuseValue(\n\t\t'comment cluster version',\n\t\t() => {\n\t\t\tclusterModel.runtime.onCamera(editor.getZoomLevel())\n\t\t\treturn clusterModel.runtime.version\n\t\t},\n\t\t[clusterModel, editor]\n\t)\n\tconst visibleNodes = useMemo(() => {\n\t\treturn Array.from(clusterModel.runtime.getVisible().values())\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, partitionVersion])\n\tconst fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel)\n\n\treturn {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t}\n}\n\n/**\n * Leaves folded inside a badge whose live anchor no longer matches the position the rendered\n * model was built with. Visible (unclustered) leaf pins track their anchor live, so they can\n * stay deferred; a badge can't follow a member, so these must pop out of clustering.\n */\nfunction findMovedClusteredLeafIds(rendered: ClusterModel, latest: { table: ClusterTable }) {\n\tif (rendered.table === latest.table) return []\n\tconst visible = rendered.runtime.getVisible()\n\tconst latestById = new Map(latest.table.leaves.map((leaf) => [leaf.id, leaf]))\n\tconst moved: string[] = []\n\tfor (const leaf of rendered.table.leaves) {\n\t\tif (visible.has(leaf.id)) continue\n\t\tconst current = latestById.get(leaf.id)\n\t\tif (!current) continue\n\t\tif (\n\t\t\tMath.abs(current.centroid.x - leaf.centroid.x) > MOVED_LEAF_EPSILON ||\n\t\t\tMath.abs(current.centroid.y - leaf.centroid.y) > MOVED_LEAF_EPSILON\n\t\t) {\n\t\t\tmoved.push(leaf.id)\n\t\t}\n\t}\n\treturn moved\n}\n\nfunction getClusterZoomBounds(editor: Editor): ClusterZoomBounds {\n\tconst cameraOptions = editor.getCameraOptions()\n\tconst baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1\n\tconst zoomSteps = cameraOptions.zoomSteps\n\treturn {\n\t\tminZoom: zoomSteps[0] * baseZoom,\n\t\tmaxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom,\n\t}\n}\n\n/**\n * Bring a thread's pin into view: switch pages if needed, then zoom to the first cluster split\n * that unfolds it from its badge (or just centre on it when it isn't clustered).\n */\nexport function revealThreadPin(\n\teditor: Editor,\n\tthread: TLCommentThread,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\toptions: CommentingOptions,\n\tduration = 200\n) {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\n\t// With clustering off the pin always renders individually, so skip the zoom-to-split (its cluster\n\t// badge never exists) and just center on the pin.\n\tif (options.enableClustering) {\n\t\tconst parentEvent = findDirectParentEvent(table, thread.id)\n\t\tif (\n\t\t\tparentEvent &&\n\t\t\tNumber.isFinite(parentEvent.zSplit) &&\n\t\t\tparentEvent.zSplit <= zoomBounds.maxZoom\n\t\t) {\n\t\t\tconst zoom = clamp(\n\t\t\t\tparentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\t\t\tzoomBounds.minZoom,\n\t\t\t\tzoomBounds.maxZoom\n\t\t\t)\n\t\t\tcenterOnPointAtZoom(editor, point, zoom, duration)\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } })\n}\n\n/**\n * Zoom to just past the zoom at which a cluster first unclusters, centered on its centroid. The\n * event that created a visible cluster is the event that splits it, and has the smallest zSplit of\n * everything applied inside it \u2014 so its zSplit is exactly the first split within those comments.\n * The animated zoom drives the runtime cursor like any manual zoom. A no-op with no split event.\n */\nexport function zoomToClusterSplit(\n\teditor: Editor,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\tnode: ClusterNode\n) {\n\tconst event = table.events.find((e) => e.result.id === node.id)\n\tif (!event || !Number.isFinite(event.zSplit)) return\n\tconst zoom = clamp(\n\t\tevent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\tzoomBounds.minZoom,\n\t\tzoomBounds.maxZoom\n\t)\n\tcenterOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS)\n}\n\nfunction findDirectParentEvent(table: ClusterTable, threadId: string): MergeEvent | undefined {\n\treturn table.events.find((event) => event.children.some((child) => child.id === threadId))\n}\n\nfunction centerOnPointAtZoom(\n\teditor: Editor,\n\tpoint: { x: number; y: number },\n\tzoom: number,\n\tduration = 200\n) {\n\tconst viewport = editor.getViewportScreenBounds()\n\t// The open sidebar shifts the target left so the pin lands mid-uncovered-area, not under it.\n\tconst offset = commentCenterScreenOffset(editor)\n\teditor.setCamera(\n\t\t{\n\t\t\tx: (viewport.w / 2 - offset) / zoom - point.x,\n\t\t\ty: viewport.h / (2 * zoom) - point.y,\n\t\t\tz: zoom,\n\t\t},\n\t\t{ animation: { duration } }\n\t)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value))\n}\n"],
5
+ "mappings": "AAAA,SAAS,WAAW,SAAS,QAAQ,gBAAgB;AACrD,SAAiB,OAAwB,gBAAgB;AACzD,SAAS,2BAA2B;AACpC,SAA8B,4BAA4B;AAE1D,SAA+B,2BAA2B;AAC1D;AAAA,EAEC;AAAA,EACA;AAAA,EACA;AAAA,OACM;AAEP,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB,iCAAiC;AAGpD,MAAM,yBAAyB;AAGtC,MAAM,4BAA4B;AAElC,MAAM,YAAiC,oBAAI,IAAI;AAC/C,MAAM,qBAAqB;AAI3B,MAAM,yBAAyB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAIA,SAAS,sBAAsB,QAAyB;AACvD,SAAO,OAAO,QAAQ,GAAG,sBAAsB;AAChD;AAqCO,SAAS,gBACf,QACA,SACA,QACoB;AAIpB,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA8B,SAAS;AACjF,QAAM,iBAAiB,OAAO,KAAK;AAUnC,QAAM,kBAAkB,OAAqB,EAAE,QAAQ,CAAC,GAAG,eAAe,OAAU,CAAC;AACrF,QAAM,eAAe;AAAA,IACpB;AAAA,IACA,MAAM;AACL,YAAM,OAAO;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,CAAC,WAAW,CAAC,cAAc,IAAI,OAAO,EAAE,CAAC;AAAA,QACxD,aAAa,IAAI,MAAM;AAAA,MACxB;AACA,YAAM,OAAO,gBAAgB;AAC7B,UAAI,sBAAsB,MAAM,KAAK,qBAAqB,MAAM,IAAI,EAAG,QAAO;AAC9E,UAAI,kBAAkB,MAAM,IAAI,EAAG,QAAO;AAC1C,sBAAgB,UAAU;AAC1B,aAAO;AAAA,IACR;AAAA,IACA,CAAC,QAAQ,SAAS,aAAa;AAAA,EAChC;AACA,QAAM,oBAAoB;AAAA,IACzB;AAAA,IACA,MAAM,qBAAqB,MAAM;AAAA,IACjC,CAAC,MAAM;AAAA,EACR;AACA,QAAM,cAAc,QAAQ,MAAM;AACjC,UAAM,QAAQ;AAAA,MACb,aAAa;AAAA,MACb;AAAA,MACA,aAAa;AAAA,IACd;AACA,UAAM,UAAU,qBAAqB,KAAK;AAC1C,YAAQ,KAAK,OAAO,aAAa,CAAC;AAClC,WAAO,EAAE,SAAS,MAAM;AAAA,EACzB,GAAG,CAAC,cAAc,mBAAmB,MAAM,CAAC;AAC5C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,WAAW;AAC9D,MAAI,eAAe;AAEnB,QAAM,SAAS,SAAS,wBAAwB,MAAM,OAAO,iBAAiB,GAAG,CAAC,MAAM,CAAC;AACzF,QAAM,UAAU,OAAO,MAAM;AAC7B,MAAI,QAAQ,YAAY,QAAQ;AAC/B,YAAQ,UAAU;AAClB,mBAAe,UAAU;AACzB,gBAAY,QAAQ,KAAK,OAAO,aAAa,CAAC;AAC9C,QAAI,cAAc,OAAO,EAAG,kBAAiB,SAAS;AACtD,qBAAiB,WAAW;AAC5B,mBAAe;AAAA,EAChB;AAGA,QAAM,gBAAgB,cAAc,SAAS,KAAK,eAAe;AACjE,MAAI,kBAAkB,eAAe,eAAe;AACnD,mBAAe,UAAU;AAGzB,gBAAY,QAAQ,SAAS,OAAO,aAAa,GAAG,cAAc,QAAQ,WAAW,CAAC;AACtF,qBAAiB,WAAW;AAC5B,mBAAe;AAAA,EAChB,WAAW,cAAc,SAAS,KAAK,kBAAkB,aAAa;AAGrE,mBAAe,UAAU;AAAA,EAC1B;AAIA,QAAM,gBAAgB,0BAA0B,cAAc,WAAW;AACzE,MAAI,cAAc,SAAS,GAAG;AAC7B,UAAM,OAAO,IAAI,IAAI,aAAa;AAClC,eAAW,MAAM,cAAe,MAAK,IAAI,EAAE;AAC3C,qBAAiB,IAAI;AAAA,EACtB;AAIA,MAAI,iBAAiB,aAAa;AACjC,UAAM,gBAAgB,IAAI,IAAI,YAAY,MAAM,OAAO,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAC7E,UAAM,iBAA2B,CAAC;AAClC,eAAW,QAAQ,aAAa,MAAM,QAAQ;AAC7C,UAAI,CAAC,cAAc,IAAI,KAAK,EAAE,GAAG;AAChC,uBAAe,KAAK,KAAK,EAAE;AAAA,MAC5B;AAAA,IACD;AAEA,QAAI,eAAe,SAAS,EAAG,cAAa,QAAQ,aAAa,cAAc;AAAA,EAChF;AAIA,YAAU,MAAM;AACf,QAAI,cAAc,SAAS,EAAG;AAC9B,QAAI,WAAW,OAAO,aAAa;AACnC,WAAO,MAAM,yCAAyC,MAAM;AAC3D,YAAM,OAAO,OAAO,aAAa;AACjC,YAAM,WAAW;AACjB,iBAAW;AACX,UAAI,QAAQ,SAAU;AACtB,qBAAe,UAAU;AACzB,uBAAiB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACF,GAAG,CAAC,eAAe,MAAM,CAAC;AAI1B,YAAU,MAAM;AACf,QAAI,iBAAiB,YAAa;AAClC,QAAI,WAAW,OAAO,aAAa;AACnC,WAAO,MAAM,2CAA2C,MAAM;AAC7D,YAAM,OAAO,OAAO,aAAa;AACjC,YAAM,WAAW;AACjB,iBAAW;AACX,UAAI,QAAQ,SAAU;AACtB,kBAAY,QAAQ,SAAS,MAAM,aAAa,QAAQ,WAAW,CAAC;AACpE,uBAAiB,WAAW;AAAA,IAC7B,CAAC;AAAA,EACF,GAAG,CAAC,cAAc,aAAa,MAAM,CAAC;AAItC,QAAM,mBAAmB,aAAa,QAAQ;AAC9C,QAAM,gBAAgB,QAAQ,MAAM;AACnC,QAAI,iBAAiB,YAAa,QAAO,CAAC;AAC1C,UAAM,YAAY,oBAAI,IAAY;AAClC,eAAW,QAAQ,aAAa,QAAQ,WAAW,EAAE,OAAO,GAAG;AAC9D,iBAAW,UAAU,KAAK,QAAS,WAAU,IAAI,MAAM;AAAA,IACxD;AACA,UAAM,YAAY,IAAI,IAAI,YAAY,MAAM,OAAO,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AACzE,WAAO,QAAQ,OAAO,CAAC,WAAW,UAAU,IAAI,OAAO,EAAE,KAAK,CAAC,UAAU,IAAI,OAAO,EAAE,CAAC;AAAA,EAGxF,GAAG,CAAC,cAAc,aAAa,SAAS,gBAAgB,CAAC;AACzD,QAAM,cAAc;AAAA,IACnB,MAAM,QAAQ,OAAO,CAAC,WAAW,cAAc,IAAI,OAAO,EAAE,KAAK,OAAO,OAAO,MAAM;AAAA,IACrF,CAAC,SAAS,eAAe,MAAM;AAAA,EAChC;AAIA;AAAA,IACC;AAAA,IACA,MAAM;AACL,mBAAa,QAAQ,SAAS,OAAO,aAAa,CAAC;AACnD,aAAO,aAAa,QAAQ;AAAA,IAC7B;AAAA,IACA,CAAC,cAAc,MAAM;AAAA,EACtB;AACA,QAAM,eAAe,QAAQ,MAAM;AAClC,WAAO,MAAM,KAAK,aAAa,QAAQ,WAAW,EAAE,OAAO,CAAC;AAAA,EAG7D,GAAG,CAAC,cAAc,gBAAgB,CAAC;AACnC,QAAM,YAAY,oBAAoB,cAAc,YAAY;AAEhE,SAAO;AAAA,IACN,OAAO;AAAA,IACP,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;AAOA,SAAS,0BAA0B,UAAwB,QAAiC;AAC3F,MAAI,SAAS,UAAU,OAAO,MAAO,QAAO,CAAC;AAC7C,QAAM,UAAU,SAAS,QAAQ,WAAW;AAC5C,QAAM,aAAa,IAAI,IAAI,OAAO,MAAM,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAC7E,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,SAAS,MAAM,QAAQ;AACzC,QAAI,QAAQ,IAAI,KAAK,EAAE,EAAG;AAC1B,UAAM,UAAU,WAAW,IAAI,KAAK,EAAE;AACtC,QAAI,CAAC,QAAS;AACd,QACC,KAAK,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,CAAC,IAAI,sBACjD,KAAK,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,CAAC,IAAI,oBAChD;AACD,YAAM,KAAK,KAAK,EAAE;AAAA,IACnB;AAAA,EACD;AACA,SAAO;AACR;AAEA,SAAS,qBAAqB,QAAmC;AAChE,QAAM,gBAAgB,OAAO,iBAAiB;AAC9C,QAAM,WAAW,cAAc,cAAc,OAAO,YAAY,IAAI;AACpE,QAAM,YAAY,cAAc;AAChC,SAAO;AAAA,IACN,SAAS,UAAU,CAAC,IAAI;AAAA,IACxB,SAAS,UAAU,UAAU,SAAS,CAAC,IAAI;AAAA,EAC5C;AACD;AAMO,SAAS,gBACf,QACA,QACA,OACA,YACA,SACA,WAAW,KACV;AACD,MAAI,OAAO,WAAW,OAAO,iBAAiB,GAAG;AAChD,WAAO,eAAe,OAAO,MAAa;AAAA,EAC3C;AAEA,QAAM,QAAQ,gBAAgB,QAAQ,OAAO,MAAM;AACnD,MAAI,CAAC,MAAO;AAIZ,MAAI,QAAQ,kBAAkB;AAC7B,UAAM,cAAc,sBAAsB,OAAO,OAAO,EAAE;AAC1D,QACC,eACA,OAAO,SAAS,YAAY,MAAM,KAClC,YAAY,UAAU,WAAW,SAChC;AACD,YAAM,OAAO;AAAA,QACZ,YAAY,SAAS;AAAA,QACrB,WAAW;AAAA,QACX,WAAW;AAAA,MACZ;AACA,0BAAoB,QAAQ,OAAO,MAAM,QAAQ;AACjD;AAAA,IACD;AAAA,EACD;AAEA,QAAM,SAAS,0BAA0B,MAAM,IAAI,OAAO,aAAa;AACvE,SAAO,cAAc,EAAE,GAAG,MAAM,IAAI,QAAQ,GAAG,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AACtF;AAQO,SAAS,mBACf,QACA,OACA,YACA,MACC;AACD,QAAM,QAAQ,MAAM,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO,KAAK,EAAE;AAC9D,MAAI,CAAC,SAAS,CAAC,OAAO,SAAS,MAAM,MAAM,EAAG;AAC9C,QAAM,OAAO;AAAA,IACZ,MAAM,SAAS;AAAA,IACf,WAAW;AAAA,IACX,WAAW;AAAA,EACZ;AACA,sBAAoB,QAAQ,KAAK,UAAU,MAAM,sBAAsB;AACxE;AAEA,SAAS,sBAAsB,OAAqB,UAA0C;AAC7F,SAAO,MAAM,OAAO,KAAK,CAAC,UAAU,MAAM,SAAS,KAAK,CAAC,UAAU,MAAM,OAAO,QAAQ,CAAC;AAC1F;AAEA,SAAS,oBACR,QACA,OACA,MACA,WAAW,KACV;AACD,QAAM,WAAW,OAAO,wBAAwB;AAEhD,QAAM,SAAS,0BAA0B,MAAM;AAC/C,SAAO;AAAA,IACN;AAAA,MACC,IAAI,SAAS,IAAI,IAAI,UAAU,OAAO,MAAM;AAAA,MAC5C,GAAG,SAAS,KAAK,IAAI,QAAQ,MAAM;AAAA,MACnC,GAAG;AAAA,IACJ;AAAA,IACA,EAAE,WAAW,EAAE,SAAS,EAAE;AAAA,EAC3B;AACD;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC/D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;",
6
+ "names": []
7
+ }
@@ -3,7 +3,7 @@ import { useMemo } from "react";
3
3
  import { renderCommentHtml } from "./comment-render.mjs";
4
4
  function CommentBody({ richText, resolveName }) {
5
5
  const html = useMemo(() => renderCommentHtml(richText, resolveName), [richText, resolveName]);
6
- return /* @__PURE__ */ jsx("div", { className: "cmt-text", dangerouslySetInnerHTML: { __html: html } });
6
+ return /* @__PURE__ */ jsx("div", { className: "tlui-cmt-text", dangerouslySetInnerHTML: { __html: html } });
7
7
  }
8
8
  export {
9
9
  CommentBody
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comment-body.tsx"],
4
- "sourcesContent": ["import { useMemo } from 'react'\nimport { TLRichText } from 'tldraw'\nimport { renderCommentHtml } from './comment-render'\n\n/** @public */\nexport interface CommentBodyProps {\n\trichText: TLRichText\n\t/** Maps a member id to its current display name, so \\@mentions show the live name. */\n\tresolveName?(id: string): string | undefined\n}\n\n/**\n * Renders a comment's rich-text body read-only through the limited comment extension set (no\n * headings), so formatting (bold, links, lists, highlight) is preserved rather than flattened, and\n * headings can never render. Use this as the `body` of a `CommentCard` on a canvas.\n * @public @react\n */\nexport function CommentBody({ richText, resolveName }: CommentBodyProps) {\n\tconst html = useMemo(() => renderCommentHtml(richText, resolveName), [richText, resolveName])\n\treturn <div className=\"cmt-text\" dangerouslySetInnerHTML={{ __html: html }} />\n}\n"],
5
- "mappings": "AAmBQ;AAnBR,SAAS,eAAe;AAExB,SAAS,yBAAyB;AAe3B,SAAS,YAAY,EAAE,UAAU,YAAY,GAAqB;AACxE,QAAM,OAAO,QAAQ,MAAM,kBAAkB,UAAU,WAAW,GAAG,CAAC,UAAU,WAAW,CAAC;AAC5F,SAAO,oBAAC,SAAI,WAAU,YAAW,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAC7E;",
4
+ "sourcesContent": ["import { useMemo } from 'react'\nimport { TLRichText } from 'tldraw'\nimport { renderCommentHtml } from './comment-render'\n\n/** @public */\nexport interface CommentBodyProps {\n\trichText: TLRichText\n\t/** Maps a member id to its current display name, so \\@mentions show the live name. */\n\tresolveName?(id: string): string | undefined\n}\n\n/**\n * Renders a comment's rich-text body read-only through the limited comment extension set (no\n * headings), so formatting (bold, links, lists, highlight) is preserved rather than flattened, and\n * headings can never render. Use this as the `body` of a `CommentCard` on a canvas.\n * @public @react\n */\nexport function CommentBody({ richText, resolveName }: CommentBodyProps) {\n\tconst html = useMemo(() => renderCommentHtml(richText, resolveName), [richText, resolveName])\n\treturn <div className=\"tlui-cmt-text\" dangerouslySetInnerHTML={{ __html: html }} />\n}\n"],
5
+ "mappings": "AAmBQ;AAnBR,SAAS,eAAe;AAExB,SAAS,yBAAyB;AAe3B,SAAS,YAAY,EAAE,UAAU,YAAY,GAAqB;AACxE,QAAM,OAAO,QAAQ,MAAM,kBAAkB,UAAU,WAAW,GAAG,CAAC,UAAU,WAAW,CAAC;AAC5F,SAAO,oBAAC,SAAI,WAAU,iBAAgB,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAClF;",
6
6
  "names": []
7
7
  }