@tldraw/commenting 5.6.0-next.d6710de420bf → 5.6.0-next.f151f7f3ebd6

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 (107) hide show
  1. package/dist-cjs/canvas/cluster-badge.js +7 -1
  2. package/dist-cjs/canvas/cluster-badge.js.map +2 -2
  3. package/dist-cjs/canvas/comment-reactions.js +10 -4
  4. package/dist-cjs/canvas/comment-reactions.js.map +2 -2
  5. package/dist-cjs/canvas/comment-render.js.map +2 -2
  6. package/dist-cjs/canvas/comment-tool.js +35 -15
  7. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  8. package/dist-cjs/canvas/comments-filter-menu.js +13 -6
  9. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  10. package/dist-cjs/canvas/comments-menu-item.js +4 -1
  11. package/dist-cjs/canvas/comments-menu-item.js.map +2 -2
  12. package/dist-cjs/canvas/comments-overflow-menu.js +9 -4
  13. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  14. package/dist-cjs/canvas/comments-sidebar.js +14 -5
  15. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  16. package/dist-cjs/canvas/comments-visibility-toggle.js +5 -1
  17. package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
  18. package/dist-cjs/canvas/options.js.map +1 -1
  19. package/dist-cjs/canvas/pending-composer.js +7 -2
  20. package/dist-cjs/canvas/pending-composer.js.map +2 -2
  21. package/dist-cjs/canvas/thread-pin.js +13 -2
  22. package/dist-cjs/canvas/thread-pin.js.map +2 -2
  23. package/dist-cjs/canvas/thread-preview.js +4 -1
  24. package/dist-cjs/canvas/thread-preview.js.map +2 -2
  25. package/dist-cjs/canvas/thread-stack.js +18 -4
  26. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  27. package/dist-cjs/canvas/thread-view.js +40 -20
  28. package/dist-cjs/canvas/thread-view.js.map +2 -2
  29. package/dist-cjs/index.d.ts +28 -8
  30. package/dist-cjs/index.js +2 -1
  31. package/dist-cjs/index.js.map +2 -2
  32. package/dist-cjs/ui/comment-composer.js +5 -0
  33. package/dist-cjs/ui/comment-composer.js.map +2 -2
  34. package/dist-cjs/ui/gesture-focus.js +37 -0
  35. package/dist-cjs/ui/gesture-focus.js.map +7 -0
  36. package/dist-cjs/ui/reaction-picker.js +5 -2
  37. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  38. package/dist-cjs/ui/reaction.js +15 -5
  39. package/dist-cjs/ui/reaction.js.map +2 -2
  40. package/dist-cjs/ui/reply-count.js +8 -2
  41. package/dist-cjs/ui/reply-count.js.map +2 -2
  42. package/dist-esm/canvas/cluster-badge.mjs +8 -1
  43. package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
  44. package/dist-esm/canvas/comment-reactions.mjs +12 -4
  45. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  46. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  47. package/dist-esm/canvas/comment-tool.mjs +37 -16
  48. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  49. package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
  50. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  51. package/dist-esm/canvas/comments-menu-item.mjs +5 -2
  52. package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
  53. package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
  54. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  55. package/dist-esm/canvas/comments-sidebar.mjs +21 -6
  56. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  57. package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
  58. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  59. package/dist-esm/canvas/options.mjs.map +1 -1
  60. package/dist-esm/canvas/pending-composer.mjs +8 -2
  61. package/dist-esm/canvas/pending-composer.mjs.map +2 -2
  62. package/dist-esm/canvas/thread-pin.mjs +15 -3
  63. package/dist-esm/canvas/thread-pin.mjs.map +2 -2
  64. package/dist-esm/canvas/thread-preview.mjs +5 -1
  65. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  66. package/dist-esm/canvas/thread-stack.mjs +19 -4
  67. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  68. package/dist-esm/canvas/thread-view.mjs +42 -21
  69. package/dist-esm/canvas/thread-view.mjs.map +2 -2
  70. package/dist-esm/index.d.mts +28 -8
  71. package/dist-esm/index.mjs +8 -2
  72. package/dist-esm/index.mjs.map +2 -2
  73. package/dist-esm/ui/comment-composer.mjs +5 -0
  74. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  75. package/dist-esm/ui/gesture-focus.mjs +17 -0
  76. package/dist-esm/ui/gesture-focus.mjs.map +7 -0
  77. package/dist-esm/ui/reaction-picker.mjs +6 -2
  78. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  79. package/dist-esm/ui/reaction.mjs +16 -8
  80. package/dist-esm/ui/reaction.mjs.map +2 -2
  81. package/dist-esm/ui/reply-count.mjs +8 -2
  82. package/dist-esm/ui/reply-count.mjs.map +2 -2
  83. package/package.json +5 -5
  84. package/src/canvas/cluster-badge.tsx +9 -1
  85. package/src/canvas/comment-reactions.tsx +18 -7
  86. package/src/canvas/comment-render.ts +6 -4
  87. package/src/canvas/comment-tool.test.ts +83 -7
  88. package/src/canvas/comment-tool.tsx +61 -19
  89. package/src/canvas/comments-filter-menu.tsx +15 -6
  90. package/src/canvas/comments-menu-item.tsx +6 -2
  91. package/src/canvas/comments-overflow-menu.tsx +11 -4
  92. package/src/canvas/comments-sidebar.tsx +23 -6
  93. package/src/canvas/comments-visibility-toggle.tsx +7 -2
  94. package/src/canvas/options.ts +1 -1
  95. package/src/canvas/pending-composer.tsx +11 -2
  96. package/src/canvas/thread-pin.tsx +17 -4
  97. package/src/canvas/thread-preview.tsx +6 -1
  98. package/src/canvas/thread-stack.tsx +21 -4
  99. package/src/canvas/thread-view.tsx +44 -22
  100. package/src/index.ts +6 -1
  101. package/src/ui/comment-composer.tsx +9 -0
  102. package/src/ui/gesture-focus.test.ts +45 -0
  103. package/src/ui/gesture-focus.ts +29 -0
  104. package/src/ui/reaction-picker.tsx +7 -2
  105. package/src/ui/reaction.test.tsx +22 -0
  106. package/src/ui/reaction.tsx +25 -14
  107. package/src/ui/reply-count.ts +9 -3
@@ -6,6 +6,7 @@ import {
6
6
  TLStateNodeConstructor,
7
7
  TLUiOverrides,
8
8
  VecLike,
9
+ defineMessages,
9
10
  } from 'tldraw'
10
11
  import { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'
11
12
  import { commentsSidebarOpen, pendingComment, regionDraft } from './state'
@@ -53,8 +54,8 @@ export function regionBetween(a: VecLike, b: VecLike): BoxModel {
53
54
  * release — like placing a sticky note — settling on a point, or on a shape when released over one.
54
55
  * With region comments enabled, dragging past the threshold draws a region rectangle instead.
55
56
  * Placement only opens a composer; the records are created when the comment is posted. The tool
56
- * stays active while the composer is open — posting returns to select, and clicking elsewhere
57
- * re-places the composer.
57
+ * stays active while the composer is open — posting returns to select, and so does clicking the
58
+ * canvas elsewhere, which cancels the placement.
58
59
  * @public
59
60
  */
60
61
  export class CommentTool extends StateNode {
@@ -113,6 +114,44 @@ export class CommentTool extends StateNode {
113
114
  }
114
115
  }
115
116
 
117
+ function pendingCommentAt(editor: Editor, point: VecLike): PendingComment {
118
+ const hit = commentTargetShapeAt(editor, point)
119
+ const anchor: TLCommentAnchor = hit
120
+ ? shapeAnchorAt(
121
+ editor,
122
+ hit.id,
123
+ point,
124
+ getCommentingOptions(editor).shouldBePrecise(editor, {
125
+ shapeId: hit.id,
126
+ point,
127
+ altKey: editor.inputs.getAltKey(),
128
+ })
129
+ )
130
+ : { type: 'point', x: point.x, y: point.y }
131
+ return { anchor, point: { x: point.x, y: point.y } }
132
+ }
133
+
134
+ /**
135
+ * Open the comment composer at a page point, anchored as a comment tool click there would be. Use
136
+ * it to start a comment from outside the tool, such as a context menu item. Enters the comment tool
137
+ * (which must be registered), since the draft composer closes when the tool exits. It doesn't check
138
+ * `canComment`: a viewer who can't comment sees the `ComposerFallback` slot instead.
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * <TldrawUiMenuItem id="comment" label="Comment" onSelect={() => startCommentAt(editor, point)} />
143
+ * ```
144
+ * @public
145
+ */
146
+ export function startCommentAt(editor: Editor, point: VecLike) {
147
+ editor.setCurrentTool('comment')
148
+ const pending = pendingCommentAt(editor, point)
149
+ pendingComment.set(editor, pending)
150
+ // Entering the tool hints the shape under the live pointer, which is wherever the caller's UI
151
+ // was (a menu item), not `point`.
152
+ editor.setHintingShapes(pending.anchor.type === 'shape' ? [pending.anchor.shapeId] : [])
153
+ }
154
+
116
155
  /** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the
117
156
  * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so
118
157
  * this shows the select-style outline while the comment tool — not select — is active. */
@@ -142,6 +181,12 @@ class CommentIdle extends StateNode {
142
181
  }
143
182
 
144
183
  override onPointerDown() {
184
+ // A press away from an open composer cancels the placement rather than moving it. The
185
+ // tool's onExit drops the draft composer; its text survives in the draft store.
186
+ if (pendingComment.get(this.editor)) {
187
+ this.editor.setCurrentTool('select')
188
+ return
189
+ }
145
190
  this.parent.transition('pointing')
146
191
  }
147
192
  }
@@ -183,20 +228,7 @@ class CommentPointing extends StateNode {
183
228
  override onPointerUp() {
184
229
  const { editor } = this
185
230
  const point = editor.inputs.getCurrentPagePoint()
186
- const hit = commentTargetShapeAt(editor, point)
187
- const anchor: TLCommentAnchor = hit
188
- ? shapeAnchorAt(
189
- editor,
190
- hit.id,
191
- point,
192
- getCommentingOptions(editor).shouldBePrecise(editor, {
193
- shapeId: hit.id,
194
- point,
195
- altKey: editor.inputs.getAltKey(),
196
- })
197
- )
198
- : { type: 'point', x: point.x, y: point.y }
199
- pendingComment.set(editor, { anchor, point: { x: point.x, y: point.y } })
231
+ pendingComment.set(editor, pendingCommentAt(editor, point))
200
232
  // Stay in the tool while the composer is open — the interaction isn't over until the
201
233
  // comment is posted or dismissed, and staying keeps the surrounding UI (style panel,
202
234
  // sidebar) from churning mid-placement.
@@ -277,17 +309,27 @@ class CommentDragging extends StateNode {
277
309
  /** @public */
278
310
  export const commentTools = [CommentTool]
279
311
 
312
+ const messages = defineMessages({
313
+ toolComment: { id: 'tool.comment', defaultMessage: 'Comment' },
314
+ })
315
+
280
316
  /** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.
281
- * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button.
317
+ * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button, and its
318
+ * `DefaultContextMenuContent` shows a comment item that opens the composer where the menu was
319
+ * opened.
282
320
  * @public */
283
321
  export const commentToolOverrides: TLUiOverrides = {
284
322
  tools(editor, tools) {
285
323
  tools.comment = {
286
324
  id: 'comment',
287
325
  icon: 'comment',
288
- label: 'Comment',
326
+ label: messages.toolComment.id,
289
327
  kbd: 'c',
290
- onSelect: () => editor.setCurrentTool('comment'),
328
+ onSelect: (source) => {
329
+ // The menu was opened by the last pointer down; the live pointer is over the menu now.
330
+ if (source === 'context-menu') startCommentAt(editor, editor.inputs.getOriginPagePoint())
331
+ else editor.setCurrentTool('comment')
332
+ },
291
333
  }
292
334
  return tools
293
335
  },
@@ -1,4 +1,5 @@
1
1
  import {
2
+ defineMessages,
2
3
  TldrawUiButton,
3
4
  TldrawUiDropdownMenuContent,
4
5
  TldrawUiDropdownMenuRoot,
@@ -14,6 +15,14 @@ import { MoreMenuIcon } from '../ui/icons'
14
15
  import { SidebarFilters } from './sidebar-filters'
15
16
  import { sidebarFilters } from './state'
16
17
 
18
+ const messages = defineMessages({
19
+ commentsFilter: { id: 'comments.filter', defaultMessage: 'Filter comments' },
20
+ commentsOnlyMyComments: { id: 'comments.only-my-comments', defaultMessage: 'Only my comments' },
21
+ commentsOnlyUnread: { id: 'comments.only-unread', defaultMessage: 'Only unread' },
22
+ commentsShowAllPages: { id: 'comments.show-all-pages', defaultMessage: 'Show all pages' },
23
+ commentsShowResolved: { id: 'comments.show-resolved', defaultMessage: 'Show resolved comments' },
24
+ })
25
+
17
26
  /** @public */
18
27
  export interface CommentsFilterMenuProps {
19
28
  /** Whether to offer the "only your threads" toggle (needs a known current user). */
@@ -40,8 +49,8 @@ export function CommentsFilterMenu({
40
49
  <TldrawUiDropdownMenuTrigger>
41
50
  <TldrawUiButton
42
51
  type="icon"
43
- tooltip={msg('comments.filter')}
44
- title={msg('comments.filter')}
52
+ tooltip={msg(messages.commentsFilter)}
53
+ title={msg(messages.commentsFilter)}
45
54
  className="tlui-cmt-header-btn"
46
55
  >
47
56
  <MoreMenuIcon />
@@ -54,14 +63,14 @@ export function CommentsFilterMenu({
54
63
  <TldrawUiMenuGroup id="comments-filter">
55
64
  <TldrawUiMenuCheckboxItem
56
65
  id="show-all-pages"
57
- label="comments.show-all-pages"
66
+ label={messages.commentsShowAllPages.id}
58
67
  checked={!filters.onlyCurrentPage}
59
68
  onSelect={() => toggle('onlyCurrentPage')}
60
69
  />
61
70
  {canFilterByAuthor && (
62
71
  <TldrawUiMenuCheckboxItem
63
72
  id="only-my-comments"
64
- label="comments.only-my-comments"
73
+ label={messages.commentsOnlyMyComments.id}
65
74
  checked={filters.onlyMine}
66
75
  onSelect={() => toggle('onlyMine')}
67
76
  />
@@ -69,14 +78,14 @@ export function CommentsFilterMenu({
69
78
  {canFilterByUnread && (
70
79
  <TldrawUiMenuCheckboxItem
71
80
  id="only-unread"
72
- label="comments.only-unread"
81
+ label={messages.commentsOnlyUnread.id}
73
82
  checked={filters.onlyUnread}
74
83
  onSelect={() => toggle('onlyUnread')}
75
84
  />
76
85
  )}
77
86
  <TldrawUiMenuCheckboxItem
78
87
  id="show-resolved"
79
- label="comments.show-resolved"
88
+ label={messages.commentsShowResolved.id}
80
89
  checked={filters.showResolved}
81
90
  onSelect={() => toggle('showResolved')}
82
91
  />
@@ -1,6 +1,10 @@
1
- import { TldrawUiMenuCheckboxItem, useEditor, useValue } from 'tldraw'
1
+ import { defineMessages, TldrawUiMenuCheckboxItem, useEditor, useValue } from 'tldraw'
2
2
  import { commentsHidden, toggleCommentsHidden } from './state'
3
3
 
4
+ const messages = defineMessages({
5
+ commentsTitle: { id: 'comments.title', defaultMessage: 'Comments' },
6
+ })
7
+
4
8
  /**
5
9
  * A checkbox menu item for a "View" menu that shows/hides comment pins on the canvas — checked
6
10
  * when comments are visible, like tldraw's own grid/snap toggles. Reads and toggles the shared
@@ -14,7 +18,7 @@ export function CommentsMenuItem() {
14
18
  return (
15
19
  <TldrawUiMenuCheckboxItem
16
20
  id="comments-visible"
17
- label="comments.title"
21
+ label={messages.commentsTitle.id}
18
22
  kbd="!c"
19
23
  readonlyOk
20
24
  checked={!hidden}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ defineMessages,
2
3
  TldrawUiButton,
3
4
  TldrawUiDropdownMenuContent,
4
5
  TldrawUiDropdownMenuGroup,
@@ -12,6 +13,12 @@ import {
12
13
  } from 'tldraw'
13
14
  import { commentsHidden, toggleCommentsHidden } from './state'
14
15
 
16
+ const messages = defineMessages({
17
+ commentsMoreOptions: { id: 'comments.more-options', defaultMessage: 'More options' },
18
+ commentsHide: { id: 'comments.hide', defaultMessage: 'Hide comments' },
19
+ commentsShow: { id: 'comments.show', defaultMessage: 'Show comments' },
20
+ })
21
+
15
22
  // A keyboard-shortcut glyph, not translatable copy — kept out of JSX as a constant.
16
23
  const HIDE_SHORTCUT = '⇧C'
17
24
 
@@ -28,11 +35,11 @@ export function CommentsOverflowMenu() {
28
35
  <TldrawUiDropdownMenuTrigger>
29
36
  <TldrawUiButton
30
37
  type="icon"
31
- tooltip={msg('comments.more-options')}
32
- title={msg('comments.more-options')}
38
+ tooltip={msg(messages.commentsMoreOptions)}
39
+ title={msg(messages.commentsMoreOptions)}
33
40
  className="tlui-cmt-header-btn"
34
41
  >
35
- <TldrawUiIcon icon="dots-vertical" label={msg('comments.more-options')} small />
42
+ <TldrawUiIcon icon="dots-vertical" label={msg(messages.commentsMoreOptions)} small />
36
43
  </TldrawUiButton>
37
44
  </TldrawUiDropdownMenuTrigger>
38
45
  <TldrawUiDropdownMenuContent
@@ -49,7 +56,7 @@ export function CommentsOverflowMenu() {
49
56
  className="tlui-cmt-menu-item"
50
57
  onClick={() => toggleCommentsHidden(editor)}
51
58
  >
52
- <span>{hidden ? msg('comments.show') : msg('comments.hide')}</span>
59
+ <span>{hidden ? msg(messages.commentsShow) : msg(messages.commentsHide)}</span>
53
60
  <span className="tlui-cmt-menu-item__shortcut">{HIDE_SHORTCUT}</span>
54
61
  </button>
55
62
  </TldrawUiDropdownMenuItem>
@@ -1,5 +1,12 @@
1
1
  import { ReactNode, useCallback } from 'react'
2
- import { EditorPortal, TLCommentThread, useEditor, useTranslation, useValue } from 'tldraw'
2
+ import {
3
+ defineMessages,
4
+ EditorPortal,
5
+ TLCommentThread,
6
+ useEditor,
7
+ useTranslation,
8
+ useValue,
9
+ } from 'tldraw'
3
10
  import { CommentListItemProps, CommentListItemRenderProps, CommentsList } from '../ui/comments-list'
4
11
  import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
5
12
  import { CommentsFilterMenu } from './comments-filter-menu'
@@ -12,6 +19,16 @@ import { richTextToPlaintext } from './rich-text'
12
19
  import { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'
13
20
  import { focusThread } from './thread-state'
14
21
 
22
+ const messages = defineMessages({
23
+ commentsEmpty: { id: 'comments.empty', defaultMessage: 'No comments on this page yet.' },
24
+ commentsEmptyFiltered: {
25
+ id: 'comments.empty-filtered',
26
+ defaultMessage: 'No comments match your filters.',
27
+ },
28
+ commentsResolved: { id: 'comments.resolved', defaultMessage: 'Resolved' },
29
+ commentsTitle: { id: 'comments.title', defaultMessage: 'Comments' },
30
+ })
31
+
15
32
  /**
16
33
  * The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,
17
34
  * plus the panel's own slots. A non-null `currentUserId` enables the "only your threads" filter, and
@@ -146,22 +163,22 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
146
163
  <SidebarPanel>
147
164
  <CommentsList
148
165
  items={items}
149
- header={header ?? msg('comments.title')}
166
+ header={header ?? msg(messages.commentsTitle)}
150
167
  headerAction={
151
168
  <div className="tlui-cmt-list__header-actions">
169
+ <CommentsVisibilityToggle />
152
170
  <CommentsFilterMenu
153
171
  canFilterByAuthor={currentUserId !== null}
154
172
  canFilterByUnread={isCommentUnread !== undefined}
155
173
  />
156
- <CommentsVisibilityToggle />
157
174
  </div>
158
175
  }
159
176
  empty={
160
177
  items.length === 0 && pageThreads.length > 0
161
- ? msg('comments.empty-filtered')
162
- : (empty ?? msg('comments.empty'))
178
+ ? msg(messages.commentsEmptyFiltered)
179
+ : (empty ?? msg(messages.commentsEmpty))
163
180
  }
164
- resolvedLabel={msg('comments.resolved')}
181
+ resolvedLabel={msg(messages.commentsResolved)}
165
182
  onSelect={focus}
166
183
  renderItem={renderItem}
167
184
  />
@@ -1,7 +1,12 @@
1
- import { TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'
1
+ import { defineMessages, TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'
2
2
  import { EyeClosedIcon, EyeOpenIcon } from '../ui/icons'
3
3
  import { commentsHidden, toggleCommentsHidden } from './state'
4
4
 
5
+ const messages = defineMessages({
6
+ commentsHide: { id: 'comments.hide', defaultMessage: 'Hide comments' },
7
+ commentsShow: { id: 'comments.show', defaultMessage: 'Show comments' },
8
+ })
9
+
5
10
  /** The sidebar header's show/hide toggle for comment pins — an eye that closes while comments
6
11
  * are hidden. The same state as the Shift+C shortcut.
7
12
  * @public @react */
@@ -9,7 +14,7 @@ export function CommentsVisibilityToggle() {
9
14
  const editor = useEditor()
10
15
  const msg = useTranslation()
11
16
  const hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])
12
- const label = hidden ? msg('comments.show') : msg('comments.hide')
17
+ const label = hidden ? msg(messages.commentsShow) : msg(messages.commentsHide)
13
18
 
14
19
  return (
15
20
  <TldrawUiButton
@@ -91,7 +91,7 @@ export interface CommentingComponents {
91
91
  /**
92
92
  * The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as
93
93
  * `children`) and owns the whole thing — box, size, shape, position. For a wording change,
94
- * translate the `comments.reacted-*` strings instead.
94
+ * translate the `comments.reacted` string instead.
95
95
  */
96
96
  ReactionTooltip?: ComponentType<ReactionTooltipProps>
97
97
  /**
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'
3
3
  import {
4
4
  createComment,
5
5
  createCommentThread,
6
+ defineMessages,
6
7
  Editor,
7
8
  EditorPortal,
8
9
  TLRichText,
@@ -27,6 +28,11 @@ import { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'
27
28
  import { useCanComment, useCommentingOptions } from './options'
28
29
  import { pendingComment } from './state'
29
30
 
31
+ const messages = defineMessages({
32
+ commentsSend: { id: 'comments.send', defaultMessage: 'Send' },
33
+ commentsAddPlaceholder: { id: 'comments.add-placeholder', defaultMessage: 'Add a comment…' },
34
+ })
35
+
30
36
  const stop = (e: { stopPropagation(): void }) => e.stopPropagation()
31
37
 
32
38
  /**
@@ -80,6 +86,9 @@ export function PendingComposer({
80
86
  // A click in the composer, or in the mention picker it spawns (portaled elsewhere), is
81
87
  // not "outside" — keep the draft open so the pick can insert.
82
88
  if (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return
89
+ // A press on the canvas is the comment tool's to handle: it ends the placement and returns
90
+ // to select. Clearing here first would let that press re-place the composer instead.
91
+ if (editor.getContainer().querySelector('.tl-canvas')?.contains(target)) return
83
92
  pendingComment.set(editor, null)
84
93
  }
85
94
  document.addEventListener('pointerdown', onPointerDown, true)
@@ -137,8 +146,8 @@ export function PendingComposer({
137
146
  {canComment ? (
138
147
  <CommentComposer
139
148
  author={me ?? UNKNOWN_COMMENT_AUTHOR}
140
- placeholder={msg('comments.add-placeholder')}
141
- sendLabel={msg('comments.send')}
149
+ placeholder={msg(messages.commentsAddPlaceholder)}
150
+ sendLabel={msg(messages.commentsSend)}
142
151
  value={text}
143
152
  onChange={(value) => {
144
153
  setText(value)
@@ -8,9 +8,10 @@ import {
8
8
  useState,
9
9
  } from 'react'
10
10
  import {
11
- type BoxModel,
11
+ defineMessages,
12
12
  Editor,
13
13
  TLCommentThread,
14
+ type BoxModel,
14
15
  useContainer,
15
16
  usePassThroughWheelEvents,
16
17
  useTranslation,
@@ -19,7 +20,7 @@ import {
19
20
  import { CommentPin } from '../ui/comment-pin'
20
21
  import { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'
21
22
  import { commitCommentMutation } from './comment-mutations'
22
- import { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'
23
+ import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
23
24
  import { type CommentingContext } from './context'
24
25
  import { useThreadComments } from './hooks'
25
26
  import { getCommentingOptions, useCanComment, useCommentingOptions } from './options'
@@ -46,6 +47,15 @@ import {
46
47
  } from './thread-state'
47
48
  import { POPOVER_OFFSET, ThreadPopover, ThreadView } from './thread-view'
48
49
 
50
+ const messages = defineMessages({
51
+ commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
52
+ commentsPinLabel: { id: 'comments.pin-label', defaultMessage: 'Comment by {name}' },
53
+ commentsPinLabelResolved: {
54
+ id: 'comments.pin-label-resolved',
55
+ defaultMessage: 'Resolved comment by {name}',
56
+ },
57
+ })
58
+
49
59
  /** The opened popover has a header row the hover preview lacks, so it opens this much higher — the
50
60
  * first comment then lands where the preview's sat. Re-measure if the header height or the preview
51
61
  * panel padding changes. */
@@ -195,8 +205,11 @@ export const ThreadPin = memo(function ThreadPin({
195
205
  <Avatar author={threadAuthor ?? UNKNOWN_COMMENT_AUTHOR} />
196
206
  )
197
207
  const pinLabel = msg(
198
- thread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'
199
- ).replace('{name}', threadAuthor?.name ?? UNKNOWN_AUTHOR)
208
+ thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
209
+ {
210
+ name: threadAuthor?.name ?? msg(messages.commentsUnknownAuthor),
211
+ }
212
+ )
200
213
 
201
214
  // Drag the marker to move the thread: position is overridden locally while dragging, then re-anchored
202
215
  // on drop. A region translates keeping its size; a barely-moved pointer is a click.
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, type CSSProperties } from 'react'
2
2
  import {
3
+ defineMessages,
3
4
  Editor,
4
5
  EditorAtom,
5
6
  EditorPortal,
@@ -17,6 +18,10 @@ import { useCommentingOptions } from './options'
17
18
  import { openStackId, openThreadId } from './state'
18
19
  import { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'
19
20
 
21
+ const messages = defineMessages({
22
+ commentsPreviewMore: { id: 'comments.preview-more', defaultMessage: '{count} more' },
23
+ })
24
+
20
25
  /**
21
26
  * Hover previews for every canvas marker — a single pin, a coincident stack, or a cluster badge.
22
27
  * Hovering shows the thread(s) behind the marker as cards; the marker's own click is unchanged.
@@ -277,7 +282,7 @@ export function ThreadPreview({
277
282
  })}
278
283
  {overflow > 0 && (
279
284
  <div className="tlui-cmt-preview-more">
280
- {msg('comments.preview-more').replace('{count}', String(overflow))}
285
+ {msg(messages.commentsPreviewMore, { count: overflow })}
281
286
  </div>
282
287
  )}
283
288
  </div>
@@ -1,5 +1,6 @@
1
1
  import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'
2
2
  import {
3
+ defineMessages,
3
4
  Editor,
4
5
  TLCommentThread,
5
6
  usePassThroughWheelEvents,
@@ -8,7 +9,6 @@ import {
8
9
  } from 'tldraw'
9
10
  import { CommentCard } from '../ui/comment-card'
10
11
  import { CountBadge } from '../ui/count-badge'
11
- import { UNKNOWN_AUTHOR } from './comment-render'
12
12
  import { type CommentingContext } from './context'
13
13
  import { useThreadComments } from './hooks'
14
14
  import { useCommentingOptions } from './options'
@@ -24,6 +24,19 @@ import {
24
24
  useResolveName,
25
25
  } from './thread-view'
26
26
 
27
+ const messages = defineMessages({
28
+ commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
29
+ commentsPinLabel: { id: 'comments.pin-label', defaultMessage: 'Comment by {name}' },
30
+ commentsPinLabelResolved: {
31
+ id: 'comments.pin-label-resolved',
32
+ defaultMessage: 'Resolved comment by {name}',
33
+ },
34
+ commentsStackLabel: {
35
+ id: 'comments.stack-label',
36
+ defaultMessage: '{count, plural, one {# comment at this spot} other {# comments at this spot}}',
37
+ },
38
+ })
39
+
27
40
  /**
28
41
  * The pin for threads whose anchors resolve to the same page point — pins zooming can never
29
42
  * separate, so they share one count badge. Clicking it lists each thread as a card; clicking a card
@@ -137,7 +150,7 @@ export const ThreadStackPin = memo(function ThreadStackPin({
137
150
  ref={badgeRef}
138
151
  type="button"
139
152
  className="tlui-cmt-button tlui-cmt-canvas-stack-badge"
140
- aria-label={msg('comments.stack-label').replace('{count}', String(threads.length))}
153
+ aria-label={msg(messages.commentsStackLabel, { count: threads.length })}
141
154
  aria-expanded={open}
142
155
  onPointerDown={(e) => e.stopPropagation()}
143
156
  onClick={(e) => {
@@ -228,8 +241,12 @@ function StackThreadCard({
228
241
  type="button"
229
242
  className="tlui-cmt-button tlui-cmt-stack-list__card-action"
230
243
  aria-label={msg(
231
- thread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'
232
- ).replace('{name}', props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR)}
244
+ thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
245
+ {
246
+ name:
247
+ props.resolveAuthor(thread.createdBy)?.name ?? msg(messages.commentsUnknownAuthor),
248
+ }
249
+ )}
233
250
  onClick={open}
234
251
  />
235
252
  <CommentCard {...toCardProps(first, props, options.components, resolveName)} />