@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
@@ -31,13 +31,11 @@ import { TLUiOverrides } from 'tldraw';
31
31
  import { VecLike } from 'tldraw';
32
32
 
33
33
  /**
34
- * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. For imprecise
35
- * shape anchors the pin uses the editor's {@link CommentingOptions.impreciseShapeAnchor} (a
36
- * normalized 0–1 spot, top-right by default) rather than the stored `x`/`y`.
34
+ * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. Imprecise shape
35
+ * anchors use {@link CommentingOptions.impreciseShapeAnchor} rather than the stored `x`/`y`.
37
36
  *
38
- * A shape anchor's `x`/`y` are normalized within the shape's own bounds and resolved through the
39
- * shape's page transform, so the pin rides every part of that transform rotating the shape
40
- * carries the pin around with it instead of leaving it behind in the bounding box.
37
+ * A shape anchor's `x`/`y` are normalized within the shape's bounds and resolved through its page
38
+ * transform, so the pin rides rotation instead of being left behind in the bounding box.
41
39
  * @public
42
40
  */
43
41
  export declare function anchorPagePoint(editor: Editor, anchor: TLCommentAnchor): {
@@ -49,7 +47,11 @@ export { Avatar }
49
47
 
50
48
  export { AvatarProps }
51
49
 
52
- /** A comment's metadata line: author name, relative time, and an edited marker. @public @react */
50
+ /**
51
+ * A comment's metadata line: author name, relative time, and an edited marker. Hovering the time
52
+ * shows the full date and time in a tooltip.
53
+ * @public @react
54
+ */
53
55
  export declare function Byline({ author, date, edited }: BylineProps): JSX.Element;
54
56
 
55
57
  /** @public */
@@ -66,14 +68,11 @@ export declare interface BylineProps {
66
68
  * thread popover (with a reply composer) on click, and shows a composer where the comment tool
67
69
  * placed a new thread. Reads/writes comment records straight from `editor.store`.
68
70
  *
69
- * It's meant as the batteries-included default every visible piece is a lever (the `CommentBody`
70
- * and `PinContent` slots on `CommentTool.configure({ components })`), and the pieces it composes
71
- * (`CommentPin`, `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a
72
- * consumer can rebuild this from parts instead.
73
- *
74
- * The host wiring — who the viewer is, how ids become names, read status, mentions — is the
75
- * {@link CommentingContext}, which `CanvasCommentsSidebar` takes too, so a host mounting both can
76
- * build it once and spread it into each.
71
+ * It's the batteries-included default: every visible piece is a slot on
72
+ * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,
73
+ * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can
74
+ * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which
75
+ * `CanvasCommentsSidebar` takes too.
77
76
  *
78
77
  * @public @react
79
78
  */
@@ -200,34 +199,49 @@ export declare interface CommentingComponents {
200
199
  ThreadPreview?: ComponentType<{
201
200
  comment: TLComment;
202
201
  }>;
202
+ /**
203
+ * A whole sidebar row. Replaces the default `<CommentListItem>`, which is exported — so a row
204
+ * that only adds an unread dot or a status chip can spread these props into it. Use
205
+ * `ThreadPreview` instead when only the preview text is changing.
206
+ */
207
+ ThreadRow?: ComponentType<CommentListItemRenderProps & {
208
+ thread: TLCommentThread;
209
+ }>;
210
+ /**
211
+ * Extra controls in an open thread's header, added ahead of the built-in resolve and dismiss
212
+ * buttons rather than replacing them. "Copy link" is already built in whenever the host supplies
213
+ * `getThreadHref`.
214
+ */
215
+ ThreadActions?: ComponentType<{
216
+ comments: TLComment[];
217
+ thread: TLCommentThread;
218
+ }>;
203
219
  /**
204
220
  * A reaction's visual, given its token. The default renders the token string for the OS emoji
205
- * font to draw (so the token is the emoji glyph). Override this to render your own palette —
206
- * return an `<img>` for custom emoji, an SVG, or anything. The token is whatever your picker
207
- * emits and is what gets stored/synced; this only controls how it's drawn.
221
+ * font. Override to draw a custom palette an `<img>`, an SVG, anything. The token is what gets
222
+ * stored and synced; this only controls how it's drawn.
208
223
  */
209
224
  ReactionContent?: ComponentType<{
210
225
  token: string;
211
226
  }>;
212
227
  /**
213
- * What the add-reaction button opens: the thing that produces a reaction token. Replaces the
214
- * default `<EmojiPicker>` grid. Pairs with `ReactionContent` (which draws whatever tokens this
215
- * emits) and with `isAllowedReaction` (which has to accept them).
228
+ * What the add-reaction button opens. Replaces the default `<EmojiPicker>` grid. Pairs with
229
+ * `ReactionContent` (which draws the tokens this emits) and `isAllowedReaction` (which must
230
+ * accept them).
216
231
  */
217
232
  ReactionPalette?: ComponentType<EmojiPickerProps>;
218
233
  /**
219
- * The hover affordance naming who reacted with an emoji. It receives the reactors and the pill
220
- * (as `children`) and returns the whole thing — so it owns the tooltip, its box, size, shape, and
221
- * position. Replaces the default (`DefaultReactionTooltip`). For a simple wording change, translate
222
- * the `comments.reacted-*` strings instead; reach for this to change the structure — a different
223
- * box, avatars, a banner anywhere on screen.
234
+ * The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as
235
+ * `children`) and owns the whole thing — box, size, shape, position. For a wording change,
236
+ * translate the `comments.reacted-*` strings instead.
224
237
  */
225
238
  ReactionTooltip?: ComponentType<ReactionTooltipProps>;
226
- /** Shown where a composer would sit when the viewer can't compose (see
227
- * {@link CommentingOptions.canComment} a signed-out viewer, a viewer role, a host that
228
- * turns commenting off). `context` says which surface is rendering it: the bottom of an open
229
- * thread popover (`'thread'`) or the placement popover the comment tool opens (`'pending'`).
230
- * Unset, those surfaces render nothing. */
239
+ /**
240
+ * Shown where a composer would sit when the viewer can't compose (see
241
+ * {@link CommentingOptions.canComment}). `context` is the surface rendering it: an open thread
242
+ * popover (`'thread'`) or the comment tool's placement popover (`'pending'`). Unset, those
243
+ * surfaces render nothing.
244
+ */
231
245
  ComposerFallback?: ComponentType<{
232
246
  context: 'pending' | 'thread';
233
247
  }>;
@@ -261,10 +275,11 @@ export declare interface CommentingContext {
261
275
  /** Whether a comment is unread for the current user (return true for unread). */
262
276
  isCommentUnread?(commentId: TLCommentId): boolean;
263
277
  /**
264
- * Called for each unread comment shown to the user in an open thread popover, so hosts can
265
- * record a read receipt. Needs `isCommentUnread` to know what's unread.
278
+ * Called with every unread comment shown to the user in an open thread popover, batched per
279
+ * report, so hosts can record read receipts without a write per comment. Needs
280
+ * `isCommentUnread` to know what's unread.
266
281
  */
267
- onCommentRead?(commentId: TLCommentId): void;
282
+ onCommentsRead?(commentIds: TLCommentId[]): void;
268
283
  /** Resolve the members matching an `@`-query in the composers (sync or async). */
269
284
  getMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>;
270
285
  /** Override a mention-picker row's content. */
@@ -294,11 +309,9 @@ export declare interface CommentingContext {
294
309
  */
295
310
  export declare interface CommentingOptions {
296
311
  /**
297
- * How comment mutations (post, reply, edit, resolve, delete) interact with the editor undo
298
- * stack. Defaults to `'ignore'` — comments are deliberately not undoable (see `TLComment`).
299
- * `'record'` is a multiplayer footgun: undoing a delete resurrects a thread a collaborator
300
- * already removed, and undoing a resolve/edit reverts their newer state. Safe only single-player
301
- * or on a non-synced local comment store.
312
+ * How comment mutations interact with the editor undo stack. Defaults to `'ignore'` — comments
313
+ * are deliberately not undoable (see `TLComment`). `'record'` is a multiplayer footgun: undoing
314
+ * a delete resurrects a thread a collaborator already removed. Safe only single-player.
302
315
  */
303
316
  readonly history: TLHistoryBatchOptions['history'];
304
317
  /**
@@ -316,56 +329,77 @@ export declare interface CommentingOptions {
316
329
  */
317
330
  readonly allowMultipleReactions: boolean;
318
331
  /**
319
- * Whether a token may be added as a reaction. Defaults to {@link isAllowedReactionEmoji} against
320
- * the built-in emoji palette, which is what keeps a scripted client from writing junk `emoji`
321
- * values the picker would never offer. Override it alongside a custom `ReactionPalette` so the
322
- * tokens that palette emits get through. Removals aren't checked — a reaction carrying an
323
- * off-palette token must still be clearable.
332
+ * Whether a token may be added as a reaction. Defaults to {@link isAllowedReactionEmoji}, which
333
+ * keeps a scripted client from writing junk values the picker would never offer. Override
334
+ * alongside a custom `ReactionPalette`. Removals aren't checked an off-palette reaction must
335
+ * still be clearable.
324
336
  */
325
337
  isAllowedReaction(token: string): boolean;
326
338
  /**
327
339
  * Whether dragging the comment tool out creates a region anchor — a comment attached to a
328
- * rectangular area of the page, drawn as a dashed box with the thread's pin on the corner the
329
- * drag released on. Off by default: comments attach to points and shapes only, and a drag just
330
- * trails the composer. A region reveals its box while the pointer is inside it, moves by its
331
- * pin, and resizes from its corners.
340
+ * rectangular area, drawn as a dashed box with the pin on the corner the drag released on. Off
341
+ * by default, where comments attach to points and shapes only and a drag trails the composer.
332
342
  */
333
343
  readonly enableRegions: boolean;
334
344
  /**
335
- * Whether the viewer may participate in commenting: composing new threads and replies, editing
336
- * and deleting comments, resolving threads, and moving pins or regions. Composers render when
337
- * it returns true; when it returns false, the {@link CommentingComponents.ComposerFallback}
338
- * slot renders in their place (or nothing, if that slot is unset) and the action affordances
339
- * are hidden. Unset, participation is allowed exactly when `currentUserId` is set.
345
+ * Whether the viewer may participate in commenting: composing, editing, deleting, resolving, and
346
+ * moving pins. When false, {@link CommentingComponents.ComposerFallback} renders in the
347
+ * composer's place and action affordances are hidden. Unset, participation is allowed exactly
348
+ * when `currentUserId` is set.
340
349
  *
341
- * Called during render via {@link useCanComment}, so reactive reads (signals) are tracked.
342
- * The comment tool itself stays registered and selectable hosts that want its toolbar button
343
- * to do something else (e.g. open a sign-in dialog) can override the tool item's `onSelect`.
344
- * Note posting still requires a `currentUserId` to author the records, so a callback that
345
- * returns true for a signed-out viewer yields a composer whose send button stays disabled.
350
+ * Called during render via {@link useCanComment}, so signal reads are tracked. Posting still
351
+ * needs a `currentUserId`, so returning true for a signed-out viewer yields a composer whose
352
+ * send button stays disabled. A callback that throws is logged and read as false, rather than
353
+ * taking the comments layer down with it.
346
354
  */
347
355
  readonly canComment: ((ctx: {
348
356
  currentUserId: null | string;
349
357
  editor: Editor;
350
358
  }) => boolean) | undefined;
359
+ /**
360
+ * Whether the viewer may make a particular write against a particular record: editing or
361
+ * deleting a comment, or deleting a thread. Unset, each is its record's owner's to make
362
+ * ({@link defaultCanModifyComment}) — you edit and delete your own comments, and delete threads
363
+ * you started. Override it to widen that (a workspace admin or moderator who may remove
364
+ * anyone's comment) or to narrow it (no edits after an hour). Where it returns false the
365
+ * affordance isn't rendered.
366
+ *
367
+ * Checked after {@link CommentingOptions.canComment}, which gates commenting as a whole: a
368
+ * viewer who may not participate gets no action affordances at all, whatever this returns.
369
+ *
370
+ * Called during render via {@link useCanModifyComment}, so reactive reads (signals) are tracked.
371
+ * A callback that throws is logged and read as false: an affordance is withheld rather than the
372
+ * comments layer lost, and a denial is what a server enforcing the same rule would have said.
373
+ *
374
+ * @example
375
+ * ```tsx
376
+ * CommentTool.configure({
377
+ * canModifyComment: (ctx) =>
378
+ * // Moderators may delete anything; everything else stays the owner's to do.
379
+ * (ctx.action !== 'edit-comment' && isModerator(ctx.currentUserId)) ||
380
+ * defaultCanModifyComment(ctx),
381
+ * })
382
+ * ```
383
+ */
384
+ readonly canModifyComment: ((ctx: CommentModificationContext) => boolean) | undefined;
351
385
  /** Normalized (0–1) spot within a shape where imprecise shape pins sit. Default top-right. */
352
386
  readonly impreciseShapeAnchor: {
353
387
  readonly x: number;
354
388
  readonly y: number;
355
389
  };
356
390
  /**
357
- * Whether a comment landing on a shape anchors precisely pinned to the exact clicked spot
358
- * within the shape or imprecisely pinned to the shape as a whole, rendered at
359
- * `impreciseShapeAnchor`. Called wherever a shape anchor is created (placing with the comment
360
- * tool, dropping a dragged pin onto a shape). Always precise by default. Return `false` for
361
- * shape-level anchoring, or decide from the context — the Alt key's state, or the shape itself,
362
- * e.g. precise only on notes. Governs new placements only; existing anchors render as stored.
391
+ * Whether a comment landing on a shape pins to the exact clicked spot, or to the shape as a
392
+ * whole (rendered at `impreciseShapeAnchor`). Always precise by default; return `false`, or
393
+ * decide from the context. Governs new placements only existing anchors render as stored.
363
394
  */
364
395
  shouldBePrecise(editor: Editor, context: ShapeCommentPrecisionContext): boolean;
365
396
  /** Component overrides. See {@link CommentingComponents}. */
366
397
  readonly components: CommentingComponents;
367
398
  }
368
399
 
400
+ /** One thread's row in a {@link CommentsList}. @public @react */
401
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect }: CommentListItemRenderProps): JSX.Element;
402
+
369
403
  /** @public */
370
404
  export declare interface CommentListItemProps {
371
405
  id: string;
@@ -381,6 +415,9 @@ export declare interface CommentListItemProps {
381
415
  count?: number;
382
416
  /** Whether this thread is the open one. */
383
417
  selected?: boolean;
418
+ /** Tallied reactions for the row (a thread's, or a single comment's when the row is one
419
+ * comment), shown as inert pills under the preview. Omit to hide. */
420
+ reactions?: ReactionSummary[];
384
421
  /**
385
422
  * Link target for the item. When set, the row renders as an anchor so browser affordances
386
423
  * (ctrl/cmd-click, middle-click) open it in a new tab; a plain click still calls `onSelect`.
@@ -388,6 +425,51 @@ export declare interface CommentListItemProps {
388
425
  href?: string;
389
426
  }
390
427
 
428
+ /**
429
+ * What a row is rendered with: the item, plus the list-level wiring it needs to be interactive.
430
+ * A custom row gets the same props the default `<CommentListItem>` does, so it can wrap the
431
+ * default rather than reimplement it.
432
+ *
433
+ * @public
434
+ */
435
+ export declare interface CommentListItemRenderProps extends CommentListItemProps {
436
+ /** Label for a resolved thread's marker on its row. */
437
+ resolvedLabel?: string;
438
+ /** Called with the thread id when the row is chosen. */
439
+ onSelect?(id: string): void;
440
+ }
441
+
442
+ /**
443
+ * A commenting write that belongs to someone in particular, and the record it targets — the
444
+ * argument to {@link CommentingOptions.canModifyComment}.
445
+ *
446
+ * Resolving, reopening, reacting, and moving a pin aren't here: none of them is anyone's in
447
+ * particular, so {@link CommentingOptions.canComment} is the only gate on them.
448
+ *
449
+ * @public
450
+ */
451
+ export declare type CommentModification = {
452
+ readonly action: 'delete-comment';
453
+ readonly comment: TLComment;
454
+ } | {
455
+ readonly action: 'delete-thread';
456
+ readonly thread: TLCommentThread;
457
+ } | {
458
+ readonly action: 'edit-comment';
459
+ readonly comment: TLComment;
460
+ };
461
+
462
+ /**
463
+ * The argument to {@link CommentingOptions.canModifyComment}: which write, against which record,
464
+ * and by whom.
465
+ *
466
+ * @public
467
+ */
468
+ export declare type CommentModificationContext = {
469
+ readonly currentUserId: null | string;
470
+ readonly editor: Editor;
471
+ } & CommentModification;
472
+
391
473
  /** A canvas comment marker: shows its `children` (or a check when resolved). Purely
392
474
  * presentational — it reflects open/resolved state via CSS; wrap it to make it clickable.
393
475
  * @public @react */
@@ -404,9 +486,8 @@ export declare interface CommentPinProps {
404
486
  }
405
487
 
406
488
  /**
407
- * The add-reaction button for one comment. Belongs with the comment card's hover actions (under
408
- * the ⋯ button) rather than in the reaction row, so opening it doesn't chase the row as reactions
409
- * are added.
489
+ * The add-reaction button for one comment. Belongs with the comment card's hover actions rather
490
+ * than in the reaction row, so opening it doesn't chase the row as reactions are added.
410
491
  * @public @react
411
492
  */
412
493
  export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
@@ -433,7 +514,7 @@ export declare interface CommentReactionsProps {
433
514
  /** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the
434
515
  * pills don't toggle. */
435
516
  currentUserId?: null | string;
436
- /** Names a reactor id for the hover list. Ids it can't name fall back to the id. */
517
+ /** Names a reactor id for the hover list. Ids it can't name fall back to a generic "Someone". */
437
518
  resolveName?(userId: string): string | undefined;
438
519
  }
439
520
 
@@ -477,8 +558,12 @@ export declare interface CommentsListProps {
477
558
  empty?: ReactNode;
478
559
  /** Label for a resolved thread's marker on its row. Defaults to "Resolved". */
479
560
  resolvedLabel?: string;
480
- /** Override how each item renders. Defaults to `<CommentListItem>`. */
481
- renderItem?(item: CommentListItemProps): ReactNode;
561
+ /**
562
+ * Override how each item renders. Defaults to `<CommentListItem>`, which is exported — so a
563
+ * row that only adds something can spread these props into it rather than start over. The list
564
+ * supplies the key, so a custom row doesn't need one.
565
+ */
566
+ renderItem?(props: CommentListItemRenderProps): ReactNode;
482
567
  }
483
568
 
484
569
  /**
@@ -496,10 +581,8 @@ export declare function CommentsMenuItem(): JSX.Element;
496
581
  export declare function CommentsOverflowMenu(): JSX.Element;
497
582
 
498
583
  /**
499
- * Whether the comments sidebar (the thread list) is open. Driven by an explicit control a button
500
- * next to Share on dotcom — rather than by which tool is active, so browsing threads is separate
501
- * from placing them. The comment tool additionally closes it on enter, keeping placement
502
- * canvas-focused.
584
+ * Whether the comments sidebar (the thread list) is open. Driven by an explicit control rather than
585
+ * by which tool is active, so browsing threads is separate from placing them.
503
586
  * @public
504
587
  */
505
588
  export declare const commentsSidebarOpen: EditorAtom<boolean>;
@@ -537,11 +620,10 @@ export declare interface CommentThreadProps {
537
620
  }
538
621
 
539
622
  /**
540
- * The comment tool. Pressing down opens the comment composer immediately at the pointer, and it
541
- * follows the pointer until release — like placing a sticky note — settling on a point, or on a
542
- * shape when released over one. (With region comments enabled, dragging past the threshold instead
543
- * draws a region rectangle.) Placement only opens a composer (via `pendingComment`); the records
544
- * are created when the comment is posted.
623
+ * The comment tool. Pressing down opens the comment composer at the pointer and it follows until
624
+ * release — like placing a sticky note — settling on a point, or on a shape when released over one.
625
+ * With region comments enabled, dragging past the threshold draws a region rectangle instead.
626
+ * Placement only opens a composer; the records are created when the comment is posted.
545
627
  * @public
546
628
  */
547
629
  export declare class CommentTool extends StateNode {
@@ -572,7 +654,7 @@ export declare class CommentTool extends StateNode {
572
654
  }
573
655
 
574
656
  /** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.
575
- * Once registered, tldraw's `DefaultToolbarContent` shows the comment button next to the eraser.
657
+ * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button.
576
658
  * @public */
577
659
  export declare const commentToolOverrides: TLUiOverrides;
578
660
 
@@ -607,6 +689,18 @@ export declare const DEFAULT_REACTION_EMOJI: string[];
607
689
  * @public */
608
690
  export declare const DEFAULT_SIDEBAR_FILTERS: SidebarFilters;
609
691
 
692
+ /**
693
+ * The default {@link CommentingOptions.canModifyComment}: a write is its record's owner's to make —
694
+ * a comment's author edits and deletes it, a thread's creator deletes the thread — and a viewer
695
+ * with no identity may make none of them.
696
+ *
697
+ * Exported so a callback can widen the default rather than restate it:
698
+ * `(ctx) => isModerator(ctx.currentUserId) || defaultCanModifyComment(ctx)`.
699
+ *
700
+ * @public
701
+ */
702
+ export declare function defaultCanModifyComment(ctx: CommentModificationContext): boolean;
703
+
610
704
  /**
611
705
  * The default {@link CommentingOptions}. Override via `CommentTool.configure({ ... })`.
612
706
  *
@@ -615,6 +709,7 @@ export declare const DEFAULT_SIDEBAR_FILTERS: SidebarFilters;
615
709
  export declare const defaultCommentingOptions: {
616
710
  readonly allowMultipleReactions: true;
617
711
  readonly canComment: undefined;
712
+ readonly canModifyComment: undefined;
618
713
  readonly components: {};
619
714
  readonly dragHistory: undefined;
620
715
  readonly enableClustering: true;
@@ -654,21 +749,18 @@ export declare function defaultRenderReaction(token: string): ReactNode;
654
749
  /**
655
750
  * Delete a comment.
656
751
  *
657
- * This is a soft delete: it sets the record's `isDeleted` flag rather than removing it, and the
658
- * server prunes the comment and its reactions once the flag is persisted. That way no client ever
659
- * removes records it doesn't own a reaction belongs to whoever left it and a server enforcing
660
- * per-record permissions has a write it can check rather than a deletion it can only refuse.
661
- * Deleting is the author's to do; the built-in UI only offers it on your own comments.
752
+ * This is a soft delete: it sets `isDeleted` rather than removing the record, and the server prunes
753
+ * the comment and its reactions once the flag is persisted so no client removes records it
754
+ * doesn't own, and a server enforcing per-record permissions has a write it can check.
662
755
  *
663
- * The write is never undoable, whatever {@link CommentingOptions.history} says: the flag is
664
- * write-once server-side, so an undo clearing it would be vetoed and rebased rather than bring the
665
- * comment back.
756
+ * Deleting is the author's to do by default; {@link CommentingOptions.canModifyComment} widens
757
+ * that, as does its counterpart on the server.
666
758
  *
667
- * Deleting a thread's last comment leaves the thread with no surface — an empty thread renders
668
- * nothing and closes it if it's open. The thread record is left for the server to prune, since
669
- * the deleter may not be its creator.
759
+ * Never undoable, whatever {@link CommentingOptions.history} says: the flag is write-once
760
+ * server-side, so an undo clearing it would be vetoed rather than bring the comment back.
670
761
  *
671
- * A comment that's already deleted, or already pruned, is a no-op.
762
+ * Deleting a thread's last comment closes it and leaves the thread record for the server to prune,
763
+ * since the deleter may not be its creator. An already-deleted comment is a no-op.
672
764
  *
673
765
  * @public
674
766
  */
@@ -677,27 +769,23 @@ export declare function deleteComment(editor: Editor, comment: TLComment): void;
677
769
  /**
678
770
  * Delete a thread and, with it, the whole conversation.
679
771
  *
680
- * A soft delete on the same model as {@link deleteComment}: the flag goes on the thread record and
681
- * the server prunes the thread, its comments, and their reactions once it's persisted. Deleting a
682
- * thread is its creator's to do a server enforcing per-record permissions vetoes anyone else
683
- * and the write is never undoable.
684
- *
685
- * Closes the thread if it's the open one. A thread that's already pruned is a no-op.
772
+ * A soft delete on the same model as {@link deleteComment}: the server prunes the thread, its
773
+ * comments, and their reactions once the flag is persisted. Deleting a thread is its creator's to
774
+ * do by default ({@link CommentingOptions.canModifyComment}), and the write is never undoable.
775
+ * Closes the thread if it's the open one; a pruned thread is a no-op.
686
776
  *
687
777
  * @public
688
778
  */
689
779
  export declare function deleteThread(editor: Editor, thread: TLCommentThread): void;
690
780
 
691
781
  /**
692
- * Replace a comment's body and stamp it as edited, which is what renders the "(edited)" marker on
693
- * its byline.
782
+ * Replace a comment's body and stamp it as edited, which renders the "(edited)" marker on its
783
+ * byline. Editing is the author's to do by default ({@link CommentingOptions.canModifyComment}),
784
+ * and a server enforcing per-record permissions rejects anyone else's. Widening one end without the
785
+ * other leaves an edit that's offered and then rejected, so widen both.
694
786
  *
695
- * Editing is the author's to do. The built-in UI only offers it on your own comments, and a server
696
- * that enforces per-record permissions rejects anyone else's edit.
697
- *
698
- * The `comment` you pass says which comment to edit; the body lands on the version the store
699
- * currently holds, so a copy you've held on to can't revert a change made since it, and can't
700
- * re-create a comment that's already been removed — editing one of those does nothing.
787
+ * The body lands on the version the store currently holds, so a stale copy can't revert a later
788
+ * change or re-create a removed comment editing one of those does nothing.
701
789
  *
702
790
  * @example
703
791
  * ```ts
@@ -740,6 +828,13 @@ export { filterMentionMembers }
740
828
  /** Open a thread and bring it into view — switch to its page if needed, then center its pin. @public */
741
829
  export declare function focusThread(editor: Editor, thread: TLCommentThread): void;
742
830
 
831
+ /**
832
+ * Format an ISO datetime as a full date and time ("Tuesday, July 22, 2025 at 4:44 PM").
833
+ * Locale-aware via Intl.DateTimeFormat.
834
+ * @public
835
+ */
836
+ export declare function formatFullDateTime(iso: string, locale?: string): string;
837
+
743
838
  /**
744
839
  * Format an ISO datetime as short relative time ("2 hr. ago", "yesterday", "last wk.").
745
840
  * Locale-aware via Intl.RelativeTimeFormat.
@@ -749,16 +844,30 @@ export declare function formatRelativeTime(iso: string, locale?: string): string
749
844
 
750
845
  /**
751
846
  * Whether the viewer may participate in commenting, per {@link CommentingOptions.canComment}
752
- * (defaulting to `currentUserId != null` when unset). Where this is false, composers give way to
753
- * the {@link CommentingComponents.ComposerFallback} slot and action affordances are hidden.
847
+ * (defaulting to `currentUserId != null` when unset). A callback that throws denies.
754
848
  *
755
- * This is a plain, untracked read — a `canComment` callback that reads signals is not observed.
756
- * In React, use {@link useCanComment} instead.
849
+ * This is a plain, untracked read — in React, use {@link useCanComment} instead.
757
850
  *
758
851
  * @public
759
852
  */
760
853
  export declare function getCanComment(editor: Editor, currentUserId: null | string | undefined): boolean;
761
854
 
855
+ /**
856
+ * Whether the viewer may make a given write against a given record, per
857
+ * {@link CommentingOptions.canModifyComment} (defaulting to {@link defaultCanModifyComment} when
858
+ * unset). Where this is false the affordance isn't rendered.
859
+ *
860
+ * This is the per-record rule alone: the built-in UI additionally requires
861
+ * {@link CommentingOptions.canComment}, since a viewer who may not participate gets no action
862
+ * affordances at all.
863
+ *
864
+ * A plain, untracked read — a `canModifyComment` callback that reads signals is not observed. In
865
+ * React, use {@link useCanModifyComment} instead.
866
+ *
867
+ * @public
868
+ */
869
+ export declare function getCanModifyComment(editor: Editor, currentUserId: null | string | undefined, modification: CommentModification): boolean;
870
+
762
871
  /**
763
872
  * The merged {@link CommentingOptions} for an editor, read off the registered comment tool (which
764
873
  * carries them via `CommentTool.configure`). Falls back to {@link defaultCommentingOptions} when
@@ -785,9 +894,8 @@ export declare function getCommentRecord(editor: Editor, id: string): TLCommentR
785
894
  export declare function getComments(editor: Editor): TLComment[];
786
895
 
787
896
  /**
788
- * Every comment thread in the store, **including soft-deleted ones** and ones left empty by their
789
- * last comment's delete records that are still present but awaiting the server's prune, and that
790
- * nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.
897
+ * Every comment thread in the store, **including soft-deleted and emptied ones** awaiting the
898
+ * server's prune, which nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.
791
899
  *
792
900
  * Non-reactive; wrap in `useValue`, or use `useCommentThreads`, to react.
793
901
  * @public
@@ -796,8 +904,7 @@ export declare function getCommentThreads(editor: Editor): TLCommentThread[];
796
904
 
797
905
  /**
798
906
  * The comments that should render: not soft-deleted. A deleted record lingers in the store until
799
- * the server prunes it, so this not {@link getComments} — is what a count or a list of your own
800
- * should be built from.
907
+ * the server prunes it, so build counts and lists from this rather than {@link getComments}.
801
908
  *
802
909
  * Non-reactive; the reactive equivalent is `useComments` (which also sorts oldest first).
803
910
  * @public
@@ -805,10 +912,9 @@ export declare function getCommentThreads(editor: Editor): TLCommentThread[];
805
912
  export declare function getLiveComments(editor: Editor): TLComment[];
806
913
 
807
914
  /**
808
- * The comment threads that should render (pins, sidebar): live — not soft-deleted and still
809
- * holding at least one live comment. A soft-deleted thread or comment is awaiting the server's
810
- * prune, as is a thread emptied by its last comment's delete; until the prune lands, the emptied
811
- * thread record lingers with no surface.
915
+ * The comment threads that should render (pins, sidebar): not soft-deleted, and still holding at
916
+ * least one live comment. A thread emptied by its last comment's delete lingers with no surface
917
+ * until the server's prune lands.
812
918
  *
813
919
  * Non-reactive; the reactive equivalent is `useCommentThreads`.
814
920
  * @public
@@ -817,12 +923,10 @@ export declare function getLiveCommentThreads(editor: Editor): TLCommentThread[]
817
923
 
818
924
  /**
819
925
  * The id passed to the most recent {@link revealThread} call that `CanvasComments` hasn't served
820
- * yet, or null when there's nothing outstanding. A request also clears when `CanvasComments`
821
- * unmounts, since nothing is left to serve it.
926
+ * yet, or null. A request also clears when `CanvasComments` unmounts.
822
927
  *
823
- * This is a plain, untracked read. In React, use {@link useRevealThreadPending} but reach for
824
- * this one inside a timer or callback that needs the value as of *now* rather than as of the
825
- * render it closed over.
928
+ * This is a plain, untracked read in React, use {@link useRevealThreadPending}, unless you need
929
+ * the value as of *now* rather than as of the render you closed over.
826
930
  *
827
931
  * @public
828
932
  */
@@ -861,13 +965,11 @@ export { MentionSuggestionOptions }
861
965
  export declare const openThreadId: EditorAtom<null | string>;
862
966
 
863
967
  /**
864
- * Write comment records to the store, under the configured
865
- * {@link CommentingOptions.history} behavior so a record you write lands on the undo stack (or
866
- * doesn't) exactly like one the built-in UI writes. Defaults to `'ignore'`.
968
+ * Write comment records to the store, under the configured {@link CommentingOptions.history}
969
+ * behavior. Defaults to `'ignore'`.
867
970
  *
868
- * Use it to seed or import threads, and to save an edit. To delete, prefer
869
- * {@link deleteComment} and {@link deleteThread} over {@link removeCommentRecords}: comments are
870
- * soft-deleted, and a synced server rejects the hard delete.
971
+ * Use it to seed or import threads, and to save an edit. To delete, prefer {@link deleteComment}
972
+ * and {@link deleteThread}: comments are soft-deleted, and a synced server rejects the hard delete.
871
973
  *
872
974
  * @public
873
975
  */
@@ -990,6 +1092,19 @@ export declare interface ReactionSummary {
990
1092
  reactors: ReactionReactor[];
991
1093
  }
992
1094
 
1095
+ /**
1096
+ * The reaction fields {@link summarizeReactions} needs — a structural subset of
1097
+ * {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the
1098
+ * editor store.
1099
+ *
1100
+ * @public
1101
+ */
1102
+ export declare interface ReactionSummaryInput {
1103
+ userId: string;
1104
+ emoji: string;
1105
+ createdAt: number;
1106
+ }
1107
+
993
1108
  /** @public */
994
1109
  export declare interface ReactionTooltipProps {
995
1110
  /** Who reacted with this emoji, in reaction order. */
@@ -1001,35 +1116,26 @@ export declare interface ReactionTooltipProps {
1001
1116
  /**
1002
1117
  * Keep shape-anchored threads alive across their shape's lifecycle:
1003
1118
  *
1004
- * - When the shape is deleted, the thread converts to a `point` anchor at the spot its pin last
1005
- * occupied, so the conversation outlives the shape instead of becoming invisible (a missing
1006
- * shape has no bounds, which hides the pin).
1119
+ * - When the shape is deleted, the thread converts to a `point` anchor where its pin last sat, so
1120
+ * the conversation outlives the shape instead of becoming invisible.
1007
1121
  * - When the shape moves to another page, the thread follows: its `pageId` and each comment's
1008
- * denormalized `pageId` update to the shape's new page, and the anchor keeps riding the shape.
1009
- * - When a deleted shape comes back (a page move re-creates it with its id preserved; undoing a
1010
- * delete restores it), the thread re-attaches unless its pin was manually moved in the
1011
- * meantime, in which case the manual placement wins.
1122
+ * denormalized `pageId` update, and the anchor keeps riding the shape.
1123
+ * - When a deleted shape comes back, the thread re-attaches unless its pin was manually moved in
1124
+ * the meantime, in which case the manual placement wins.
1012
1125
  *
1013
1126
  * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each
1014
- * store write is its own operation so at no single moment does "this shape is being moved"
1015
- * exist as an observable event. The handlers therefore cooperate across operations:
1016
- * `beforeDelete` snapshots where each affected pin sits (every record is still in the store at
1017
- * that point, so page bounds resolve even for children of a deleted frame); the
1018
- * operation-complete pass converts threads whose shape is really gone, remembering the original
1019
- * anchor; `afterCreate` notes a reappeared shape id, and the operation-complete pass restores
1020
- * the anchor once the store has settled (creation order within an operation is arbitrary, so a
1021
- * fresh shape may not resolve an ancestor page mid-operation). Undo/redo of a move
1022
- * replays as a `parentId` update (remove + add of one id squash to an update in the history
1023
- * diff), so an `afterChange` handler re-homes threads on cross-page reparents — including
1024
- * threads anchored to descendants, which move with their parent without change events of their
1025
- * own.
1026
- *
1027
- * Remote changes are ignored: the client that performed the operation runs this same
1028
- * maintenance and syncs the resulting thread updates. Writes honour the
1029
- * {@link CommentingOptions.history} option, so the consumer decides whether they're undoable.
1030
- *
1031
- * Registered by `CanvasComments` on mount; parts-built consumers can call this directly.
1032
- * Returns a cleanup function that unregisters all handlers.
1127
+ * store write is its own operation, so "this shape is being moved" is never observable as a single
1128
+ * event. The handlers therefore cooperate across operations: `beforeDelete` snapshots where each
1129
+ * affected pin sits, the operation-complete pass converts threads whose shape is really gone, and
1130
+ * `afterCreate` plus that same pass restore the anchor once the store has settled. Undo/redo of a
1131
+ * move replays as a `parentId` update, so `afterChange` re-homes threads on cross-page reparents —
1132
+ * including threads anchored to descendants, which move without change events of their own.
1133
+ *
1134
+ * Remote changes are ignored: the client that performed the operation runs this same maintenance
1135
+ * and syncs the result. Writes honour the {@link CommentingOptions.history} option.
1136
+ *
1137
+ * Registered by `CanvasComments` on mount; parts-built consumers can call this directly. Returns a
1138
+ * cleanup function that unregisters all handlers.
1033
1139
  *
1034
1140
  * @public
1035
1141
  */
@@ -1039,12 +1145,10 @@ export declare function registerCommentAnchorLifecycle(editor: Editor): () => vo
1039
1145
  * Remove comment records from the store by id, under the configured
1040
1146
  * {@link CommentingOptions.history} behavior.
1041
1147
  *
1042
- * This is a hard delete, which is rarely what you want for a comment or a thread: the built-in UI
1043
- * soft-deletes them ({@link deleteComment}, {@link deleteThread}) so the server can prune the
1044
- * records including reactions, which belong to whoever left them rather than to the deleter. A
1045
- * server that enforces per-record permissions will veto a hard delete outright. Reach for this on
1046
- * a local, unsynced comment store, or to drop a reaction (which is a hard delete — see
1047
- * {@link toggleCommentReaction}).
1148
+ * This is a hard delete, which is rarely what you want: the built-in UI soft-deletes
1149
+ * ({@link deleteComment}, {@link deleteThread}) so the server can prune the records, and a server
1150
+ * enforcing per-record permissions vetoes a hard delete outright. Reach for this on a local,
1151
+ * unsynced store, or to drop a reaction (see {@link toggleCommentReaction}).
1048
1152
  *
1049
1153
  * @public
1050
1154
  */
@@ -1065,21 +1169,20 @@ export declare function reopenThread(editor: Editor, thread: TLCommentThread): v
1065
1169
  * Mark a thread resolved, stamping who resolved it and when. Resolved threads keep their pin (a
1066
1170
  * checked one) and are hidden from the sidebar until its "show resolved" filter is on.
1067
1171
  *
1068
- * Only the resolution is written: the rest of the thread is read fresh, so resolving with a stale
1069
- * copy in hand won't drag a pin back to where it used to be. A no-op on a thread that's gone.
1172
+ * Only the resolution is written the rest of the thread is read fresh, so a stale copy can't drag
1173
+ * a pin back. A no-op on a thread that's gone.
1070
1174
  *
1071
1175
  * @public
1072
1176
  */
1073
1177
  export declare function resolveThread(editor: Editor, thread: TLCommentThread, userId: string): void;
1074
1178
 
1075
1179
  /**
1076
- * Open a thread and bring it into view, given a thread id or the id of any comment in it. Use it
1077
- * to jump to a thread from outside the canvas — a notification, a deep link, your own list.
1180
+ * Open a thread and bring it into view, given a thread id or the id of any comment in it. Use it to
1181
+ * jump to a thread from outside the canvas — a notification, a deep link, your own list.
1078
1182
  *
1079
- * The request is served by `CanvasComments`, so it works before the records have arrived: the
1080
- * layer waits for them to sync in, switches pages if it needs to, unhides pins, zooms in far
1081
- * enough to split the thread out of any cluster it's folded into, and then opens it. That also
1082
- * means nothing happens if `CanvasComments` isn't mounted.
1183
+ * The request is served by `CanvasComments`, so it works before the records have arrived: the layer
1184
+ * waits for them, switches pages, unhides pins, and zooms far enough to split the thread out of any
1185
+ * cluster. That also means nothing happens if `CanvasComments` isn't mounted.
1083
1186
  *
1084
1187
  * To open a thread you already hold and skip the wait, see {@link focusThread}.
1085
1188
  *
@@ -1113,11 +1216,9 @@ export declare interface SendButtonProps {
1113
1216
 
1114
1217
  /**
1115
1218
  * A shape anchor for a page point. `x`/`y` are the point's normalized (0–1) offset within the
1116
- * shape's own bounds taken in the shape's own space, so a pin placed on a rotated shape records
1117
- * the spot it was dropped on rather than a spot in the bounding box. Remembered either way: when
1118
- * `precise` the pin sits at exactly `x`/`y`; otherwise it sits at the consumer's imprecise default
1119
- * (top-right out of the box). Placement gestures get `precise` from the `shouldBePrecise`
1120
- * commenting option (always precise, by default).
1219
+ * shape's own bounds, taken in the shape's own space, so a pin on a rotated shape records the spot
1220
+ * it was dropped on. Remembered either way: when `precise` the pin sits at exactly `x`/`y`,
1221
+ * otherwise at the consumer's imprecise default.
1121
1222
  * @public
1122
1223
  */
1123
1224
  export declare function shapeAnchorAt(editor: Editor, shapeId: TLShapeId, page: {
@@ -1160,29 +1261,45 @@ export declare interface SidebarFilters {
1160
1261
  * @public */
1161
1262
  export declare const sidebarFilters: EditorAtom<SidebarFilters>;
1162
1263
 
1264
+ /** A list row paired with the sort key that isn't part of what the row displays. @public */
1265
+ export declare interface SidebarRow {
1266
+ item: CommentListItemProps;
1267
+ /** When the thread's most recent comment was posted — what the list orders by. */
1268
+ lastActivity: number;
1269
+ }
1270
+
1163
1271
  /**
1164
- * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used
1165
- * so the row stays stable as later reactions arrive. `active` marks the emoji the current user
1166
- * reacted with (the pills render those highlighted), and `reactors` lists who reacted with it, in
1167
- * reaction order, for the hover list. `resolveName` names each reactor; an id it can't name falls
1168
- * back to the id itself.
1272
+ * Order the list: unresolved threads first, then by most recent activity, id as a stable tiebreak.
1273
+ * Recency is the thread's *latest* comment, not its first, so a thread someone just replied to rises
1274
+ * to the top instead of staying wherever it was started. (The row still shows the thread's opening
1275
+ * comment and its date that's what identifies the thread; only the ordering follows the replies.)
1276
+ *
1277
+ * Exported so a hand-built list can match the sidebar's ordering instead of re-deriving it.
1169
1278
  *
1170
1279
  * @public
1171
1280
  */
1172
- export declare function summarizeReactions(reactions: TLCommentReaction[], currentUserId?: null | string, resolveName?: (userId: string) => string | undefined): ReactionSummary[];
1281
+ export declare function sortSidebarRows(rows: readonly SidebarRow[]): readonly SidebarRow[];
1282
+
1283
+ /**
1284
+ * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so
1285
+ * the row stays stable as later reactions arrive. `active` marks the current user's emoji and
1286
+ * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't
1287
+ * name falls back to a generic "Someone", never the raw user id.
1288
+ *
1289
+ * @public
1290
+ */
1291
+ export declare function summarizeReactions(reactions: readonly ReactionSummaryInput[], currentUserId?: null | string, resolveName?: (userId: string) => string | undefined): ReactionSummary[];
1173
1292
 
1174
1293
  /**
1175
1294
  * Typed reads of comment records on the editor store.
1176
1295
  *
1177
- * Comment threads and comments live on the editor's local store so the canvas can render them
1178
- * reactively, but they are opt-in records that aren't part of the `TLRecord` union (they ride the
1179
- * sync server's object-store lane on the wire see `TLCommentThread`). `editor.store` is therefore
1180
- * statically typed `Store<TLRecord>` and doesn't know about them, so every access has to reinterpret
1181
- * the type. These helpers own that reinterpretation — an `unknown` hop to exactly the type the store
1182
- * expects, so the rest of each call stays checked — behind one boundary, and keep call sites typed.
1296
+ * Comment records live on the editor's local store so the canvas can render them reactively, but
1297
+ * they're opt-in and aren't part of the `TLRecord` union so `editor.store` is statically typed
1298
+ * `Store<TLRecord>` and every access has to reinterpret the type. These helpers own that
1299
+ * reinterpretation behind one boundary and keep call sites typed.
1183
1300
  *
1184
- * Writes do the same reinterpretation, but they also answer to the undo/redo policy, so they live
1185
- * with the rest of the write path in `comment-mutations.ts`.
1301
+ * Writes do the same, but also answer to the undo/redo policy, so they live in
1302
+ * `comment-mutations.ts`.
1186
1303
  */
1187
1304
  /**
1188
1305
  * A record that lives in a comment thread: the thread itself, one of its messages, or a reaction
@@ -1194,14 +1311,12 @@ export declare type TLCommentRecord = TLComment | TLCommentReaction | TLCommentT
1194
1311
  /**
1195
1312
  * Toggle one user's reaction with a given emoji on a comment.
1196
1313
  *
1197
- * Each reaction is its own record keyed by (comment, user, emoji), so this only ever touches that
1198
- * one user's own records two people reacting at once never conflict. Behaviour depends on the
1199
- * `allowMultipleReactions` option:
1314
+ * Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's
1315
+ * own records and two people reacting at once never conflict. Behaviour depends on
1316
+ * `allowMultipleReactions`:
1200
1317
  *
1201
- * - **multiple** (default): the emoji toggles independently add it if absent, remove it if
1202
- * present, leaving the user's other reactions alone.
1203
- * - **single**: picking a new emoji first removes the user's existing reaction(s) on the comment,
1204
- * then adds this one (a replace); picking the one they already have removes it.
1318
+ * - **multiple** (default): the emoji toggles independently, leaving other reactions alone.
1319
+ * - **single**: a new emoji replaces the user's existing reaction; the same one removes it.
1205
1320
  *
1206
1321
  * @public
1207
1322
  */
@@ -1223,6 +1338,14 @@ export declare function toggleCommentsSidebar(editor: Editor): void;
1223
1338
  */
1224
1339
  export declare function useCanComment(currentUserId: null | string | undefined): boolean;
1225
1340
 
1341
+ /**
1342
+ * Reactive React hook for {@link getCanModifyComment}: a `canModifyComment` callback that reads
1343
+ * signals re-evaluates when they change.
1344
+ *
1345
+ * @public
1346
+ */
1347
+ export declare function useCanModifyComment(currentUserId: null | string | undefined, modification: CommentModification): boolean;
1348
+
1226
1349
  /**
1227
1350
  * Whether commenting is licensed for this editor. Enabled in development; in production it requires
1228
1351
  * a tldraw license that includes the commenting feature (or the collaboration umbrella that grants
@@ -1230,7 +1353,7 @@ export declare function useCanComment(currentUserId: null | string | undefined):
1230
1353
  * pending, so gated UI stays hidden until the license is confirmed.
1231
1354
  *
1232
1355
  * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment
1233
- * tool's toolbar button) gate on this. Use it to gate any custom commenting UI the same way.
1356
+ * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.
1234
1357
  * @public
1235
1358
  */
1236
1359
  export declare function useCommentingEnabled(): boolean;
@@ -1278,11 +1401,9 @@ export declare function useOpenThreadId(): null | string;
1278
1401
  /**
1279
1402
  * Reactive React hook for {@link getRevealThreadPending}.
1280
1403
  *
1281
- * Use it to notice a reveal that never lands — most often a deep link to a comment that has since
1282
- * been deleted. Give it a grace period before you act: a request also sits here while its records
1283
- * are still syncing in, which is the normal case on a cold load. Re-check with
1284
- * {@link getRevealThreadPending} when the grace period elapses, since the request can clear inside
1285
- * it without this hook's value having caught up yet.
1404
+ * Use it to notice a reveal that never lands — usually a deep link to a deleted comment. Give it a
1405
+ * grace period first, since a request also sits here while its records sync in, and re-check with
1406
+ * {@link getRevealThreadPending} when it elapses.
1286
1407
  *
1287
1408
  * @public
1288
1409
  */