@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.31036673d3f8

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 (279) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1139 -329
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-body.js +1 -1
  8. package/dist-cjs/canvas/comment-body.js.map +2 -2
  9. package/dist-cjs/canvas/comment-drafts.js +54 -0
  10. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  11. package/dist-cjs/canvas/comment-mutations.js +123 -0
  12. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  13. package/dist-cjs/canvas/comment-reactions.js +163 -0
  14. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  15. package/dist-cjs/canvas/comment-render.js +5 -3
  16. package/dist-cjs/canvas/comment-render.js.map +2 -2
  17. package/dist-cjs/canvas/comment-store.js +19 -9
  18. package/dist-cjs/canvas/comment-store.js.map +2 -2
  19. package/dist-cjs/canvas/comment-tool.js +64 -14
  20. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  21. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  22. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  24. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overlay.js +429 -474
  26. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  27. package/dist-cjs/canvas/comments-sidebar.js +35 -32
  28. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  29. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  30. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  31. package/dist-cjs/canvas/context.js +17 -0
  32. package/dist-cjs/canvas/context.js.map +7 -0
  33. package/dist-cjs/canvas/hooks.js +22 -3
  34. package/dist-cjs/canvas/hooks.js.map +2 -2
  35. package/dist-cjs/canvas/options.js +19 -2
  36. package/dist-cjs/canvas/options.js.map +2 -2
  37. package/dist-cjs/canvas/pin-stacking.js +56 -0
  38. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  39. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  40. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  41. package/dist-cjs/canvas/state.js +28 -14
  42. package/dist-cjs/canvas/state.js.map +2 -2
  43. package/dist-cjs/canvas/thread-preview.js +185 -0
  44. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  45. package/dist-cjs/canvas/thread-stack.js +199 -0
  46. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  47. package/dist-cjs/canvas/thread-state.js +67 -21
  48. package/dist-cjs/canvas/thread-state.js.map +2 -2
  49. package/dist-cjs/canvas/thread-view.js +397 -0
  50. package/dist-cjs/canvas/thread-view.js.map +7 -0
  51. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  52. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  53. package/dist-cjs/clustering/runtime.js +119 -7
  54. package/dist-cjs/clustering/runtime.js.map +2 -2
  55. package/dist-cjs/clustering/types.js.map +1 -1
  56. package/dist-cjs/index.d.ts +763 -333
  57. package/dist-cjs/index.js +46 -28
  58. package/dist-cjs/index.js.map +2 -2
  59. package/dist-cjs/ui/byline.js +6 -4
  60. package/dist-cjs/ui/byline.js.map +2 -2
  61. package/dist-cjs/ui/comment-card.js +15 -8
  62. package/dist-cjs/ui/comment-card.js.map +2 -2
  63. package/dist-cjs/ui/comment-composer.js +137 -36
  64. package/dist-cjs/ui/comment-composer.js.map +2 -2
  65. package/dist-cjs/ui/comment-pin.js +24 -2
  66. package/dist-cjs/ui/comment-pin.js.map +2 -2
  67. package/dist-cjs/ui/comment-thread.js +9 -7
  68. package/dist-cjs/ui/comment-thread.js.map +2 -2
  69. package/dist-cjs/ui/comments-list.js +30 -18
  70. package/dist-cjs/ui/comments-list.js.map +2 -2
  71. package/dist-cjs/ui/count-badge.js +3 -2
  72. package/dist-cjs/ui/count-badge.js.map +2 -2
  73. package/dist-cjs/ui/emoji-picker.js +54 -0
  74. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  75. package/dist-cjs/ui/empty-state.js +3 -3
  76. package/dist-cjs/ui/empty-state.js.map +2 -2
  77. package/dist-cjs/ui/format-time.js +4 -1
  78. package/dist-cjs/ui/format-time.js.map +2 -2
  79. package/dist-cjs/ui/reaction-picker.js +73 -0
  80. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  81. package/dist-cjs/ui/reaction.js +59 -6
  82. package/dist-cjs/ui/reaction.js.map +2 -2
  83. package/dist-cjs/ui/reactions.js +20 -7
  84. package/dist-cjs/ui/reactions.js.map +2 -2
  85. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  86. package/dist-cjs/ui/reply-count.js.map +7 -0
  87. package/dist-cjs/ui/send-button.js +20 -1
  88. package/dist-cjs/ui/send-button.js.map +2 -2
  89. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  90. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  91. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  92. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  93. package/dist-esm/canvas/cluster-input.mjs +2 -2
  94. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  95. package/dist-esm/canvas/comment-body.mjs +1 -1
  96. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  97. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  98. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  99. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  100. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  101. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  102. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  103. package/dist-esm/canvas/comment-render.mjs +5 -3
  104. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  105. package/dist-esm/canvas/comment-store.mjs +19 -9
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +67 -17
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +450 -479
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +38 -35
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  119. package/dist-esm/canvas/context.mjs +1 -0
  120. package/dist-esm/canvas/context.mjs.map +7 -0
  121. package/dist-esm/canvas/hooks.mjs +28 -5
  122. package/dist-esm/canvas/hooks.mjs.map +2 -2
  123. package/dist-esm/canvas/options.mjs +21 -3
  124. package/dist-esm/canvas/options.mjs.map +2 -2
  125. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  126. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  127. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  128. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  129. package/dist-esm/canvas/state.mjs +29 -19
  130. package/dist-esm/canvas/state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-preview.mjs +170 -0
  132. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  133. package/dist-esm/canvas/thread-stack.mjs +190 -0
  134. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-state.mjs +71 -22
  136. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-view.mjs +402 -0
  138. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  139. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  140. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  141. package/dist-esm/clustering/runtime.mjs +119 -7
  142. package/dist-esm/clustering/runtime.mjs.map +2 -2
  143. package/dist-esm/index.d.mts +763 -333
  144. package/dist-esm/index.mjs +87 -41
  145. package/dist-esm/index.mjs.map +2 -2
  146. package/dist-esm/ui/byline.mjs +6 -4
  147. package/dist-esm/ui/byline.mjs.map +2 -2
  148. package/dist-esm/ui/comment-card.mjs +15 -8
  149. package/dist-esm/ui/comment-card.mjs.map +2 -2
  150. package/dist-esm/ui/comment-composer.mjs +148 -36
  151. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  152. package/dist-esm/ui/comment-pin.mjs +24 -2
  153. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  154. package/dist-esm/ui/comment-thread.mjs +9 -7
  155. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  156. package/dist-esm/ui/comments-list.mjs +43 -29
  157. package/dist-esm/ui/comments-list.mjs.map +2 -2
  158. package/dist-esm/ui/count-badge.mjs +3 -2
  159. package/dist-esm/ui/count-badge.mjs.map +2 -2
  160. package/dist-esm/ui/emoji-picker.mjs +34 -0
  161. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  162. package/dist-esm/ui/empty-state.mjs +3 -3
  163. package/dist-esm/ui/empty-state.mjs.map +2 -2
  164. package/dist-esm/ui/format-time.mjs +4 -1
  165. package/dist-esm/ui/format-time.mjs.map +2 -2
  166. package/dist-esm/ui/reaction-picker.mjs +60 -0
  167. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  168. package/dist-esm/ui/reaction.mjs +60 -7
  169. package/dist-esm/ui/reaction.mjs.map +2 -2
  170. package/dist-esm/ui/reactions.mjs +21 -8
  171. package/dist-esm/ui/reactions.mjs.map +2 -2
  172. package/dist-esm/ui/reply-count.mjs +9 -0
  173. package/dist-esm/ui/reply-count.mjs.map +7 -0
  174. package/dist-esm/ui/send-button.mjs +20 -1
  175. package/dist-esm/ui/send-button.mjs.map +2 -2
  176. package/dist-esm/ui/tooltip-button.mjs +12 -0
  177. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  178. package/package.json +6 -6
  179. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  180. package/src/canvas/anchor-lifecycle.ts +205 -0
  181. package/src/canvas/canvas.css +461 -46
  182. package/src/canvas/cluster-input.test.ts +31 -23
  183. package/src/canvas/cluster-input.ts +3 -4
  184. package/src/canvas/comment-body.tsx +1 -1
  185. package/src/canvas/comment-drafts.test.ts +55 -0
  186. package/src/canvas/comment-drafts.ts +46 -0
  187. package/src/canvas/comment-mutations.test.ts +410 -0
  188. package/src/canvas/comment-mutations.ts +288 -0
  189. package/src/canvas/comment-reactions.test.ts +132 -0
  190. package/src/canvas/comment-reactions.tsx +245 -0
  191. package/src/canvas/comment-render.test.ts +1 -1
  192. package/src/canvas/comment-render.ts +9 -5
  193. package/src/canvas/comment-store.ts +67 -27
  194. package/src/canvas/comment-tool.test.ts +162 -0
  195. package/src/canvas/comment-tool.tsx +105 -23
  196. package/src/canvas/comments-filter-menu.tsx +23 -32
  197. package/src/canvas/comments-overflow-menu.tsx +14 -21
  198. package/src/canvas/comments-overlay.tsx +695 -622
  199. package/src/canvas/comments-sidebar.test.ts +45 -0
  200. package/src/canvas/comments-sidebar.tsx +76 -58
  201. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  202. package/src/canvas/context.ts +47 -0
  203. package/src/canvas/hooks.ts +52 -11
  204. package/src/canvas/options.test.ts +52 -2
  205. package/src/canvas/options.ts +133 -9
  206. package/src/canvas/pin-stacking.test.ts +114 -0
  207. package/src/canvas/pin-stacking.ts +57 -0
  208. package/src/canvas/sidebar-filters.ts +4 -3
  209. package/src/canvas/state.ts +92 -35
  210. package/src/canvas/thread-preview.test.ts +91 -0
  211. package/src/canvas/thread-preview.tsx +304 -0
  212. package/src/canvas/thread-stack.tsx +252 -0
  213. package/src/canvas/thread-state.test.ts +209 -0
  214. package/src/canvas/thread-state.ts +137 -36
  215. package/src/canvas/thread-view.tsx +516 -0
  216. package/src/clustering/computeClusterTable.test.ts +24 -23
  217. package/src/clustering/computeClusterTable.ts +5 -5
  218. package/src/clustering/runtime.test.ts +217 -7
  219. package/src/clustering/runtime.ts +171 -17
  220. package/src/clustering/types.ts +9 -9
  221. package/src/index.ts +76 -40
  222. package/src/ui/byline.tsx +7 -5
  223. package/src/ui/comment-card.tsx +20 -10
  224. package/src/ui/comment-composer.tsx +206 -40
  225. package/src/ui/comment-pin.tsx +26 -2
  226. package/src/ui/comment-thread.tsx +13 -6
  227. package/src/ui/comments-list.test.ts +31 -0
  228. package/src/ui/comments-list.tsx +45 -21
  229. package/src/ui/comments.css +566 -281
  230. package/src/ui/count-badge.tsx +9 -2
  231. package/src/ui/emoji-picker.test.ts +25 -0
  232. package/src/ui/emoji-picker.tsx +76 -0
  233. package/src/ui/empty-state.tsx +3 -3
  234. package/src/ui/format-time.ts +7 -2
  235. package/src/ui/reaction-picker.tsx +109 -0
  236. package/src/ui/reaction.tsx +121 -6
  237. package/src/ui/reactions.tsx +69 -10
  238. package/src/ui/reply-count.ts +16 -0
  239. package/src/ui/send-button.tsx +18 -3
  240. package/src/ui/tooltip-button.tsx +20 -0
  241. package/dist-cjs/canvas/region-options.js +0 -49
  242. package/dist-cjs/canvas/region-options.js.map +0 -7
  243. package/dist-cjs/ui/avatar.js +0 -43
  244. package/dist-cjs/ui/avatar.js.map +0 -7
  245. package/dist-cjs/ui/comment-mention.js +0 -42
  246. package/dist-cjs/ui/comment-mention.js.map +0 -7
  247. package/dist-cjs/ui/comment-text.js.map +0 -7
  248. package/dist-cjs/ui/mention-list.js +0 -70
  249. package/dist-cjs/ui/mention-list.js.map +0 -7
  250. package/dist-cjs/ui/mention-suggestion.js +0 -206
  251. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  252. package/dist-cjs/ui/mention.js.map +0 -7
  253. package/dist-cjs/ui/render-markdown.js +0 -63
  254. package/dist-cjs/ui/render-markdown.js.map +0 -7
  255. package/dist-esm/canvas/region-options.mjs +0 -29
  256. package/dist-esm/canvas/region-options.mjs.map +0 -7
  257. package/dist-esm/ui/avatar.mjs +0 -23
  258. package/dist-esm/ui/avatar.mjs.map +0 -7
  259. package/dist-esm/ui/comment-mention.mjs +0 -22
  260. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  261. package/dist-esm/ui/comment-text.mjs +0 -9
  262. package/dist-esm/ui/comment-text.mjs.map +0 -7
  263. package/dist-esm/ui/mention-list.mjs +0 -50
  264. package/dist-esm/ui/mention-list.mjs.map +0 -7
  265. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  266. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  267. package/dist-esm/ui/mention.mjs +0 -11
  268. package/dist-esm/ui/mention.mjs.map +0 -7
  269. package/dist-esm/ui/render-markdown.mjs +0 -45
  270. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  271. package/src/canvas/region-options.ts +0 -57
  272. package/src/ui/avatar.tsx +0 -31
  273. package/src/ui/comment-mention.ts +0 -47
  274. package/src/ui/comment-text.tsx +0 -12
  275. package/src/ui/mention-list.tsx +0 -106
  276. package/src/ui/mention-suggestion.test.ts +0 -18
  277. package/src/ui/mention-suggestion.tsx +0 -285
  278. package/src/ui/mention.tsx +0 -9
  279. package/src/ui/render-markdown.tsx +0 -72
@@ -22,94 +22,98 @@ __export(comments_overlay_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(comments_overlay_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
+ var import_mentions = require("@tldraw/mentions");
25
26
  var import_react = require("react");
26
27
  var import_react_dom = require("react-dom");
27
28
  var import_tldraw = require("tldraw");
28
29
  var import_computeClusterTable = require("../clustering/computeClusterTable");
29
30
  var import_runtime = require("../clustering/runtime");
30
- var import_comment_card = require("../ui/comment-card");
31
31
  var import_comment_composer = require("../ui/comment-composer");
32
32
  var import_comment_extensions = require("../ui/comment-extensions");
33
33
  var import_comment_pin = require("../ui/comment-pin");
34
- var import_comment_thread = require("../ui/comment-thread");
35
34
  var import_count_badge = require("../ui/count-badge");
36
- var import_mention_suggestion = require("../ui/mention-suggestion");
35
+ var import_anchor_lifecycle = require("./anchor-lifecycle");
37
36
  var import_cluster_input = require("./cluster-input");
38
- var import_comment_body = require("./comment-body");
37
+ var import_comment_drafts = require("./comment-drafts");
38
+ var import_comment_mutations = require("./comment-mutations");
39
39
  var import_comment_render = require("./comment-render");
40
40
  var import_comment_store = require("./comment-store");
41
41
  var import_hooks = require("./hooks");
42
42
  var import_license = require("./license");
43
43
  var import_options = require("./options");
44
- var import_region_options = require("./region-options");
44
+ var import_pin_stacking = require("./pin-stacking");
45
45
  var import_state = require("./state");
46
+ var import_thread_preview = require("./thread-preview");
47
+ var import_thread_stack = require("./thread-stack");
46
48
  var import_thread_state = require("./thread-state");
49
+ var import_thread_view = require("./thread-view");
47
50
  const stop = (e) => e.stopPropagation();
48
- const initialOf = (name) => ((0, import_tldraw.getFirstCharacter)(name.trim()) || "?").toUpperCase();
51
+ const isCanvasPanGesture = (editor, e) => e.button !== 0 || editor.inputs.keys.has("Space");
52
+ function forwardPointerEventToCanvas(container, e) {
53
+ const cvs = container.querySelector(".tl-canvas");
54
+ if (!cvs) return;
55
+ const newEvent = new PointerEvent(e.type, e.nativeEvent);
56
+ newEvent.isSpecialRedispatchedEvent = true;
57
+ cvs.dispatchEvent(newEvent);
58
+ }
49
59
  const CLUSTER_FADE_MS = 150;
50
60
  const CLUSTER_EXPAND_ZOOM_MS = 450;
51
- const draftAvatar = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_pin.CommentPin, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
52
- "svg",
53
- {
54
- viewBox: "0 0 24 24",
55
- width: "15",
56
- height: "15",
57
- fill: "none",
58
- stroke: "currentColor",
59
- strokeWidth: "2",
60
- strokeLinecap: "round",
61
- strokeLinejoin: "round",
62
- "aria-hidden": "true",
63
- children: [
64
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 20h9" }),
65
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" })
66
- ]
67
- }
68
- ) });
69
- function toCardProps(comment, props, components) {
70
- const Body = components.CommentBody;
71
- const body = Body ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Body, { comment }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_body.CommentBody, { richText: comment.body, resolveName: props.resolveName });
72
- return {
73
- author: props.resolveName(comment.authorId) ?? import_comment_render.UNKNOWN_AUTHOR,
74
- body,
75
- date: new Date(comment.createdAt).toISOString(),
76
- you: comment.authorId === props.currentUserId,
77
- edited: comment.editedAt != null
78
- };
79
- }
61
+ const CLUSTER_SPLIT_ZOOM_FACTOR = 1.05;
62
+ const CLUSTER_CULL_MARGIN_PX = 120;
63
+ const THREAD_HEADER_BLOCK = 36;
80
64
  function CanvasComments(props) {
81
65
  const commentingEnabled = (0, import_license.useCommentingEnabled)();
82
66
  if (!commentingEnabled) return null;
83
67
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CanvasCommentsLayer, { ...props });
84
68
  }
69
+ function useTrailingPortalHost(container) {
70
+ const [host, setHost] = (0, import_react.useState)(null);
71
+ (0, import_react.useLayoutEffect)(() => {
72
+ const elm = container.ownerDocument.createElement("div");
73
+ elm.style.display = "contents";
74
+ container.appendChild(elm);
75
+ setHost(elm);
76
+ return () => {
77
+ elm.remove();
78
+ setHost(null);
79
+ };
80
+ }, [container]);
81
+ return host;
82
+ }
85
83
  function CanvasCommentsLayer(props) {
86
84
  const editor = (0, import_tldraw.useEditor)();
87
85
  const options = (0, import_options.useCommentingOptions)();
88
86
  const container = (0, import_tldraw.useContainer)();
89
- const regionOptions = (0, import_react.useMemo)(
90
- () => ({ ...import_region_options.DEFAULT_REGION_COMMENT_OPTIONS, ...props.regionOptions }),
91
- [props.regionOptions]
92
- );
93
- (0, import_react.useEffect)(() => (0, import_region_options.setRegionCommentOptions)(editor, regionOptions), [editor, regionOptions]);
87
+ const portalHost = useTrailingPortalHost(container);
94
88
  const layerRef = (0, import_react.useRef)(null);
95
- (0, import_tldraw.usePassThroughWheelEvents)(layerRef);
96
89
  (0, import_tldraw.usePassThroughMouseOverEvents)(layerRef);
97
- const deepLinkHandled = (0, import_react.useRef)(false);
98
- const threads = (0, import_hooks.useCommentThreads)(editor);
90
+ const allThreads = (0, import_hooks.useCommentThreads)(editor);
99
91
  const pending = (0, import_state.usePendingComment)();
92
+ const canComment = (0, import_options.useCanComment)(props.currentUserId);
93
+ const canRenderComposer = canComment || options.components.ComposerFallback != null;
94
+ const showPendingComposer = pending != null && canRenderComposer;
95
+ (0, import_react.useEffect)(() => {
96
+ if (pending && !showPendingComposer) import_state.pendingComment.set(editor, null);
97
+ }, [editor, pending, showPendingComposer]);
100
98
  const openId = (0, import_tldraw.useValue)("open thread id", () => import_state.openThreadId.get(editor), [editor]);
101
- const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor;
102
- const [movedThreadIds, setMovedThreadIds] = (0, import_react.useState)(EMPTY_SET);
99
+ const showResolved = (0, import_tldraw.useValue)("show resolved", () => import_state.sidebarFilters.get(editor).showResolved, [
100
+ editor
101
+ ]);
102
+ const threads = (0, import_react.useMemo)(
103
+ () => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),
104
+ [allThreads, showResolved, openId]
105
+ );
106
+ (0, import_react.useEffect)(() => (0, import_anchor_lifecycle.registerCommentAnchorLifecycle)(editor), [editor]);
107
+ const [heldThreadIds, setHeldThreadIds] = (0, import_react.useState)(EMPTY_SET);
103
108
  const adoptOnRebuild = (0, import_react.useRef)(false);
104
109
  const clusterLeaves = (0, import_tldraw.useValue)(
105
110
  "comment cluster leaves",
106
111
  () => (0, import_cluster_input.collectClusterLeaves)(
107
112
  editor,
108
- threads.filter((thread) => !movedThreadIds.has(thread.id)),
109
- import_state.openThreadId.get(editor),
110
- impreciseShapeAnchor
113
+ threads.filter((thread) => !heldThreadIds.has(thread.id)),
114
+ import_state.openThreadId.get(editor)
111
115
  ),
112
- [editor, threads, impreciseShapeAnchor, movedThreadIds]
116
+ [editor, threads, heldThreadIds]
113
117
  );
114
118
  const clusterZoomBounds = (0, import_tldraw.useValue)(
115
119
  "comment cluster zoom bounds",
@@ -124,21 +128,41 @@ function CanvasCommentsLayer(props) {
124
128
  }, [clusterLeaves, clusterZoomBounds, editor]);
125
129
  const [renderedModel, setRenderedModel] = (0, import_react.useState)(latestModel);
126
130
  let clusterModel = renderedModel;
127
- if (renderedModel !== latestModel && (adoptOnRebuild.current || hasRemovedLeaves(renderedModel.table, latestModel.table))) {
131
+ const pageId = (0, import_tldraw.useValue)("comment cluster page", () => editor.getCurrentPageId(), [editor]);
132
+ const pageRef = (0, import_react.useRef)(pageId);
133
+ if (pageRef.current !== pageId) {
134
+ pageRef.current = pageId;
135
+ adoptOnRebuild.current = false;
136
+ latestModel.runtime.seed(editor.getZoomLevel());
137
+ if (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET);
138
+ setRenderedModel(latestModel);
139
+ clusterModel = latestModel;
140
+ }
141
+ const rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current;
142
+ if (renderedModel !== latestModel && rejoinPending) {
128
143
  adoptOnRebuild.current = false;
129
144
  latestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible());
130
145
  setRenderedModel(latestModel);
131
146
  clusterModel = latestModel;
147
+ } else if (heldThreadIds.size === 0 && renderedModel === latestModel) {
148
+ adoptOnRebuild.current = false;
132
149
  }
133
150
  const newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel);
134
151
  if (newlyMovedIds.length > 0) {
135
- console.debug(`[comments] pins popped out of clustering: ${newlyMovedIds.join(", ")}`);
136
- const next = new Set(movedThreadIds);
152
+ const next = new Set(heldThreadIds);
137
153
  for (const id of newlyMovedIds) next.add(id);
138
- setMovedThreadIds(next);
154
+ setHeldThreadIds(next);
155
+ }
156
+ {
157
+ const latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
158
+ for (const leaf of clusterModel.table.leaves) {
159
+ if (!latestLeafIds.has(leaf.id)) {
160
+ clusterModel.runtime.detachLeaf(leaf.id);
161
+ }
162
+ }
139
163
  }
140
164
  (0, import_react.useEffect)(() => {
141
- if (movedThreadIds.size === 0) return;
165
+ if (heldThreadIds.size === 0) return;
142
166
  let lastZoom = editor.getZoomLevel();
143
167
  return (0, import_tldraw.react)("rejoin moved comment pins on zoom out", () => {
144
168
  const zoom = editor.getZoomLevel();
@@ -146,9 +170,9 @@ function CanvasCommentsLayer(props) {
146
170
  lastZoom = zoom;
147
171
  if (zoom >= prevZoom) return;
148
172
  adoptOnRebuild.current = true;
149
- setMovedThreadIds(EMPTY_SET);
173
+ setHeldThreadIds(EMPTY_SET);
150
174
  });
151
- }, [movedThreadIds, editor]);
175
+ }, [heldThreadIds, editor]);
152
176
  (0, import_react.useEffect)(() => {
153
177
  if (clusterModel === latestModel) return;
154
178
  let lastZoom = editor.getZoomLevel();
@@ -161,90 +185,113 @@ function CanvasCommentsLayer(props) {
161
185
  setRenderedModel(latestModel);
162
186
  });
163
187
  }, [clusterModel, latestModel, editor]);
188
+ const partitionVersion = clusterModel.runtime.version;
164
189
  const orphanThreads = (0, import_react.useMemo)(() => {
165
190
  if (clusterModel === latestModel) return [];
166
- const renderedIds = new Set(clusterModel.table.leaves.map((leaf) => leaf.id));
191
+ const displayed = /* @__PURE__ */ new Set();
192
+ for (const node of clusterModel.runtime.getVisible().values()) {
193
+ for (const member of node.members) displayed.add(member);
194
+ }
167
195
  const latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
168
- return threads.filter((thread) => latestIds.has(thread.id) && !renderedIds.has(thread.id));
169
- }, [clusterModel, latestModel, threads]);
170
- const movedThreads = (0, import_react.useMemo)(
171
- () => threads.filter((thread) => movedThreadIds.has(thread.id) && thread.id !== openId),
172
- [threads, movedThreadIds, openId]
196
+ return threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id));
197
+ }, [clusterModel, latestModel, threads, partitionVersion]);
198
+ const heldThreads = (0, import_react.useMemo)(
199
+ () => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),
200
+ [threads, heldThreadIds, openId]
173
201
  );
174
- const clusterCursor = (0, import_tldraw.useValue)(
175
- "comment cluster cursor",
202
+ (0, import_tldraw.useValue)(
203
+ "comment cluster version",
176
204
  () => {
177
205
  clusterModel.runtime.onCamera(editor.getZoomLevel());
178
- return clusterModel.runtime.k;
206
+ return clusterModel.runtime.version;
179
207
  },
180
208
  [clusterModel, editor]
181
209
  );
182
210
  const visibleNodes = (0, import_react.useMemo)(() => {
183
- const nodes = Array.from(clusterModel.runtime.getVisible().values());
184
- console.debug(
185
- `[comments] cluster cursor k=${clusterCursor} \u2192 re-rendering ${nodes.length} visible nodes`
186
- );
187
- return nodes;
188
- }, [clusterModel, clusterCursor]);
211
+ return Array.from(clusterModel.runtime.getVisible().values());
212
+ }, [clusterModel, partitionVersion]);
189
213
  const fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel);
190
214
  const threadsById = (0, import_react.useMemo)(
191
215
  () => new Map(threads.map((thread) => [thread.id, thread])),
192
216
  [threads]
193
217
  );
218
+ const pinStacks = (0, import_tldraw.useValue)("comment pin stacks", () => (0, import_pin_stacking.computePinStacks)(editor, threads), [
219
+ editor,
220
+ threads
221
+ ]);
194
222
  const openThread = openId ? threadsById.get(openId) : null;
195
223
  const hidden = (0, import_tldraw.useValue)("comments hidden", () => import_state.commentsHidden.get(editor), [editor]);
196
224
  (0, import_react.useEffect)(() => {
197
225
  return () => {
198
226
  import_state.openThreadId.set(editor, null);
227
+ import_state.openStackId.set(editor, null);
199
228
  import_state.pendingComment.set(editor, null);
229
+ import_state.revealThreadRequest.set(editor, null);
200
230
  };
201
231
  }, [editor]);
202
232
  (0, import_react.useEffect)(() => {
203
- if (deepLinkHandled.current) return;
204
- const id = new URLSearchParams(window.location.search).get("comment");
205
- if (!id) {
206
- deepLinkHandled.current = true;
207
- return;
233
+ const key = import_state.openStackId.get(editor);
234
+ if (!key) return;
235
+ for (const id of pinStacks.keys()) {
236
+ const thread = threadsById.get(id);
237
+ if (!thread) continue;
238
+ const point = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor);
239
+ if (point && (0, import_pin_stacking.pinStackKey)(point) === key) return;
208
240
  }
209
- const record = (0, import_comment_store.getCommentRecord)(editor, id);
210
- if (!record) return;
211
- let thread;
212
- if (record.typeName === "comment") {
213
- thread = threadsById.get(record.threadId);
214
- } else {
215
- thread = record;
216
- }
217
- if (!thread) return;
218
- deepLinkHandled.current = true;
219
- revealDeepLinkedThread(
220
- editor,
221
- thread,
222
- clusterModel.table,
223
- clusterZoomBounds,
224
- options,
225
- impreciseShapeAnchor
226
- );
227
- import_state.openThreadId.set(editor, thread.id);
228
- }, [clusterModel.table, clusterZoomBounds, editor, threadsById, impreciseShapeAnchor, options]);
241
+ import_state.openStackId.set(editor, null);
242
+ }, [editor, pinStacks, threadsById]);
243
+ const requestedRevealThread = (0, import_tldraw.useValue)(
244
+ "requested reveal thread",
245
+ () => {
246
+ const id = import_state.revealThreadRequest.get(editor);
247
+ if (!id) return null;
248
+ const record = (0, import_comment_store.getCommentRecord)(editor, id);
249
+ if (!record) return null;
250
+ const thread = record.typeName === "comment" ? (0, import_comment_store.getCommentRecord)(editor, record.threadId) : record;
251
+ return thread?.typeName === "comment-thread" ? thread : null;
252
+ },
253
+ [editor]
254
+ );
255
+ (0, import_react.useEffect)(() => {
256
+ if (!requestedRevealThread) return;
257
+ import_state.revealThreadRequest.set(editor, null);
258
+ import_state.commentsHidden.set(editor, false);
259
+ revealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options);
260
+ import_state.openThreadId.set(editor, requestedRevealThread.id);
261
+ }, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options]);
262
+ const revealClusteredThread = (0, import_react.useCallback)(
263
+ (thread) => {
264
+ revealThreadPin(
265
+ editor,
266
+ thread,
267
+ clusterModel.table,
268
+ clusterZoomBounds,
269
+ options,
270
+ CLUSTER_EXPAND_ZOOM_MS
271
+ );
272
+ import_state.openThreadId.set(editor, thread.id);
273
+ },
274
+ [clusterModel.table, clusterZoomBounds, editor, options]
275
+ );
229
276
  const zoomToClusterSplit = (0, import_react.useCallback)(
230
277
  (node) => {
231
278
  const event = clusterModel.table.events.find((e) => e.result.id === node.id);
232
279
  if (!event || !Number.isFinite(event.zSplit)) return;
233
280
  const zoom = clamp(
234
- event.zSplit * options.clusterSplitZoomFactor,
281
+ event.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
235
282
  clusterZoomBounds.minZoom,
236
283
  clusterZoomBounds.maxZoom
237
284
  );
238
285
  centerOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS);
239
286
  },
240
- [clusterModel, clusterZoomBounds, editor, options]
287
+ [clusterModel, clusterZoomBounds, editor]
241
288
  );
242
289
  (0, import_react.useEffect)(() => {
243
290
  const onKeyDown = (e) => {
244
291
  if (e.key !== "Escape" || import_state.openThreadId.get(editor) === null) return;
245
- if ((0, import_mention_suggestion.isMentionPickerOpen)()) return;
292
+ if ((0, import_mentions.isMentionPickerOpen)()) return;
246
293
  const target = e.target;
247
- if (target && target.closest(".cmt-editing")) return;
294
+ if (target && target.closest(".tlui-cmt-editing")) return;
248
295
  import_state.openThreadId.set(editor, null);
249
296
  e.preventDefault();
250
297
  e.stopPropagation();
@@ -264,79 +311,78 @@ function CanvasCommentsLayer(props) {
264
311
  return () => document.removeEventListener("keydown", onKeyDown, true);
265
312
  }, [editor]);
266
313
  if (hidden) return null;
314
+ const stackGroupOf = (node) => {
315
+ const group = pinStacks.get(node.members[0]);
316
+ if (!group) return null;
317
+ return node.members.every((id) => group.includes(id)) ? group : null;
318
+ };
319
+ const renderedThreadIds = /* @__PURE__ */ new Set();
320
+ if (options.enableClustering) {
321
+ for (const { node } of fadeNodes) {
322
+ if (node.count === 1) renderedThreadIds.add(node.id);
323
+ else if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id);
324
+ }
325
+ for (const thread of orphanThreads) renderedThreadIds.add(thread.id);
326
+ for (const thread of heldThreads) renderedThreadIds.add(thread.id);
327
+ } else {
328
+ for (const thread of threads) renderedThreadIds.add(thread.id);
329
+ }
330
+ if (openThread) renderedThreadIds.add(openThread.id);
331
+ const renderThreadPin = (thread) => {
332
+ const group = pinStacks.get(thread.id);
333
+ if (group) {
334
+ const owner = group.find((id) => renderedThreadIds.has(id));
335
+ if (owner !== thread.id) return null;
336
+ const stackThreads = group.map((id) => threadsById.get(id)).filter((t) => t !== void 0);
337
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_thread_stack.ThreadStackPin, { editor, threads: stackThreads, ...props });
338
+ }
339
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThreadPin, { editor, thread, ...props });
340
+ };
341
+ if (!portalHost) return null;
267
342
  return (0, import_react_dom.createPortal)(
268
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: layerRef, className: "cmt-canvas-layer", children: [
343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: layerRef, className: "tlui-cmt-canvas-layer", children: [
269
344
  options.enableClustering ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
270
345
  fadeNodes.map(({ node, phase }) => {
271
346
  let content;
347
+ const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
272
348
  if (node.count === 1) {
273
349
  const thread = threadsById.get(node.id);
274
350
  if (!thread) return null;
351
+ content = renderThreadPin(thread);
352
+ } else if (stackGroup) {
353
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id));
354
+ content = owner && node.members.includes(owner) ? renderThreadPin(threadsById.get(owner)) : null;
355
+ } else {
275
356
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
276
- ThreadPin,
357
+ ClusterBadge,
277
358
  {
278
359
  editor,
279
- thread,
280
- ...props,
281
- regionOptions
360
+ node,
361
+ onExpand: zoomToClusterSplit,
362
+ onSelectThread: revealClusteredThread,
363
+ threadsById,
364
+ currentUserId: props.currentUserId,
365
+ resolveAuthor: props.resolveAuthor
282
366
  }
283
367
  );
284
- } else {
285
- content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ClusterBadge, { editor, node, onExpand: zoomToClusterSplit });
286
368
  }
287
369
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: clusterFadeClassName(phase), children: content }, `cluster-fade:${node.id}`);
288
370
  }),
289
- orphanThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
290
- ThreadPin,
291
- {
292
- editor,
293
- thread,
294
- ...props,
295
- regionOptions
296
- },
297
- thread.id
298
- )),
299
- movedThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
300
- ThreadPin,
301
- {
302
- editor,
303
- thread,
304
- ...props,
305
- regionOptions
306
- },
307
- thread.id
308
- ))
371
+ orphanThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id)),
372
+ heldThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id))
309
373
  ] }) : (
310
374
  // Clustering off: every thread renders as its own live pin (each returns null when it's
311
375
  // not on the current page or its anchor is missing). The open thread is excluded here and
312
376
  // rendered once below, mirroring how the clustering path keeps it out of the cluster leaves —
313
377
  // otherwise it would mount a second, stacked pin.
314
- threads.filter((thread) => thread.id !== openId).map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
315
- ThreadPin,
316
- {
317
- editor,
318
- thread,
319
- ...props,
320
- regionOptions
321
- },
322
- thread.id
323
- ))
324
- ),
325
- openThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
326
- ThreadPin,
327
- {
328
- editor,
329
- thread: openThread,
330
- ...props,
331
- regionOptions
332
- },
333
- `open:${openThread.id}`
378
+ threads.filter((thread) => thread.id !== openId).map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id))
334
379
  ),
380
+ openThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(openThread) }, `open:${openThread.id}`),
335
381
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionDraftBox, { editor }),
336
- pending?.anchor.type === "region" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionBox, { editor, box: pending.anchor }),
337
- pending && props.currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PendingComposer, { editor, pending, ...props })
382
+ pending?.anchor.type === "region" && showPendingComposer && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionBox, { editor, box: pending.anchor }),
383
+ pending && showPendingComposer && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PendingComposer, { editor, pending, ...props })
338
384
  ] }),
339
- container
385
+ portalHost
340
386
  );
341
387
  }
342
388
  const EMPTY_SET = /* @__PURE__ */ new Set();
@@ -405,7 +451,7 @@ function cancelClusterFadeFrame(frame) {
405
451
  else window.clearTimeout(frame);
406
452
  }
407
453
  function clusterFadeClassName(phase) {
408
- return `cmt-cluster-fade cmt-cluster-fade--${phase}`;
454
+ return `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`;
409
455
  }
410
456
  function findMovedClusteredLeafIds(rendered, latest) {
411
457
  if (rendered.table === latest.table) return [];
@@ -422,11 +468,6 @@ function findMovedClusteredLeafIds(rendered, latest) {
422
468
  }
423
469
  return moved;
424
470
  }
425
- function hasRemovedLeaves(rendered, latest) {
426
- if (rendered.leaves.length === 0) return false;
427
- const latestIds = new Set(latest.leaves.map((leaf) => leaf.id));
428
- return rendered.leaves.some((leaf) => !latestIds.has(leaf.id));
429
- }
430
471
  function getClusterZoomBounds(editor) {
431
472
  const cameraOptions = editor.getCameraOptions();
432
473
  const baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1;
@@ -436,34 +477,36 @@ function getClusterZoomBounds(editor) {
436
477
  maxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom
437
478
  };
438
479
  }
439
- function revealDeepLinkedThread(editor, thread, table, zoomBounds, options, impreciseShapeAnchor) {
480
+ function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
440
481
  if (thread.pageId !== editor.getCurrentPageId()) {
441
482
  editor.setCurrentPage(thread.pageId);
442
483
  }
443
- const point = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor, impreciseShapeAnchor);
484
+ const point = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor);
444
485
  if (!point) return;
445
486
  if (options.enableClustering) {
446
487
  const parentEvent = findDirectParentEvent(table, thread.id);
447
488
  if (parentEvent && Number.isFinite(parentEvent.zSplit) && parentEvent.zSplit <= zoomBounds.maxZoom) {
448
489
  const zoom = clamp(
449
- parentEvent.zSplit * options.clusterSplitZoomFactor,
490
+ parentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
450
491
  zoomBounds.minZoom,
451
492
  zoomBounds.maxZoom
452
493
  );
453
- centerOnPointAtZoom(editor, point, zoom);
494
+ centerOnPointAtZoom(editor, point, zoom, duration);
454
495
  return;
455
496
  }
456
497
  }
457
- editor.centerOnPoint(point, { animation: { duration: 200 } });
498
+ const offset = (0, import_thread_state.commentCenterScreenOffset)(editor) / editor.getZoomLevel();
499
+ editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } });
458
500
  }
459
501
  function findDirectParentEvent(table, threadId) {
460
502
  return table.events.find((event) => event.children.some((child) => child.id === threadId));
461
503
  }
462
504
  function centerOnPointAtZoom(editor, point, zoom, duration = 200) {
463
505
  const viewport = editor.getViewportScreenBounds();
506
+ const offset = (0, import_thread_state.commentCenterScreenOffset)(editor);
464
507
  editor.setCamera(
465
508
  {
466
- x: viewport.w / (2 * zoom) - point.x,
509
+ x: (viewport.w / 2 - offset) / zoom - point.x,
467
510
  y: viewport.h / (2 * zoom) - point.y,
468
511
  z: zoom
469
512
  },
@@ -476,8 +519,16 @@ function clamp(value, min, max) {
476
519
  const ClusterBadge = (0, import_react.memo)(function ClusterBadge2({
477
520
  editor,
478
521
  node,
479
- onExpand
522
+ onExpand,
523
+ onSelectThread,
524
+ threadsById,
525
+ ...props
480
526
  }) {
527
+ const container = (0, import_tldraw.useContainer)();
528
+ const msg = (0, import_tldraw.useTranslation)();
529
+ const badgeRef = (0, import_react.useRef)(null);
530
+ const { previewShown, previewHandlers } = (0, import_thread_preview.useMarkerPreview)(editor, `cluster:${node.id}`);
531
+ (0, import_tldraw.usePassThroughWheelEvents)(badgeRef);
481
532
  const point = (0, import_tldraw.useValue)(
482
533
  "cluster badge point",
483
534
  () => {
@@ -487,46 +538,63 @@ const ClusterBadge = (0, import_react.memo)(function ClusterBadge2({
487
538
  },
488
539
  [editor, node]
489
540
  );
490
- if (!point) return null;
491
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
492
- "div",
493
- {
494
- className: "cmt-canvas-cluster",
495
- style: { left: point.x, top: point.y },
496
- onPointerDown: stop,
497
- onClick: (e) => {
498
- e.stopPropagation();
499
- onExpand(node);
500
- },
501
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_count_badge.CountBadge, { count: node.count })
541
+ const previewThreads = (0, import_react.useMemo)(() => {
542
+ if (!previewShown) return [];
543
+ const threads = [];
544
+ for (const id of node.members) {
545
+ const thread = threadsById.get(id);
546
+ if (thread) threads.push(thread);
502
547
  }
503
- );
548
+ return (0, import_thread_preview.sortThreadsForPreview)(threads);
549
+ }, [previewShown, node.members, threadsById]);
550
+ if (!point) return null;
551
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
553
+ "button",
554
+ {
555
+ ref: badgeRef,
556
+ type: "button",
557
+ className: "tlui-cmt-button tlui-cmt-canvas-cluster",
558
+ style: { left: point.x, top: point.y },
559
+ "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
560
+ onPointerDown: (e) => {
561
+ if (isCanvasPanGesture(editor, e)) {
562
+ forwardPointerEventToCanvas(container, e);
563
+ return;
564
+ }
565
+ e.stopPropagation();
566
+ },
567
+ onClick: (e) => {
568
+ e.stopPropagation();
569
+ onExpand(node);
570
+ },
571
+ ...previewHandlers,
572
+ onFocus: previewHandlers.onPointerEnter,
573
+ onBlur: previewHandlers.onPointerLeave,
574
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_count_badge.CountBadge, { count: node.count })
575
+ }
576
+ ),
577
+ previewShown && previewThreads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
578
+ import_thread_preview.ThreadPreview,
579
+ {
580
+ editor,
581
+ threads: previewThreads,
582
+ container,
583
+ variant: "list",
584
+ point,
585
+ onSelectThread,
586
+ ...previewHandlers,
587
+ ...props
588
+ }
589
+ )
590
+ ] });
504
591
  });
505
592
  function isInInflatedViewport(editor, point) {
506
593
  const viewport = editor.getViewportScreenBounds();
507
- const margin = (0, import_options.getCommentingOptions)(editor).clusterCullMargin;
594
+ const margin = CLUSTER_CULL_MARGIN_PX;
508
595
  return point.x >= -margin && point.y >= -margin && point.x <= viewport.w + margin && point.y <= viewport.h + margin;
509
596
  }
510
- function ThreadPopover({
511
- container,
512
- style,
513
- children
514
- }) {
515
- const ref = (0, import_react.useRef)(null);
516
- (0, import_tldraw.usePassThroughWheelEvents)(ref);
517
- (0, import_tldraw.usePassThroughMouseOverEvents)(ref);
518
- return (0, import_react_dom.createPortal)(
519
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: "cmt-canvas-popover", style, onPointerDown: stop, children }),
520
- container
521
- );
522
- }
523
- function RegionBox({
524
- editor,
525
- box,
526
- movable,
527
- onPreview,
528
- onCommit
529
- }) {
597
+ function RegionBox({ editor, box }) {
530
598
  const rect = (0, import_tldraw.useValue)(
531
599
  "region rect",
532
600
  () => {
@@ -536,38 +604,7 @@ function RegionBox({
536
604
  },
537
605
  [editor, box.x, box.y, box.w, box.h]
538
606
  );
539
- const grabRef = (0, import_react.useRef)(null);
540
- const translated = (e) => {
541
- const g = grabRef.current;
542
- const p = editor.screenToPage({ x: e.clientX, y: e.clientY });
543
- return { ...g.box, x: g.box.x + (p.x - g.page.x), y: g.box.y + (p.y - g.page.y) };
544
- };
545
- const startMove = (e) => {
546
- e.stopPropagation();
547
- grabRef.current = { page: editor.screenToPage({ x: e.clientX, y: e.clientY }), box };
548
- e.currentTarget.setPointerCapture(e.pointerId);
549
- };
550
- const onMove = (e) => {
551
- if (grabRef.current) onPreview?.(translated(e));
552
- };
553
- const endMove = (e) => {
554
- if (!grabRef.current) return;
555
- const bounds = translated(e);
556
- grabRef.current = null;
557
- if (e.currentTarget.hasPointerCapture(e.pointerId))
558
- e.currentTarget.releasePointerCapture(e.pointerId);
559
- onCommit?.(bounds);
560
- };
561
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
562
- "div",
563
- {
564
- className: movable ? "cmt-canvas-region cmt-canvas-region--movable" : "cmt-canvas-region",
565
- style: rect,
566
- onPointerDown: movable ? startMove : void 0,
567
- onPointerMove: movable ? onMove : void 0,
568
- onPointerUp: movable ? endMove : void 0
569
- }
570
- );
607
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-canvas-region", style: rect });
571
608
  }
572
609
  function RegionDraftBox({ editor }) {
573
610
  const box = (0, import_tldraw.useValue)("region draft", () => import_state.regionDraft.get(editor), [editor]);
@@ -580,12 +617,6 @@ const REGION_CORNERS = [
580
617
  { x: 0, y: 1, cursor: "nesw-resize" },
581
618
  { x: 1, y: 1, cursor: "nwse-resize" }
582
619
  ];
583
- const REGION_EDGES = [
584
- { x: 0.5, y: 0, cursor: "ns-resize" },
585
- { x: 1, y: 0.5, cursor: "ew-resize" },
586
- { x: 0.5, y: 1, cursor: "ns-resize" },
587
- { x: 0, y: 0.5, cursor: "ew-resize" }
588
- ];
589
620
  const REGION_HANDLE_MARGIN_PX = 12;
590
621
  function resizeRegion(box, handle, cursor) {
591
622
  const controlsX = handle.x !== 0.5;
@@ -635,7 +666,7 @@ function RegionResizeHandles({
635
666
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: points.map((h) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
636
667
  "div",
637
668
  {
638
- className: "cmt-canvas-region-handle",
669
+ className: "tlui-cmt-canvas-region-handle",
639
670
  style: { left: h.left, top: h.top, cursor: h.cursor },
640
671
  onPointerDown: startResize,
641
672
  onPointerMove: onResize(h),
@@ -647,33 +678,22 @@ function RegionResizeHandles({
647
678
  const ThreadPin = (0, import_react.memo)(function ThreadPin2({
648
679
  editor,
649
680
  thread,
650
- regionOptions,
651
681
  ...props
652
682
  }) {
653
- const {
654
- currentUserId,
655
- resolveName,
656
- onPostComment,
657
- isCommentUnread,
658
- onCommentRead,
659
- getMentionSuggestions,
660
- renderMentionSuggestion
661
- } = props;
683
+ const { resolveAuthor } = props;
662
684
  const options = (0, import_options.useCommentingOptions)();
663
- const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor;
685
+ const canComment = (0, import_options.useCanComment)(props.currentUserId);
664
686
  const container = (0, import_tldraw.useContainer)();
665
- const comments = (0, import_hooks.useThreadComments)(editor, thread.id);
666
687
  const msg = (0, import_tldraw.useTranslation)();
688
+ const comments = (0, import_hooks.useThreadComments)(editor, thread.id);
667
689
  const open = (0, import_tldraw.useValue)("thread open", () => import_state.openThreadId.get(editor) === thread.id, [
668
690
  editor,
669
691
  thread.id
670
692
  ]);
671
- const [reply, setReply] = (0, import_react.useState)(import_comment_extensions.EMPTY_COMMENT);
672
- const [editingId, setEditingId] = (0, import_react.useState)(null);
673
- const [editText, setEditText] = (0, import_react.useState)(import_comment_extensions.EMPTY_COMMENT);
674
693
  const [dragPagePoint, setDragPagePoint] = (0, import_react.useState)(null);
675
694
  const [resizeBounds, setResizeBounds] = (0, import_react.useState)(null);
676
- const [pinHovered, setPinHovered] = (0, import_react.useState)(false);
695
+ const { previewShown, previewHandlers } = (0, import_thread_preview.useMarkerPreview)(editor, `pin:${thread.id}`);
696
+ const previewThreads = (0, import_react.useMemo)(() => [thread], [thread]);
677
697
  const pointerInRegion = (0, import_tldraw.useValue)(
678
698
  "pointer in region",
679
699
  () => {
@@ -686,25 +706,30 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
686
706
  },
687
707
  [editor, thread.anchor, thread.pageId]
688
708
  );
689
- const revealed = open || resizeBounds != null || regionOptions.reveal === "pointer" && pointerInRegion || regionOptions.reveal === "pin-hover" && pinHovered;
709
+ const revealed = open || resizeBounds != null || pointerInRegion;
710
+ const pinCorner = thread.anchor.type === "region" ? (0, import_thread_state.regionAnchorPinCorner)(thread.anchor) : import_thread_state.REGION_PIN_CORNER;
690
711
  const resizeHandles = (0, import_react.useMemo)(
691
- () => regionOptions.resize === "edges" ? REGION_EDGES : REGION_CORNERS.filter(
692
- (c) => c.x !== regionOptions.pinCorner.x || c.y !== regionOptions.pinCorner.y
693
- ),
694
- [regionOptions.resize, regionOptions.pinCorner]
712
+ () => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),
713
+ [pinCorner]
695
714
  );
696
715
  const dragRef = (0, import_react.useRef)(null);
697
716
  const markerRef = (0, import_react.useRef)(null);
717
+ (0, import_tldraw.usePassThroughWheelEvents)(markerRef);
718
+ (0, import_react.useEffect)(() => {
719
+ return () => {
720
+ if (dragRef.current) editor.setHintingShapes([]);
721
+ };
722
+ }, [editor]);
698
723
  (0, import_react.useEffect)(() => {
699
724
  if (!open) return;
700
725
  const onPointerDown = (e) => {
701
726
  const target = e.target;
702
727
  if (!target) return;
703
- if (target.closest(".cmt-canvas-popover")) return;
728
+ if (target.closest(".tlui-cmt-canvas-popover")) return;
704
729
  const marker = markerRef.current;
705
730
  if (marker && marker.contains(target)) return;
706
- if (target.closest(".cmt-canvas-region-handle, .cmt-canvas-region--movable")) return;
707
- if (target.closest(".tlui-menu, [data-radix-popper-content-wrapper], .cmt-mention-popup"))
731
+ if (target.closest(".tlui-cmt-canvas-region-handle, .tlui-cmt-canvas-region--movable")) return;
732
+ if (target.closest(".tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup"))
708
733
  return;
709
734
  import_state.openThreadId.set(editor, null);
710
735
  };
@@ -715,165 +740,68 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
715
740
  "pin point",
716
741
  () => {
717
742
  if (thread.pageId !== editor.getCurrentPageId()) return null;
718
- const pagePoint = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor, impreciseShapeAnchor);
719
- return pagePoint ? editor.pageToViewport(pagePoint) : null;
743
+ const pagePoint = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor);
744
+ if (!pagePoint) return null;
745
+ const viewportPoint = editor.pageToViewport(pagePoint);
746
+ const inset = (0, import_thread_state.impreciseShapePinInset)(editor, thread.anchor);
747
+ return inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint;
720
748
  },
721
- [editor, thread.anchor, thread.pageId, impreciseShapeAnchor]
749
+ [editor, thread.anchor, thread.pageId]
722
750
  );
723
- const visible = point !== null;
724
- (0, import_react.useEffect)(() => {
725
- if (!open || !visible || !isCommentUnread || !onCommentRead) return;
726
- for (const comment of comments) {
727
- if (isCommentUnread(comment.id)) {
728
- onCommentRead(comment.id);
729
- }
730
- }
731
- }, [open, visible, comments, isCommentUnread, onCommentRead]);
732
751
  if (!point) return null;
733
- const postReply = () => {
734
- if ((0, import_comment_extensions.isCommentEmpty)(reply) || !currentUserId) return;
735
- (0, import_state.commitCommentMutation)(editor, () => {
736
- const comment = (0, import_tldraw.createComment)({
737
- threadId: thread.id,
738
- pageId: thread.pageId,
739
- authorId: currentUserId,
740
- body: reply
741
- });
742
- (0, import_comment_store.putCommentRecords)(editor, [comment]);
743
- if (onPostComment) onPostComment(comment);
744
- });
745
- setReply(import_comment_extensions.EMPTY_COMMENT);
746
- };
747
- const toggleResolve = () => {
748
- if (!currentUserId) return;
749
- (0, import_state.commitCommentMutation)(editor, () => {
750
- (0, import_comment_store.putCommentRecords)(editor, [
751
- {
752
- ...thread,
753
- resolved: thread.resolved ? null : { at: Date.now(), by: currentUserId }
754
- }
755
- ]);
756
- });
757
- };
758
- const deleteThread = () => {
759
- import_state.openThreadId.set(editor, null);
760
- (0, import_state.commitCommentMutation)(
761
- editor,
762
- () => (0, import_comment_store.removeCommentRecords)(editor, [thread.id, ...comments.map((c) => c.id)])
763
- );
764
- };
765
- const startEdit = (comment) => {
766
- setEditingId(comment.id);
767
- setEditText(comment.body);
768
- };
769
- const saveEdit = () => {
770
- const comment = comments.find((c) => c.id === editingId);
771
- if (!comment || (0, import_comment_extensions.isCommentEmpty)(editText)) return;
772
- (0, import_state.commitCommentMutation)(editor, () => {
773
- (0, import_comment_store.putCommentRecords)(editor, [{ ...comment, body: editText, editedAt: Date.now() }]);
774
- });
775
- setEditingId(null);
776
- };
777
- const renderComment = (card, index) => {
778
- const comment = comments[index];
779
- if (editingId === comment.id) {
780
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
781
- "div",
782
- {
783
- className: "cmt-editing",
784
- onKeyDown: (e) => {
785
- if (e.key === "Escape") {
786
- setEditingId(null);
787
- e.stopPropagation();
788
- }
789
- },
790
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
791
- import_comment_composer.CommentComposer,
792
- {
793
- author: card.author,
794
- placeholder: msg("comments.edit-placeholder"),
795
- value: editText,
796
- onChange: setEditText,
797
- onSubmit: saveEdit,
798
- sendLabel: msg("comments.save"),
799
- disabled: (0, import_comment_extensions.isCommentEmpty)(editText),
800
- getMentionSuggestions,
801
- renderMentionSuggestion,
802
- autoFocus: true
803
- }
804
- )
805
- }
806
- );
807
- }
808
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
809
- import_comment_card.CommentCard,
810
- {
811
- ...card,
812
- actions: comment.authorId === currentUserId ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
813
- "button",
814
- {
815
- className: "cmt-thread__action",
816
- title: msg("comments.edit"),
817
- onClick: () => startEdit(comment),
818
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "dots-horizontal", label: msg("comments.edit"), small: true })
819
- }
820
- ) : void 0
821
- }
822
- );
823
- };
824
- const headerActions = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
825
- currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
826
- "button",
827
- {
828
- className: "cmt-thread__action",
829
- title: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
830
- onClick: toggleResolve,
831
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
832
- import_tldraw.TldrawUiIcon,
833
- {
834
- icon: "check",
835
- label: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
836
- small: true
837
- }
838
- )
839
- }
840
- ),
841
- currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
842
- "button",
843
- {
844
- className: "cmt-thread__action",
845
- title: msg("comments.delete"),
846
- onClick: deleteThread,
847
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "trash", label: msg("comments.delete"), small: true })
848
- }
849
- ),
850
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
851
- "button",
852
- {
853
- className: "cmt-thread__action",
854
- title: msg("comments.dismiss"),
855
- onClick: () => import_state.openThreadId.set(editor, null),
856
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "cross-2", label: msg("comments.dismiss"), small: true })
857
- }
858
- )
859
- ] });
860
752
  const PinContent = options.components.PinContent;
861
- const pinContent = PinContent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinContent, { thread, comments }) : initialOf(resolveName(thread.createdBy) ?? import_comment_render.UNKNOWN_AUTHOR);
753
+ const threadAuthor = resolveAuthor(thread.createdBy);
754
+ const pinContent = PinContent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinContent, { thread, comments }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_mentions.Avatar, { author: threadAuthor ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR });
755
+ const pinLabel = msg(
756
+ thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
757
+ ).replace("{name}", threadAuthor?.name ?? import_comment_render.UNKNOWN_AUTHOR);
862
758
  const isRegion = thread.anchor.type === "region";
863
- const pinMovable = regionOptions.move !== "body";
864
- const bodyMovable = regionOptions.move !== "pin";
865
759
  const startDrag = (e) => {
760
+ if (isCanvasPanGesture(editor, e)) {
761
+ forwardPointerEventToCanvas(container, e);
762
+ return;
763
+ }
866
764
  e.stopPropagation();
867
- dragRef.current = { startX: e.clientX, startY: e.clientY, moved: false };
765
+ const grabPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
766
+ const anchorPage = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor);
767
+ const inset = (0, import_thread_state.impreciseShapePinInset)(editor, thread.anchor);
768
+ if (anchorPage && inset) {
769
+ const zoom = editor.getZoomLevel();
770
+ anchorPage.x += inset.x / zoom;
771
+ anchorPage.y += inset.y / zoom;
772
+ }
773
+ dragRef.current = {
774
+ startX: e.clientX,
775
+ startY: e.clientY,
776
+ moved: false,
777
+ offsetX: anchorPage ? anchorPage.x - grabPage.x : 0,
778
+ offsetY: anchorPage ? anchorPage.y - grabPage.y : 0
779
+ };
868
780
  e.currentTarget.setPointerCapture(e.pointerId);
869
781
  };
870
782
  const onDrag = (e) => {
871
783
  const drag = dragRef.current;
872
784
  if (!drag) return;
873
- if (isRegion && !pinMovable) return;
785
+ if (!canComment) return;
874
786
  if (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;
875
787
  drag.moved = true;
876
- setDragPagePoint(editor.screenToPage({ x: e.clientX, y: e.clientY }));
788
+ const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
789
+ const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
790
+ setDragPagePoint(pagePoint);
791
+ if (!isRegion) {
792
+ const hit = (0, import_thread_state.commentTargetShapeAt)(editor, pagePoint);
793
+ editor.setHintingShapes(hit ? [hit.id] : []);
794
+ }
795
+ };
796
+ const cancelDrag = (e) => {
797
+ const drag = dragRef.current;
798
+ dragRef.current = null;
799
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
800
+ e.currentTarget.releasePointerCapture(e.pointerId);
801
+ }
802
+ if (!drag) return;
803
+ setDragPagePoint(null);
804
+ editor.setHintingShapes([]);
877
805
  };
878
806
  const endDrag = (e) => {
879
807
  const drag = dragRef.current;
@@ -882,55 +810,59 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
882
810
  e.currentTarget.releasePointerCapture(e.pointerId);
883
811
  }
884
812
  if (!drag) return;
813
+ editor.setHintingShapes([]);
885
814
  if (!drag.moved) {
886
815
  import_state.openThreadId.set(editor, import_state.openThreadId.get(editor) === thread.id ? null : thread.id);
887
816
  return;
888
817
  }
889
- const pagePoint = editor.screenToPage({ x: e.clientX, y: e.clientY });
818
+ const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
819
+ const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
890
820
  setDragPagePoint(null);
891
821
  let anchor;
892
822
  if (thread.anchor.type === "region") {
893
823
  anchor = {
894
824
  ...thread.anchor,
895
- x: pagePoint.x - regionOptions.pinCorner.x * thread.anchor.w,
896
- y: pagePoint.y - regionOptions.pinCorner.y * thread.anchor.h
825
+ x: pagePoint.x - pinCorner.x * thread.anchor.w,
826
+ y: pagePoint.y - pinCorner.y * thread.anchor.h
897
827
  };
898
828
  } else {
899
- const hit = editor.getShapeAtPoint(pagePoint, { hitInside: true });
900
- anchor = hit ? (0, import_thread_state.shapeAnchorAt)(editor, hit.id, pagePoint, e.altKey) : { type: "point", x: pagePoint.x, y: pagePoint.y };
829
+ const hit = (0, import_thread_state.commentTargetShapeAt)(editor, pagePoint);
830
+ anchor = hit ? (0, import_thread_state.shapeAnchorAt)(
831
+ editor,
832
+ hit.id,
833
+ pagePoint,
834
+ (0, import_options.getCommentingOptions)(editor).shouldBePrecise(editor, {
835
+ shapeId: hit.id,
836
+ point: pagePoint,
837
+ altKey: e.altKey
838
+ })
839
+ ) : { type: "point", x: pagePoint.x, y: pagePoint.y };
901
840
  }
902
- (0, import_state.commitCommentMutation)(editor, () => (0, import_comment_store.putCommentRecords)(editor, [{ ...thread, anchor }]), "drag");
841
+ (0, import_comment_mutations.commitCommentMutation)(editor, () => (0, import_comment_mutations.putRecordsInCommit)(editor, [{ ...thread, anchor }]), "drag");
903
842
  };
904
- const livePinPage = resizeBounds ? (0, import_thread_state.regionPinPoint)(resizeBounds, regionOptions.pinCorner) : dragPagePoint;
905
- const renderPoint = livePinPage ? editor.pageToViewport(livePinPage) : point;
843
+ const livePinPage = resizeBounds ? (0, import_thread_state.regionPinPoint)(resizeBounds, pinCorner) : dragPagePoint;
844
+ const renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point;
845
+ const renderPoint = isRegion ? { x: renderPointBase.x - 17, y: renderPointBase.y + 17 } : renderPointBase;
906
846
  const regionAnchor = thread.anchor.type === "region" ? thread.anchor : void 0;
907
847
  const movedRegion = regionAnchor && dragPagePoint ? {
908
848
  ...regionAnchor,
909
- x: dragPagePoint.x - regionOptions.pinCorner.x * regionAnchor.w,
910
- y: dragPagePoint.y - regionOptions.pinCorner.y * regionAnchor.h
849
+ x: dragPagePoint.x - pinCorner.x * regionAnchor.w,
850
+ y: dragPagePoint.y - pinCorner.y * regionAnchor.h
911
851
  } : regionAnchor;
912
852
  const regionBoxBounds = resizeBounds ?? movedRegion;
913
853
  const commitResize = (bounds) => {
914
854
  setResizeBounds(null);
915
- editor.run(
916
- () => (0, import_comment_store.putCommentRecords)(editor, [{ ...thread, anchor: { type: "region", ...bounds } }]),
917
- {
918
- history: "ignore"
919
- }
855
+ if (!canComment) return;
856
+ (0, import_comment_mutations.commitCommentMutation)(
857
+ editor,
858
+ // Spread the existing anchor first so the region's pin corner survives a resize.
859
+ () => (0, import_comment_mutations.putRecordsInCommit)(editor, [{ ...thread, anchor: { ...regionAnchor, ...bounds } }]),
860
+ "drag"
920
861
  );
921
862
  };
922
863
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
923
- regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
924
- RegionBox,
925
- {
926
- editor,
927
- box: regionBoxBounds,
928
- movable: bodyMovable && !dragPagePoint,
929
- onPreview: setResizeBounds,
930
- onCommit: commitResize
931
- }
932
- ),
933
- regionBoxBounds && revealed && !dragPagePoint && regionOptions.resize !== "none" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
864
+ regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionBox, { editor, box: regionBoxBounds }),
865
+ regionBoxBounds && revealed && !dragPagePoint && canComment && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
934
866
  RegionResizeHandles,
935
867
  {
936
868
  editor,
@@ -943,51 +875,59 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
943
875
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
944
876
  "div",
945
877
  {
946
- className: open ? "cmt-canvas-pin cmt-canvas-pin--open" : "cmt-canvas-pin",
878
+ className: [
879
+ "tlui-cmt-canvas-pin",
880
+ open && "tlui-cmt-canvas-pin--open",
881
+ dragPagePoint && "tlui-cmt-canvas-pin--dragging"
882
+ ].filter(Boolean).join(" "),
947
883
  style: { left: renderPoint.x, top: renderPoint.y },
948
884
  children: [
949
885
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
950
- "div",
886
+ "button",
951
887
  {
952
888
  ref: markerRef,
953
- className: "cmt-canvas-pin__marker",
889
+ type: "button",
890
+ className: "tlui-cmt-button tlui-cmt-canvas-pin__marker",
891
+ "aria-label": pinLabel,
892
+ "aria-expanded": open,
954
893
  onPointerDown: startDrag,
955
894
  onPointerMove: onDrag,
956
895
  onPointerUp: endDrag,
957
- onPointerEnter: () => setPinHovered(true),
958
- onPointerLeave: () => setPinHovered(false),
896
+ onPointerCancel: cancelDrag,
897
+ onClick: (e) => {
898
+ if (e.detail !== 0) return;
899
+ import_state.openThreadId.set(editor, import_state.openThreadId.get(editor) === thread.id ? null : thread.id);
900
+ },
901
+ onPointerEnter: previewHandlers.onPointerEnter,
902
+ onPointerLeave: previewHandlers.onPointerLeave,
903
+ onFocus: previewHandlers.onPointerEnter,
904
+ onBlur: previewHandlers.onPointerLeave,
959
905
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_pin.CommentPin, { resolved: thread.resolved != null, open, children: pinContent })
960
906
  }
961
907
  ),
962
908
  open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
963
- ThreadPopover,
909
+ import_thread_view.ThreadPopover,
964
910
  {
965
911
  container,
966
- style: { left: renderPoint.x + 36, top: renderPoint.y - 28 },
967
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
968
- import_comment_thread.CommentThread,
969
- {
970
- header: msg("comments.thread-title"),
971
- headerActions,
972
- renderComment,
973
- comments: comments.map((c) => toCardProps(c, props, options.components)),
974
- resolvedBanner: thread.resolved ? msg("comments.resolved-by").replace(
975
- "{name}",
976
- resolveName(thread.resolved.by) ?? import_comment_render.UNKNOWN_AUTHOR
977
- ) : void 0,
978
- composer: currentUserId && !thread.resolved ? {
979
- author: resolveName(currentUserId) ?? import_comment_render.UNKNOWN_AUTHOR,
980
- placeholder: msg("comments.reply-placeholder"),
981
- sendLabel: msg("comments.send"),
982
- value: reply,
983
- onChange: setReply,
984
- onSubmit: postReply,
985
- disabled: (0, import_comment_extensions.isCommentEmpty)(reply),
986
- getMentionSuggestions,
987
- renderMentionSuggestion
988
- } : void 0
989
- }
990
- )
912
+ style: {
913
+ left: renderPoint.x + import_thread_view.POPOVER_OFFSET.thread.x,
914
+ top: renderPoint.y + import_thread_view.POPOVER_OFFSET.thread.y - THREAD_HEADER_BLOCK
915
+ },
916
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_thread_view.ThreadView, { editor, thread, ...props })
917
+ }
918
+ ),
919
+ previewShown && !dragPagePoint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
920
+ import_thread_preview.ThreadPreview,
921
+ {
922
+ editor,
923
+ threads: previewThreads,
924
+ container,
925
+ variant: "thread",
926
+ point: renderPoint,
927
+ onSelectThread: () => import_state.openThreadId.set(editor, thread.id),
928
+ ...previewHandlers,
929
+ currentUserId: props.currentUserId,
930
+ resolveAuthor
991
931
  }
992
932
  )
993
933
  ]
@@ -999,12 +939,18 @@ function PendingComposer({
999
939
  editor,
1000
940
  pending,
1001
941
  currentUserId,
1002
- resolveName,
942
+ resolveAuthor,
1003
943
  onPostComment,
1004
944
  getMentionSuggestions,
1005
945
  renderMentionSuggestion
1006
946
  }) {
1007
- const [text, setText] = (0, import_react.useState)(import_comment_extensions.EMPTY_COMMENT);
947
+ const ComposerFallback = (0, import_options.useCommentingOptions)().components.ComposerFallback;
948
+ const canComment = (0, import_options.useCanComment)(currentUserId);
949
+ const me = currentUserId ? resolveAuthor(currentUserId) : void 0;
950
+ const draftAvatar = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_pin.CommentPin, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_mentions.Avatar, { author: me ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR }) });
951
+ const [text, setText] = (0, import_react.useState)(
952
+ () => (0, import_comment_drafts.getCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT) ?? import_comment_extensions.EMPTY_COMMENT
953
+ );
1008
954
  const ref = (0, import_react.useRef)(null);
1009
955
  const msg = (0, import_tldraw.useTranslation)();
1010
956
  const container = (0, import_tldraw.useContainer)();
@@ -1019,7 +965,7 @@ function PendingComposer({
1019
965
  const el = ref.current;
1020
966
  const target = e.target;
1021
967
  if (!el || !target) return;
1022
- if (el.contains(target) || target.closest(".cmt-mention-popup")) return;
968
+ if (el.contains(target) || target.closest(".tlui-cmt-mention-popup")) return;
1023
969
  import_state.pendingComment.set(editor, null);
1024
970
  };
1025
971
  document.addEventListener("pointerdown", onPointerDown, true);
@@ -1027,7 +973,7 @@ function PendingComposer({
1027
973
  }, [editor]);
1028
974
  const submit = () => {
1029
975
  if ((0, import_comment_extensions.isCommentEmpty)(text) || !currentUserId) return;
1030
- (0, import_state.commitCommentMutation)(editor, () => {
976
+ (0, import_comment_mutations.commitCommentMutation)(editor, () => {
1031
977
  const pageId = editor.getCurrentPageId();
1032
978
  const thread = (0, import_tldraw.createCommentThread)({
1033
979
  pageId,
@@ -1040,10 +986,11 @@ function PendingComposer({
1040
986
  authorId: currentUserId,
1041
987
  body: text
1042
988
  });
1043
- (0, import_comment_store.putCommentRecords)(editor, [thread, comment]);
989
+ (0, import_comment_mutations.putRecordsInCommit)(editor, [thread, comment]);
1044
990
  if (onPostComment) onPostComment(comment);
1045
991
  });
1046
992
  setText(import_comment_extensions.EMPTY_COMMENT);
993
+ (0, import_comment_drafts.clearCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT);
1047
994
  import_state.pendingComment.set(editor, null);
1048
995
  };
1049
996
  return (0, import_react_dom.createPortal)(
@@ -1051,28 +998,36 @@ function PendingComposer({
1051
998
  "div",
1052
999
  {
1053
1000
  ref,
1054
- className: "cmt-canvas-composer",
1001
+ className: [
1002
+ "tlui-cmt-canvas-composer",
1003
+ pending.anchor.type === "region" && "tlui-cmt-canvas-composer--region",
1004
+ !canComment && "tlui-cmt-canvas-composer--fallback"
1005
+ ].filter(Boolean).join(" "),
1055
1006
  style: { left: point.x, top: point.y },
1056
1007
  onPointerDown: stop,
1008
+ onContextMenu: stop,
1057
1009
  onKeyDown: (e) => {
1058
- if (e.key === "Escape" && !(0, import_mention_suggestion.isMentionPickerOpen)()) import_state.pendingComment.set(editor, null);
1010
+ if (e.key === "Escape" && !(0, import_mentions.isMentionPickerOpen)()) import_state.pendingComment.set(editor, null);
1059
1011
  },
1060
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1012
+ children: canComment ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1061
1013
  import_comment_composer.CommentComposer,
1062
1014
  {
1063
- author: currentUserId ? resolveName(currentUserId) ?? import_comment_render.UNKNOWN_AUTHOR : "",
1015
+ author: me ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR,
1064
1016
  placeholder: msg("comments.add-placeholder"),
1065
1017
  sendLabel: msg("comments.send"),
1066
1018
  value: text,
1067
- onChange: setText,
1019
+ onChange: (value) => {
1020
+ setText(value);
1021
+ (0, import_comment_drafts.saveCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT, value);
1022
+ },
1068
1023
  onSubmit: submit,
1069
- disabled: (0, import_comment_extensions.isCommentEmpty)(text),
1024
+ disabled: (0, import_comment_extensions.isCommentEmpty)(text) || !currentUserId,
1070
1025
  getMentionSuggestions,
1071
1026
  renderMentionSuggestion,
1072
1027
  autoFocus: true,
1073
1028
  leading: draftAvatar
1074
1029
  }
1075
- )
1030
+ ) : ComposerFallback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComposerFallback, { context: "pending" })
1076
1031
  }
1077
1032
  ),
1078
1033
  container