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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1040 -230
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-drafts.js +54 -0
  8. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  9. package/dist-cjs/canvas/comment-mutations.js +123 -0
  10. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  11. package/dist-cjs/canvas/comment-reactions.js +163 -0
  12. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  13. package/dist-cjs/canvas/comment-render.js +5 -3
  14. package/dist-cjs/canvas/comment-render.js.map +2 -2
  15. package/dist-cjs/canvas/comment-store.js +19 -9
  16. package/dist-cjs/canvas/comment-store.js.map +2 -2
  17. package/dist-cjs/canvas/comment-tool.js +64 -14
  18. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  19. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  20. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  21. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  22. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overlay.js +377 -437
  24. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  25. package/dist-cjs/canvas/comments-sidebar.js +34 -31
  26. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  27. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  28. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  29. package/dist-cjs/canvas/context.js +17 -0
  30. package/dist-cjs/canvas/context.js.map +7 -0
  31. package/dist-cjs/canvas/hooks.js +22 -3
  32. package/dist-cjs/canvas/hooks.js.map +2 -2
  33. package/dist-cjs/canvas/options.js +19 -2
  34. package/dist-cjs/canvas/options.js.map +2 -2
  35. package/dist-cjs/canvas/pin-stacking.js +56 -0
  36. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  37. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  38. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  39. package/dist-cjs/canvas/state.js +28 -14
  40. package/dist-cjs/canvas/state.js.map +2 -2
  41. package/dist-cjs/canvas/thread-preview.js +185 -0
  42. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  43. package/dist-cjs/canvas/thread-stack.js +199 -0
  44. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  45. package/dist-cjs/canvas/thread-state.js +67 -21
  46. package/dist-cjs/canvas/thread-state.js.map +2 -2
  47. package/dist-cjs/canvas/thread-view.js +397 -0
  48. package/dist-cjs/canvas/thread-view.js.map +7 -0
  49. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  50. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  51. package/dist-cjs/clustering/runtime.js +9 -9
  52. package/dist-cjs/clustering/runtime.js.map +2 -2
  53. package/dist-cjs/clustering/types.js.map +1 -1
  54. package/dist-cjs/index.d.ts +763 -353
  55. package/dist-cjs/index.js +46 -28
  56. package/dist-cjs/index.js.map +2 -2
  57. package/dist-cjs/ui/byline.js +3 -1
  58. package/dist-cjs/ui/byline.js.map +2 -2
  59. package/dist-cjs/ui/comment-card.js +12 -5
  60. package/dist-cjs/ui/comment-card.js.map +2 -2
  61. package/dist-cjs/ui/comment-composer.js +112 -17
  62. package/dist-cjs/ui/comment-composer.js.map +2 -2
  63. package/dist-cjs/ui/comment-pin.js +20 -2
  64. package/dist-cjs/ui/comment-pin.js.map +2 -2
  65. package/dist-cjs/ui/comment-thread.js +4 -2
  66. package/dist-cjs/ui/comment-thread.js.map +2 -2
  67. package/dist-cjs/ui/comments-list.js +22 -10
  68. package/dist-cjs/ui/comments-list.js.map +2 -2
  69. package/dist-cjs/ui/count-badge.js +3 -2
  70. package/dist-cjs/ui/count-badge.js.map +2 -2
  71. package/dist-cjs/ui/emoji-picker.js +54 -0
  72. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  73. package/dist-cjs/ui/format-time.js +4 -1
  74. package/dist-cjs/ui/format-time.js.map +2 -2
  75. package/dist-cjs/ui/reaction-picker.js +73 -0
  76. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  77. package/dist-cjs/ui/reaction.js +50 -4
  78. package/dist-cjs/ui/reaction.js.map +2 -2
  79. package/dist-cjs/ui/reactions.js +20 -15
  80. package/dist-cjs/ui/reactions.js.map +2 -2
  81. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  82. package/dist-cjs/ui/reply-count.js.map +7 -0
  83. package/dist-cjs/ui/send-button.js +20 -1
  84. package/dist-cjs/ui/send-button.js.map +2 -2
  85. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  86. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  87. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  89. package/dist-esm/canvas/cluster-input.mjs +2 -2
  90. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  91. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  92. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  93. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  94. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  95. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  96. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  97. package/dist-esm/canvas/comment-render.mjs +5 -3
  98. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  99. package/dist-esm/canvas/comment-store.mjs +19 -9
  100. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  101. package/dist-esm/canvas/comment-tool.mjs +67 -17
  102. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  103. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  104. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  105. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  106. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  107. package/dist-esm/canvas/comments-overlay.mjs +398 -442
  108. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  109. package/dist-esm/canvas/comments-sidebar.mjs +37 -34
  110. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  112. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  113. package/dist-esm/canvas/context.mjs +1 -0
  114. package/dist-esm/canvas/context.mjs.map +7 -0
  115. package/dist-esm/canvas/hooks.mjs +28 -5
  116. package/dist-esm/canvas/hooks.mjs.map +2 -2
  117. package/dist-esm/canvas/options.mjs +21 -3
  118. package/dist-esm/canvas/options.mjs.map +2 -2
  119. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  120. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  121. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  122. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  123. package/dist-esm/canvas/state.mjs +29 -19
  124. package/dist-esm/canvas/state.mjs.map +2 -2
  125. package/dist-esm/canvas/thread-preview.mjs +170 -0
  126. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  127. package/dist-esm/canvas/thread-stack.mjs +190 -0
  128. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  129. package/dist-esm/canvas/thread-state.mjs +71 -22
  130. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-view.mjs +402 -0
  132. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  133. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  134. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  135. package/dist-esm/clustering/runtime.mjs +9 -9
  136. package/dist-esm/clustering/runtime.mjs.map +2 -2
  137. package/dist-esm/index.d.mts +763 -353
  138. package/dist-esm/index.mjs +87 -41
  139. package/dist-esm/index.mjs.map +2 -2
  140. package/dist-esm/ui/byline.mjs +3 -1
  141. package/dist-esm/ui/byline.mjs.map +2 -2
  142. package/dist-esm/ui/comment-card.mjs +12 -5
  143. package/dist-esm/ui/comment-card.mjs.map +2 -2
  144. package/dist-esm/ui/comment-composer.mjs +122 -16
  145. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  146. package/dist-esm/ui/comment-pin.mjs +20 -2
  147. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  148. package/dist-esm/ui/comment-thread.mjs +4 -2
  149. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  150. package/dist-esm/ui/comments-list.mjs +39 -25
  151. package/dist-esm/ui/comments-list.mjs.map +2 -2
  152. package/dist-esm/ui/count-badge.mjs +3 -2
  153. package/dist-esm/ui/count-badge.mjs.map +2 -2
  154. package/dist-esm/ui/emoji-picker.mjs +34 -0
  155. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  156. package/dist-esm/ui/format-time.mjs +4 -1
  157. package/dist-esm/ui/format-time.mjs.map +2 -2
  158. package/dist-esm/ui/reaction-picker.mjs +60 -0
  159. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  160. package/dist-esm/ui/reaction.mjs +51 -5
  161. package/dist-esm/ui/reaction.mjs.map +2 -2
  162. package/dist-esm/ui/reactions.mjs +21 -16
  163. package/dist-esm/ui/reactions.mjs.map +2 -2
  164. package/dist-esm/ui/reply-count.mjs +9 -0
  165. package/dist-esm/ui/reply-count.mjs.map +7 -0
  166. package/dist-esm/ui/send-button.mjs +20 -1
  167. package/dist-esm/ui/send-button.mjs.map +2 -2
  168. package/dist-esm/ui/tooltip-button.mjs +12 -0
  169. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  170. package/package.json +6 -6
  171. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  172. package/src/canvas/anchor-lifecycle.ts +205 -0
  173. package/src/canvas/canvas.css +439 -24
  174. package/src/canvas/cluster-input.test.ts +31 -23
  175. package/src/canvas/cluster-input.ts +3 -4
  176. package/src/canvas/comment-drafts.test.ts +55 -0
  177. package/src/canvas/comment-drafts.ts +46 -0
  178. package/src/canvas/comment-mutations.test.ts +410 -0
  179. package/src/canvas/comment-mutations.ts +288 -0
  180. package/src/canvas/comment-reactions.test.ts +132 -0
  181. package/src/canvas/comment-reactions.tsx +245 -0
  182. package/src/canvas/comment-render.ts +9 -5
  183. package/src/canvas/comment-store.ts +67 -27
  184. package/src/canvas/comment-tool.test.ts +162 -0
  185. package/src/canvas/comment-tool.tsx +105 -23
  186. package/src/canvas/comments-filter-menu.tsx +23 -32
  187. package/src/canvas/comments-overflow-menu.tsx +12 -19
  188. package/src/canvas/comments-overlay.tsx +602 -562
  189. package/src/canvas/comments-sidebar.test.ts +45 -0
  190. package/src/canvas/comments-sidebar.tsx +75 -57
  191. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  192. package/src/canvas/context.ts +47 -0
  193. package/src/canvas/hooks.ts +52 -11
  194. package/src/canvas/options.test.ts +52 -2
  195. package/src/canvas/options.ts +133 -9
  196. package/src/canvas/pin-stacking.test.ts +114 -0
  197. package/src/canvas/pin-stacking.ts +57 -0
  198. package/src/canvas/sidebar-filters.ts +4 -3
  199. package/src/canvas/state.ts +92 -35
  200. package/src/canvas/thread-preview.test.ts +91 -0
  201. package/src/canvas/thread-preview.tsx +304 -0
  202. package/src/canvas/thread-stack.tsx +252 -0
  203. package/src/canvas/thread-state.test.ts +209 -0
  204. package/src/canvas/thread-state.ts +137 -36
  205. package/src/canvas/thread-view.tsx +516 -0
  206. package/src/clustering/computeClusterTable.test.ts +24 -23
  207. package/src/clustering/computeClusterTable.ts +5 -5
  208. package/src/clustering/runtime.test.ts +34 -0
  209. package/src/clustering/runtime.ts +20 -15
  210. package/src/clustering/types.ts +9 -9
  211. package/src/index.ts +76 -40
  212. package/src/ui/byline.tsx +4 -2
  213. package/src/ui/comment-card.tsx +17 -7
  214. package/src/ui/comment-composer.tsx +160 -13
  215. package/src/ui/comment-pin.tsx +22 -2
  216. package/src/ui/comment-thread.tsx +6 -1
  217. package/src/ui/comments-list.test.ts +31 -0
  218. package/src/ui/comments-list.tsx +32 -12
  219. package/src/ui/comments.css +489 -204
  220. package/src/ui/count-badge.tsx +9 -2
  221. package/src/ui/emoji-picker.test.ts +25 -0
  222. package/src/ui/emoji-picker.tsx +76 -0
  223. package/src/ui/format-time.ts +7 -2
  224. package/src/ui/reaction-picker.tsx +109 -0
  225. package/src/ui/reaction.tsx +116 -4
  226. package/src/ui/reactions.tsx +68 -13
  227. package/src/ui/reply-count.ts +16 -0
  228. package/src/ui/send-button.tsx +18 -3
  229. package/src/ui/tooltip-button.tsx +20 -0
  230. package/dist-cjs/canvas/region-options.js +0 -49
  231. package/dist-cjs/canvas/region-options.js.map +0 -7
  232. package/dist-cjs/ui/avatar.js +0 -51
  233. package/dist-cjs/ui/avatar.js.map +0 -7
  234. package/dist-cjs/ui/comment-mention.js +0 -42
  235. package/dist-cjs/ui/comment-mention.js.map +0 -7
  236. package/dist-cjs/ui/comment-text.js.map +0 -7
  237. package/dist-cjs/ui/mention-list.js +0 -70
  238. package/dist-cjs/ui/mention-list.js.map +0 -7
  239. package/dist-cjs/ui/mention-suggestion.js +0 -206
  240. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  241. package/dist-cjs/ui/mention.js.map +0 -7
  242. package/dist-cjs/ui/render-markdown.js +0 -63
  243. package/dist-cjs/ui/render-markdown.js.map +0 -7
  244. package/dist-esm/canvas/region-options.mjs +0 -29
  245. package/dist-esm/canvas/region-options.mjs.map +0 -7
  246. package/dist-esm/ui/avatar.mjs +0 -31
  247. package/dist-esm/ui/avatar.mjs.map +0 -7
  248. package/dist-esm/ui/comment-mention.mjs +0 -22
  249. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  250. package/dist-esm/ui/comment-text.mjs +0 -9
  251. package/dist-esm/ui/comment-text.mjs.map +0 -7
  252. package/dist-esm/ui/mention-list.mjs +0 -50
  253. package/dist-esm/ui/mention-list.mjs.map +0 -7
  254. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  255. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  256. package/dist-esm/ui/mention.mjs +0 -11
  257. package/dist-esm/ui/mention.mjs.map +0 -7
  258. package/dist-esm/ui/render-markdown.mjs +0 -45
  259. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  260. package/src/canvas/region-options.ts +0 -57
  261. package/src/ui/avatar.tsx +0 -38
  262. package/src/ui/comment-mention.ts +0 -47
  263. package/src/ui/comment-text.tsx +0 -12
  264. package/src/ui/mention-list.tsx +0 -106
  265. package/src/ui/mention-suggestion.test.ts +0 -18
  266. package/src/ui/mention-suggestion.tsx +0 -285
  267. package/src/ui/mention.tsx +0 -9
  268. package/src/ui/render-markdown.tsx +0 -72
@@ -22,83 +22,88 @@ __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;
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]);
102
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)(
@@ -106,10 +111,9 @@ function CanvasCommentsLayer(props) {
106
111
  () => (0, import_cluster_input.collectClusterLeaves)(
107
112
  editor,
108
113
  threads.filter((thread) => !heldThreadIds.has(thread.id)),
109
- import_state.openThreadId.get(editor),
110
- impreciseShapeAnchor
114
+ import_state.openThreadId.get(editor)
111
115
  ),
112
- [editor, threads, impreciseShapeAnchor, heldThreadIds]
116
+ [editor, threads, heldThreadIds]
113
117
  );
114
118
  const clusterZoomBounds = (0, import_tldraw.useValue)(
115
119
  "comment cluster zoom bounds",
@@ -211,58 +215,81 @@ function CanvasCommentsLayer(props) {
211
215
  () => new Map(threads.map((thread) => [thread.id, thread])),
212
216
  [threads]
213
217
  );
218
+ const pinStacks = (0, import_tldraw.useValue)("comment pin stacks", () => (0, import_pin_stacking.computePinStacks)(editor, threads), [
219
+ editor,
220
+ threads
221
+ ]);
214
222
  const openThread = openId ? threadsById.get(openId) : null;
215
223
  const hidden = (0, import_tldraw.useValue)("comments hidden", () => import_state.commentsHidden.get(editor), [editor]);
216
224
  (0, import_react.useEffect)(() => {
217
225
  return () => {
218
226
  import_state.openThreadId.set(editor, null);
227
+ import_state.openStackId.set(editor, null);
219
228
  import_state.pendingComment.set(editor, null);
229
+ import_state.revealThreadRequest.set(editor, null);
220
230
  };
221
231
  }, [editor]);
222
232
  (0, import_react.useEffect)(() => {
223
- if (deepLinkHandled.current) return;
224
- const id = new URLSearchParams(window.location.search).get("comment");
225
- if (!id) {
226
- deepLinkHandled.current = true;
227
- 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;
228
240
  }
229
- const record = (0, import_comment_store.getCommentRecord)(editor, id);
230
- if (!record) return;
231
- let thread;
232
- if (record.typeName === "comment") {
233
- thread = threadsById.get(record.threadId);
234
- } else {
235
- thread = record;
236
- }
237
- if (!thread) return;
238
- deepLinkHandled.current = true;
239
- revealDeepLinkedThread(
240
- editor,
241
- thread,
242
- clusterModel.table,
243
- clusterZoomBounds,
244
- options,
245
- impreciseShapeAnchor
246
- );
247
- import_state.openThreadId.set(editor, thread.id);
248
- }, [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
+ );
249
276
  const zoomToClusterSplit = (0, import_react.useCallback)(
250
277
  (node) => {
251
278
  const event = clusterModel.table.events.find((e) => e.result.id === node.id);
252
279
  if (!event || !Number.isFinite(event.zSplit)) return;
253
280
  const zoom = clamp(
254
- event.zSplit * options.clusterSplitZoomFactor,
281
+ event.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
255
282
  clusterZoomBounds.minZoom,
256
283
  clusterZoomBounds.maxZoom
257
284
  );
258
285
  centerOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS);
259
286
  },
260
- [clusterModel, clusterZoomBounds, editor, options]
287
+ [clusterModel, clusterZoomBounds, editor]
261
288
  );
262
289
  (0, import_react.useEffect)(() => {
263
290
  const onKeyDown = (e) => {
264
291
  if (e.key !== "Escape" || import_state.openThreadId.get(editor) === null) return;
265
- if ((0, import_mention_suggestion.isMentionPickerOpen)()) return;
292
+ if ((0, import_mentions.isMentionPickerOpen)()) return;
266
293
  const target = e.target;
267
294
  if (target && target.closest(".tlui-cmt-editing")) return;
268
295
  import_state.openThreadId.set(editor, null);
@@ -284,79 +311,78 @@ function CanvasCommentsLayer(props) {
284
311
  return () => document.removeEventListener("keydown", onKeyDown, true);
285
312
  }, [editor]);
286
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;
287
342
  return (0, import_react_dom.createPortal)(
288
343
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: layerRef, className: "tlui-cmt-canvas-layer", children: [
289
344
  options.enableClustering ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
290
345
  fadeNodes.map(({ node, phase }) => {
291
346
  let content;
347
+ const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
292
348
  if (node.count === 1) {
293
349
  const thread = threadsById.get(node.id);
294
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 {
295
356
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
296
- ThreadPin,
357
+ ClusterBadge,
297
358
  {
298
359
  editor,
299
- thread,
300
- ...props,
301
- regionOptions
360
+ node,
361
+ onExpand: zoomToClusterSplit,
362
+ onSelectThread: revealClusteredThread,
363
+ threadsById,
364
+ currentUserId: props.currentUserId,
365
+ resolveAuthor: props.resolveAuthor
302
366
  }
303
367
  );
304
- } else {
305
- content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ClusterBadge, { editor, node, onExpand: zoomToClusterSplit });
306
368
  }
307
369
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: clusterFadeClassName(phase), children: content }, `cluster-fade:${node.id}`);
308
370
  }),
309
- orphanThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
310
- ThreadPin,
311
- {
312
- editor,
313
- thread,
314
- ...props,
315
- regionOptions
316
- },
317
- thread.id
318
- )),
319
- heldThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
320
- ThreadPin,
321
- {
322
- editor,
323
- thread,
324
- ...props,
325
- regionOptions
326
- },
327
- thread.id
328
- ))
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))
329
373
  ] }) : (
330
374
  // Clustering off: every thread renders as its own live pin (each returns null when it's
331
375
  // not on the current page or its anchor is missing). The open thread is excluded here and
332
376
  // rendered once below, mirroring how the clustering path keeps it out of the cluster leaves —
333
377
  // otherwise it would mount a second, stacked pin.
334
- threads.filter((thread) => thread.id !== openId).map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
335
- ThreadPin,
336
- {
337
- editor,
338
- thread,
339
- ...props,
340
- regionOptions
341
- },
342
- thread.id
343
- ))
344
- ),
345
- openThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
- ThreadPin,
347
- {
348
- editor,
349
- thread: openThread,
350
- ...props,
351
- regionOptions
352
- },
353
- `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))
354
379
  ),
380
+ openThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(openThread) }, `open:${openThread.id}`),
355
381
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionDraftBox, { editor }),
356
- pending?.anchor.type === "region" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RegionBox, { editor, box: pending.anchor }),
357
- 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 })
358
384
  ] }),
359
- container
385
+ portalHost
360
386
  );
361
387
  }
362
388
  const EMPTY_SET = /* @__PURE__ */ new Set();
@@ -451,34 +477,36 @@ function getClusterZoomBounds(editor) {
451
477
  maxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom
452
478
  };
453
479
  }
454
- function revealDeepLinkedThread(editor, thread, table, zoomBounds, options, impreciseShapeAnchor) {
480
+ function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
455
481
  if (thread.pageId !== editor.getCurrentPageId()) {
456
482
  editor.setCurrentPage(thread.pageId);
457
483
  }
458
- const point = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor, impreciseShapeAnchor);
484
+ const point = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor);
459
485
  if (!point) return;
460
486
  if (options.enableClustering) {
461
487
  const parentEvent = findDirectParentEvent(table, thread.id);
462
488
  if (parentEvent && Number.isFinite(parentEvent.zSplit) && parentEvent.zSplit <= zoomBounds.maxZoom) {
463
489
  const zoom = clamp(
464
- parentEvent.zSplit * options.clusterSplitZoomFactor,
490
+ parentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
465
491
  zoomBounds.minZoom,
466
492
  zoomBounds.maxZoom
467
493
  );
468
- centerOnPointAtZoom(editor, point, zoom);
494
+ centerOnPointAtZoom(editor, point, zoom, duration);
469
495
  return;
470
496
  }
471
497
  }
472
- 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 } });
473
500
  }
474
501
  function findDirectParentEvent(table, threadId) {
475
502
  return table.events.find((event) => event.children.some((child) => child.id === threadId));
476
503
  }
477
504
  function centerOnPointAtZoom(editor, point, zoom, duration = 200) {
478
505
  const viewport = editor.getViewportScreenBounds();
506
+ const offset = (0, import_thread_state.commentCenterScreenOffset)(editor);
479
507
  editor.setCamera(
480
508
  {
481
- x: viewport.w / (2 * zoom) - point.x,
509
+ x: (viewport.w / 2 - offset) / zoom - point.x,
482
510
  y: viewport.h / (2 * zoom) - point.y,
483
511
  z: zoom
484
512
  },
@@ -491,8 +519,16 @@ function clamp(value, min, max) {
491
519
  const ClusterBadge = (0, import_react.memo)(function ClusterBadge2({
492
520
  editor,
493
521
  node,
494
- onExpand
522
+ onExpand,
523
+ onSelectThread,
524
+ threadsById,
525
+ ...props
495
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);
496
532
  const point = (0, import_tldraw.useValue)(
497
533
  "cluster badge point",
498
534
  () => {
@@ -502,46 +538,63 @@ const ClusterBadge = (0, import_react.memo)(function ClusterBadge2({
502
538
  },
503
539
  [editor, node]
504
540
  );
505
- if (!point) return null;
506
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
507
- "div",
508
- {
509
- className: "tlui-cmt-canvas-cluster",
510
- style: { left: point.x, top: point.y },
511
- onPointerDown: stop,
512
- onClick: (e) => {
513
- e.stopPropagation();
514
- onExpand(node);
515
- },
516
- 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);
517
547
  }
518
- );
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
+ ] });
519
591
  });
520
592
  function isInInflatedViewport(editor, point) {
521
593
  const viewport = editor.getViewportScreenBounds();
522
- const margin = (0, import_options.getCommentingOptions)(editor).clusterCullMargin;
594
+ const margin = CLUSTER_CULL_MARGIN_PX;
523
595
  return point.x >= -margin && point.y >= -margin && point.x <= viewport.w + margin && point.y <= viewport.h + margin;
524
596
  }
525
- function ThreadPopover({
526
- container,
527
- style,
528
- children
529
- }) {
530
- const ref = (0, import_react.useRef)(null);
531
- (0, import_tldraw.usePassThroughWheelEvents)(ref);
532
- (0, import_tldraw.usePassThroughMouseOverEvents)(ref);
533
- return (0, import_react_dom.createPortal)(
534
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: "tlui-cmt-canvas-popover", style, onPointerDown: stop, children }),
535
- container
536
- );
537
- }
538
- function RegionBox({
539
- editor,
540
- box,
541
- movable,
542
- onPreview,
543
- onCommit
544
- }) {
597
+ function RegionBox({ editor, box }) {
545
598
  const rect = (0, import_tldraw.useValue)(
546
599
  "region rect",
547
600
  () => {
@@ -551,38 +604,7 @@ function RegionBox({
551
604
  },
552
605
  [editor, box.x, box.y, box.w, box.h]
553
606
  );
554
- const grabRef = (0, import_react.useRef)(null);
555
- const translated = (e) => {
556
- const g = grabRef.current;
557
- const p = editor.screenToPage({ x: e.clientX, y: e.clientY });
558
- return { ...g.box, x: g.box.x + (p.x - g.page.x), y: g.box.y + (p.y - g.page.y) };
559
- };
560
- const startMove = (e) => {
561
- e.stopPropagation();
562
- grabRef.current = { page: editor.screenToPage({ x: e.clientX, y: e.clientY }), box };
563
- e.currentTarget.setPointerCapture(e.pointerId);
564
- };
565
- const onMove = (e) => {
566
- if (grabRef.current) onPreview?.(translated(e));
567
- };
568
- const endMove = (e) => {
569
- if (!grabRef.current) return;
570
- const bounds = translated(e);
571
- grabRef.current = null;
572
- if (e.currentTarget.hasPointerCapture(e.pointerId))
573
- e.currentTarget.releasePointerCapture(e.pointerId);
574
- onCommit?.(bounds);
575
- };
576
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
577
- "div",
578
- {
579
- className: movable ? "tlui-cmt-canvas-region tlui-cmt-canvas-region--movable" : "tlui-cmt-canvas-region",
580
- style: rect,
581
- onPointerDown: movable ? startMove : void 0,
582
- onPointerMove: movable ? onMove : void 0,
583
- onPointerUp: movable ? endMove : void 0
584
- }
585
- );
607
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-canvas-region", style: rect });
586
608
  }
587
609
  function RegionDraftBox({ editor }) {
588
610
  const box = (0, import_tldraw.useValue)("region draft", () => import_state.regionDraft.get(editor), [editor]);
@@ -595,12 +617,6 @@ const REGION_CORNERS = [
595
617
  { x: 0, y: 1, cursor: "nesw-resize" },
596
618
  { x: 1, y: 1, cursor: "nwse-resize" }
597
619
  ];
598
- const REGION_EDGES = [
599
- { x: 0.5, y: 0, cursor: "ns-resize" },
600
- { x: 1, y: 0.5, cursor: "ew-resize" },
601
- { x: 0.5, y: 1, cursor: "ns-resize" },
602
- { x: 0, y: 0.5, cursor: "ew-resize" }
603
- ];
604
620
  const REGION_HANDLE_MARGIN_PX = 12;
605
621
  function resizeRegion(box, handle, cursor) {
606
622
  const controlsX = handle.x !== 0.5;
@@ -662,33 +678,22 @@ function RegionResizeHandles({
662
678
  const ThreadPin = (0, import_react.memo)(function ThreadPin2({
663
679
  editor,
664
680
  thread,
665
- regionOptions,
666
681
  ...props
667
682
  }) {
668
- const {
669
- currentUserId,
670
- resolveName,
671
- onPostComment,
672
- isCommentUnread,
673
- onCommentRead,
674
- getMentionSuggestions,
675
- renderMentionSuggestion
676
- } = props;
683
+ const { resolveAuthor } = props;
677
684
  const options = (0, import_options.useCommentingOptions)();
678
- const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor;
685
+ const canComment = (0, import_options.useCanComment)(props.currentUserId);
679
686
  const container = (0, import_tldraw.useContainer)();
680
- const comments = (0, import_hooks.useThreadComments)(editor, thread.id);
681
687
  const msg = (0, import_tldraw.useTranslation)();
688
+ const comments = (0, import_hooks.useThreadComments)(editor, thread.id);
682
689
  const open = (0, import_tldraw.useValue)("thread open", () => import_state.openThreadId.get(editor) === thread.id, [
683
690
  editor,
684
691
  thread.id
685
692
  ]);
686
- const [reply, setReply] = (0, import_react.useState)(import_comment_extensions.EMPTY_COMMENT);
687
- const [editingId, setEditingId] = (0, import_react.useState)(null);
688
- const [editText, setEditText] = (0, import_react.useState)(import_comment_extensions.EMPTY_COMMENT);
689
693
  const [dragPagePoint, setDragPagePoint] = (0, import_react.useState)(null);
690
694
  const [resizeBounds, setResizeBounds] = (0, import_react.useState)(null);
691
- 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]);
692
697
  const pointerInRegion = (0, import_tldraw.useValue)(
693
698
  "pointer in region",
694
699
  () => {
@@ -701,15 +706,20 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
701
706
  },
702
707
  [editor, thread.anchor, thread.pageId]
703
708
  );
704
- 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;
705
711
  const resizeHandles = (0, import_react.useMemo)(
706
- () => regionOptions.resize === "edges" ? REGION_EDGES : REGION_CORNERS.filter(
707
- (c) => c.x !== regionOptions.pinCorner.x || c.y !== regionOptions.pinCorner.y
708
- ),
709
- [regionOptions.resize, regionOptions.pinCorner]
712
+ () => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),
713
+ [pinCorner]
710
714
  );
711
715
  const dragRef = (0, import_react.useRef)(null);
712
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]);
713
723
  (0, import_react.useEffect)(() => {
714
724
  if (!open) return;
715
725
  const onPointerDown = (e) => {
@@ -730,165 +740,68 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
730
740
  "pin point",
731
741
  () => {
732
742
  if (thread.pageId !== editor.getCurrentPageId()) return null;
733
- const pagePoint = (0, import_thread_state.anchorPagePoint)(editor, thread.anchor, impreciseShapeAnchor);
734
- 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;
735
748
  },
736
- [editor, thread.anchor, thread.pageId, impreciseShapeAnchor]
749
+ [editor, thread.anchor, thread.pageId]
737
750
  );
738
- const visible = point !== null;
739
- (0, import_react.useEffect)(() => {
740
- if (!open || !visible || !isCommentUnread || !onCommentRead) return;
741
- for (const comment of comments) {
742
- if (isCommentUnread(comment.id)) {
743
- onCommentRead(comment.id);
744
- }
745
- }
746
- }, [open, visible, comments, isCommentUnread, onCommentRead]);
747
751
  if (!point) return null;
748
- const postReply = () => {
749
- if ((0, import_comment_extensions.isCommentEmpty)(reply) || !currentUserId) return;
750
- (0, import_state.commitCommentMutation)(editor, () => {
751
- const comment = (0, import_tldraw.createComment)({
752
- threadId: thread.id,
753
- pageId: thread.pageId,
754
- authorId: currentUserId,
755
- body: reply
756
- });
757
- (0, import_comment_store.putCommentRecords)(editor, [comment]);
758
- if (onPostComment) onPostComment(comment);
759
- });
760
- setReply(import_comment_extensions.EMPTY_COMMENT);
761
- };
762
- const toggleResolve = () => {
763
- if (!currentUserId) return;
764
- (0, import_state.commitCommentMutation)(editor, () => {
765
- (0, import_comment_store.putCommentRecords)(editor, [
766
- {
767
- ...thread,
768
- resolved: thread.resolved ? null : { at: Date.now(), by: currentUserId }
769
- }
770
- ]);
771
- });
772
- };
773
- const deleteThread = () => {
774
- import_state.openThreadId.set(editor, null);
775
- (0, import_state.commitCommentMutation)(
776
- editor,
777
- () => (0, import_comment_store.removeCommentRecords)(editor, [thread.id, ...comments.map((c) => c.id)])
778
- );
779
- };
780
- const startEdit = (comment) => {
781
- setEditingId(comment.id);
782
- setEditText(comment.body);
783
- };
784
- const saveEdit = () => {
785
- const comment = comments.find((c) => c.id === editingId);
786
- if (!comment || (0, import_comment_extensions.isCommentEmpty)(editText)) return;
787
- (0, import_state.commitCommentMutation)(editor, () => {
788
- (0, import_comment_store.putCommentRecords)(editor, [{ ...comment, body: editText, editedAt: Date.now() }]);
789
- });
790
- setEditingId(null);
791
- };
792
- const renderComment = (card, index) => {
793
- const comment = comments[index];
794
- if (editingId === comment.id) {
795
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
796
- "div",
797
- {
798
- className: "tlui-cmt-editing",
799
- onKeyDown: (e) => {
800
- if (e.key === "Escape") {
801
- setEditingId(null);
802
- e.stopPropagation();
803
- }
804
- },
805
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
806
- import_comment_composer.CommentComposer,
807
- {
808
- author: card.author,
809
- placeholder: msg("comments.edit-placeholder"),
810
- value: editText,
811
- onChange: setEditText,
812
- onSubmit: saveEdit,
813
- sendLabel: msg("comments.save"),
814
- disabled: (0, import_comment_extensions.isCommentEmpty)(editText),
815
- getMentionSuggestions,
816
- renderMentionSuggestion,
817
- autoFocus: true
818
- }
819
- )
820
- }
821
- );
822
- }
823
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
824
- import_comment_card.CommentCard,
825
- {
826
- ...card,
827
- actions: comment.authorId === currentUserId ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
828
- "button",
829
- {
830
- className: "tlui-cmt-thread__action",
831
- title: msg("comments.edit"),
832
- onClick: () => startEdit(comment),
833
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "dots-horizontal", label: msg("comments.edit"), small: true })
834
- }
835
- ) : void 0
836
- }
837
- );
838
- };
839
- const headerActions = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
840
- currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
841
- "button",
842
- {
843
- className: "tlui-cmt-thread__action",
844
- title: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
845
- onClick: toggleResolve,
846
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
847
- import_tldraw.TldrawUiIcon,
848
- {
849
- icon: "check",
850
- label: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
851
- small: true
852
- }
853
- )
854
- }
855
- ),
856
- currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
857
- "button",
858
- {
859
- className: "tlui-cmt-thread__action",
860
- title: msg("comments.delete"),
861
- onClick: deleteThread,
862
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "trash", label: msg("comments.delete"), small: true })
863
- }
864
- ),
865
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
866
- "button",
867
- {
868
- className: "tlui-cmt-thread__action",
869
- title: msg("comments.dismiss"),
870
- onClick: () => import_state.openThreadId.set(editor, null),
871
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "cross-2", label: msg("comments.dismiss"), small: true })
872
- }
873
- )
874
- ] });
875
752
  const PinContent = options.components.PinContent;
876
- 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);
877
758
  const isRegion = thread.anchor.type === "region";
878
- const pinMovable = regionOptions.move !== "body";
879
- const bodyMovable = regionOptions.move !== "pin";
880
759
  const startDrag = (e) => {
760
+ if (isCanvasPanGesture(editor, e)) {
761
+ forwardPointerEventToCanvas(container, e);
762
+ return;
763
+ }
881
764
  e.stopPropagation();
882
- 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
+ };
883
780
  e.currentTarget.setPointerCapture(e.pointerId);
884
781
  };
885
782
  const onDrag = (e) => {
886
783
  const drag = dragRef.current;
887
784
  if (!drag) return;
888
- if (isRegion && !pinMovable) return;
785
+ if (!canComment) return;
889
786
  if (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;
890
787
  drag.moved = true;
891
- 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([]);
892
805
  };
893
806
  const endDrag = (e) => {
894
807
  const drag = dragRef.current;
@@ -897,55 +810,59 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
897
810
  e.currentTarget.releasePointerCapture(e.pointerId);
898
811
  }
899
812
  if (!drag) return;
813
+ editor.setHintingShapes([]);
900
814
  if (!drag.moved) {
901
815
  import_state.openThreadId.set(editor, import_state.openThreadId.get(editor) === thread.id ? null : thread.id);
902
816
  return;
903
817
  }
904
- 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 };
905
820
  setDragPagePoint(null);
906
821
  let anchor;
907
822
  if (thread.anchor.type === "region") {
908
823
  anchor = {
909
824
  ...thread.anchor,
910
- x: pagePoint.x - regionOptions.pinCorner.x * thread.anchor.w,
911
- 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
912
827
  };
913
828
  } else {
914
- const hit = editor.getShapeAtPoint(pagePoint, { hitInside: true });
915
- 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 };
916
840
  }
917
- (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");
918
842
  };
919
- const livePinPage = resizeBounds ? (0, import_thread_state.regionPinPoint)(resizeBounds, regionOptions.pinCorner) : dragPagePoint;
920
- 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;
921
846
  const regionAnchor = thread.anchor.type === "region" ? thread.anchor : void 0;
922
847
  const movedRegion = regionAnchor && dragPagePoint ? {
923
848
  ...regionAnchor,
924
- x: dragPagePoint.x - regionOptions.pinCorner.x * regionAnchor.w,
925
- y: dragPagePoint.y - regionOptions.pinCorner.y * regionAnchor.h
849
+ x: dragPagePoint.x - pinCorner.x * regionAnchor.w,
850
+ y: dragPagePoint.y - pinCorner.y * regionAnchor.h
926
851
  } : regionAnchor;
927
852
  const regionBoxBounds = resizeBounds ?? movedRegion;
928
853
  const commitResize = (bounds) => {
929
854
  setResizeBounds(null);
930
- editor.run(
931
- () => (0, import_comment_store.putCommentRecords)(editor, [{ ...thread, anchor: { type: "region", ...bounds } }]),
932
- {
933
- history: "ignore"
934
- }
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"
935
861
  );
936
862
  };
937
863
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
938
- regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
939
- RegionBox,
940
- {
941
- editor,
942
- box: regionBoxBounds,
943
- movable: bodyMovable && !dragPagePoint,
944
- onPreview: setResizeBounds,
945
- onCommit: commitResize
946
- }
947
- ),
948
- 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)(
949
866
  RegionResizeHandles,
950
867
  {
951
868
  editor,
@@ -958,51 +875,59 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
958
875
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
959
876
  "div",
960
877
  {
961
- className: open ? "tlui-cmt-canvas-pin tlui-cmt-canvas-pin--open" : "tlui-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(" "),
962
883
  style: { left: renderPoint.x, top: renderPoint.y },
963
884
  children: [
964
885
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
965
- "div",
886
+ "button",
966
887
  {
967
888
  ref: markerRef,
968
- className: "tlui-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,
969
893
  onPointerDown: startDrag,
970
894
  onPointerMove: onDrag,
971
895
  onPointerUp: endDrag,
972
- onPointerEnter: () => setPinHovered(true),
973
- 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,
974
905
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_pin.CommentPin, { resolved: thread.resolved != null, open, children: pinContent })
975
906
  }
976
907
  ),
977
908
  open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
978
- ThreadPopover,
909
+ import_thread_view.ThreadPopover,
979
910
  {
980
911
  container,
981
- style: { left: renderPoint.x + 36, top: renderPoint.y - 28 },
982
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
983
- import_comment_thread.CommentThread,
984
- {
985
- header: msg("comments.thread-title"),
986
- headerActions,
987
- renderComment,
988
- comments: comments.map((c) => toCardProps(c, props, options.components)),
989
- resolvedBanner: thread.resolved ? msg("comments.resolved-by").replace(
990
- "{name}",
991
- resolveName(thread.resolved.by) ?? import_comment_render.UNKNOWN_AUTHOR
992
- ) : void 0,
993
- composer: currentUserId && !thread.resolved ? {
994
- author: resolveName(currentUserId) ?? import_comment_render.UNKNOWN_AUTHOR,
995
- placeholder: msg("comments.reply-placeholder"),
996
- sendLabel: msg("comments.send"),
997
- value: reply,
998
- onChange: setReply,
999
- onSubmit: postReply,
1000
- disabled: (0, import_comment_extensions.isCommentEmpty)(reply),
1001
- getMentionSuggestions,
1002
- renderMentionSuggestion
1003
- } : void 0
1004
- }
1005
- )
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
1006
931
  }
1007
932
  )
1008
933
  ]
@@ -1014,12 +939,18 @@ function PendingComposer({
1014
939
  editor,
1015
940
  pending,
1016
941
  currentUserId,
1017
- resolveName,
942
+ resolveAuthor,
1018
943
  onPostComment,
1019
944
  getMentionSuggestions,
1020
945
  renderMentionSuggestion
1021
946
  }) {
1022
- 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
+ );
1023
954
  const ref = (0, import_react.useRef)(null);
1024
955
  const msg = (0, import_tldraw.useTranslation)();
1025
956
  const container = (0, import_tldraw.useContainer)();
@@ -1042,7 +973,7 @@ function PendingComposer({
1042
973
  }, [editor]);
1043
974
  const submit = () => {
1044
975
  if ((0, import_comment_extensions.isCommentEmpty)(text) || !currentUserId) return;
1045
- (0, import_state.commitCommentMutation)(editor, () => {
976
+ (0, import_comment_mutations.commitCommentMutation)(editor, () => {
1046
977
  const pageId = editor.getCurrentPageId();
1047
978
  const thread = (0, import_tldraw.createCommentThread)({
1048
979
  pageId,
@@ -1055,10 +986,11 @@ function PendingComposer({
1055
986
  authorId: currentUserId,
1056
987
  body: text
1057
988
  });
1058
- (0, import_comment_store.putCommentRecords)(editor, [thread, comment]);
989
+ (0, import_comment_mutations.putRecordsInCommit)(editor, [thread, comment]);
1059
990
  if (onPostComment) onPostComment(comment);
1060
991
  });
1061
992
  setText(import_comment_extensions.EMPTY_COMMENT);
993
+ (0, import_comment_drafts.clearCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT);
1062
994
  import_state.pendingComment.set(editor, null);
1063
995
  };
1064
996
  return (0, import_react_dom.createPortal)(
@@ -1066,28 +998,36 @@ function PendingComposer({
1066
998
  "div",
1067
999
  {
1068
1000
  ref,
1069
- className: "tlui-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(" "),
1070
1006
  style: { left: point.x, top: point.y },
1071
1007
  onPointerDown: stop,
1008
+ onContextMenu: stop,
1072
1009
  onKeyDown: (e) => {
1073
- 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);
1074
1011
  },
1075
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1012
+ children: canComment ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1076
1013
  import_comment_composer.CommentComposer,
1077
1014
  {
1078
- author: currentUserId ? resolveName(currentUserId) ?? import_comment_render.UNKNOWN_AUTHOR : "",
1015
+ author: me ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR,
1079
1016
  placeholder: msg("comments.add-placeholder"),
1080
1017
  sendLabel: msg("comments.send"),
1081
1018
  value: text,
1082
- onChange: setText,
1019
+ onChange: (value) => {
1020
+ setText(value);
1021
+ (0, import_comment_drafts.saveCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT, value);
1022
+ },
1083
1023
  onSubmit: submit,
1084
- disabled: (0, import_comment_extensions.isCommentEmpty)(text),
1024
+ disabled: (0, import_comment_extensions.isCommentEmpty)(text) || !currentUserId,
1085
1025
  getMentionSuggestions,
1086
1026
  renderMentionSuggestion,
1087
1027
  autoFocus: true,
1088
1028
  leading: draftAvatar
1089
1029
  }
1090
- )
1030
+ ) : ComposerFallback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComposerFallback, { context: "pending" })
1091
1031
  }
1092
1032
  ),
1093
1033
  container