@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02

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 (238) hide show
  1. package/commenting.css +63 -71
  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 +0 -1
  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 +77 -870
  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 +134 -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 +323 -202
  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 +12 -0
  77. package/dist-cjs/ui/format-time.js.map +2 -2
  78. package/dist-cjs/ui/icons.js +115 -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 +0 -1
  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 +54 -878
  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 +126 -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 +323 -202
  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 +12 -0
  161. package/dist-esm/ui/format-time.mjs.map +2 -2
  162. package/dist-esm/ui/icons.mjs +95 -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 +5 -5
  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.tsx +8 -12
  189. package/src/canvas/comments-filter-menu.tsx +9 -16
  190. package/src/canvas/comments-overflow-menu.tsx +8 -3
  191. package/src/canvas/comments-overlay.tsx +154 -1344
  192. package/src/canvas/comments-sidebar.tsx +40 -19
  193. package/src/canvas/comments-visibility-toggle.tsx +6 -30
  194. package/src/canvas/context.ts +4 -3
  195. package/src/canvas/hooks.test.ts +94 -0
  196. package/src/canvas/hooks.ts +17 -3
  197. package/src/canvas/license.ts +1 -1
  198. package/src/canvas/mobile-placement.ts +115 -0
  199. package/src/canvas/options.test.ts +124 -2
  200. package/src/canvas/options.ts +192 -58
  201. package/src/canvas/pending-composer.tsx +157 -0
  202. package/src/canvas/pin-stacking.test.ts +111 -1
  203. package/src/canvas/pin-stacking.ts +46 -0
  204. package/src/canvas/region-box.tsx +124 -0
  205. package/src/canvas/state.ts +20 -28
  206. package/src/canvas/thread-pin.tsx +418 -0
  207. package/src/canvas/thread-preview.tsx +77 -87
  208. package/src/canvas/thread-stack.tsx +19 -29
  209. package/src/canvas/thread-state.test.ts +51 -0
  210. package/src/canvas/thread-state.ts +56 -23
  211. package/src/canvas/thread-view.test.ts +72 -0
  212. package/src/canvas/thread-view.tsx +231 -133
  213. package/src/clustering/computeClusterTable.ts +12 -3
  214. package/src/clustering/replay.test.ts +0 -7
  215. package/src/clustering/replay.ts +131 -32
  216. package/src/clustering/runtime.test.ts +50 -6
  217. package/src/clustering/runtime.ts +42 -39
  218. package/src/clustering/schedule.test.ts +0 -6
  219. package/src/clustering/screen-offsets.test.ts +171 -0
  220. package/src/clustering/types.ts +10 -0
  221. package/src/index.ts +15 -2
  222. package/src/ui/byline.tsx +16 -6
  223. package/src/ui/comment-composer.tsx +25 -69
  224. package/src/ui/comment-pin.tsx +3 -16
  225. package/src/ui/comments-list.tsx +40 -29
  226. package/src/ui/comments.css +58 -60
  227. package/src/ui/format-time.test.ts +69 -0
  228. package/src/ui/format-time.ts +20 -0
  229. package/src/ui/icons.tsx +116 -0
  230. package/src/ui/reaction-picker.tsx +9 -16
  231. package/src/ui/reaction.tsx +10 -6
  232. package/src/ui/send-button.tsx +3 -8
  233. package/src/ui/visual-viewport.test.ts +36 -0
  234. package/src/ui/visual-viewport.ts +27 -0
  235. package/dist-cjs/ui/tooltip-button.js.map +0 -7
  236. package/dist-esm/ui/tooltip-button.mjs +0 -12
  237. package/dist-esm/ui/tooltip-button.mjs.map +0 -7
  238. package/src/ui/tooltip-button.tsx +0 -20
@@ -24,69 +24,29 @@ 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);
@@ -104,121 +64,28 @@ function CanvasCommentsLayer(props) {
104
64
  [allThreads, showResolved, openId]
105
65
  );
106
66
  (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);
67
+ const {
68
+ model: clusterModel,
69
+ zoomBounds: clusterZoomBounds,
70
+ fadeNodes,
71
+ orphanThreads,
72
+ heldThreads
73
+ } = (0, import_cluster_model.useClusterModel)(editor, threads, openId);
214
74
  const threadsById = (0, import_react.useMemo)(
215
75
  () => new Map(threads.map((thread) => [thread.id, thread])),
216
76
  [threads]
217
77
  );
218
- const pinStacks = (0, import_tldraw.useValue)("comment pin stacks", () => (0, import_pin_stacking.computePinStacks)(editor, threads), [
219
- editor,
220
- threads
221
- ]);
78
+ const pinStacksRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
79
+ const pinStacks = (0, import_tldraw.useValue)(
80
+ "comment pin stacks",
81
+ () => {
82
+ const stacks = (0, import_pin_stacking.computePinStacks)(editor, threads);
83
+ if ((0, import_pin_stacking.pinStacksEqual)(pinStacksRef.current, stacks)) return pinStacksRef.current;
84
+ pinStacksRef.current = stacks;
85
+ return stacks;
86
+ },
87
+ [editor, threads]
88
+ );
222
89
  const openThread = openId ? threadsById.get(openId) : null;
223
90
  const hidden = (0, import_tldraw.useValue)("comments hidden", () => import_state.commentsHidden.get(editor), [editor]);
224
91
  (0, import_react.useEffect)(() => {
@@ -229,17 +96,18 @@ function CanvasCommentsLayer(props) {
229
96
  import_state.revealThreadRequest.set(editor, null);
230
97
  };
231
98
  }, [editor]);
99
+ const openStackKeyIsStale = (0, import_tldraw.useValue)(
100
+ "open stack key stale",
101
+ () => {
102
+ const key = import_state.openStackId.get(editor);
103
+ if (!key) return false;
104
+ return !(0, import_pin_stacking.isOpenStackKeyLive)(editor, key, pinStacks, threadsById);
105
+ },
106
+ [editor, pinStacks, threadsById]
107
+ );
232
108
  (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]);
109
+ if (openStackKeyIsStale) import_state.openStackId.set(editor, null);
110
+ }, [editor, openStackKeyIsStale]);
243
111
  const requestedRevealThread = (0, import_tldraw.useValue)(
244
112
  "requested reveal thread",
245
113
  () => {
@@ -256,35 +124,28 @@ function CanvasCommentsLayer(props) {
256
124
  if (!requestedRevealThread) return;
257
125
  import_state.revealThreadRequest.set(editor, null);
258
126
  import_state.commentsHidden.set(editor, false);
259
- revealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options);
127
+ (0, import_cluster_model.revealThreadPin)(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options);
260
128
  import_state.openThreadId.set(editor, requestedRevealThread.id);
261
129
  }, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options]);
262
130
  const revealClusteredThread = (0, import_react.useCallback)(
263
131
  (thread) => {
264
- revealThreadPin(
132
+ (0, import_cluster_model.revealThreadPin)(
265
133
  editor,
266
134
  thread,
267
135
  clusterModel.table,
268
136
  clusterZoomBounds,
269
137
  options,
270
- CLUSTER_EXPAND_ZOOM_MS
138
+ import_cluster_model.CLUSTER_EXPAND_ZOOM_MS
271
139
  );
272
140
  import_state.openThreadId.set(editor, thread.id);
273
141
  },
274
142
  [clusterModel.table, clusterZoomBounds, editor, options]
275
143
  );
276
- const zoomToClusterSplit = (0, import_react.useCallback)(
144
+ const expandCluster = (0, import_react.useCallback)(
277
145
  (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);
146
+ (0, import_cluster_model.zoomToClusterSplit)(editor, clusterModel.table, clusterZoomBounds, node);
286
147
  },
287
- [clusterModel, clusterZoomBounds, editor]
148
+ [clusterModel.table, clusterZoomBounds, editor]
288
149
  );
289
150
  (0, import_react.useEffect)(() => {
290
151
  const onKeyDown = (e) => {
@@ -336,701 +197,47 @@ function CanvasCommentsLayer(props) {
336
197
  const stackThreads = group.map((id) => threadsById.get(id)).filter((t) => t !== void 0);
337
198
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_thread_stack.ThreadStackPin, { editor, threads: stackThreads, ...props });
338
199
  }
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);
200
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_thread_pin.ThreadPin, { editor, thread, ...props });
653
201
  };
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
- );
862
- };
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,
202
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.EditorPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tlui-cmt-canvas-layer", children: [
203
+ options.enableClustering ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
204
+ fadeNodes.map(({ node, phase }) => {
205
+ let content;
206
+ const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
207
+ if (node.count === 1) {
208
+ const thread = threadsById.get(node.id);
209
+ if (!thread) return null;
210
+ content = renderThreadPin(thread);
211
+ } else if (stackGroup) {
212
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id));
213
+ content = owner && node.members.includes(owner) ? renderThreadPin(threadsById.get(owner)) : null;
214
+ } else {
215
+ content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
216
+ import_cluster_badge.ClusterBadge,
921
217
  {
922
218
  editor,
923
- threads: previewThreads,
924
- container,
925
- variant: "thread",
926
- point: renderPoint,
927
- onSelectThread: () => import_state.openThreadId.set(editor, thread.id),
928
- ...previewHandlers,
219
+ node,
220
+ onExpand: expandCluster,
221
+ onSelectThread: revealClusteredThread,
222
+ threadsById,
929
223
  currentUserId: props.currentUserId,
930
- resolveAuthor
224
+ resolveAuthor: props.resolveAuthor
931
225
  }
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
- }
226
+ );
227
+ }
228
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cluster_fade.clusterFadeClassName)(phase), children: content }, `cluster-fade:${node.id}`);
229
+ }),
230
+ orphanThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id)),
231
+ heldThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id))
232
+ ] }) : (
233
+ // Clustering off: every thread renders its own pin. The open thread is excluded and
234
+ // rendered once below, or it would mount a second, stacked pin.
235
+ threads.filter((thread) => thread.id !== openId).map((thread) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(thread) }, thread.id))
1032
236
  ),
1033
- container
1034
- );
237
+ openThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: renderThreadPin(openThread) }, `open:${openThread.id}`),
238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_region_box.RegionDraftBox, { editor }),
239
+ pending?.anchor.type === "region" && showPendingComposer && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_region_box.RegionBox, { editor, box: pending.anchor }),
240
+ pending && showPendingComposer && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_pending_composer.PendingComposer, { editor, pending, ...props })
241
+ ] }) });
1035
242
  }
1036
243
  //# sourceMappingURL=comments-overlay.js.map