@tldraw/commenting 5.3.0-next.d7d8ced023d5 → 5.3.0-next.dec36c5c2930

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