@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
@@ -1,8 +1,11 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Avatar, isMentionPickerOpen } from "@tldraw/mentions";
2
3
  import {
4
+ Fragment as Fragment2,
3
5
  memo,
4
6
  useCallback,
5
7
  useEffect,
8
+ useLayoutEffect,
6
9
  useMemo,
7
10
  useRef,
8
11
  useState
@@ -11,9 +14,7 @@ import { createPortal } from "react-dom";
11
14
  import {
12
15
  createComment,
13
16
  createCommentThread,
14
- getFirstCharacter,
15
17
  react,
16
- TldrawUiIcon,
17
18
  useContainer,
18
19
  useEditor,
19
20
  usePassThroughMouseOverEvents,
@@ -23,92 +24,110 @@ import {
23
24
  } from "tldraw";
24
25
  import { computeClusterTable } from "../clustering/computeClusterTable.mjs";
25
26
  import { createClusterRuntime } from "../clustering/runtime.mjs";
26
- import { CommentCard } from "../ui/comment-card.mjs";
27
27
  import { CommentComposer } from "../ui/comment-composer.mjs";
28
28
  import { EMPTY_COMMENT, isCommentEmpty } from "../ui/comment-extensions.mjs";
29
29
  import { CommentPin } from "../ui/comment-pin.mjs";
30
- import { CommentThread } from "../ui/comment-thread.mjs";
31
30
  import { CountBadge } from "../ui/count-badge.mjs";
32
- import { isMentionPickerOpen } from "../ui/mention-suggestion.mjs";
31
+ import { registerCommentAnchorLifecycle } from "./anchor-lifecycle.mjs";
33
32
  import { collectClusterLeaves } from "./cluster-input.mjs";
34
- import { CommentBody } from "./comment-body.mjs";
35
- import { UNKNOWN_AUTHOR } from "./comment-render.mjs";
36
- import { getCommentRecord, putCommentRecords, removeCommentRecords } from "./comment-store.mjs";
33
+ import {
34
+ clearCommentDraft,
35
+ getCommentDraft,
36
+ NEW_COMMENT_DRAFT,
37
+ saveCommentDraft
38
+ } from "./comment-drafts.mjs";
39
+ import { commitCommentMutation, putRecordsInCommit } from "./comment-mutations.mjs";
40
+ import { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from "./comment-render.mjs";
41
+ import { getCommentRecord } from "./comment-store.mjs";
37
42
  import { useCommentThreads, useThreadComments } from "./hooks.mjs";
38
43
  import { useCommentingEnabled } from "./license.mjs";
39
44
  import {
40
45
  getCommentingOptions,
46
+ useCanComment,
41
47
  useCommentingOptions
42
48
  } from "./options.mjs";
43
- import {
44
- DEFAULT_REGION_COMMENT_OPTIONS,
45
- setRegionCommentOptions
46
- } from "./region-options.mjs";
49
+ import { computePinStacks, pinStackKey } from "./pin-stacking.mjs";
47
50
  import {
48
51
  commentsHidden,
49
- commitCommentMutation,
52
+ openStackId,
50
53
  openThreadId,
51
54
  pendingComment,
52
55
  regionDraft,
56
+ revealThreadRequest,
57
+ sidebarFilters,
53
58
  toggleCommentsHidden,
54
59
  usePendingComment
55
60
  } from "./state.mjs";
56
- import { anchorPagePoint, regionPinPoint, shapeAnchorAt } from "./thread-state.mjs";
61
+ import { ThreadPreview, sortThreadsForPreview, useMarkerPreview } from "./thread-preview.mjs";
62
+ import { ThreadStackPin } from "./thread-stack.mjs";
63
+ import {
64
+ anchorPagePoint,
65
+ commentCenterScreenOffset,
66
+ commentTargetShapeAt,
67
+ impreciseShapePinInset,
68
+ REGION_PIN_CORNER,
69
+ regionAnchorPinCorner,
70
+ regionPinPoint,
71
+ shapeAnchorAt
72
+ } from "./thread-state.mjs";
73
+ import { POPOVER_OFFSET, ThreadPopover, ThreadView } from "./thread-view.mjs";
57
74
  const stop = (e) => e.stopPropagation();
58
- const initialOf = (name) => (getFirstCharacter(name.trim()) || "?").toUpperCase();
75
+ const isCanvasPanGesture = (editor, e) => e.button !== 0 || editor.inputs.keys.has("Space");
76
+ function forwardPointerEventToCanvas(container, e) {
77
+ const cvs = container.querySelector(".tl-canvas");
78
+ if (!cvs) return;
79
+ const newEvent = new PointerEvent(e.type, e.nativeEvent);
80
+ newEvent.isSpecialRedispatchedEvent = true;
81
+ cvs.dispatchEvent(newEvent);
82
+ }
59
83
  const CLUSTER_FADE_MS = 150;
60
84
  const CLUSTER_EXPAND_ZOOM_MS = 450;
61
- const draftAvatar = /* @__PURE__ */ jsx(CommentPin, { children: /* @__PURE__ */ jsxs(
62
- "svg",
63
- {
64
- viewBox: "0 0 24 24",
65
- width: "15",
66
- height: "15",
67
- fill: "none",
68
- stroke: "currentColor",
69
- strokeWidth: "2",
70
- strokeLinecap: "round",
71
- strokeLinejoin: "round",
72
- "aria-hidden": "true",
73
- children: [
74
- /* @__PURE__ */ jsx("path", { d: "M12 20h9" }),
75
- /* @__PURE__ */ jsx("path", { d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" })
76
- ]
77
- }
78
- ) });
79
- function toCardProps(comment, props, components) {
80
- const Body = components.CommentBody;
81
- const body = Body ? /* @__PURE__ */ jsx(Body, { comment }) : /* @__PURE__ */ jsx(CommentBody, { richText: comment.body, resolveName: props.resolveName });
82
- return {
83
- author: props.resolveName(comment.authorId) ?? UNKNOWN_AUTHOR,
84
- body,
85
- date: new Date(comment.createdAt).toISOString(),
86
- you: comment.authorId === props.currentUserId,
87
- edited: comment.editedAt != null
88
- };
89
- }
85
+ const CLUSTER_SPLIT_ZOOM_FACTOR = 1.05;
86
+ const CLUSTER_CULL_MARGIN_PX = 120;
87
+ const THREAD_HEADER_BLOCK = 36;
90
88
  function CanvasComments(props) {
91
89
  const commentingEnabled = useCommentingEnabled();
92
90
  if (!commentingEnabled) return null;
93
91
  return /* @__PURE__ */ jsx(CanvasCommentsLayer, { ...props });
94
92
  }
93
+ function useTrailingPortalHost(container) {
94
+ const [host, setHost] = useState(null);
95
+ useLayoutEffect(() => {
96
+ const elm = container.ownerDocument.createElement("div");
97
+ elm.style.display = "contents";
98
+ container.appendChild(elm);
99
+ setHost(elm);
100
+ return () => {
101
+ elm.remove();
102
+ setHost(null);
103
+ };
104
+ }, [container]);
105
+ return host;
106
+ }
95
107
  function CanvasCommentsLayer(props) {
96
108
  const editor = useEditor();
97
109
  const options = useCommentingOptions();
98
110
  const container = useContainer();
99
- const regionOptions = useMemo(
100
- () => ({ ...DEFAULT_REGION_COMMENT_OPTIONS, ...props.regionOptions }),
101
- [props.regionOptions]
102
- );
103
- useEffect(() => setRegionCommentOptions(editor, regionOptions), [editor, regionOptions]);
111
+ const portalHost = useTrailingPortalHost(container);
104
112
  const layerRef = useRef(null);
105
- usePassThroughWheelEvents(layerRef);
106
113
  usePassThroughMouseOverEvents(layerRef);
107
- const deepLinkHandled = useRef(false);
108
- const threads = useCommentThreads(editor);
114
+ const allThreads = useCommentThreads(editor);
109
115
  const pending = usePendingComment();
116
+ const canComment = useCanComment(props.currentUserId);
117
+ const canRenderComposer = canComment || options.components.ComposerFallback != null;
118
+ const showPendingComposer = pending != null && canRenderComposer;
119
+ useEffect(() => {
120
+ if (pending && !showPendingComposer) pendingComment.set(editor, null);
121
+ }, [editor, pending, showPendingComposer]);
110
122
  const openId = useValue("open thread id", () => openThreadId.get(editor), [editor]);
111
- const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor;
123
+ const showResolved = useValue("show resolved", () => sidebarFilters.get(editor).showResolved, [
124
+ editor
125
+ ]);
126
+ const threads = useMemo(
127
+ () => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),
128
+ [allThreads, showResolved, openId]
129
+ );
130
+ useEffect(() => registerCommentAnchorLifecycle(editor), [editor]);
112
131
  const [heldThreadIds, setHeldThreadIds] = useState(EMPTY_SET);
113
132
  const adoptOnRebuild = useRef(false);
114
133
  const clusterLeaves = useValue(
@@ -116,10 +135,9 @@ function CanvasCommentsLayer(props) {
116
135
  () => collectClusterLeaves(
117
136
  editor,
118
137
  threads.filter((thread) => !heldThreadIds.has(thread.id)),
119
- openThreadId.get(editor),
120
- impreciseShapeAnchor
138
+ openThreadId.get(editor)
121
139
  ),
122
- [editor, threads, impreciseShapeAnchor, heldThreadIds]
140
+ [editor, threads, heldThreadIds]
123
141
  );
124
142
  const clusterZoomBounds = useValue(
125
143
  "comment cluster zoom bounds",
@@ -221,53 +239,76 @@ function CanvasCommentsLayer(props) {
221
239
  () => new Map(threads.map((thread) => [thread.id, thread])),
222
240
  [threads]
223
241
  );
242
+ const pinStacks = useValue("comment pin stacks", () => computePinStacks(editor, threads), [
243
+ editor,
244
+ threads
245
+ ]);
224
246
  const openThread = openId ? threadsById.get(openId) : null;
225
247
  const hidden = useValue("comments hidden", () => commentsHidden.get(editor), [editor]);
226
248
  useEffect(() => {
227
249
  return () => {
228
250
  openThreadId.set(editor, null);
251
+ openStackId.set(editor, null);
229
252
  pendingComment.set(editor, null);
253
+ revealThreadRequest.set(editor, null);
230
254
  };
231
255
  }, [editor]);
232
256
  useEffect(() => {
233
- if (deepLinkHandled.current) return;
234
- const id = new URLSearchParams(window.location.search).get("comment");
235
- if (!id) {
236
- deepLinkHandled.current = true;
237
- return;
238
- }
239
- const record = getCommentRecord(editor, id);
240
- if (!record) return;
241
- let thread;
242
- if (record.typeName === "comment") {
243
- thread = threadsById.get(record.threadId);
244
- } else {
245
- thread = record;
257
+ const key = openStackId.get(editor);
258
+ if (!key) return;
259
+ for (const id of pinStacks.keys()) {
260
+ const thread = threadsById.get(id);
261
+ if (!thread) continue;
262
+ const point = anchorPagePoint(editor, thread.anchor);
263
+ if (point && pinStackKey(point) === key) return;
246
264
  }
247
- if (!thread) return;
248
- deepLinkHandled.current = true;
249
- revealDeepLinkedThread(
250
- editor,
251
- thread,
252
- clusterModel.table,
253
- clusterZoomBounds,
254
- options,
255
- impreciseShapeAnchor
256
- );
257
- openThreadId.set(editor, thread.id);
258
- }, [clusterModel.table, clusterZoomBounds, editor, threadsById, impreciseShapeAnchor, options]);
265
+ openStackId.set(editor, null);
266
+ }, [editor, pinStacks, threadsById]);
267
+ const requestedRevealThread = useValue(
268
+ "requested reveal thread",
269
+ () => {
270
+ const id = revealThreadRequest.get(editor);
271
+ if (!id) return null;
272
+ const record = getCommentRecord(editor, id);
273
+ if (!record) return null;
274
+ const thread = record.typeName === "comment" ? getCommentRecord(editor, record.threadId) : record;
275
+ return thread?.typeName === "comment-thread" ? thread : null;
276
+ },
277
+ [editor]
278
+ );
279
+ useEffect(() => {
280
+ if (!requestedRevealThread) return;
281
+ revealThreadRequest.set(editor, null);
282
+ commentsHidden.set(editor, false);
283
+ revealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options);
284
+ openThreadId.set(editor, requestedRevealThread.id);
285
+ }, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options]);
286
+ const revealClusteredThread = useCallback(
287
+ (thread) => {
288
+ revealThreadPin(
289
+ editor,
290
+ thread,
291
+ clusterModel.table,
292
+ clusterZoomBounds,
293
+ options,
294
+ CLUSTER_EXPAND_ZOOM_MS
295
+ );
296
+ openThreadId.set(editor, thread.id);
297
+ },
298
+ [clusterModel.table, clusterZoomBounds, editor, options]
299
+ );
259
300
  const zoomToClusterSplit = useCallback(
260
301
  (node) => {
261
302
  const event = clusterModel.table.events.find((e) => e.result.id === node.id);
262
303
  if (!event || !Number.isFinite(event.zSplit)) return;
263
304
  const zoom = clamp(
264
- event.zSplit * options.clusterSplitZoomFactor,
305
+ event.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
265
306
  clusterZoomBounds.minZoom,
266
307
  clusterZoomBounds.maxZoom
267
308
  );
268
309
  centerOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS);
269
310
  },
270
- [clusterModel, clusterZoomBounds, editor, options]
311
+ [clusterModel, clusterZoomBounds, editor]
271
312
  );
272
313
  useEffect(() => {
273
314
  const onKeyDown = (e) => {
@@ -294,79 +335,80 @@ function CanvasCommentsLayer(props) {
294
335
  return () => document.removeEventListener("keydown", onKeyDown, true);
295
336
  }, [editor]);
296
337
  if (hidden) return null;
338
+ const stackGroupOf = (node) => {
339
+ const group = pinStacks.get(node.members[0]);
340
+ if (!group) return null;
341
+ return node.members.every((id) => group.includes(id)) ? group : null;
342
+ };
343
+ const renderedThreadIds = /* @__PURE__ */ new Set();
344
+ if (options.enableClustering) {
345
+ for (const { node } of fadeNodes) {
346
+ if (node.count === 1) renderedThreadIds.add(node.id);
347
+ else if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id);
348
+ }
349
+ for (const thread of orphanThreads) renderedThreadIds.add(thread.id);
350
+ for (const thread of heldThreads) renderedThreadIds.add(thread.id);
351
+ } else {
352
+ for (const thread of threads) renderedThreadIds.add(thread.id);
353
+ }
354
+ if (openThread) renderedThreadIds.add(openThread.id);
355
+ const renderThreadPin = (thread) => {
356
+ const group = pinStacks.get(thread.id);
357
+ if (group) {
358
+ const owner = group.find((id) => renderedThreadIds.has(id));
359
+ if (owner !== thread.id) return null;
360
+ const stackThreads = group.map((id) => threadsById.get(id)).filter((t) => t !== void 0);
361
+ return /* @__PURE__ */ jsx(ThreadStackPin, { editor, threads: stackThreads, ...props });
362
+ }
363
+ return /* @__PURE__ */ jsx(ThreadPin, { editor, thread, ...props });
364
+ };
365
+ if (!portalHost) return null;
297
366
  return createPortal(
298
367
  /* @__PURE__ */ jsxs("div", { ref: layerRef, className: "tlui-cmt-canvas-layer", children: [
299
368
  options.enableClustering ? /* @__PURE__ */ jsxs(Fragment, { children: [
300
369
  fadeNodes.map(({ node, phase }) => {
301
370
  let content;
371
+ const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
302
372
  if (node.count === 1) {
303
373
  const thread = threadsById.get(node.id);
304
374
  if (!thread) return null;
375
+ content = renderThreadPin(thread);
376
+ } else if (stackGroup) {
377
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id));
378
+ content = owner && node.members.includes(owner) ? renderThreadPin(threadsById.get(owner)) : null;
379
+ } else {
305
380
  content = /* @__PURE__ */ jsx(
306
- ThreadPin,
381
+ ClusterBadge,
307
382
  {
308
383
  editor,
309
- thread,
310
- ...props,
311
- regionOptions
384
+ node,
385
+ onExpand: zoomToClusterSplit,
386
+ onSelectThread: revealClusteredThread,
387
+ threadsById,
388
+ currentUserId: props.currentUserId,
389
+ resolveAuthor: props.resolveAuthor
312
390
  }
313
391
  );
314
- } else {
315
- content = /* @__PURE__ */ jsx(ClusterBadge, { editor, node, onExpand: zoomToClusterSplit });
316
392
  }
317
393
  return /* @__PURE__ */ jsx("div", { className: clusterFadeClassName(phase), children: content }, `cluster-fade:${node.id}`);
318
394
  }),
319
- orphanThreads.map((thread) => /* @__PURE__ */ jsx(
320
- ThreadPin,
321
- {
322
- editor,
323
- thread,
324
- ...props,
325
- regionOptions
326
- },
327
- thread.id
328
- )),
329
- heldThreads.map((thread) => /* @__PURE__ */ jsx(
330
- ThreadPin,
331
- {
332
- editor,
333
- thread,
334
- ...props,
335
- regionOptions
336
- },
337
- thread.id
338
- ))
395
+ orphanThreads.map((thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id)),
396
+ heldThreads.map((thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id))
339
397
  ] }) : (
340
398
  // Clustering off: every thread renders as its own live pin (each returns null when it's
341
399
  // not on the current page or its anchor is missing). The open thread is excluded here and
342
400
  // rendered once below, mirroring how the clustering path keeps it out of the cluster leaves —
343
401
  // otherwise it would mount a second, stacked pin.
344
- (threads.filter((thread) => thread.id !== openId).map((thread) => /* @__PURE__ */ jsx(
345
- ThreadPin,
346
- {
347
- editor,
348
- thread,
349
- ...props,
350
- regionOptions
351
- },
352
- thread.id
353
- )))
354
- ),
355
- openThread && /* @__PURE__ */ jsx(
356
- ThreadPin,
357
- {
358
- editor,
359
- thread: openThread,
360
- ...props,
361
- regionOptions
362
- },
363
- `open:${openThread.id}`
402
+ (threads.filter((thread) => thread.id !== openId).map(
403
+ (thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id)
404
+ ))
364
405
  ),
406
+ openThread && /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(openThread) }, `open:${openThread.id}`),
365
407
  /* @__PURE__ */ jsx(RegionDraftBox, { editor }),
366
- pending?.anchor.type === "region" && /* @__PURE__ */ jsx(RegionBox, { editor, box: pending.anchor }),
367
- pending && props.currentUserId && /* @__PURE__ */ jsx(PendingComposer, { editor, pending, ...props })
408
+ pending?.anchor.type === "region" && showPendingComposer && /* @__PURE__ */ jsx(RegionBox, { editor, box: pending.anchor }),
409
+ pending && showPendingComposer && /* @__PURE__ */ jsx(PendingComposer, { editor, pending, ...props })
368
410
  ] }),
369
- container
411
+ portalHost
370
412
  );
371
413
  }
372
414
  const EMPTY_SET = /* @__PURE__ */ new Set();
@@ -461,34 +503,36 @@ function getClusterZoomBounds(editor) {
461
503
  maxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom
462
504
  };
463
505
  }
464
- function revealDeepLinkedThread(editor, thread, table, zoomBounds, options, impreciseShapeAnchor) {
506
+ function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
465
507
  if (thread.pageId !== editor.getCurrentPageId()) {
466
508
  editor.setCurrentPage(thread.pageId);
467
509
  }
468
- const point = anchorPagePoint(editor, thread.anchor, impreciseShapeAnchor);
510
+ const point = anchorPagePoint(editor, thread.anchor);
469
511
  if (!point) return;
470
512
  if (options.enableClustering) {
471
513
  const parentEvent = findDirectParentEvent(table, thread.id);
472
514
  if (parentEvent && Number.isFinite(parentEvent.zSplit) && parentEvent.zSplit <= zoomBounds.maxZoom) {
473
515
  const zoom = clamp(
474
- parentEvent.zSplit * options.clusterSplitZoomFactor,
516
+ parentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
475
517
  zoomBounds.minZoom,
476
518
  zoomBounds.maxZoom
477
519
  );
478
- centerOnPointAtZoom(editor, point, zoom);
520
+ centerOnPointAtZoom(editor, point, zoom, duration);
479
521
  return;
480
522
  }
481
523
  }
482
- editor.centerOnPoint(point, { animation: { duration: 200 } });
524
+ const offset = commentCenterScreenOffset(editor) / editor.getZoomLevel();
525
+ editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } });
483
526
  }
484
527
  function findDirectParentEvent(table, threadId) {
485
528
  return table.events.find((event) => event.children.some((child) => child.id === threadId));
486
529
  }
487
530
  function centerOnPointAtZoom(editor, point, zoom, duration = 200) {
488
531
  const viewport = editor.getViewportScreenBounds();
532
+ const offset = commentCenterScreenOffset(editor);
489
533
  editor.setCamera(
490
534
  {
491
- x: viewport.w / (2 * zoom) - point.x,
535
+ x: (viewport.w / 2 - offset) / zoom - point.x,
492
536
  y: viewport.h / (2 * zoom) - point.y,
493
537
  z: zoom
494
538
  },
@@ -501,8 +545,16 @@ function clamp(value, min, max) {
501
545
  const ClusterBadge = memo(function ClusterBadge2({
502
546
  editor,
503
547
  node,
504
- onExpand
548
+ onExpand,
549
+ onSelectThread,
550
+ threadsById,
551
+ ...props
505
552
  }) {
553
+ const container = useContainer();
554
+ const msg = useTranslation();
555
+ const badgeRef = useRef(null);
556
+ const { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`);
557
+ usePassThroughWheelEvents(badgeRef);
506
558
  const point = useValue(
507
559
  "cluster badge point",
508
560
  () => {
@@ -512,46 +564,63 @@ const ClusterBadge = memo(function ClusterBadge2({
512
564
  },
513
565
  [editor, node]
514
566
  );
515
- if (!point) return null;
516
- return /* @__PURE__ */ jsx(
517
- "div",
518
- {
519
- className: "tlui-cmt-canvas-cluster",
520
- style: { left: point.x, top: point.y },
521
- onPointerDown: stop,
522
- onClick: (e) => {
523
- e.stopPropagation();
524
- onExpand(node);
525
- },
526
- children: /* @__PURE__ */ jsx(CountBadge, { count: node.count })
567
+ const previewThreads = useMemo(() => {
568
+ if (!previewShown) return [];
569
+ const threads = [];
570
+ for (const id of node.members) {
571
+ const thread = threadsById.get(id);
572
+ if (thread) threads.push(thread);
527
573
  }
528
- );
574
+ return sortThreadsForPreview(threads);
575
+ }, [previewShown, node.members, threadsById]);
576
+ if (!point) return null;
577
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
578
+ /* @__PURE__ */ jsx(
579
+ "button",
580
+ {
581
+ ref: badgeRef,
582
+ type: "button",
583
+ className: "tlui-cmt-button tlui-cmt-canvas-cluster",
584
+ style: { left: point.x, top: point.y },
585
+ "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
586
+ onPointerDown: (e) => {
587
+ if (isCanvasPanGesture(editor, e)) {
588
+ forwardPointerEventToCanvas(container, e);
589
+ return;
590
+ }
591
+ e.stopPropagation();
592
+ },
593
+ onClick: (e) => {
594
+ e.stopPropagation();
595
+ onExpand(node);
596
+ },
597
+ ...previewHandlers,
598
+ onFocus: previewHandlers.onPointerEnter,
599
+ onBlur: previewHandlers.onPointerLeave,
600
+ children: /* @__PURE__ */ jsx(CountBadge, { count: node.count })
601
+ }
602
+ ),
603
+ previewShown && previewThreads.length > 0 && /* @__PURE__ */ jsx(
604
+ ThreadPreview,
605
+ {
606
+ editor,
607
+ threads: previewThreads,
608
+ container,
609
+ variant: "list",
610
+ point,
611
+ onSelectThread,
612
+ ...previewHandlers,
613
+ ...props
614
+ }
615
+ )
616
+ ] });
529
617
  });
530
618
  function isInInflatedViewport(editor, point) {
531
619
  const viewport = editor.getViewportScreenBounds();
532
- const margin = getCommentingOptions(editor).clusterCullMargin;
620
+ const margin = CLUSTER_CULL_MARGIN_PX;
533
621
  return point.x >= -margin && point.y >= -margin && point.x <= viewport.w + margin && point.y <= viewport.h + margin;
534
622
  }
535
- function ThreadPopover({
536
- container,
537
- style,
538
- children
539
- }) {
540
- const ref = useRef(null);
541
- usePassThroughWheelEvents(ref);
542
- usePassThroughMouseOverEvents(ref);
543
- return createPortal(
544
- /* @__PURE__ */ jsx("div", { ref, className: "tlui-cmt-canvas-popover", style, onPointerDown: stop, children }),
545
- container
546
- );
547
- }
548
- function RegionBox({
549
- editor,
550
- box,
551
- movable,
552
- onPreview,
553
- onCommit
554
- }) {
623
+ function RegionBox({ editor, box }) {
555
624
  const rect = useValue(
556
625
  "region rect",
557
626
  () => {
@@ -561,38 +630,7 @@ function RegionBox({
561
630
  },
562
631
  [editor, box.x, box.y, box.w, box.h]
563
632
  );
564
- const grabRef = useRef(null);
565
- const translated = (e) => {
566
- const g = grabRef.current;
567
- const p = editor.screenToPage({ x: e.clientX, y: e.clientY });
568
- return { ...g.box, x: g.box.x + (p.x - g.page.x), y: g.box.y + (p.y - g.page.y) };
569
- };
570
- const startMove = (e) => {
571
- e.stopPropagation();
572
- grabRef.current = { page: editor.screenToPage({ x: e.clientX, y: e.clientY }), box };
573
- e.currentTarget.setPointerCapture(e.pointerId);
574
- };
575
- const onMove = (e) => {
576
- if (grabRef.current) onPreview?.(translated(e));
577
- };
578
- const endMove = (e) => {
579
- if (!grabRef.current) return;
580
- const bounds = translated(e);
581
- grabRef.current = null;
582
- if (e.currentTarget.hasPointerCapture(e.pointerId))
583
- e.currentTarget.releasePointerCapture(e.pointerId);
584
- onCommit?.(bounds);
585
- };
586
- return /* @__PURE__ */ jsx(
587
- "div",
588
- {
589
- className: movable ? "tlui-cmt-canvas-region tlui-cmt-canvas-region--movable" : "tlui-cmt-canvas-region",
590
- style: rect,
591
- onPointerDown: movable ? startMove : void 0,
592
- onPointerMove: movable ? onMove : void 0,
593
- onPointerUp: movable ? endMove : void 0
594
- }
595
- );
633
+ return /* @__PURE__ */ jsx("div", { className: "tlui-cmt-canvas-region", style: rect });
596
634
  }
597
635
  function RegionDraftBox({ editor }) {
598
636
  const box = useValue("region draft", () => regionDraft.get(editor), [editor]);
@@ -605,12 +643,6 @@ const REGION_CORNERS = [
605
643
  { x: 0, y: 1, cursor: "nesw-resize" },
606
644
  { x: 1, y: 1, cursor: "nwse-resize" }
607
645
  ];
608
- const REGION_EDGES = [
609
- { x: 0.5, y: 0, cursor: "ns-resize" },
610
- { x: 1, y: 0.5, cursor: "ew-resize" },
611
- { x: 0.5, y: 1, cursor: "ns-resize" },
612
- { x: 0, y: 0.5, cursor: "ew-resize" }
613
- ];
614
646
  const REGION_HANDLE_MARGIN_PX = 12;
615
647
  function resizeRegion(box, handle, cursor) {
616
648
  const controlsX = handle.x !== 0.5;
@@ -672,33 +704,22 @@ function RegionResizeHandles({
672
704
  const ThreadPin = memo(function ThreadPin2({
673
705
  editor,
674
706
  thread,
675
- regionOptions,
676
707
  ...props
677
708
  }) {
678
- const {
679
- currentUserId,
680
- resolveName,
681
- onPostComment,
682
- isCommentUnread,
683
- onCommentRead,
684
- getMentionSuggestions,
685
- renderMentionSuggestion
686
- } = props;
709
+ const { resolveAuthor } = props;
687
710
  const options = useCommentingOptions();
688
- const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor;
711
+ const canComment = useCanComment(props.currentUserId);
689
712
  const container = useContainer();
690
- const comments = useThreadComments(editor, thread.id);
691
713
  const msg = useTranslation();
714
+ const comments = useThreadComments(editor, thread.id);
692
715
  const open = useValue("thread open", () => openThreadId.get(editor) === thread.id, [
693
716
  editor,
694
717
  thread.id
695
718
  ]);
696
- const [reply, setReply] = useState(EMPTY_COMMENT);
697
- const [editingId, setEditingId] = useState(null);
698
- const [editText, setEditText] = useState(EMPTY_COMMENT);
699
719
  const [dragPagePoint, setDragPagePoint] = useState(null);
700
720
  const [resizeBounds, setResizeBounds] = useState(null);
701
- const [pinHovered, setPinHovered] = useState(false);
721
+ const { previewShown, previewHandlers } = useMarkerPreview(editor, `pin:${thread.id}`);
722
+ const previewThreads = useMemo(() => [thread], [thread]);
702
723
  const pointerInRegion = useValue(
703
724
  "pointer in region",
704
725
  () => {
@@ -711,15 +732,20 @@ const ThreadPin = memo(function ThreadPin2({
711
732
  },
712
733
  [editor, thread.anchor, thread.pageId]
713
734
  );
714
- const revealed = open || resizeBounds != null || regionOptions.reveal === "pointer" && pointerInRegion || regionOptions.reveal === "pin-hover" && pinHovered;
735
+ const revealed = open || resizeBounds != null || pointerInRegion;
736
+ const pinCorner = thread.anchor.type === "region" ? regionAnchorPinCorner(thread.anchor) : REGION_PIN_CORNER;
715
737
  const resizeHandles = useMemo(
716
- () => regionOptions.resize === "edges" ? REGION_EDGES : REGION_CORNERS.filter(
717
- (c) => c.x !== regionOptions.pinCorner.x || c.y !== regionOptions.pinCorner.y
718
- ),
719
- [regionOptions.resize, regionOptions.pinCorner]
738
+ () => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),
739
+ [pinCorner]
720
740
  );
721
741
  const dragRef = useRef(null);
722
742
  const markerRef = useRef(null);
743
+ usePassThroughWheelEvents(markerRef);
744
+ useEffect(() => {
745
+ return () => {
746
+ if (dragRef.current) editor.setHintingShapes([]);
747
+ };
748
+ }, [editor]);
723
749
  useEffect(() => {
724
750
  if (!open) return;
725
751
  const onPointerDown = (e) => {
@@ -740,165 +766,68 @@ const ThreadPin = memo(function ThreadPin2({
740
766
  "pin point",
741
767
  () => {
742
768
  if (thread.pageId !== editor.getCurrentPageId()) return null;
743
- const pagePoint = anchorPagePoint(editor, thread.anchor, impreciseShapeAnchor);
744
- return pagePoint ? editor.pageToViewport(pagePoint) : null;
769
+ const pagePoint = anchorPagePoint(editor, thread.anchor);
770
+ if (!pagePoint) return null;
771
+ const viewportPoint = editor.pageToViewport(pagePoint);
772
+ const inset = impreciseShapePinInset(editor, thread.anchor);
773
+ return inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint;
745
774
  },
746
- [editor, thread.anchor, thread.pageId, impreciseShapeAnchor]
775
+ [editor, thread.anchor, thread.pageId]
747
776
  );
748
- const visible = point !== null;
749
- useEffect(() => {
750
- if (!open || !visible || !isCommentUnread || !onCommentRead) return;
751
- for (const comment of comments) {
752
- if (isCommentUnread(comment.id)) {
753
- onCommentRead(comment.id);
754
- }
755
- }
756
- }, [open, visible, comments, isCommentUnread, onCommentRead]);
757
777
  if (!point) return null;
758
- const postReply = () => {
759
- if (isCommentEmpty(reply) || !currentUserId) return;
760
- commitCommentMutation(editor, () => {
761
- const comment = createComment({
762
- threadId: thread.id,
763
- pageId: thread.pageId,
764
- authorId: currentUserId,
765
- body: reply
766
- });
767
- putCommentRecords(editor, [comment]);
768
- if (onPostComment) onPostComment(comment);
769
- });
770
- setReply(EMPTY_COMMENT);
771
- };
772
- const toggleResolve = () => {
773
- if (!currentUserId) return;
774
- commitCommentMutation(editor, () => {
775
- putCommentRecords(editor, [
776
- {
777
- ...thread,
778
- resolved: thread.resolved ? null : { at: Date.now(), by: currentUserId }
779
- }
780
- ]);
781
- });
782
- };
783
- const deleteThread = () => {
784
- openThreadId.set(editor, null);
785
- commitCommentMutation(
786
- editor,
787
- () => removeCommentRecords(editor, [thread.id, ...comments.map((c) => c.id)])
788
- );
789
- };
790
- const startEdit = (comment) => {
791
- setEditingId(comment.id);
792
- setEditText(comment.body);
793
- };
794
- const saveEdit = () => {
795
- const comment = comments.find((c) => c.id === editingId);
796
- if (!comment || isCommentEmpty(editText)) return;
797
- commitCommentMutation(editor, () => {
798
- putCommentRecords(editor, [{ ...comment, body: editText, editedAt: Date.now() }]);
799
- });
800
- setEditingId(null);
801
- };
802
- const renderComment = (card, index) => {
803
- const comment = comments[index];
804
- if (editingId === comment.id) {
805
- return /* @__PURE__ */ jsx(
806
- "div",
807
- {
808
- className: "tlui-cmt-editing",
809
- onKeyDown: (e) => {
810
- if (e.key === "Escape") {
811
- setEditingId(null);
812
- e.stopPropagation();
813
- }
814
- },
815
- children: /* @__PURE__ */ jsx(
816
- CommentComposer,
817
- {
818
- author: card.author,
819
- placeholder: msg("comments.edit-placeholder"),
820
- value: editText,
821
- onChange: setEditText,
822
- onSubmit: saveEdit,
823
- sendLabel: msg("comments.save"),
824
- disabled: isCommentEmpty(editText),
825
- getMentionSuggestions,
826
- renderMentionSuggestion,
827
- autoFocus: true
828
- }
829
- )
830
- }
831
- );
832
- }
833
- return /* @__PURE__ */ jsx(
834
- CommentCard,
835
- {
836
- ...card,
837
- actions: comment.authorId === currentUserId ? /* @__PURE__ */ jsx(
838
- "button",
839
- {
840
- className: "tlui-cmt-thread__action",
841
- title: msg("comments.edit"),
842
- onClick: () => startEdit(comment),
843
- children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "dots-horizontal", label: msg("comments.edit"), small: true })
844
- }
845
- ) : void 0
846
- }
847
- );
848
- };
849
- const headerActions = /* @__PURE__ */ jsxs(Fragment, { children: [
850
- currentUserId && /* @__PURE__ */ jsx(
851
- "button",
852
- {
853
- className: "tlui-cmt-thread__action",
854
- title: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
855
- onClick: toggleResolve,
856
- children: /* @__PURE__ */ jsx(
857
- TldrawUiIcon,
858
- {
859
- icon: "check",
860
- label: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
861
- small: true
862
- }
863
- )
864
- }
865
- ),
866
- currentUserId && /* @__PURE__ */ jsx(
867
- "button",
868
- {
869
- className: "tlui-cmt-thread__action",
870
- title: msg("comments.delete"),
871
- onClick: deleteThread,
872
- children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "trash", label: msg("comments.delete"), small: true })
873
- }
874
- ),
875
- /* @__PURE__ */ jsx(
876
- "button",
877
- {
878
- className: "tlui-cmt-thread__action",
879
- title: msg("comments.dismiss"),
880
- onClick: () => openThreadId.set(editor, null),
881
- children: /* @__PURE__ */ jsx(TldrawUiIcon, { icon: "cross-2", label: msg("comments.dismiss"), small: true })
882
- }
883
- )
884
- ] });
885
778
  const PinContent = options.components.PinContent;
886
- const pinContent = PinContent ? /* @__PURE__ */ jsx(PinContent, { thread, comments }) : initialOf(resolveName(thread.createdBy) ?? UNKNOWN_AUTHOR);
779
+ const threadAuthor = resolveAuthor(thread.createdBy);
780
+ const pinContent = PinContent ? /* @__PURE__ */ jsx(PinContent, { thread, comments }) : /* @__PURE__ */ jsx(Avatar, { author: threadAuthor ?? UNKNOWN_COMMENT_AUTHOR });
781
+ const pinLabel = msg(
782
+ thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
783
+ ).replace("{name}", threadAuthor?.name ?? UNKNOWN_AUTHOR);
887
784
  const isRegion = thread.anchor.type === "region";
888
- const pinMovable = regionOptions.move !== "body";
889
- const bodyMovable = regionOptions.move !== "pin";
890
785
  const startDrag = (e) => {
786
+ if (isCanvasPanGesture(editor, e)) {
787
+ forwardPointerEventToCanvas(container, e);
788
+ return;
789
+ }
891
790
  e.stopPropagation();
892
- dragRef.current = { startX: e.clientX, startY: e.clientY, moved: false };
791
+ const grabPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
792
+ const anchorPage = anchorPagePoint(editor, thread.anchor);
793
+ const inset = impreciseShapePinInset(editor, thread.anchor);
794
+ if (anchorPage && inset) {
795
+ const zoom = editor.getZoomLevel();
796
+ anchorPage.x += inset.x / zoom;
797
+ anchorPage.y += inset.y / zoom;
798
+ }
799
+ dragRef.current = {
800
+ startX: e.clientX,
801
+ startY: e.clientY,
802
+ moved: false,
803
+ offsetX: anchorPage ? anchorPage.x - grabPage.x : 0,
804
+ offsetY: anchorPage ? anchorPage.y - grabPage.y : 0
805
+ };
893
806
  e.currentTarget.setPointerCapture(e.pointerId);
894
807
  };
895
808
  const onDrag = (e) => {
896
809
  const drag = dragRef.current;
897
810
  if (!drag) return;
898
- if (isRegion && !pinMovable) return;
811
+ if (!canComment) return;
899
812
  if (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;
900
813
  drag.moved = true;
901
- setDragPagePoint(editor.screenToPage({ x: e.clientX, y: e.clientY }));
814
+ const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
815
+ const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
816
+ setDragPagePoint(pagePoint);
817
+ if (!isRegion) {
818
+ const hit = commentTargetShapeAt(editor, pagePoint);
819
+ editor.setHintingShapes(hit ? [hit.id] : []);
820
+ }
821
+ };
822
+ const cancelDrag = (e) => {
823
+ const drag = dragRef.current;
824
+ dragRef.current = null;
825
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
826
+ e.currentTarget.releasePointerCapture(e.pointerId);
827
+ }
828
+ if (!drag) return;
829
+ setDragPagePoint(null);
830
+ editor.setHintingShapes([]);
902
831
  };
903
832
  const endDrag = (e) => {
904
833
  const drag = dragRef.current;
@@ -907,55 +836,59 @@ const ThreadPin = memo(function ThreadPin2({
907
836
  e.currentTarget.releasePointerCapture(e.pointerId);
908
837
  }
909
838
  if (!drag) return;
839
+ editor.setHintingShapes([]);
910
840
  if (!drag.moved) {
911
841
  openThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id);
912
842
  return;
913
843
  }
914
- const pagePoint = editor.screenToPage({ x: e.clientX, y: e.clientY });
844
+ const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
845
+ const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
915
846
  setDragPagePoint(null);
916
847
  let anchor;
917
848
  if (thread.anchor.type === "region") {
918
849
  anchor = {
919
850
  ...thread.anchor,
920
- x: pagePoint.x - regionOptions.pinCorner.x * thread.anchor.w,
921
- y: pagePoint.y - regionOptions.pinCorner.y * thread.anchor.h
851
+ x: pagePoint.x - pinCorner.x * thread.anchor.w,
852
+ y: pagePoint.y - pinCorner.y * thread.anchor.h
922
853
  };
923
854
  } else {
924
- const hit = editor.getShapeAtPoint(pagePoint, { hitInside: true });
925
- anchor = hit ? shapeAnchorAt(editor, hit.id, pagePoint, e.altKey) : { type: "point", x: pagePoint.x, y: pagePoint.y };
855
+ const hit = commentTargetShapeAt(editor, pagePoint);
856
+ anchor = hit ? shapeAnchorAt(
857
+ editor,
858
+ hit.id,
859
+ pagePoint,
860
+ getCommentingOptions(editor).shouldBePrecise(editor, {
861
+ shapeId: hit.id,
862
+ point: pagePoint,
863
+ altKey: e.altKey
864
+ })
865
+ ) : { type: "point", x: pagePoint.x, y: pagePoint.y };
926
866
  }
927
- commitCommentMutation(editor, () => putCommentRecords(editor, [{ ...thread, anchor }]), "drag");
867
+ commitCommentMutation(editor, () => putRecordsInCommit(editor, [{ ...thread, anchor }]), "drag");
928
868
  };
929
- const livePinPage = resizeBounds ? regionPinPoint(resizeBounds, regionOptions.pinCorner) : dragPagePoint;
930
- const renderPoint = livePinPage ? editor.pageToViewport(livePinPage) : point;
869
+ const livePinPage = resizeBounds ? regionPinPoint(resizeBounds, pinCorner) : dragPagePoint;
870
+ const renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point;
871
+ const renderPoint = isRegion ? { x: renderPointBase.x - 17, y: renderPointBase.y + 17 } : renderPointBase;
931
872
  const regionAnchor = thread.anchor.type === "region" ? thread.anchor : void 0;
932
873
  const movedRegion = regionAnchor && dragPagePoint ? {
933
874
  ...regionAnchor,
934
- x: dragPagePoint.x - regionOptions.pinCorner.x * regionAnchor.w,
935
- y: dragPagePoint.y - regionOptions.pinCorner.y * regionAnchor.h
875
+ x: dragPagePoint.x - pinCorner.x * regionAnchor.w,
876
+ y: dragPagePoint.y - pinCorner.y * regionAnchor.h
936
877
  } : regionAnchor;
937
878
  const regionBoxBounds = resizeBounds ?? movedRegion;
938
879
  const commitResize = (bounds) => {
939
880
  setResizeBounds(null);
940
- editor.run(
941
- () => putCommentRecords(editor, [{ ...thread, anchor: { type: "region", ...bounds } }]),
942
- {
943
- history: "ignore"
944
- }
881
+ if (!canComment) return;
882
+ commitCommentMutation(
883
+ editor,
884
+ // Spread the existing anchor first so the region's pin corner survives a resize.
885
+ () => putRecordsInCommit(editor, [{ ...thread, anchor: { ...regionAnchor, ...bounds } }]),
886
+ "drag"
945
887
  );
946
888
  };
947
889
  return /* @__PURE__ */ jsxs(Fragment, { children: [
948
- regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ jsx(
949
- RegionBox,
950
- {
951
- editor,
952
- box: regionBoxBounds,
953
- movable: bodyMovable && !dragPagePoint,
954
- onPreview: setResizeBounds,
955
- onCommit: commitResize
956
- }
957
- ),
958
- regionBoxBounds && revealed && !dragPagePoint && regionOptions.resize !== "none" && /* @__PURE__ */ jsx(
890
+ regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ jsx(RegionBox, { editor, box: regionBoxBounds }),
891
+ regionBoxBounds && revealed && !dragPagePoint && canComment && /* @__PURE__ */ jsx(
959
892
  RegionResizeHandles,
960
893
  {
961
894
  editor,
@@ -968,19 +901,33 @@ const ThreadPin = memo(function ThreadPin2({
968
901
  /* @__PURE__ */ jsxs(
969
902
  "div",
970
903
  {
971
- className: open ? "tlui-cmt-canvas-pin tlui-cmt-canvas-pin--open" : "tlui-cmt-canvas-pin",
904
+ className: [
905
+ "tlui-cmt-canvas-pin",
906
+ open && "tlui-cmt-canvas-pin--open",
907
+ dragPagePoint && "tlui-cmt-canvas-pin--dragging"
908
+ ].filter(Boolean).join(" "),
972
909
  style: { left: renderPoint.x, top: renderPoint.y },
973
910
  children: [
974
911
  /* @__PURE__ */ jsx(
975
- "div",
912
+ "button",
976
913
  {
977
914
  ref: markerRef,
978
- className: "tlui-cmt-canvas-pin__marker",
915
+ type: "button",
916
+ className: "tlui-cmt-button tlui-cmt-canvas-pin__marker",
917
+ "aria-label": pinLabel,
918
+ "aria-expanded": open,
979
919
  onPointerDown: startDrag,
980
920
  onPointerMove: onDrag,
981
921
  onPointerUp: endDrag,
982
- onPointerEnter: () => setPinHovered(true),
983
- onPointerLeave: () => setPinHovered(false),
922
+ onPointerCancel: cancelDrag,
923
+ onClick: (e) => {
924
+ if (e.detail !== 0) return;
925
+ openThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id);
926
+ },
927
+ onPointerEnter: previewHandlers.onPointerEnter,
928
+ onPointerLeave: previewHandlers.onPointerLeave,
929
+ onFocus: previewHandlers.onPointerEnter,
930
+ onBlur: previewHandlers.onPointerLeave,
984
931
  children: /* @__PURE__ */ jsx(CommentPin, { resolved: thread.resolved != null, open, children: pinContent })
985
932
  }
986
933
  ),
@@ -988,31 +935,25 @@ const ThreadPin = memo(function ThreadPin2({
988
935
  ThreadPopover,
989
936
  {
990
937
  container,
991
- style: { left: renderPoint.x + 36, top: renderPoint.y - 28 },
992
- children: /* @__PURE__ */ jsx(
993
- CommentThread,
994
- {
995
- header: msg("comments.thread-title"),
996
- headerActions,
997
- renderComment,
998
- comments: comments.map((c) => toCardProps(c, props, options.components)),
999
- resolvedBanner: thread.resolved ? msg("comments.resolved-by").replace(
1000
- "{name}",
1001
- resolveName(thread.resolved.by) ?? UNKNOWN_AUTHOR
1002
- ) : void 0,
1003
- composer: currentUserId && !thread.resolved ? {
1004
- author: resolveName(currentUserId) ?? UNKNOWN_AUTHOR,
1005
- placeholder: msg("comments.reply-placeholder"),
1006
- sendLabel: msg("comments.send"),
1007
- value: reply,
1008
- onChange: setReply,
1009
- onSubmit: postReply,
1010
- disabled: isCommentEmpty(reply),
1011
- getMentionSuggestions,
1012
- renderMentionSuggestion
1013
- } : void 0
1014
- }
1015
- )
938
+ style: {
939
+ left: renderPoint.x + POPOVER_OFFSET.thread.x,
940
+ top: renderPoint.y + POPOVER_OFFSET.thread.y - THREAD_HEADER_BLOCK
941
+ },
942
+ children: /* @__PURE__ */ jsx(ThreadView, { editor, thread, ...props })
943
+ }
944
+ ),
945
+ previewShown && !dragPagePoint && /* @__PURE__ */ jsx(
946
+ ThreadPreview,
947
+ {
948
+ editor,
949
+ threads: previewThreads,
950
+ container,
951
+ variant: "thread",
952
+ point: renderPoint,
953
+ onSelectThread: () => openThreadId.set(editor, thread.id),
954
+ ...previewHandlers,
955
+ currentUserId: props.currentUserId,
956
+ resolveAuthor
1016
957
  }
1017
958
  )
1018
959
  ]
@@ -1024,12 +965,18 @@ function PendingComposer({
1024
965
  editor,
1025
966
  pending,
1026
967
  currentUserId,
1027
- resolveName,
968
+ resolveAuthor,
1028
969
  onPostComment,
1029
970
  getMentionSuggestions,
1030
971
  renderMentionSuggestion
1031
972
  }) {
1032
- const [text, setText] = useState(EMPTY_COMMENT);
973
+ const ComposerFallback = useCommentingOptions().components.ComposerFallback;
974
+ const canComment = useCanComment(currentUserId);
975
+ const me = currentUserId ? resolveAuthor(currentUserId) : void 0;
976
+ const draftAvatar = /* @__PURE__ */ jsx(CommentPin, { children: /* @__PURE__ */ jsx(Avatar, { author: me ?? UNKNOWN_COMMENT_AUTHOR }) });
977
+ const [text, setText] = useState(
978
+ () => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT
979
+ );
1033
980
  const ref = useRef(null);
1034
981
  const msg = useTranslation();
1035
982
  const container = useContainer();
@@ -1065,10 +1012,11 @@ function PendingComposer({
1065
1012
  authorId: currentUserId,
1066
1013
  body: text
1067
1014
  });
1068
- putCommentRecords(editor, [thread, comment]);
1015
+ putRecordsInCommit(editor, [thread, comment]);
1069
1016
  if (onPostComment) onPostComment(comment);
1070
1017
  });
1071
1018
  setText(EMPTY_COMMENT);
1019
+ clearCommentDraft(NEW_COMMENT_DRAFT);
1072
1020
  pendingComment.set(editor, null);
1073
1021
  };
1074
1022
  return createPortal(
@@ -1076,28 +1024,36 @@ function PendingComposer({
1076
1024
  "div",
1077
1025
  {
1078
1026
  ref,
1079
- className: "tlui-cmt-canvas-composer",
1027
+ className: [
1028
+ "tlui-cmt-canvas-composer",
1029
+ pending.anchor.type === "region" && "tlui-cmt-canvas-composer--region",
1030
+ !canComment && "tlui-cmt-canvas-composer--fallback"
1031
+ ].filter(Boolean).join(" "),
1080
1032
  style: { left: point.x, top: point.y },
1081
1033
  onPointerDown: stop,
1034
+ onContextMenu: stop,
1082
1035
  onKeyDown: (e) => {
1083
1036
  if (e.key === "Escape" && !isMentionPickerOpen()) pendingComment.set(editor, null);
1084
1037
  },
1085
- children: /* @__PURE__ */ jsx(
1038
+ children: canComment ? /* @__PURE__ */ jsx(
1086
1039
  CommentComposer,
1087
1040
  {
1088
- author: currentUserId ? resolveName(currentUserId) ?? UNKNOWN_AUTHOR : "",
1041
+ author: me ?? UNKNOWN_COMMENT_AUTHOR,
1089
1042
  placeholder: msg("comments.add-placeholder"),
1090
1043
  sendLabel: msg("comments.send"),
1091
1044
  value: text,
1092
- onChange: setText,
1045
+ onChange: (value) => {
1046
+ setText(value);
1047
+ saveCommentDraft(NEW_COMMENT_DRAFT, value);
1048
+ },
1093
1049
  onSubmit: submit,
1094
- disabled: isCommentEmpty(text),
1050
+ disabled: isCommentEmpty(text) || !currentUserId,
1095
1051
  getMentionSuggestions,
1096
1052
  renderMentionSuggestion,
1097
1053
  autoFocus: true,
1098
1054
  leading: draftAvatar
1099
1055
  }
1100
- )
1056
+ ) : ComposerFallback && /* @__PURE__ */ jsx(ComposerFallback, { context: "pending" })
1101
1057
  }
1102
1058
  ),
1103
1059
  container