@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,461 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ createComment,
5
+ EditorPortal,
6
+ TldrawUiButton,
7
+ TldrawUiDropdownMenuContent,
8
+ TldrawUiDropdownMenuGroup,
9
+ TldrawUiDropdownMenuItem,
10
+ TldrawUiDropdownMenuRoot,
11
+ TldrawUiDropdownMenuTrigger,
12
+ TldrawUiIcon,
13
+ useContainer,
14
+ usePassThroughWheelEvents,
15
+ useTranslation,
16
+ useValue
17
+ } from "tldraw";
18
+ import { CommentCard } from "../ui/comment-card.mjs";
19
+ import { CommentComposer } from "../ui/comment-composer.mjs";
20
+ import { EMPTY_COMMENT, isCommentEmpty } from "../ui/comment-extensions.mjs";
21
+ import { CommentThread } from "../ui/comment-thread.mjs";
22
+ import { CommentBody } from "./comment-body.mjs";
23
+ import {
24
+ clearCommentDraft,
25
+ getCommentDraft,
26
+ replyDraftSlot,
27
+ saveCommentDraft
28
+ } from "./comment-drafts.mjs";
29
+ import {
30
+ commitCommentMutation,
31
+ deleteComment,
32
+ deleteThread,
33
+ editComment,
34
+ reopenThread,
35
+ resolveThread
36
+ } from "./comment-mutations.mjs";
37
+ import { CommentReactionPicker, CommentReactions } from "./comment-reactions.mjs";
38
+ import { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from "./comment-render.mjs";
39
+ import { useThreadComments } from "./hooks.mjs";
40
+ import { useIsMobileCommenting, useMobilePlacement } from "./mobile-placement.mjs";
41
+ import {
42
+ getCanModifyComment,
43
+ useCanComment,
44
+ useCanModifyComment,
45
+ useCommentingOptions
46
+ } from "./options.mjs";
47
+ import { openThreadId } from "./state.mjs";
48
+ const stop = (e) => e.stopPropagation();
49
+ function useResolveName(resolveAuthor) {
50
+ return useCallback((id) => resolveAuthor(id)?.name, [resolveAuthor]);
51
+ }
52
+ const LINK_COPIED_MS = 2e3;
53
+ function absoluteThreadLink(href, base = window.location.href) {
54
+ try {
55
+ return new URL(href, base).toString();
56
+ } catch {
57
+ return href;
58
+ }
59
+ }
60
+ function useCopyLink(href) {
61
+ const [copied, setCopied] = useState(false);
62
+ useEffect(() => {
63
+ if (!copied) return;
64
+ const timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS);
65
+ return () => window.clearTimeout(timeout);
66
+ }, [copied]);
67
+ const copy = useCallback(() => {
68
+ if (href === void 0) return;
69
+ navigator.clipboard?.writeText(absoluteThreadLink(href)).then(
70
+ () => setCopied(true),
71
+ () => setCopied(false)
72
+ );
73
+ }, [href]);
74
+ return [copied, copy];
75
+ }
76
+ function toCardProps(comment, props, components, resolveName) {
77
+ const Body = components.CommentBody;
78
+ const body = Body ? /* @__PURE__ */ jsx(Body, { comment }) : /* @__PURE__ */ jsx(CommentBody, { richText: comment.body, resolveName });
79
+ return {
80
+ author: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,
81
+ body,
82
+ date: new Date(comment.createdAt).toISOString(),
83
+ you: comment.authorId === props.currentUserId,
84
+ edited: comment.editedAt != null
85
+ };
86
+ }
87
+ const PIN_SIZE = 28;
88
+ const MARKER_SIZE = PIN_SIZE;
89
+ const CARD_TOP_Y = -28;
90
+ const PREVIEW_GAP = 6;
91
+ const POPOVER_OFFSET = {
92
+ thread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },
93
+ list: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 }
94
+ };
95
+ function ThreadPopover({
96
+ base,
97
+ children
98
+ }) {
99
+ const ref = useRef(null);
100
+ usePassThroughWheelEvents(ref);
101
+ const isMobile = useIsMobileCommenting();
102
+ const placed = useMobilePlacement(ref, base, isMobile);
103
+ return /* @__PURE__ */ jsx(EditorPortal, { children: /* @__PURE__ */ jsx(
104
+ "div",
105
+ {
106
+ ref,
107
+ className: "tlui-cmt-canvas-popover",
108
+ style: { left: placed.left, top: placed.top },
109
+ onPointerDown: stop,
110
+ onContextMenu: stop,
111
+ children
112
+ }
113
+ ) });
114
+ }
115
+ const ThreadView = memo(function ThreadView2({
116
+ editor,
117
+ thread,
118
+ ...props
119
+ }) {
120
+ const {
121
+ currentUserId,
122
+ resolveAuthor,
123
+ onPostComment,
124
+ isCommentUnread,
125
+ onCommentsRead,
126
+ getMentionSuggestions,
127
+ renderMentionSuggestion,
128
+ getThreadHref
129
+ } = props;
130
+ const options = useCommentingOptions();
131
+ const comments = useThreadComments(editor, thread.id);
132
+ const msg = useTranslation();
133
+ const resolveName = useResolveName(resolveAuthor);
134
+ const cards = useMemo(
135
+ () => comments.map(
136
+ (c) => toCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)
137
+ ),
138
+ [comments, currentUserId, resolveAuthor, options.components, resolveName]
139
+ );
140
+ const me = currentUserId ? resolveAuthor(currentUserId) : void 0;
141
+ const canComment = useCanComment(currentUserId);
142
+ const commentPermissions = useValue(
143
+ "comment permissions",
144
+ () => new Map(
145
+ comments.map((comment) => [
146
+ comment.id,
147
+ {
148
+ edit: getCanModifyComment(editor, currentUserId, { action: "edit-comment", comment }),
149
+ delete: getCanModifyComment(editor, currentUserId, {
150
+ action: "delete-comment",
151
+ comment
152
+ })
153
+ }
154
+ ])
155
+ ),
156
+ [editor, currentUserId, comments]
157
+ );
158
+ const canModifyThread = useCanModifyComment(currentUserId, { action: "delete-thread", thread });
159
+ const canDeleteThread = canComment && canModifyThread;
160
+ const ComposerFallback = options.components.ComposerFallback;
161
+ const [reply, setReply] = useState(
162
+ () => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT
163
+ );
164
+ const [editingId, setEditingId] = useState(null);
165
+ const [editText, setEditText] = useState(EMPTY_COMMENT);
166
+ const canReply = canComment && !thread.resolved;
167
+ const container = useContainer();
168
+ const editReturnFocus = useRef(null);
169
+ const [focusReply, setFocusReply] = useState(false);
170
+ const tabTaken = useRef(false);
171
+ const swallowTabUp = useRef(false);
172
+ useEffect(() => {
173
+ if (!canReply) {
174
+ setFocusReply(false);
175
+ tabTaken.current = false;
176
+ return;
177
+ }
178
+ const doc = container.ownerDocument;
179
+ const onKeyDown = (e) => {
180
+ if (e.key !== "Tab" || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return;
181
+ if (e.defaultPrevented || tabTaken.current) return;
182
+ if (e.target !== container && e.target !== doc.body) return;
183
+ tabTaken.current = true;
184
+ swallowTabUp.current = true;
185
+ setFocusReply(true);
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ };
189
+ const onKeyUp = (e) => {
190
+ if (e.key !== "Tab" || !swallowTabUp.current) return;
191
+ swallowTabUp.current = false;
192
+ e.preventDefault();
193
+ e.stopPropagation();
194
+ };
195
+ doc.addEventListener("keydown", onKeyDown, true);
196
+ doc.addEventListener("keyup", onKeyUp, true);
197
+ return () => {
198
+ doc.removeEventListener("keydown", onKeyDown, true);
199
+ doc.removeEventListener("keyup", onKeyUp, true);
200
+ };
201
+ }, [canReply, container]);
202
+ useEffect(() => {
203
+ if (editingId !== null) return;
204
+ const resolve = editReturnFocus.current;
205
+ editReturnFocus.current = null;
206
+ resolve?.()?.focus();
207
+ }, [editingId]);
208
+ useEffect(() => {
209
+ if (!isCommentUnread || !onCommentsRead) return;
210
+ const unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id);
211
+ if (unreadIds.length > 0) {
212
+ onCommentsRead(unreadIds);
213
+ }
214
+ }, [comments, isCommentUnread, onCommentsRead]);
215
+ const postReply = () => {
216
+ if (isCommentEmpty(reply) || !currentUserId) return;
217
+ const comment = commitCommentMutation(editor, ({ put }) => {
218
+ const comment2 = createComment({
219
+ threadId: thread.id,
220
+ pageId: thread.pageId,
221
+ authorId: currentUserId,
222
+ body: reply
223
+ });
224
+ put([comment2]);
225
+ return comment2;
226
+ });
227
+ setReply(EMPTY_COMMENT);
228
+ clearCommentDraft(replyDraftSlot(thread.id));
229
+ onPostComment?.(comment);
230
+ };
231
+ const toggleResolve = () => {
232
+ if (!currentUserId) return;
233
+ if (thread.resolved) reopenThread(editor, thread);
234
+ else resolveThread(editor, thread, currentUserId);
235
+ };
236
+ const removeThread = () => {
237
+ deleteThread(editor, thread);
238
+ };
239
+ const ThreadActions = options.components.ThreadActions;
240
+ const threadHref = getThreadHref?.(thread.id);
241
+ const [linkCopied, copyThreadLink] = useCopyLink(threadHref);
242
+ const startEdit = (comment, { fromMoreMenu = false } = {}) => {
243
+ const active = container.ownerDocument.activeElement;
244
+ editReturnFocus.current = fromMoreMenu ? () => container.querySelector(`[data-cmt-more-for="${comment.id}"]`) : active instanceof HTMLElement && active !== container && active !== document.body ? () => active.isConnected ? active : null : null;
245
+ setEditingId(comment.id);
246
+ setEditText(comment.body);
247
+ };
248
+ const saveEdit = () => {
249
+ const comment = comments.find((c) => c.id === editingId);
250
+ if (!comment || isCommentEmpty(editText)) return;
251
+ editComment(editor, comment, editText);
252
+ setEditingId(null);
253
+ };
254
+ const renderComment = (card, index) => {
255
+ const comment = comments[index];
256
+ const permissions = commentPermissions.get(comment.id);
257
+ if (editingId === comment.id) {
258
+ return /* @__PURE__ */ jsx(
259
+ "div",
260
+ {
261
+ className: "tlui-cmt-editing",
262
+ onKeyDown: (e) => {
263
+ if (e.key === "Escape") {
264
+ setEditingId(null);
265
+ e.stopPropagation();
266
+ }
267
+ },
268
+ children: /* @__PURE__ */ jsx(
269
+ CommentComposer,
270
+ {
271
+ author: card.author,
272
+ placeholder: msg("comments.edit-placeholder"),
273
+ value: editText,
274
+ onChange: setEditText,
275
+ onSubmit: saveEdit,
276
+ sendLabel: msg("comments.save"),
277
+ disabled: isCommentEmpty(editText),
278
+ getMentionSuggestions,
279
+ renderMentionSuggestion,
280
+ autoFocus: true
281
+ }
282
+ )
283
+ }
284
+ );
285
+ }
286
+ return /* @__PURE__ */ jsx(
287
+ CommentCard,
288
+ {
289
+ ...card,
290
+ footer: /* @__PURE__ */ jsx(
291
+ CommentReactions,
292
+ {
293
+ comment,
294
+ currentUserId: canComment ? currentUserId : null,
295
+ resolveName
296
+ }
297
+ ),
298
+ actions: canComment && /* @__PURE__ */ jsxs(Fragment, { children: [
299
+ (permissions?.edit || permissions?.delete) && /* @__PURE__ */ jsxs(TldrawUiDropdownMenuRoot, { id: `comment-actions-${comment.id}`, children: [
300
+ /* @__PURE__ */ jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsx(
301
+ TldrawUiButton,
302
+ {
303
+ type: "icon",
304
+ tooltip: msg("comments.more-options"),
305
+ title: msg("comments.more-options"),
306
+ className: "tlui-cmt-thread__action",
307
+ "data-cmt-more-for": comment.id,
308
+ children: /* @__PURE__ */ jsx(
309
+ TldrawUiIcon,
310
+ {
311
+ icon: "dots-vertical",
312
+ label: msg("comments.more-options"),
313
+ small: true
314
+ }
315
+ )
316
+ }
317
+ ) }),
318
+ /* @__PURE__ */ jsx(
319
+ TldrawUiDropdownMenuContent,
320
+ {
321
+ className: "tlui-cmt-menu",
322
+ side: "bottom",
323
+ align: "start",
324
+ alignOffset: 0,
325
+ sideOffset: 4,
326
+ children: /* @__PURE__ */ jsxs(TldrawUiDropdownMenuGroup, { children: [
327
+ permissions?.edit && /* @__PURE__ */ jsx(TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ jsx(
328
+ "button",
329
+ {
330
+ type: "button",
331
+ className: "tlui-cmt-menu-item",
332
+ onClick: () => startEdit(comment, { fromMoreMenu: true }),
333
+ children: /* @__PURE__ */ jsx("span", { children: msg("comments.edit") })
334
+ }
335
+ ) }),
336
+ permissions?.delete && /* @__PURE__ */ jsx(TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ jsx(
337
+ "button",
338
+ {
339
+ type: "button",
340
+ className: "tlui-cmt-menu-item tlui-cmt-menu-item--danger",
341
+ onClick: () => deleteComment(editor, comment),
342
+ children: /* @__PURE__ */ jsx("span", { children: msg("action.delete") })
343
+ }
344
+ ) })
345
+ ] })
346
+ }
347
+ )
348
+ ] }),
349
+ /* @__PURE__ */ jsx(CommentReactionPicker, { comment, currentUserId })
350
+ ] })
351
+ }
352
+ );
353
+ };
354
+ const resolveLabel = msg(thread.resolved ? "comments.reopen" : "comments.resolve");
355
+ const headerActions = /* @__PURE__ */ jsxs(Fragment, { children: [
356
+ ThreadActions && /* @__PURE__ */ jsx(ThreadActions, { thread, comments }),
357
+ (threadHref !== void 0 || canDeleteThread) && /* @__PURE__ */ jsxs(TldrawUiDropdownMenuRoot, { id: `comment-thread-actions-${thread.id}`, children: [
358
+ /* @__PURE__ */ jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsx(
359
+ TldrawUiButton,
360
+ {
361
+ type: "icon",
362
+ tooltip: msg("comments.more-options"),
363
+ title: msg("comments.more-options"),
364
+ className: "tlui-cmt-thread__action",
365
+ children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "dots-vertical", label: msg("comments.more-options"), small: true })
366
+ }
367
+ ) }),
368
+ /* @__PURE__ */ jsx(
369
+ TldrawUiDropdownMenuContent,
370
+ {
371
+ className: "tlui-cmt-menu",
372
+ side: "bottom",
373
+ align: "start",
374
+ alignOffset: 0,
375
+ sideOffset: 4,
376
+ children: /* @__PURE__ */ jsxs(TldrawUiDropdownMenuGroup, { children: [
377
+ threadHref !== void 0 && /* @__PURE__ */ jsx(TldrawUiDropdownMenuItem, { noClose: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: "tlui-cmt-menu-item", onClick: copyThreadLink, children: /* @__PURE__ */ jsx("span", { children: msg(linkCopied ? "comments.link-copied" : "comments.copy-link") }) }) }),
378
+ canDeleteThread && /* @__PURE__ */ jsx(TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ jsx(
379
+ "button",
380
+ {
381
+ type: "button",
382
+ className: "tlui-cmt-menu-item tlui-cmt-menu-item--danger",
383
+ onClick: removeThread,
384
+ children: /* @__PURE__ */ jsx("span", { children: msg("comments.delete") })
385
+ }
386
+ ) })
387
+ ] })
388
+ }
389
+ )
390
+ ] }),
391
+ canComment && currentUserId && /* @__PURE__ */ jsx(
392
+ TldrawUiButton,
393
+ {
394
+ type: "icon",
395
+ tooltip: resolveLabel,
396
+ title: resolveLabel,
397
+ className: "tlui-cmt-thread__action",
398
+ onClick: toggleResolve,
399
+ children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "check", label: resolveLabel, small: true })
400
+ }
401
+ ),
402
+ /* @__PURE__ */ jsx(
403
+ TldrawUiButton,
404
+ {
405
+ type: "icon",
406
+ tooltip: msg("comments.dismiss"),
407
+ title: msg("comments.dismiss"),
408
+ className: "tlui-cmt-thread__action",
409
+ onClick: () => openThreadId.set(editor, null),
410
+ children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "cross-2", label: msg("comments.dismiss"), small: true })
411
+ }
412
+ )
413
+ ] });
414
+ return /* @__PURE__ */ jsx(
415
+ CommentThread,
416
+ {
417
+ header: msg("comments.thread-title"),
418
+ headerActions,
419
+ renderComment,
420
+ comments: cards,
421
+ resolvedBanner: thread.resolved ? msg("comments.resolved-by").replace(
422
+ "{name}",
423
+ resolveAuthor(thread.resolved.by)?.name ?? UNKNOWN_AUTHOR
424
+ ) : void 0,
425
+ composer: canReply ? {
426
+ author: me ?? UNKNOWN_COMMENT_AUTHOR,
427
+ placeholder: msg("comments.reply-placeholder"),
428
+ sendLabel: msg("comments.send"),
429
+ value: reply,
430
+ onChange: (value) => {
431
+ setReply(value);
432
+ saveCommentDraft(replyDraftSlot(thread.id), value);
433
+ },
434
+ onSubmit: postReply,
435
+ // Up in the empty reply box edits the comment directly above it, chat-style —
436
+ // only when you're allowed to edit it (the same gate as the Edit link).
437
+ onArrowUpWhenEmpty: () => {
438
+ const last = comments[comments.length - 1];
439
+ if (last && commentPermissions.get(last.id)?.edit) startEdit(last);
440
+ },
441
+ // No user, no author for the record — dead send button.
442
+ disabled: isCommentEmpty(reply) || !currentUserId,
443
+ getMentionSuggestions,
444
+ renderMentionSuggestion,
445
+ // Reuses the composer's own focus path, so the caret lands at the end of a
446
+ // restored draft rather than in front of it.
447
+ autoFocus: focusReply
448
+ } : void 0,
449
+ footer: !canComment && !thread.resolved && ComposerFallback ? /* @__PURE__ */ jsx(ComposerFallback, { context: "thread" }) : void 0
450
+ }
451
+ );
452
+ });
453
+ export {
454
+ POPOVER_OFFSET,
455
+ ThreadPopover,
456
+ ThreadView,
457
+ absoluteThreadLink,
458
+ toCardProps,
459
+ useResolveName
460
+ };
461
+ //# sourceMappingURL=thread-view.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/thread-view.tsx"],
4
+ "sourcesContent": ["import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tEditor,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tTLRichText,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuGroup,\n\tTldrawUiDropdownMenuItem,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiIcon,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard, CommentCardProps } from '../ui/comment-card'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentThread } from '../ui/comment-thread'\nimport { CommentBody } from './comment-body'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\treplyDraftSlot,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport {\n\tcommitCommentMutation,\n\tdeleteComment,\n\tdeleteThread,\n\teditComment,\n\treopenThread,\n\tresolveThread,\n} from './comment-mutations'\nimport { CommentReactionPicker, CommentReactions } from './comment-reactions'\nimport { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport {\n\ttype CommentingComponents,\n\tgetCanModifyComment,\n\tuseCanComment,\n\tuseCanModifyComment,\n\tuseCommentingOptions,\n} from './options'\nimport { openThreadId } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * A name-only view of an author resolver, for the mention/rich-text paths. Stable identity, so\n * `CommentBody`'s memoized render doesn't recompute on every render of its host.\n */\nexport function useResolveName(resolveAuthor: CommentingContext['resolveAuthor']) {\n\treturn useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n}\n\n/** How long the copy-link item reads \"Link copied\" before reverting. */\nconst LINK_COPIED_MS = 2000\n\n/**\n * What a thread's `getThreadHref` should put on the clipboard. Hosts may hand back a relative href,\n * which is meaningless once pasted elsewhere, so it's resolved against the current document first.\n * An href the URL parser can't make sense of is copied as-is rather than dropped.\n *\n * @internal\n */\nexport function absoluteThreadLink(href: string, base = window.location.href): string {\n\ttry {\n\t\treturn new URL(href, base).toString()\n\t} catch {\n\t\treturn href\n\t}\n}\n\n/**\n * Copy a thread's link, with a flag that stays set briefly afterwards so the control can confirm.\n * Only set once the write resolves, so a withheld clipboard doesn't claim a copy that didn't happen.\n */\nfunction useCopyLink(href: string | undefined) {\n\tconst [copied, setCopied] = useState(false)\n\tuseEffect(() => {\n\t\tif (!copied) return\n\t\tconst timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [copied])\n\tconst copy = useCallback(() => {\n\t\tif (href === undefined) return\n\t\tnavigator.clipboard?.writeText(absoluteThreadLink(href)).then(\n\t\t\t() => setCopied(true),\n\t\t\t() => setCopied(false)\n\t\t)\n\t}, [href])\n\treturn [copied, copy] as const\n}\n\nexport function toCardProps(\n\tcomment: TLComment,\n\tprops: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'>,\n\tcomponents: CommentingComponents,\n\tresolveName: (id: string) => string | undefined\n): CommentCardProps {\n\tconst Body = components.CommentBody\n\t// The `CommentBody` component slot overrides the built-in rich-text default (which resolves\n\t// mention ids to names).\n\tconst body = Body ? (\n\t\t<Body comment={comment} />\n\t) : (\n\t\t<CommentBody richText={comment.body} resolveName={resolveName} />\n\t)\n\treturn {\n\t\tauthor: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\tbody,\n\t\tdate: new Date(comment.createdAt).toISOString(),\n\t\tyou: comment.authorId === props.currentUserId,\n\t\tedited: comment.editedAt != null,\n\t}\n}\n\n/** A pin is this square (mirrors `--tlui-cmt-pin-size`). The two marker kinds differ in size *and*\n * anchor point, so lining their previews up means correcting for both. Keep in sync with the CSS. */\nconst PIN_SIZE = 28\n\n/** A cluster/stack badge is this square (`--tlui-cmt-marker-size`) \u2014 pin-sized, so the marker\n * kinds read as one family. Keep in sync with the stylesheet. */\nconst MARKER_SIZE = PIN_SIZE\n\n/** Where a popover's top card sits vertically, measured from the marker visual's middle. */\nconst CARD_TOP_Y = -28\n\n/** The horizontal gap between a marker visual's right edge and its popover/preview panel. The\n * preview's hover bridge spans exactly this (see `--tlui-cmt-preview-bridge`). */\nconst PREVIEW_GAP = 6\n\n/**\n * Where a marker's popover sits relative to the marker's anchor point. The hover preview uses the\n * same origins, so moving a popover here moves its preview with it.\n *\n * Both marker kinds hang off their point the same way (`translate(0, -100%)`), differing only in\n * size \u2014 so each offset clears its own width plus the shared gap, and re-bases the shared card-top\n * measure from its bottom anchor to its middle.\n */\nexport const POPOVER_OFFSET = {\n\tthread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },\n\tlist: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 },\n} as const\n\n/** The open thread's popover container, portaled above the UI panels. A wheel over it passes\n * through to the canvas (unless it scrolls its own content), like tldraw's panels. */\nexport function ThreadPopover({\n\tbase,\n\tchildren,\n}: {\n\tbase: { x: number; y: number }\n\tchildren: ReactNode\n}) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\t// On mobile the popover slides off its fixed offset to stay above the software keyboard and\n\t// on-screen; desktop keeps the fixed offset (base returned unchanged).\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, base, isMobile)\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* contextmenu also stops here: portals bubble React events to the canvas's context-menu\n\t\t\t trigger (the layer mounts inside it), which would open the canvas menu over this panel. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName=\"tlui-cmt-canvas-popover\"\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/**\n * One thread's interactive view: its comments, the reply composer, edit-in-place on your own\n * comments, and the resolve/delete actions. Read receipts are reported for every unread comment\n * while mounted, so only mount it where the thread is actually being shown.\n *\n * Memoized because the popover position rides the pin's per-frame render point, so the pin\n * re-renders on every camera frame while a thread is open.\n */\nexport const ThreadView = memo(function ThreadView({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread }) {\n\tconst {\n\t\tcurrentUserId,\n\t\tresolveAuthor,\n\t\tonPostComment,\n\t\tisCommentUnread,\n\t\tonCommentsRead,\n\t\tgetMentionSuggestions,\n\t\trenderMentionSuggestion,\n\t\tgetThreadHref,\n\t} = props\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst msg = useTranslation()\n\tconst resolveName = useResolveName(resolveAuthor)\n\t// Rebuilt only when the comments change, not on every render \u2014 each of which would otherwise\n\t// re-allocate a date string and body element per comment.\n\tconst cards = useMemo(\n\t\t() =>\n\t\t\tcomments.map((c) =>\n\t\t\t\ttoCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)\n\t\t\t),\n\t\t[comments, currentUserId, resolveAuthor, options.components, resolveName]\n\t)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// Composing, editing, deleting, and resolving are all gated on the viewer's permission. Where it's\n\t// withheld the composer gives way to the ComposerFallback slot and the affordances are hidden.\n\tconst canComment = useCanComment(currentUserId)\n\t// Editing and deleting are further per-record: the author's to do by default, wider or narrower\n\t// under a `canModifyComment` callback. Computed for the thread's comments in one pass, since\n\t// `renderComment` is a callback rather than a component and can't call a hook per comment.\n\tconst commentPermissions = useValue(\n\t\t'comment permissions',\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\tcomments.map((comment) => [\n\t\t\t\t\tcomment.id,\n\t\t\t\t\t{\n\t\t\t\t\t\tedit: getCanModifyComment(editor, currentUserId, { action: 'edit-comment', comment }),\n\t\t\t\t\t\tdelete: getCanModifyComment(editor, currentUserId, {\n\t\t\t\t\t\t\taction: 'delete-comment',\n\t\t\t\t\t\t\tcomment,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t),\n\t\t[editor, currentUserId, comments]\n\t)\n\t// Deleting a thread is its creator's by default (and server-enforced); `canModifyComment` is what\n\t// widens that. Still a commenting write, so `canComment` stays the outer gate.\n\tconst canModifyThread = useCanModifyComment(currentUserId, { action: 'delete-thread', thread })\n\tconst canDeleteThread = canComment && canModifyThread\n\tconst ComposerFallback = options.components.ComposerFallback\n\t// An unsent reply survives closing the thread (saved on every change, keyed by thread id) \u2014\n\t// the flip side of dismissing without a discard warning.\n\tconst [reply, setReply] = useState<TLRichText>(\n\t\t() => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT\n\t)\n\tconst [editingId, setEditingId] = useState<string | null>(null)\n\tconst [editText, setEditText] = useState<TLRichText>(EMPTY_COMMENT)\n\tconst canReply = canComment && !thread.resolved\n\tconst container = useContainer()\n\t// Resolved when the composer closes rather than held as an element: the card unmounts while the\n\t// composer stands in its place, so the node captured on the way in is gone by the way out.\n\tconst editReturnFocus = useRef<(() => HTMLElement | null) | null>(null)\n\n\t// Tab from the canvas drops the caret in the reply box: a pin click leaves focus on the editor\n\t// container, so the first Tab would otherwise walk the app's UI. Capture phase, once per open thread.\n\tconst [focusReply, setFocusReply] = useState(false)\n\tconst tabTaken = useRef(false)\n\tconst swallowTabUp = useRef(false)\n\tuseEffect(() => {\n\t\tif (!canReply) {\n\t\t\t// Resolving unmounts the reply box under an open thread. Reopening it should feel like a\n\t\t\t// freshly opened thread: no autoFocus left armed from the last Tab, and Tab available again.\n\t\t\tsetFocusReply(false)\n\t\t\ttabTaken.current = false\n\t\t\treturn\n\t\t}\n\t\tconst doc = container.ownerDocument\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tif (e.defaultPrevented || tabTaken.current) return\n\t\t\t// Focus rests on the container (or nothing) after a pin click. Anywhere else is deliberate, and\n\t\t\t// Tab belongs to whatever holds it.\n\t\t\tif (e.target !== container && e.target !== doc.body) return\n\t\t\ttabTaken.current = true\n\t\t\tswallowTabUp.current = true\n\t\t\tsetFocusReply(true)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\t// The select tool navigates shapes on Tab's *keyup*, so a quick tap would both focus the reply and\n\t\t// step the selection. Swallow the release of the press we took, and only that one.\n\t\tconst onKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || !swallowTabUp.current) return\n\t\t\tswallowTabUp.current = false\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdoc.addEventListener('keydown', onKeyDown, true)\n\t\tdoc.addEventListener('keyup', onKeyUp, true)\n\t\treturn () => {\n\t\t\tdoc.removeEventListener('keydown', onKeyDown, true)\n\t\t\tdoc.removeEventListener('keyup', onKeyUp, true)\n\t\t}\n\t}, [canReply, container])\n\n\t// Leaving the edit composer unmounts it while it holds focus, dropping focus to the editor\n\t// container \u2014 outside the thread, so a Tab from there walks the app's UI. Hand focus back to\n\t// whatever opened the composer instead.\n\tuseEffect(() => {\n\t\tif (editingId !== null) return\n\t\tconst resolve = editReturnFocus.current\n\t\teditReturnFocus.current = null\n\t\t// Runs after React has swapped the card back in, so the target is mounted again by now.\n\t\tresolve?.()?.focus()\n\t}, [editingId])\n\n\t// Reported as one batch so the host can record the receipts in a single write. The write flips\n\t// isCommentUnread to false, so re-runs find nothing to report.\n\tuseEffect(() => {\n\t\tif (!isCommentUnread || !onCommentsRead) return\n\t\tconst unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id)\n\t\tif (unreadIds.length > 0) {\n\t\t\tonCommentsRead(unreadIds)\n\t\t}\n\t}, [comments, isCommentUnread, onCommentsRead])\n\n\tconst postReply = () => {\n\t\tif (isCommentEmpty(reply) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId: thread.pageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: reply,\n\t\t\t})\n\t\t\tput([comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetReply(EMPTY_COMMENT)\n\t\tclearCommentDraft(replyDraftSlot(thread.id))\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted reply.\n\t\tonPostComment?.(comment)\n\t}\n\n\tconst toggleResolve = () => {\n\t\tif (!currentUserId) return\n\t\tif (thread.resolved) reopenThread(editor, thread)\n\t\telse resolveThread(editor, thread, currentUserId)\n\t}\n\n\t// No `currentUserId` check: unlike a resolve, a delete stamps nothing on the record, so who may\n\t// make it is `canModifyComment`'s call alone (which withholds it from an unidentified viewer by\n\t// default).\n\tconst removeThread = () => {\n\t\tdeleteThread(editor, thread)\n\t}\n\n\tconst ThreadActions = options.components.ThreadActions\n\t// The host's URL for this thread. Its presence is what puts \"copy link\" in the header menu.\n\tconst threadHref = getThreadHref?.(thread.id)\n\tconst [linkCopied, copyThreadLink] = useCopyLink(threadHref)\n\n\tconst startEdit = (comment: TLComment, { fromMoreMenu = false } = {}) => {\n\t\t// Edit from the \u22EF menu comes back to that button, looked up again on the way out since the card\n\t\t// unmounts. Otherwise return to whatever held focus, ignoring the body and the editor container.\n\t\tconst active = container.ownerDocument.activeElement\n\t\teditReturnFocus.current = fromMoreMenu\n\t\t\t? () => container.querySelector<HTMLElement>(`[data-cmt-more-for=\"${comment.id}\"]`)\n\t\t\t: active instanceof HTMLElement && active !== container && active !== document.body\n\t\t\t\t? () => (active.isConnected ? active : null)\n\t\t\t\t: null\n\t\tsetEditingId(comment.id)\n\t\tsetEditText(comment.body)\n\t}\n\n\tconst saveEdit = () => {\n\t\tconst comment = comments.find((c) => c.id === editingId)\n\t\tif (!comment || isCommentEmpty(editText)) return\n\t\teditComment(editor, comment, editText)\n\t\tsetEditingId(null)\n\t}\n\n\t// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,\n\t// with the affordances the viewer is allowed on it.\n\tconst renderComment = (card: CommentCardProps, index: number): ReactNode => {\n\t\tconst comment = comments[index]\n\t\tconst permissions = commentPermissions.get(comment.id)\n\t\tif (editingId === comment.id) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"tlui-cmt-editing\"\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === 'Escape') {\n\t\t\t\t\t\t\tsetEditingId(null)\n\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={card.author}\n\t\t\t\t\t\tplaceholder={msg('comments.edit-placeholder')}\n\t\t\t\t\t\tvalue={editText}\n\t\t\t\t\t\tonChange={setEditText}\n\t\t\t\t\t\tonSubmit={saveEdit}\n\t\t\t\t\t\tsendLabel={msg('comments.save')}\n\t\t\t\t\t\tdisabled={isCommentEmpty(editText)}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n\t\treturn (\n\t\t\t<CommentCard\n\t\t\t\t{...card}\n\t\t\t\tfooter={\n\t\t\t\t\t<CommentReactions\n\t\t\t\t\t\tcomment={comment}\n\t\t\t\t\t\t// Reacting is a commenting write: without `canComment` the tally renders\n\t\t\t\t\t\t// read-only (no identity \u2192 pills aren't clickable).\n\t\t\t\t\t\tcurrentUserId={canComment ? currentUserId : null}\n\t\t\t\t\t\tresolveName={resolveName}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tactions={\n\t\t\t\t\tcanComment && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{(permissions?.edit || permissions?.delete) && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-actions-${comment.id}`}>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t\t<TldrawUiButton\n\t\t\t\t\t\t\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\ttitle={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t\t\t\t\t\tdata-cmt-more-for={comment.id}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<TldrawUiIcon\n\t\t\t\t\t\t\t\t\t\t\t\ticon=\"dots-vertical\"\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\t\tsmall\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</TldrawUiButton>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t\t\t{permissions?.edit && (\n\t\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => startEdit(comment, { fromMoreMenu: true })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('comments.edit')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t{permissions?.delete && (\n\t\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => deleteComment(editor, comment)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('action.delete')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{/* Last so react is always the rightmost pill. */}\n\t\t\t\t\t\t\t<CommentReactionPicker comment={comment} currentUserId={currentUserId} />\n\t\t\t\t\t\t</>\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a\n\t// resolve stamps into `resolved.by`.\n\tconst resolveLabel = msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')\n\tconst headerActions = (\n\t\t<>\n\t\t\t{/* Host verbs \u2014 assign, link a ticket \u2014 sit ahead of the built-in actions. */}\n\t\t\t{ThreadActions && <ThreadActions thread={thread} comments={comments} />}\n\t\t\t{(threadHref !== undefined || canDeleteThread) && (\n\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-thread-actions-${thread.id}`}>\n\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t<TldrawUiButton\n\t\t\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\ttitle={msg('comments.more-options')}\n\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<TldrawUiIcon icon=\"dots-vertical\" label={msg('comments.more-options')} small />\n\t\t\t\t\t\t</TldrawUiButton>\n\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t>\n\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t{/* A link is a read affordance: offered to anyone who can see the thread,\n\t\t\t\t\t\t\t including a viewer who can't comment. `noClose` keeps the menu up so the\n\t\t\t\t\t\t\t item can confirm the copy in place. */}\n\t\t\t\t\t\t\t{threadHref !== undefined && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem noClose>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" className=\"tlui-cmt-menu-item\" onClick={copyThreadLink}>\n\t\t\t\t\t\t\t\t\t\t<span>{msg(linkCopied ? 'comments.link-copied' : 'comments.copy-link')}</span>\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{canDeleteThread && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\t\tonClick={removeThread}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span>{msg('comments.delete')}</span>\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t)}\n\t\t\t{canComment && currentUserId && (\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={resolveLabel}\n\t\t\t\t\ttitle={resolveLabel}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tonClick={toggleResolve}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"check\" label={resolveLabel} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t)}\n\t\t\t<TldrawUiButton\n\t\t\t\ttype=\"icon\"\n\t\t\t\ttooltip={msg('comments.dismiss')}\n\t\t\t\ttitle={msg('comments.dismiss')}\n\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\tonClick={() => openThreadId.set(editor, null)}\n\t\t\t>\n\t\t\t\t<TldrawUiIcon icon=\"cross-2\" label={msg('comments.dismiss')} small />\n\t\t\t</TldrawUiButton>\n\t\t</>\n\t)\n\n\treturn (\n\t\t<CommentThread\n\t\t\theader={msg('comments.thread-title')}\n\t\t\theaderActions={headerActions}\n\t\t\trenderComment={renderComment}\n\t\t\tcomments={cards}\n\t\t\tresolvedBanner={\n\t\t\t\tthread.resolved\n\t\t\t\t\t? msg('comments.resolved-by').replace(\n\t\t\t\t\t\t\t'{name}',\n\t\t\t\t\t\t\tresolveAuthor(thread.resolved.by)?.name ?? UNKNOWN_AUTHOR\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tcomposer={\n\t\t\t\tcanReply\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tauthor: me ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\t\t\tplaceholder: msg('comments.reply-placeholder'),\n\t\t\t\t\t\t\tsendLabel: msg('comments.send'),\n\t\t\t\t\t\t\tvalue: reply,\n\t\t\t\t\t\t\tonChange: (value: TLRichText) => {\n\t\t\t\t\t\t\t\tsetReply(value)\n\t\t\t\t\t\t\t\tsaveCommentDraft(replyDraftSlot(thread.id), value)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tonSubmit: postReply,\n\t\t\t\t\t\t\t// Up in the empty reply box edits the comment directly above it, chat-style \u2014\n\t\t\t\t\t\t\t// only when you're allowed to edit it (the same gate as the Edit link).\n\t\t\t\t\t\t\tonArrowUpWhenEmpty: () => {\n\t\t\t\t\t\t\t\tconst last = comments[comments.length - 1]\n\t\t\t\t\t\t\t\tif (last && commentPermissions.get(last.id)?.edit) startEdit(last)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\t\tdisabled: isCommentEmpty(reply) || !currentUserId,\n\t\t\t\t\t\t\tgetMentionSuggestions,\n\t\t\t\t\t\t\trenderMentionSuggestion,\n\t\t\t\t\t\t\t// Reuses the composer's own focus path, so the caret lands at the end of a\n\t\t\t\t\t\t\t// restored draft rather than in front of it.\n\t\t\t\t\t\t\tautoFocus: focusReply,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tfooter={\n\t\t\t\t!canComment && !thread.resolved && ComposerFallback ? (\n\t\t\t\t\t<ComposerFallback context=\"thread\" />\n\t\t\t\t) : undefined\n\t\t\t}\n\t\t/>\n\t)\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["ThreadView", "comment"]
7
+ }
@@ -1,14 +1,14 @@
1
1
  import { mstEdges } from "./mst.mjs";
2
2
  import { cappedReplay } from "./replay.mjs";
3
3
  import { contract, finalize } from "./schedule.mjs";
4
- function computeClusterTable(leaves, options) {
4
+ function computeClusterTable(leaves, options, screenOffsets) {
5
5
  const opts = resolveOptions(options);
6
6
  const leafNodes = leaves.map(leafToNode);
7
7
  const edges = mstEdges(leaves);
8
8
  if (leaves.length < 2) {
9
9
  return { events: [], leaves: leafNodes };
10
10
  }
11
- const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax });
11
+ const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax }, screenOffsets);
12
12
  const contracted = contract(raw, opts.eps);
13
13
  const events = finalize(contracted, {
14
14
  Tc: opts.Tc,
@@ -20,10 +20,10 @@ function computeClusterTable(leaves, options) {
20
20
  return { events, leaves: leafNodes };
21
21
  }
22
22
  function resolveOptions(options) {
23
- const Tc = options.Tc ?? 40;
24
- const Tu = options.Tu ?? 1.5 * Tc;
25
- const eps = options.eps ?? 0.12;
26
- const Dmax = options.Dmax ?? 3 * Tc;
23
+ const Tc = options.Tc ?? 22;
24
+ const Tu = options.Tu ?? 1.2 * Tc;
25
+ const eps = options.eps ?? 0.7;
26
+ const Dmax = options.Dmax ?? 3.75 * Tc;
27
27
  const maxSplitZoom = options.maxSplitZoom ?? 6;
28
28
  const { minZoom, maxZoom } = options;
29
29
  if (!Number.isFinite(Tc) || Tc <= 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/clustering/computeClusterTable.ts"],
4
- "sourcesContent": ["import { mstEdges } from './mst'\nimport { cappedReplay } from './replay'\nimport { contract, finalize } from './schedule'\nimport type { ClusterNode, ClusterOptions, ClusterTable, LeafInput } from './types'\n\ninterface ResolvedClusterOptions {\n\tTc: number\n\tTu: number\n\teps: number\n\tDmax: number\n\tminZoom: number\n\tmaxZoom: number\n\tmaxSplitZoom: number\n}\n\n/** @public */\nexport function computeClusterTable(\n\tleaves: readonly LeafInput[],\n\toptions: ClusterOptions\n): ClusterTable {\n\tconst opts = resolveOptions(options)\n\tconst leafNodes = leaves.map(leafToNode)\n\tconst edges = mstEdges(leaves)\n\n\tif (leaves.length < 2) {\n\t\treturn { events: [], leaves: leafNodes }\n\t}\n\n\tconst raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax })\n\tconst contracted = contract(raw, opts.eps)\n\tconst events = finalize(contracted, {\n\t\tTc: opts.Tc,\n\t\tTu: opts.Tu,\n\t\tminZoom: opts.minZoom,\n\t\tmaxZoom: opts.maxZoom,\n\t\tmaxSplitZoom: opts.maxSplitZoom,\n\t})\n\n\treturn { events, leaves: leafNodes }\n}\n\nfunction resolveOptions(options: ClusterOptions): ResolvedClusterOptions {\n\tconst Tc = options.Tc ?? 40\n\tconst Tu = options.Tu ?? 1.5 * Tc\n\tconst eps = options.eps ?? 0.12\n\tconst Dmax = options.Dmax ?? 3 * Tc\n\tconst maxSplitZoom = options.maxSplitZoom ?? 6\n\tconst { minZoom, maxZoom } = options\n\n\tif (!Number.isFinite(Tc) || Tc <= 0) {\n\t\tthrow new Error('Tc must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(Tu) || Tu <= Tc) {\n\t\tthrow new Error('Tu must be finite and greater than Tc')\n\t}\n\tif (!Number.isFinite(eps) || eps < 0) {\n\t\tthrow new Error('eps must be finite and greater than or equal to 0')\n\t}\n\tif (!Number.isFinite(Dmax) || Dmax < Tc) {\n\t\tthrow new Error('Dmax must be finite and greater than or equal to Tc')\n\t}\n\tif (!Number.isFinite(minZoom) || minZoom <= 0) {\n\t\tthrow new Error('minZoom must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n\t\tthrow new Error('maxZoom must be finite and greater than minZoom')\n\t}\n\tif (!Number.isFinite(maxSplitZoom) || maxSplitZoom <= 0) {\n\t\tthrow new Error('maxSplitZoom must be finite and greater than 0')\n\t}\n\n\treturn { Tc, Tu, eps, Dmax, minZoom, maxZoom, maxSplitZoom }\n}\n\nfunction leafToNode(leaf: LeafInput): ClusterNode {\n\treturn {\n\t\tid: leaf.id,\n\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\tcount: 1,\n\t\tmembers: [leaf.id],\n\t}\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgB;AAc5B,SAAS,oBACf,QACA,SACe;AACf,QAAM,OAAO,eAAe,OAAO;AACnC,QAAM,YAAY,OAAO,IAAI,UAAU;AACvC,QAAM,QAAQ,SAAS,MAAM;AAE7B,MAAI,OAAO,SAAS,GAAG;AACtB,WAAO,EAAE,QAAQ,CAAC,GAAG,QAAQ,UAAU;AAAA,EACxC;AAEA,QAAM,MAAM,aAAa,QAAQ,OAAO,EAAE,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC;AACxE,QAAM,aAAa,SAAS,KAAK,KAAK,GAAG;AACzC,QAAM,SAAS,SAAS,YAAY;AAAA,IACnC,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,cAAc,KAAK;AAAA,EACpB,CAAC;AAED,SAAO,EAAE,QAAQ,QAAQ,UAAU;AACpC;AAEA,SAAS,eAAe,SAAiD;AACxE,QAAM,KAAK,QAAQ,MAAM;AACzB,QAAM,KAAK,QAAQ,MAAM,MAAM;AAC/B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,OAAO,QAAQ,QAAQ,IAAI;AACjC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,EAAE,SAAS,QAAQ,IAAI;AAE7B,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,GAAG;AACpC,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACvD;AACA,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,IAAI;AACrC,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACxD;AACA,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACrC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACpE;AACA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;AACxC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC9C,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC5D;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,SAAS;AACpD,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GAAG;AACxD,UAAM,IAAI,MAAM,gDAAgD;AAAA,EACjE;AAEA,SAAO,EAAE,IAAI,IAAI,KAAK,MAAM,SAAS,SAAS,aAAa;AAC5D;AAEA,SAAS,WAAW,MAA8B;AACjD,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,EAAE;AAAA,IAC7C,OAAO;AAAA,IACP,SAAS,CAAC,KAAK,EAAE;AAAA,EAClB;AACD;",
4
+ "sourcesContent": ["import { mstEdges } from './mst'\nimport { cappedReplay } from './replay'\nimport { contract, finalize } from './schedule'\nimport type {\n\tClusterNode,\n\tClusterOptions,\n\tClusterTable,\n\tLeafInput,\n\tLeafScreenOffsets,\n} from './types'\n\ninterface ResolvedClusterOptions {\n\tTc: number\n\tTu: number\n\teps: number\n\tDmax: number\n\tminZoom: number\n\tmaxZoom: number\n\tmaxSplitZoom: number\n}\n\n/** @internal */\nexport function computeClusterTable(\n\tleaves: readonly LeafInput[],\n\toptions: ClusterOptions,\n\t// Render offsets for markers that draw off their anchor (imprecise pins), keyed by leaf id.\n\t// Omitted or empty, the run is identical \u2014 output and code paths \u2014 to an offset-unaware one.\n\tscreenOffsets?: LeafScreenOffsets\n): ClusterTable {\n\tconst opts = resolveOptions(options)\n\tconst leafNodes = leaves.map(leafToNode)\n\tconst edges = mstEdges(leaves)\n\n\tif (leaves.length < 2) {\n\t\treturn { events: [], leaves: leafNodes }\n\t}\n\n\tconst raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax }, screenOffsets)\n\tconst contracted = contract(raw, opts.eps)\n\tconst events = finalize(contracted, {\n\t\tTc: opts.Tc,\n\t\tTu: opts.Tu,\n\t\tminZoom: opts.minZoom,\n\t\tmaxZoom: opts.maxZoom,\n\t\tmaxSplitZoom: opts.maxSplitZoom,\n\t})\n\n\treturn { events, leaves: leafNodes }\n}\n\nfunction resolveOptions(options: ClusterOptions): ResolvedClusterOptions {\n\tconst Tc = options.Tc ?? 22\n\tconst Tu = options.Tu ?? 1.2 * Tc\n\tconst eps = options.eps ?? 0.7\n\tconst Dmax = options.Dmax ?? 3.75 * Tc\n\tconst maxSplitZoom = options.maxSplitZoom ?? 6\n\tconst { minZoom, maxZoom } = options\n\n\tif (!Number.isFinite(Tc) || Tc <= 0) {\n\t\tthrow new Error('Tc must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(Tu) || Tu <= Tc) {\n\t\tthrow new Error('Tu must be finite and greater than Tc')\n\t}\n\tif (!Number.isFinite(eps) || eps < 0) {\n\t\tthrow new Error('eps must be finite and greater than or equal to 0')\n\t}\n\tif (!Number.isFinite(Dmax) || Dmax < Tc) {\n\t\tthrow new Error('Dmax must be finite and greater than or equal to Tc')\n\t}\n\tif (!Number.isFinite(minZoom) || minZoom <= 0) {\n\t\tthrow new Error('minZoom must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n\t\tthrow new Error('maxZoom must be finite and greater than minZoom')\n\t}\n\tif (!Number.isFinite(maxSplitZoom) || maxSplitZoom <= 0) {\n\t\tthrow new Error('maxSplitZoom must be finite and greater than 0')\n\t}\n\n\treturn { Tc, Tu, eps, Dmax, minZoom, maxZoom, maxSplitZoom }\n}\n\nfunction leafToNode(leaf: LeafInput): ClusterNode {\n\treturn {\n\t\tid: leaf.id,\n\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\tcount: 1,\n\t\tmembers: [leaf.id],\n\t}\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgB;AAoB5B,SAAS,oBACf,QACA,SAGA,eACe;AACf,QAAM,OAAO,eAAe,OAAO;AACnC,QAAM,YAAY,OAAO,IAAI,UAAU;AACvC,QAAM,QAAQ,SAAS,MAAM;AAE7B,MAAI,OAAO,SAAS,GAAG;AACtB,WAAO,EAAE,QAAQ,CAAC,GAAG,QAAQ,UAAU;AAAA,EACxC;AAEA,QAAM,MAAM,aAAa,QAAQ,OAAO,EAAE,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,GAAG,aAAa;AACvF,QAAM,aAAa,SAAS,KAAK,KAAK,GAAG;AACzC,QAAM,SAAS,SAAS,YAAY;AAAA,IACnC,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,cAAc,KAAK;AAAA,EACpB,CAAC;AAED,SAAO,EAAE,QAAQ,QAAQ,UAAU;AACpC;AAEA,SAAS,eAAe,SAAiD;AACxE,QAAM,KAAK,QAAQ,MAAM;AACzB,QAAM,KAAK,QAAQ,MAAM,MAAM;AAC/B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,OAAO,QAAQ,QAAQ,OAAO;AACpC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,EAAE,SAAS,QAAQ,IAAI;AAE7B,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,GAAG;AACpC,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACvD;AACA,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,IAAI;AACrC,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACxD;AACA,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACrC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACpE;AACA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;AACxC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC9C,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC5D;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,SAAS;AACpD,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GAAG;AACxD,UAAM,IAAI,MAAM,gDAAgD;AAAA,EACjE;AAEA,SAAO,EAAE,IAAI,IAAI,KAAK,MAAM,SAAS,SAAS,aAAa;AAC5D;AAEA,SAAS,WAAW,MAA8B;AACjD,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,EAAE;AAAA,IAC7C,OAAO;AAAA,IACP,SAAS,CAAC,KAAK,EAAE;AAAA,EAClB;AACD;",
6
6
  "names": []
7
7
  }