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

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