@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
@@ -39,7 +39,6 @@ const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
39
39
  threads,
40
40
  ...props
41
41
  }) {
42
- const container = (0, import_tldraw.useContainer)();
43
42
  const msg = (0, import_tldraw.useTranslation)();
44
43
  const badgeRef = (0, import_react.useRef)(null);
45
44
  (0, import_tldraw.usePassThroughWheelEvents)(badgeRef);
@@ -65,7 +64,10 @@ const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
65
64
  const first = threads[0];
66
65
  if (first.pageId !== editor.getCurrentPageId()) return null;
67
66
  const pagePoint = (0, import_thread_state.anchorPagePoint)(editor, first.anchor);
68
- return pagePoint ? editor.pageToViewport(pagePoint) : null;
67
+ if (!pagePoint) return null;
68
+ const viewportPoint = editor.pageToViewport(pagePoint);
69
+ const inset = (0, import_thread_state.impreciseShapePinInset)(editor, first.anchor);
70
+ return inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint;
69
71
  },
70
72
  [editor, threads]
71
73
  );
@@ -129,7 +131,6 @@ const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
129
131
  {
130
132
  editor,
131
133
  threads,
132
- container,
133
134
  variant: "list",
134
135
  point,
135
136
  onSelectThread: (thread) => {
@@ -144,10 +145,9 @@ const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
144
145
  open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
145
146
  import_thread_view.ThreadPopover,
146
147
  {
147
- container,
148
- style: {
149
- left: point.x + import_thread_view.POPOVER_OFFSET.list.x,
150
- top: point.y + import_thread_view.POPOVER_OFFSET.list.y - liftForHeader
148
+ base: {
149
+ x: point.x + import_thread_view.POPOVER_OFFSET.list.x,
150
+ y: point.y + import_thread_view.POPOVER_OFFSET.list.y - liftForHeader
151
151
  },
152
152
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-stack-list", children: threads.map(
153
153
  (thread) => thread.id === openId ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-stack-list__thread", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_thread_view.ThreadView, { editor, thread, ...props }) }, thread.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-stack.tsx"],
4
- "sourcesContent": ["import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { CountBadge } from '../ui/count-badge'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { pinStackKey } from './pin-stacking'\nimport { openStackId, openThreadId } from './state'\nimport { ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { anchorPagePoint } from './thread-state'\nimport {\n\tPOPOVER_OFFSET,\n\tThreadPopover,\n\tThreadView,\n\ttoCardProps,\n\tuseResolveName,\n} from './thread-view'\n\n/**\n * The pin for threads whose anchors resolve to the same page point \u2014 pins zooming can never\n * separate, so instead of stacked markers they share one count badge. Clicking it opens a\n * popover listing each thread as a card; clicking a card expands that thread in place (via the\n * single open-thread state, so expanding one collapses another).\n */\nexport const ThreadStackPin = memo(function ThreadStackPin({\n\teditor,\n\tthreads,\n\t...props\n}: CommentingContext & {\n\teditor: Editor\n\t/** The stack's threads, oldest first. All resolve to the same anchor point. */\n\tthreads: readonly TLCommentThread[]\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\t// The badge takes pointer events (to open on click), so wheel input over it would otherwise be\n\t// swallowed instead of zooming \u2014 pass it through to the canvas, as the pin and cluster badge do.\n\tusePassThroughWheelEvents(badgeRef)\n\t// Hovering the badge previews its threads; clicking still opens them as the interactive list.\n\t// The preview is what makes the badge legible before you commit to opening it.\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `stack:${threads[0].id}`)\n\t// The list stays open while a member thread is expanded, and on its own after the member\n\t// collapses \u2014 so Escape steps back: expanded thread \u2192 card list \u2192 closed. Held in editor\n\t// state (not component state) because this pin remounts as its owning render path changes.\n\t// Keyed by the coincident page point, not a member id, so the open state survives losing a\n\t// member (including the oldest): the survivors keep the same key. Falls back to a thread id\n\t// only when the anchor can't resolve (off page), where the list isn't shown anyway.\n\tconst stackId = useValue(\n\t\t'stack id',\n\t\t() => {\n\t\t\tconst pagePoint = anchorPagePoint(editor, threads[0].anchor)\n\t\t\treturn pagePoint ? pinStackKey(pagePoint) : threads[0].id\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst listOpen = useValue('stack list open', () => openStackId.get(editor) === stackId, [\n\t\teditor,\n\t\tstackId,\n\t])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\tconst openMember = threads.find((thread) => thread.id === openId)\n\tconst open = listOpen || openMember !== undefined\n\n\tconst point = useValue(\n\t\t'stack point',\n\t\t() => {\n\t\t\tconst first = threads[0]\n\t\t\tif (first.pageId !== editor.getCurrentPageId()) return null\n\t\t\t// The badge hangs off its anchor point bottom-left (transform: translate(0, -100%)),\n\t\t\t// like a pin and like the cluster badge \u2014 but it sits at the raw page point with no pin\n\t\t\t// inset. The inset only tucks an imprecise single pin inside its shape; applying it here\n\t\t\t// would offset the badge from where the cluster badge sits (cluster centroids average\n\t\t\t// raw anchor points) and make it hop as pins flip between them.\n\t\t\tconst pagePoint = anchorPagePoint(editor, first.anchor)\n\t\t\treturn pagePoint ? editor.pageToViewport(pagePoint) : null\n\t\t},\n\t\t[editor, threads]\n\t)\n\n\t// Clicking outside the popover (and off the badge) closes the whole stack \u2014 mirrors the\n\t// single pin's dismiss. Capture phase + class checks, since the popover portals elsewhere.\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!target) return\n\t\t\tif (target.closest('.tlui-cmt-canvas-popover')) return\n\t\t\tconst badge = badgeRef.current\n\t\t\tif (badge && badge.contains(target)) return\n\t\t\t// A click inside a menu/popover layered above us belongs to that layer; defer to its\n\t\t\t// own dismissal instead of closing the stack out from under it.\n\t\t\tif (\n\t\t\t\ttarget.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')\n\t\t\t)\n\t\t\t\treturn\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [open, editor])\n\n\t// Escape with only the card list showing closes it. When a member thread is expanded, the\n\t// layer's Escape handler collapses that first and marks the event consumed \u2014 stepping back\n\t// to the list rather than closing everything at once.\n\tuseEffect(() => {\n\t\tif (!listOpen) return\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || e.defaultPrevented) return\n\t\t\topenStackId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [listOpen, editor])\n\n\tif (!point) return null\n\n\tconst toggle = () => {\n\t\tif (open) {\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t} else {\n\t\t\topenStackId.set(editor, stackId)\n\t\t}\n\t}\n\n\t// The expanded thread gains a \"Comment\" header that pushes its first comment down from where the\n\t// hover preview showed it. When that thread is the list's first, lift the whole list by the\n\t// header block so its \"You\" holds position across hover -> open: the single thread's\n\t// THREAD_HEADER_BLOCK (36) plus the 8px margin above the expanded entry, less the 2px top the\n\t// preview card sits its \"You\" down by. Only the first entry \u2014 lifting the list can't also hold a\n\t// lower thread's neighbours in place.\n\tconst liftForHeader = threads[0]?.id === openId ? 42 : 0\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"tlui-cmt-canvas-pin\" style={{ left: point.x, top: point.y }}>\n\t\t\t\t<button\n\t\t\t\t\tref={badgeRef}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-stack-badge\"\n\t\t\t\t\taria-label={msg('comments.stack-label').replace('{count}', String(threads.length))}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\ttoggle()\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<CountBadge count={threads.length} open={open} />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t{previewShown && !open && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={threads}\n\t\t\t\t\tcontainer={container}\n\t\t\t\t\t// Lines the preview up with the stack list itself, so opening it leaves the cards\n\t\t\t\t\t// exactly where the preview had them.\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\t// Picking a card from the preview lands in the same place clicking the badge and\n\t\t\t\t\t// then the card would: the list open, that thread expanded within it.\n\t\t\t\t\tonSelectThread={(thread) => {\n\t\t\t\t\t\topenStackId.set(editor, stackId)\n\t\t\t\t\t\topenThreadId.set(editor, thread.id)\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\tresolveAuthor={props.resolveAuthor}\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t{open && (\n\t\t\t\t<ThreadPopover\n\t\t\t\t\tcontainer={container}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tleft: point.x + POPOVER_OFFSET.list.x,\n\t\t\t\t\t\ttop: point.y + POPOVER_OFFSET.list.y - liftForHeader,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"tlui-cmt-stack-list\">\n\t\t\t\t\t\t{threads.map((thread) =>\n\t\t\t\t\t\t\tthread.id === openId ? (\n\t\t\t\t\t\t\t\t<div key={thread.id} className=\"tlui-cmt-stack-list__thread\">\n\t\t\t\t\t\t\t\t\t<ThreadView editor={editor} thread={thread} {...props} />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<StackThreadCard\n\t\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\tthread={thread}\n\t\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\t\tonOpen={() => openThreadId.set(editor, thread.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</ThreadPopover>\n\t\t\t)}\n\t\t</>\n\t)\n})\n\n/** A collapsed stack entry: the thread's first comment as a card; clicking expands the thread. */\nfunction StackThreadCard({\n\teditor,\n\tthread,\n\tonOpen,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread; onOpen(): void }) {\n\tconst msg = useTranslation()\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\tconst first = comments[0]\n\tif (!first) return null\n\tconst open = (e: ReactMouseEvent) => {\n\t\te.stopPropagation()\n\t\tonOpen()\n\t}\n\treturn (\n\t\t<div className=\"tlui-cmt-stack-list__card\" onClick={open}>\n\t\t\t{/* The card's keyboard affordance. A button *wrapping* the card would put the comment\n\t\t\t body inside it, and a body renders its links as real anchors \u2014 interactive content\n\t\t\t nested in a button, which is an invalid content model and reads to assistive tech as a\n\t\t\t broken control. So the button covers the card as a sibling instead, leaving the body's\n\t\t\t own links above it and still reachable. */}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-stack-list__card-action\"\n\t\t\t\taria-label={msg(\n\t\t\t\t\tthread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'\n\t\t\t\t).replace('{name}', props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR)}\n\t\t\t\tonClick={open}\n\t\t\t/>\n\t\t\t<CommentCard {...toCardProps(first, props, options.components, resolveName)} />\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { CountBadge } from '../ui/count-badge'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { pinStackKey } from './pin-stacking'\nimport { openStackId, openThreadId } from './state'\nimport { ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { anchorPagePoint, impreciseShapePinInset } from './thread-state'\nimport {\n\tPOPOVER_OFFSET,\n\tThreadPopover,\n\tThreadView,\n\ttoCardProps,\n\tuseResolveName,\n} from './thread-view'\n\n/**\n * The pin for threads whose anchors resolve to the same page point \u2014 pins zooming can never\n * separate, so they share one count badge. Clicking it lists each thread as a card; clicking a card\n * expands that thread in place, via the single open-thread state.\n */\nexport const ThreadStackPin = memo(function ThreadStackPin({\n\teditor,\n\tthreads,\n\t...props\n}: CommentingContext & {\n\teditor: Editor\n\t/** The stack's threads, oldest first. All resolve to the same anchor point. */\n\tthreads: readonly TLCommentThread[]\n}) {\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\t// The badge takes pointer events (to open on click), so wheel input over it would otherwise be\n\t// swallowed instead of zooming \u2014 pass it through to the canvas, as the pin and cluster badge do.\n\tusePassThroughWheelEvents(badgeRef)\n\t// Hovering the badge previews its threads; clicking still opens them as the interactive list.\n\t// The preview is what makes the badge legible before you commit to opening it.\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `stack:${threads[0].id}`)\n\t// The list stays open while a member thread is expanded, so Escape steps back: expanded thread ->\n\t// card list -> closed. Held in editor state because this pin remounts as its owning render path\n\t// changes, and keyed by the coincident page point so the open state survives losing a member.\n\tconst stackId = useValue(\n\t\t'stack id',\n\t\t() => {\n\t\t\tconst pagePoint = anchorPagePoint(editor, threads[0].anchor)\n\t\t\treturn pagePoint ? pinStackKey(pagePoint) : threads[0].id\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst listOpen = useValue('stack list open', () => openStackId.get(editor) === stackId, [\n\t\teditor,\n\t\tstackId,\n\t])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\tconst openMember = threads.find((thread) => thread.id === openId)\n\tconst open = listOpen || openMember !== undefined\n\n\tconst point = useValue(\n\t\t'stack point',\n\t\t() => {\n\t\t\tconst first = threads[0]\n\t\t\tif (first.pageId !== editor.getCurrentPageId()) return null\n\t\t\t// The badge hangs off its anchor point bottom-left like a pin, and applies the same imprecise-shape\n\t\t\t// inset the pins it stands in for would (see ThreadPin) \u2014 otherwise the marker would snap from tucked\n\t\t\t// inside the shape to the raw corner the moment a second comment turns a pin into a stack.\n\t\t\tconst pagePoint = anchorPagePoint(editor, first.anchor)\n\t\t\tif (!pagePoint) return null\n\t\t\tconst viewportPoint = editor.pageToViewport(pagePoint)\n\t\t\tconst inset = impreciseShapePinInset(editor, first.anchor)\n\t\t\treturn inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint\n\t\t},\n\t\t[editor, threads]\n\t)\n\n\t// Clicking outside the popover (and off the badge) closes the whole stack \u2014 mirrors the\n\t// single pin's dismiss. Capture phase + class checks, since the popover portals elsewhere.\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!target) return\n\t\t\tif (target.closest('.tlui-cmt-canvas-popover')) return\n\t\t\tconst badge = badgeRef.current\n\t\t\tif (badge && badge.contains(target)) return\n\t\t\t// A click inside a menu/popover layered above us belongs to that layer; defer to its\n\t\t\t// own dismissal instead of closing the stack out from under it.\n\t\t\tif (\n\t\t\t\ttarget.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')\n\t\t\t)\n\t\t\t\treturn\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [open, editor])\n\n\t// Escape with only the card list showing closes it. When a member thread is expanded, the\n\t// layer's Escape handler collapses that first and marks the event consumed \u2014 stepping back\n\t// to the list rather than closing everything at once.\n\tuseEffect(() => {\n\t\tif (!listOpen) return\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || e.defaultPrevented) return\n\t\t\topenStackId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [listOpen, editor])\n\n\tif (!point) return null\n\n\tconst toggle = () => {\n\t\tif (open) {\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t} else {\n\t\t\topenStackId.set(editor, stackId)\n\t\t}\n\t}\n\n\t// The expanded thread gains a header that pushes its first comment down from where the hover preview\n\t// showed it. When that thread is the list's first, lift the whole list so its \"You\" holds position\n\t// across hover -> open. Only the first entry \u2014 lifting can't also hold a lower thread's neighbours.\n\tconst liftForHeader = threads[0]?.id === openId ? 42 : 0\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"tlui-cmt-canvas-pin\" style={{ left: point.x, top: point.y }}>\n\t\t\t\t<button\n\t\t\t\t\tref={badgeRef}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-stack-badge\"\n\t\t\t\t\taria-label={msg('comments.stack-label').replace('{count}', String(threads.length))}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\ttoggle()\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<CountBadge count={threads.length} open={open} />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t{previewShown && !open && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={threads}\n\t\t\t\t\t// Lines the preview up with the stack list itself, so opening it leaves the cards\n\t\t\t\t\t// exactly where the preview had them.\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\t// Picking a card from the preview lands in the same place clicking the badge and\n\t\t\t\t\t// then the card would: the list open, that thread expanded within it.\n\t\t\t\t\tonSelectThread={(thread) => {\n\t\t\t\t\t\topenStackId.set(editor, stackId)\n\t\t\t\t\t\topenThreadId.set(editor, thread.id)\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\tresolveAuthor={props.resolveAuthor}\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t{open && (\n\t\t\t\t<ThreadPopover\n\t\t\t\t\tbase={{\n\t\t\t\t\t\tx: point.x + POPOVER_OFFSET.list.x,\n\t\t\t\t\t\ty: point.y + POPOVER_OFFSET.list.y - liftForHeader,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"tlui-cmt-stack-list\">\n\t\t\t\t\t\t{threads.map((thread) =>\n\t\t\t\t\t\t\tthread.id === openId ? (\n\t\t\t\t\t\t\t\t<div key={thread.id} className=\"tlui-cmt-stack-list__thread\">\n\t\t\t\t\t\t\t\t\t<ThreadView editor={editor} thread={thread} {...props} />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<StackThreadCard\n\t\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\tthread={thread}\n\t\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\t\tonOpen={() => openThreadId.set(editor, thread.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</ThreadPopover>\n\t\t\t)}\n\t\t</>\n\t)\n})\n\n/** A collapsed stack entry: the thread's first comment as a card; clicking expands the thread. */\nfunction StackThreadCard({\n\teditor,\n\tthread,\n\tonOpen,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread; onOpen(): void }) {\n\tconst msg = useTranslation()\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\tconst first = comments[0]\n\tif (!first) return null\n\tconst open = (e: ReactMouseEvent) => {\n\t\te.stopPropagation()\n\t\tonOpen()\n\t}\n\treturn (\n\t\t<div className=\"tlui-cmt-stack-list__card\" onClick={open}>\n\t\t\t{/* The card's keyboard affordance. A button *wrapping* the card would put the comment\n\t\t\t body inside it, and a body renders its links as real anchors \u2014 interactive content\n\t\t\t nested in a button, which is an invalid content model and reads to assistive tech as a\n\t\t\t broken control. So the button covers the card as a sibling instead, leaving the body's\n\t\t\t own links above it and still reachable. */}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-stack-list__card-action\"\n\t\t\t\taria-label={msg(\n\t\t\t\t\tthread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'\n\t\t\t\t).replace('{name}', props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR)}\n\t\t\t\tonClick={open}\n\t\t\t/>\n\t\t\t<CommentCard {...toCardProps(first, props, options.components, resolveName)} />\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["ThreadStackPin"]
7
7
  }
@@ -25,6 +25,8 @@ __export(thread_state_exports, {
25
25
  commentTargetShapeAt: () => commentTargetShapeAt,
26
26
  focusThread: () => focusThread,
27
27
  impreciseShapePinInset: () => impreciseShapePinInset,
28
+ isBoxInInflatedViewport: () => isBoxInInflatedViewport,
29
+ isInInflatedViewport: () => isInInflatedViewport,
28
30
  regionAnchorPinCorner: () => regionAnchorPinCorner,
29
31
  regionPinPoint: () => regionPinPoint,
30
32
  shapeAnchorAt: () => shapeAnchorAt
@@ -35,6 +37,19 @@ var import_options = require("./options");
35
37
  var import_state = require("./state");
36
38
  var import_thread_view = require("./thread-view");
37
39
  const IMPRECISE_PIN_INSET_PX = 20;
40
+ const MARKER_CULL_MARGIN_PX = 120;
41
+ function isInInflatedViewport(editor, point) {
42
+ const viewport = editor.getViewportScreenBounds();
43
+ const margin = MARKER_CULL_MARGIN_PX;
44
+ return point.x >= -margin && point.y >= -margin && point.x <= viewport.w + margin && point.y <= viewport.h + margin;
45
+ }
46
+ function isBoxInInflatedViewport(editor, box) {
47
+ const viewport = editor.getViewportScreenBounds();
48
+ const margin = MARKER_CULL_MARGIN_PX;
49
+ const min = editor.pageToViewport({ x: box.x, y: box.y });
50
+ const max = editor.pageToViewport({ x: box.x + box.w, y: box.y + box.h });
51
+ return max.x >= -margin && max.y >= -margin && min.x <= viewport.w + margin && min.y <= viewport.h + margin;
52
+ }
38
53
  function impreciseShapePinInset(editor, anchor) {
39
54
  if (anchor.type !== "shape" || anchor.isPrecise) return null;
40
55
  const spot = (0, import_options.getCommentingOptions)(editor).impreciseShapeAnchor;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-state.ts"],
4
- "sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tMat,\n\tTLCommentAnchor,\n\tTLCommentThread,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n\tVecLike,\n} from 'tldraw'\nimport { getCommentingOptions } from './options'\nimport { commentsSidebarOpen, openThreadId } from './state'\nimport { POPOVER_OFFSET } from './thread-view'\n\n/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px \u2014\n * most of the marker sits within the shape, with a small overhang past the corner. */\nexport const IMPRECISE_PIN_INSET_PX = 20\n\n/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker\n * extends up-right of its anchor point, so step it toward the shape's centre. Screen px \u2014 the\n * pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */\nexport function impreciseShapePinInset(\n\teditor: Editor,\n\tanchor: TLCommentThread['anchor']\n): { x: number; y: number } | null {\n\tif (anchor.type !== 'shape' || anchor.isPrecise) return null\n\tconst spot = getCommentingOptions(editor).impreciseShapeAnchor\n\tconst inset = {\n\t\tx: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,\n\t\ty: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,\n\t}\n\t// The spot is a corner of the shape's own bounds, so which way \"toward the centre\" points turns\n\t// with the shape: without this a rotated shape's pin would step out of the shape, not into it.\n\tconst rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0\n\treturn rotation === 0 ? inset : Vec.Rot(inset, rotation)\n}\n\n/** The corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right). Pin\n * position, composer placement, region move, and which corner has no resize handle all derive\n * from it. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\n\n/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else\n * {@link REGION_PIN_CORNER}. @internal */\nexport function regionAnchorPinCorner(\n\tanchor: Extract<TLCommentAnchor, { type: 'region' }>\n): VecLike {\n\tif (anchor.pinX !== undefined && anchor.pinY !== undefined) {\n\t\treturn { x: anchor.pinX, y: anchor.pinY }\n\t}\n\treturn REGION_PIN_CORNER\n}\n\n/** The page point of a region's pin corner. */\nexport function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {\n\treturn {\n\t\tx: region.x + corner.x * region.w,\n\t\ty: region.y + corner.y * region.h,\n\t}\n}\n\n/**\n * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. For imprecise\n * shape anchors the pin uses the editor's {@link CommentingOptions.impreciseShapeAnchor} (a\n * normalized 0\u20131 spot, top-right by default) rather than the stored `x`/`y`.\n *\n * A shape anchor's `x`/`y` are normalized within the shape's own bounds and resolved through the\n * shape's page transform, so the pin rides every part of that transform \u2014 rotating the shape\n * carries the pin around with it instead of leaving it behind in the bounding box.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst shape = editor.getShape(anchor.shapeId as TLShapeId)\n\t\t\tif (!shape) return null\n\t\t\tconst transform = editor.getShapePageTransform(shape)\n\t\t\tif (!transform) return null\n\t\t\tconst { point, size } = editor.getShapeGeometry(shape).bounds\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.\n\t\t\tconst spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor\n\t\t\treturn Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))\n\t\t}\n\t\tcase 'point':\n\t\t\treturn { x: anchor.x, y: anchor.y }\n\t\tcase 'region':\n\t\t\treturn regionPinPoint(anchor, regionAnchorPinCorner(anchor))\n\t\tcase 'page':\n\t\t\treturn null\n\t}\n}\n\n/**\n * The shape a comment placed at a page point should anchor to, or undefined for empty canvas.\n *\n * Uses the editor's hit-test margin, the same slack select and hover use. Without it, shapes whose\n * geometry is an open path \u2014 arrows, lines, draw strokes \u2014 are unhittable in practice: their\n * geometry reports a positive distance for every point off the stroke, so a zero margin only\n * matches a pixel-perfect click right on the line.\n *\n * `hitFrameInside` lets a click inside a frame's body anchor to the frame \u2014 without it a frame is\n * hit only on its edge/label (the select-tool convention), so the frame's interior would fall\n * through to a bare point. A child shape under the pointer still wins (children sort above the\n * frame), so only a frame's empty interior anchors the frame.\n *\n * @internal\n */\nexport function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {\n\treturn editor.getShapeAtPoint(page, {\n\t\thitInside: true,\n\t\thitFrameInside: true,\n\t\tmargin: editor.getHitTestMargin(),\n\t})\n}\n\n/**\n * A shape anchor for a page point. `x`/`y` are the point's normalized (0\u20131) offset within the\n * shape's own bounds \u2014 taken in the shape's own space, so a pin placed on a rotated shape records\n * the spot it was dropped on rather than a spot in the bounding box. Remembered either way: when\n * `precise` the pin sits at exactly `x`/`y`; otherwise it sits at the consumer's imprecise default\n * (top-right out of the box). Placement gestures get `precise` from the `shouldBePrecise`\n * commenting option (always precise, by default).\n * @public\n */\nexport function shapeAnchorAt(\n\teditor: Editor,\n\tshapeId: TLShapeId,\n\tpage: { x: number; y: number },\n\tprecise: boolean\n): TLCommentAnchor {\n\tconst shape = editor.getShape(shapeId)\n\tconst bounds = shape && editor.getShapeGeometry(shape).bounds\n\tif (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\tconst local = editor.getPointInShapeSpace(shape, page)\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (local.x - bounds.minX) / bounds.w,\n\t\ty: (local.y - bounds.minY) / bounds.h,\n\t\tisPrecise: precise,\n\t}\n}\n\n/** Open a thread and bring it into view \u2014 switch to its page if needed, then center its pin. @public */\nexport function focusThread(editor: Editor, thread: TLCommentThread): void {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\topenThreadId.set(editor, thread.id)\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })\n}\n\n/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */\nconst CENTER_MARGIN_PX = 8\n\n/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus\n * the thread panel's fixed width (300px, `.tlui-cmt-thread`). */\nconst THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300\n\n/**\n * How far right of a centered-on pin the true viewport center should sit, in screen px. While the\n * comments sidebar covers the viewport's right edge, centering a pin dead-center puts the thread\n * popover that opens on it under the sidebar \u2014 so centering aims the pin at the middle of the\n * uncovered area instead, nudged further left if the thread UI would still reach the sidebar, but\n * never past the viewport's left edge. Zero when the sidebar is closed or not on screen.\n * @internal\n */\nexport function commentCenterScreenOffset(editor: Editor): number {\n\tif (!commentsSidebarOpen.get(editor)) return 0\n\tconst sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')\n\tif (!sidebar) return 0\n\tconst viewport = editor.getViewportScreenBounds()\n\t// Viewport screen bounds are the container's client rect, so this is container-local.\n\tconst sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x\n\tif (sidebarLeft >= viewport.w) return 0\n\t// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the\n\t// sidebar \u2014 but never past the left edge (the edge wins when there's no room for both).\n\tconst pinX = Math.max(\n\t\tMath.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),\n\t\tCENTER_MARGIN_PX\n\t)\n\treturn viewport.w / 2 - pinX\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tMat,\n\tTLCommentAnchor,\n\tTLCommentThread,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n\tVecLike,\n} from 'tldraw'\nimport { getCommentingOptions } from './options'\nimport { commentsSidebarOpen, openThreadId } from './state'\nimport { POPOVER_OFFSET } from './thread-view'\n\n/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px \u2014\n * most of the marker sits within the shape, with a small overhang past the corner. */\nexport const IMPRECISE_PIN_INSET_PX = 20\n\n/** Screen-pixel margin by which the viewport is inflated when culling canvas markers (thread pins\n * and cluster badges), so a marker just off-screen is already mounted when a pan brings it in. */\nconst MARKER_CULL_MARGIN_PX = 120\n\n/**\n * Whether a viewport-space point sits within the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for screen-fixed markers: off-screen markers\n * return null from their position signal and unmount instead of tracking every camera frame.\n * @internal\n */\nexport function isInInflatedViewport(editor: Editor, point: VecLike): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\treturn (\n\t\tpoint.x >= -margin &&\n\t\tpoint.y >= -margin &&\n\t\tpoint.x <= viewport.w + margin &&\n\t\tpoint.y <= viewport.h + margin\n\t)\n}\n\n/**\n * Whether any part of a page-space box overlaps the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for region-anchored threads, whose dashed box can\n * be on screen while the pin corner itself is not.\n * @internal\n */\nexport function isBoxInInflatedViewport(editor: Editor, box: BoxModel): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\t// Zoom is positive, so the page box's corners keep their order through the transform.\n\tconst min = editor.pageToViewport({ x: box.x, y: box.y })\n\tconst max = editor.pageToViewport({ x: box.x + box.w, y: box.y + box.h })\n\treturn (\n\t\tmax.x >= -margin &&\n\t\tmax.y >= -margin &&\n\t\tmin.x <= viewport.w + margin &&\n\t\tmin.y <= viewport.h + margin\n\t)\n}\n\n/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker\n * extends up-right of its anchor point, so step it toward the shape's centre. Screen px \u2014 the\n * pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */\nexport function impreciseShapePinInset(\n\teditor: Editor,\n\tanchor: TLCommentThread['anchor']\n): { x: number; y: number } | null {\n\tif (anchor.type !== 'shape' || anchor.isPrecise) return null\n\tconst spot = getCommentingOptions(editor).impreciseShapeAnchor\n\tconst inset = {\n\t\tx: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,\n\t\ty: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,\n\t}\n\t// The spot is a corner of the shape's own bounds, so which way \"toward the centre\" points turns\n\t// with the shape: without this a rotated shape's pin would step out of the shape, not into it.\n\tconst rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0\n\treturn rotation === 0 ? inset : Vec.Rot(inset, rotation)\n}\n\n/** The corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right). Pin\n * position, composer placement, region move, and which corner has no resize handle all derive\n * from it. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\n\n/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else\n * {@link REGION_PIN_CORNER}. @internal */\nexport function regionAnchorPinCorner(\n\tanchor: Extract<TLCommentAnchor, { type: 'region' }>\n): VecLike {\n\tif (anchor.pinX !== undefined && anchor.pinY !== undefined) {\n\t\treturn { x: anchor.pinX, y: anchor.pinY }\n\t}\n\treturn REGION_PIN_CORNER\n}\n\n/** The page point of a region's pin corner. */\nexport function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {\n\treturn {\n\t\tx: region.x + corner.x * region.w,\n\t\ty: region.y + corner.y * region.h,\n\t}\n}\n\n/**\n * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. Imprecise shape\n * anchors use {@link CommentingOptions.impreciseShapeAnchor} rather than the stored `x`/`y`.\n *\n * A shape anchor's `x`/`y` are normalized within the shape's bounds and resolved through its page\n * transform, so the pin rides rotation instead of being left behind in the bounding box.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst shape = editor.getShape(anchor.shapeId as TLShapeId)\n\t\t\tif (!shape) return null\n\t\t\tconst transform = editor.getShapePageTransform(shape)\n\t\t\tif (!transform) return null\n\t\t\tconst { point, size } = editor.getShapeGeometry(shape).bounds\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.\n\t\t\tconst spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor\n\t\t\treturn Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))\n\t\t}\n\t\tcase 'point':\n\t\t\treturn { x: anchor.x, y: anchor.y }\n\t\tcase 'region':\n\t\t\treturn regionPinPoint(anchor, regionAnchorPinCorner(anchor))\n\t\tcase 'page':\n\t\t\treturn null\n\t}\n}\n\n/**\n * The shape a comment placed at a page point should anchor to, or undefined for empty canvas.\n *\n * Uses the editor's hit-test margin, the same slack select and hover use \u2014 without it, open-path\n * shapes (arrows, lines, draw strokes) are unhittable in practice.\n *\n * `hitFrameInside` lets a click inside a frame's body anchor to the frame, which it otherwise\n * wouldn't (the select-tool convention hits only the edge/label). A child shape under the pointer\n * still wins, so only a frame's empty interior anchors the frame.\n *\n * @internal\n */\nexport function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {\n\treturn editor.getShapeAtPoint(page, {\n\t\thitInside: true,\n\t\thitFrameInside: true,\n\t\tmargin: editor.getHitTestMargin(),\n\t})\n}\n\n/**\n * A shape anchor for a page point. `x`/`y` are the point's normalized (0\u20131) offset within the\n * shape's own bounds, taken in the shape's own space, so a pin on a rotated shape records the spot\n * it was dropped on. Remembered either way: when `precise` the pin sits at exactly `x`/`y`,\n * otherwise at the consumer's imprecise default.\n * @public\n */\nexport function shapeAnchorAt(\n\teditor: Editor,\n\tshapeId: TLShapeId,\n\tpage: { x: number; y: number },\n\tprecise: boolean\n): TLCommentAnchor {\n\tconst shape = editor.getShape(shapeId)\n\tconst bounds = shape && editor.getShapeGeometry(shape).bounds\n\tif (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\tconst local = editor.getPointInShapeSpace(shape, page)\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (local.x - bounds.minX) / bounds.w,\n\t\ty: (local.y - bounds.minY) / bounds.h,\n\t\tisPrecise: precise,\n\t}\n}\n\n/** Open a thread and bring it into view \u2014 switch to its page if needed, then center its pin. @public */\nexport function focusThread(editor: Editor, thread: TLCommentThread): void {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\topenThreadId.set(editor, thread.id)\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })\n}\n\n/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */\nconst CENTER_MARGIN_PX = 8\n\n/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus\n * the thread panel's fixed width (300px, `.tlui-cmt-thread`). */\nconst THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300\n\n/**\n * How far right of a centered-on pin the true viewport center should sit, in screen px. While the\n * comments sidebar covers the viewport's right edge, dead-centering a pin would put its thread\n * popover under the sidebar \u2014 so centering aims at the middle of the uncovered area instead, never\n * past the viewport's left edge. Zero when the sidebar is closed or not on screen.\n * @internal\n */\nexport function commentCenterScreenOffset(editor: Editor): number {\n\tif (!commentsSidebarOpen.get(editor)) return 0\n\tconst sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')\n\tif (!sidebar) return 0\n\tconst viewport = editor.getViewportScreenBounds()\n\t// Viewport screen bounds are the container's client rect, so this is container-local.\n\tconst sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x\n\tif (sidebarLeft >= viewport.w) return 0\n\t// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the\n\t// sidebar \u2014 but never past the left edge (the edge wins when there's no room for both).\n\tconst pinX = Math.max(\n\t\tMath.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),\n\t\tCENTER_MARGIN_PX\n\t)\n\treturn viewport.w / 2 - pinX\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -21,31 +21,55 @@ __export(thread_view_exports, {
21
21
  POPOVER_OFFSET: () => POPOVER_OFFSET,
22
22
  ThreadPopover: () => ThreadPopover,
23
23
  ThreadView: () => ThreadView,
24
+ absoluteThreadLink: () => absoluteThreadLink,
24
25
  toCardProps: () => toCardProps,
25
26
  useResolveName: () => useResolveName
26
27
  });
27
28
  module.exports = __toCommonJS(thread_view_exports);
28
29
  var import_jsx_runtime = require("react/jsx-runtime");
29
30
  var import_react = require("react");
30
- var import_react_dom = require("react-dom");
31
31
  var import_tldraw = require("tldraw");
32
32
  var import_comment_card = require("../ui/comment-card");
33
33
  var import_comment_composer = require("../ui/comment-composer");
34
34
  var import_comment_extensions = require("../ui/comment-extensions");
35
35
  var import_comment_thread = require("../ui/comment-thread");
36
- var import_tooltip_button = require("../ui/tooltip-button");
37
36
  var import_comment_body = require("./comment-body");
38
37
  var import_comment_drafts = require("./comment-drafts");
39
38
  var import_comment_mutations = require("./comment-mutations");
40
39
  var import_comment_reactions = require("./comment-reactions");
41
40
  var import_comment_render = require("./comment-render");
42
41
  var import_hooks = require("./hooks");
42
+ var import_mobile_placement = require("./mobile-placement");
43
43
  var import_options = require("./options");
44
44
  var import_state = require("./state");
45
45
  const stop = (e) => e.stopPropagation();
46
46
  function useResolveName(resolveAuthor) {
47
47
  return (0, import_react.useCallback)((id) => resolveAuthor(id)?.name, [resolveAuthor]);
48
48
  }
49
+ const LINK_COPIED_MS = 2e3;
50
+ function absoluteThreadLink(href, base = window.location.href) {
51
+ try {
52
+ return new URL(href, base).toString();
53
+ } catch {
54
+ return href;
55
+ }
56
+ }
57
+ function useCopyLink(href) {
58
+ const [copied, setCopied] = (0, import_react.useState)(false);
59
+ (0, import_react.useEffect)(() => {
60
+ if (!copied) return;
61
+ const timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS);
62
+ return () => window.clearTimeout(timeout);
63
+ }, [copied]);
64
+ const copy = (0, import_react.useCallback)(() => {
65
+ if (href === void 0) return;
66
+ navigator.clipboard?.writeText(absoluteThreadLink(href)).then(
67
+ () => setCopied(true),
68
+ () => setCopied(false)
69
+ );
70
+ }, [href]);
71
+ return [copied, copy];
72
+ }
49
73
  function toCardProps(comment, props, components, resolveName) {
50
74
  const Body = components.CommentBody;
51
75
  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 });
@@ -66,31 +90,26 @@ const POPOVER_OFFSET = {
66
90
  list: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 }
67
91
  };
68
92
  function ThreadPopover({
69
- container,
70
- style,
93
+ base,
71
94
  children
72
95
  }) {
73
96
  const ref = (0, import_react.useRef)(null);
74
97
  (0, import_tldraw.usePassThroughWheelEvents)(ref);
75
- (0, import_tldraw.usePassThroughMouseOverEvents)(ref);
76
- return (0, import_react_dom.createPortal)(
77
- // contextmenu also stops here: portals bubble React events to the canvas's context-menu
78
- // trigger (the layer mounts inside it), which would open the canvas menu over this panel.
79
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
80
- "div",
81
- {
82
- ref,
83
- className: "tlui-cmt-canvas-popover",
84
- style,
85
- onPointerDown: stop,
86
- onContextMenu: stop,
87
- children
88
- }
89
- ),
90
- container
91
- );
98
+ const isMobile = (0, import_mobile_placement.useIsMobileCommenting)();
99
+ const placed = (0, import_mobile_placement.useMobilePlacement)(ref, base, isMobile);
100
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.EditorPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
101
+ "div",
102
+ {
103
+ ref,
104
+ className: "tlui-cmt-canvas-popover",
105
+ style: { left: placed.left, top: placed.top },
106
+ onPointerDown: stop,
107
+ onContextMenu: stop,
108
+ children
109
+ }
110
+ ) });
92
111
  }
93
- function ThreadView({
112
+ const ThreadView = (0, import_react.memo)(function ThreadView2({
94
113
  editor,
95
114
  thread,
96
115
  ...props
@@ -100,16 +119,41 @@ function ThreadView({
100
119
  resolveAuthor,
101
120
  onPostComment,
102
121
  isCommentUnread,
103
- onCommentRead,
122
+ onCommentsRead,
104
123
  getMentionSuggestions,
105
- renderMentionSuggestion
124
+ renderMentionSuggestion,
125
+ getThreadHref
106
126
  } = props;
107
127
  const options = (0, import_options.useCommentingOptions)();
108
128
  const comments = (0, import_hooks.useThreadComments)(editor, thread.id);
109
129
  const msg = (0, import_tldraw.useTranslation)();
110
130
  const resolveName = useResolveName(resolveAuthor);
131
+ const cards = (0, import_react.useMemo)(
132
+ () => comments.map(
133
+ (c) => toCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)
134
+ ),
135
+ [comments, currentUserId, resolveAuthor, options.components, resolveName]
136
+ );
111
137
  const me = currentUserId ? resolveAuthor(currentUserId) : void 0;
112
138
  const canComment = (0, import_options.useCanComment)(currentUserId);
139
+ const commentPermissions = (0, import_tldraw.useValue)(
140
+ "comment permissions",
141
+ () => new Map(
142
+ comments.map((comment) => [
143
+ comment.id,
144
+ {
145
+ edit: (0, import_options.getCanModifyComment)(editor, currentUserId, { action: "edit-comment", comment }),
146
+ delete: (0, import_options.getCanModifyComment)(editor, currentUserId, {
147
+ action: "delete-comment",
148
+ comment
149
+ })
150
+ }
151
+ ])
152
+ ),
153
+ [editor, currentUserId, comments]
154
+ );
155
+ const canModifyThread = (0, import_options.useCanModifyComment)(currentUserId, { action: "delete-thread", thread });
156
+ const canDeleteThread = canComment && canModifyThread;
113
157
  const ComposerFallback = options.components.ComposerFallback;
114
158
  const [reply, setReply] = (0, import_react.useState)(
115
159
  () => (0, import_comment_drafts.getCommentDraft)((0, import_comment_drafts.replyDraftSlot)(thread.id)) ?? import_comment_extensions.EMPTY_COMMENT
@@ -159,27 +203,27 @@ function ThreadView({
159
203
  resolve?.()?.focus();
160
204
  }, [editingId]);
161
205
  (0, import_react.useEffect)(() => {
162
- if (!isCommentUnread || !onCommentRead) return;
163
- for (const comment of comments) {
164
- if (isCommentUnread(comment.id)) {
165
- onCommentRead(comment.id);
166
- }
206
+ if (!isCommentUnread || !onCommentsRead) return;
207
+ const unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id);
208
+ if (unreadIds.length > 0) {
209
+ onCommentsRead(unreadIds);
167
210
  }
168
- }, [comments, isCommentUnread, onCommentRead]);
211
+ }, [comments, isCommentUnread, onCommentsRead]);
169
212
  const postReply = () => {
170
213
  if ((0, import_comment_extensions.isCommentEmpty)(reply) || !currentUserId) return;
171
- (0, import_comment_mutations.commitCommentMutation)(editor, () => {
172
- const comment = (0, import_tldraw.createComment)({
214
+ const comment = (0, import_comment_mutations.commitCommentMutation)(editor, ({ put }) => {
215
+ const comment2 = (0, import_tldraw.createComment)({
173
216
  threadId: thread.id,
174
217
  pageId: thread.pageId,
175
218
  authorId: currentUserId,
176
219
  body: reply
177
220
  });
178
- (0, import_comment_mutations.putRecordsInCommit)(editor, [comment]);
179
- if (onPostComment) onPostComment(comment);
221
+ put([comment2]);
222
+ return comment2;
180
223
  });
181
224
  setReply(import_comment_extensions.EMPTY_COMMENT);
182
225
  (0, import_comment_drafts.clearCommentDraft)((0, import_comment_drafts.replyDraftSlot)(thread.id));
226
+ onPostComment?.(comment);
183
227
  };
184
228
  const toggleResolve = () => {
185
229
  if (!currentUserId) return;
@@ -187,9 +231,11 @@ function ThreadView({
187
231
  else (0, import_comment_mutations.resolveThread)(editor, thread, currentUserId);
188
232
  };
189
233
  const removeThread = () => {
190
- if (!currentUserId) return;
191
234
  (0, import_comment_mutations.deleteThread)(editor, thread);
192
235
  };
236
+ const ThreadActions = options.components.ThreadActions;
237
+ const threadHref = getThreadHref?.(thread.id);
238
+ const [linkCopied, copyThreadLink] = useCopyLink(threadHref);
193
239
  const startEdit = (comment, { fromMoreMenu = false } = {}) => {
194
240
  const active = container.ownerDocument.activeElement;
195
241
  editReturnFocus.current = fromMoreMenu ? () => container.querySelector(`[data-cmt-more-for="${comment.id}"]`) : active instanceof HTMLElement && active !== container && active !== document.body ? () => active.isConnected ? active : null : null;
@@ -204,6 +250,7 @@ function ThreadView({
204
250
  };
205
251
  const renderComment = (card, index) => {
206
252
  const comment = comments[index];
253
+ const permissions = commentPermissions.get(comment.id);
207
254
  if (editingId === comment.id) {
208
255
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
209
256
  "div",
@@ -246,12 +293,13 @@ function ThreadView({
246
293
  }
247
294
  ),
248
295
  actions: canComment && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
249
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_reactions.CommentReactionPicker, { comment, currentUserId }),
250
- comment.authorId === currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuRoot, { id: `comment-actions-${comment.id}`, children: [
296
+ (permissions?.edit || permissions?.delete) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuRoot, { id: `comment-actions-${comment.id}`, children: [
251
297
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
252
- import_tooltip_button.TooltipButton,
298
+ import_tldraw.TldrawUiButton,
253
299
  {
300
+ type: "icon",
254
301
  tooltip: msg("comments.more-options"),
302
+ title: msg("comments.more-options"),
255
303
  className: "tlui-cmt-thread__action",
256
304
  "data-cmt-more-for": comment.id,
257
305
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -273,7 +321,7 @@ function ThreadView({
273
321
  alignOffset: 0,
274
322
  sideOffset: 4,
275
323
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuGroup, { children: [
276
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
324
+ permissions?.edit && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
277
325
  "button",
278
326
  {
279
327
  type: "button",
@@ -282,7 +330,7 @@ function ThreadView({
282
330
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: msg("comments.edit") })
283
331
  }
284
332
  ) }),
285
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
333
+ permissions?.delete && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
286
334
  "button",
287
335
  {
288
336
  type: "button",
@@ -294,17 +342,22 @@ function ThreadView({
294
342
  ] })
295
343
  }
296
344
  )
297
- ] })
345
+ ] }),
346
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_comment_reactions.CommentReactionPicker, { comment, currentUserId })
298
347
  ] })
299
348
  }
300
349
  );
301
350
  };
351
+ const resolveLabel = msg(thread.resolved ? "comments.reopen" : "comments.resolve");
302
352
  const headerActions = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
303
- canComment && currentUserId && currentUserId === thread.createdBy && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuRoot, { id: `comment-thread-actions-${thread.id}`, children: [
353
+ ThreadActions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThreadActions, { thread, comments }),
354
+ (threadHref !== void 0 || canDeleteThread) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuRoot, { id: `comment-thread-actions-${thread.id}`, children: [
304
355
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
305
- import_tooltip_button.TooltipButton,
356
+ import_tldraw.TldrawUiButton,
306
357
  {
358
+ type: "icon",
307
359
  tooltip: msg("comments.more-options"),
360
+ title: msg("comments.more-options"),
308
361
  className: "tlui-cmt-thread__action",
309
362
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "dots-vertical", label: msg("comments.more-options"), small: true })
310
363
  }
@@ -317,38 +370,38 @@ function ThreadView({
317
370
  align: "start",
318
371
  alignOffset: 0,
319
372
  sideOffset: 4,
320
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuGroup, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
321
- "button",
322
- {
323
- type: "button",
324
- className: "tlui-cmt-menu-item tlui-cmt-menu-item--danger",
325
- onClick: removeThread,
326
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: msg("comments.delete") })
327
- }
328
- ) }) })
373
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiDropdownMenuGroup, { children: [
374
+ threadHref !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { noClose: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "tlui-cmt-menu-item", onClick: copyThreadLink, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: msg(linkCopied ? "comments.link-copied" : "comments.copy-link") }) }) }),
375
+ canDeleteThread && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ className: "tlui-cmt-menu-item tlui-cmt-menu-item--danger",
380
+ onClick: removeThread,
381
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: msg("comments.delete") })
382
+ }
383
+ ) })
384
+ ] })
329
385
  }
330
386
  )
331
387
  ] }),
332
388
  canComment && currentUserId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
333
- import_tooltip_button.TooltipButton,
389
+ import_tldraw.TldrawUiButton,
334
390
  {
335
- tooltip: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
391
+ type: "icon",
392
+ tooltip: resolveLabel,
393
+ title: resolveLabel,
336
394
  className: "tlui-cmt-thread__action",
337
395
  onClick: toggleResolve,
338
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
339
- import_tldraw.TldrawUiIcon,
340
- {
341
- icon: "check",
342
- label: msg(thread.resolved ? "comments.reopen" : "comments.resolve"),
343
- small: true
344
- }
345
- )
396
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "check", label: resolveLabel, small: true })
346
397
  }
347
398
  ),
348
399
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
349
- import_tooltip_button.TooltipButton,
400
+ import_tldraw.TldrawUiButton,
350
401
  {
402
+ type: "icon",
351
403
  tooltip: msg("comments.dismiss"),
404
+ title: msg("comments.dismiss"),
352
405
  className: "tlui-cmt-thread__action",
353
406
  onClick: () => import_state.openThreadId.set(editor, null),
354
407
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiIcon, { icon: "cross-2", label: msg("comments.dismiss"), small: true })
@@ -361,7 +414,7 @@ function ThreadView({
361
414
  header: msg("comments.thread-title"),
362
415
  headerActions,
363
416
  renderComment,
364
- comments: comments.map((c) => toCardProps(c, props, options.components, resolveName)),
417
+ comments: cards,
365
418
  resolvedBanner: thread.resolved ? msg("comments.resolved-by").replace(
366
419
  "{name}",
367
420
  resolveAuthor(thread.resolved.by)?.name ?? import_comment_render.UNKNOWN_AUTHOR
@@ -377,10 +430,10 @@ function ThreadView({
377
430
  },
378
431
  onSubmit: postReply,
379
432
  // Up in the empty reply box edits the comment directly above it, chat-style —
380
- // only when that comment is yours (the same gate as the Edit link).
433
+ // only when you're allowed to edit it (the same gate as the Edit link).
381
434
  onArrowUpWhenEmpty: () => {
382
435
  const last = comments[comments.length - 1];
383
- if (last && last.authorId === currentUserId) startEdit(last);
436
+ if (last && commentPermissions.get(last.id)?.edit) startEdit(last);
384
437
  },
385
438
  // No user, no author for the record — dead send button.
386
439
  disabled: (0, import_comment_extensions.isCommentEmpty)(reply) || !currentUserId,
@@ -393,5 +446,5 @@ function ThreadView({
393
446
  footer: !canComment && !thread.resolved && ComposerFallback ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComposerFallback, { context: "thread" }) : void 0
394
447
  }
395
448
  );
396
- }
449
+ });
397
450
  //# sourceMappingURL=thread-view.js.map