@tldraw/commenting 0.0.0-bootstrap

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