@tldraw/commenting 5.6.0-canary.ff4411b6d84d → 5.6.0-next.01b6e97e0728

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 (104) 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 +8 -2
  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 +6 -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 +12 -6
  30. package/dist-cjs/index.js +1 -1
  31. package/dist-cjs/ui/comment-composer.js +5 -0
  32. package/dist-cjs/ui/comment-composer.js.map +2 -2
  33. package/dist-cjs/ui/gesture-focus.js +37 -0
  34. package/dist-cjs/ui/gesture-focus.js.map +7 -0
  35. package/dist-cjs/ui/reaction-picker.js +5 -2
  36. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  37. package/dist-cjs/ui/reaction.js +15 -5
  38. package/dist-cjs/ui/reaction.js.map +2 -2
  39. package/dist-cjs/ui/reply-count.js +8 -2
  40. package/dist-cjs/ui/reply-count.js.map +2 -2
  41. package/dist-esm/canvas/cluster-badge.mjs +8 -1
  42. package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
  43. package/dist-esm/canvas/comment-reactions.mjs +12 -4
  44. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  45. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  46. package/dist-esm/canvas/comment-tool.mjs +10 -3
  47. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  48. package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
  49. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  50. package/dist-esm/canvas/comments-menu-item.mjs +5 -2
  51. package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
  52. package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
  53. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  54. package/dist-esm/canvas/comments-sidebar.mjs +21 -6
  55. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  56. package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
  57. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  58. package/dist-esm/canvas/options.mjs.map +1 -1
  59. package/dist-esm/canvas/pending-composer.mjs +7 -2
  60. package/dist-esm/canvas/pending-composer.mjs.map +2 -2
  61. package/dist-esm/canvas/thread-pin.mjs +15 -3
  62. package/dist-esm/canvas/thread-pin.mjs.map +2 -2
  63. package/dist-esm/canvas/thread-preview.mjs +5 -1
  64. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  65. package/dist-esm/canvas/thread-stack.mjs +19 -4
  66. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  67. package/dist-esm/canvas/thread-view.mjs +42 -21
  68. package/dist-esm/canvas/thread-view.mjs.map +2 -2
  69. package/dist-esm/index.d.mts +12 -6
  70. package/dist-esm/index.mjs +1 -1
  71. package/dist-esm/ui/comment-composer.mjs +5 -0
  72. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  73. package/dist-esm/ui/gesture-focus.mjs +17 -0
  74. package/dist-esm/ui/gesture-focus.mjs.map +7 -0
  75. package/dist-esm/ui/reaction-picker.mjs +6 -2
  76. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  77. package/dist-esm/ui/reaction.mjs +16 -8
  78. package/dist-esm/ui/reaction.mjs.map +2 -2
  79. package/dist-esm/ui/reply-count.mjs +8 -2
  80. package/dist-esm/ui/reply-count.mjs.map +2 -2
  81. package/package.json +5 -5
  82. package/src/canvas/cluster-badge.tsx +9 -1
  83. package/src/canvas/comment-reactions.tsx +18 -7
  84. package/src/canvas/comment-render.ts +6 -4
  85. package/src/canvas/comment-tool.test.ts +45 -1
  86. package/src/canvas/comment-tool.tsx +14 -3
  87. package/src/canvas/comments-filter-menu.tsx +15 -6
  88. package/src/canvas/comments-menu-item.tsx +6 -2
  89. package/src/canvas/comments-overflow-menu.tsx +11 -4
  90. package/src/canvas/comments-sidebar.tsx +23 -6
  91. package/src/canvas/comments-visibility-toggle.tsx +7 -2
  92. package/src/canvas/options.ts +1 -1
  93. package/src/canvas/pending-composer.tsx +8 -2
  94. package/src/canvas/thread-pin.tsx +17 -4
  95. package/src/canvas/thread-preview.tsx +6 -1
  96. package/src/canvas/thread-stack.tsx +21 -4
  97. package/src/canvas/thread-view.tsx +44 -22
  98. package/src/ui/comment-composer.tsx +9 -0
  99. package/src/ui/gesture-focus.test.ts +45 -0
  100. package/src/ui/gesture-focus.ts +29 -0
  101. package/src/ui/reaction-picker.tsx +7 -2
  102. package/src/ui/reaction.test.tsx +22 -0
  103. package/src/ui/reaction.tsx +25 -14
  104. package/src/ui/reply-count.ts +9 -3
@@ -1,5 +1,6 @@
1
1
  import { memo, useMemo, useRef } from 'react'
2
2
  import {
3
+ defineMessages,
3
4
  Editor,
4
5
  TLCommentThread,
5
6
  useContainer,
@@ -14,6 +15,13 @@ import { type CommentingContext } from './context'
14
15
  import { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'
15
16
  import { impreciseShapePinInset, isInInflatedViewport } from './thread-state'
16
17
 
18
+ const messages = defineMessages({
19
+ commentsClusterLabel: {
20
+ id: 'comments.cluster-label',
21
+ defaultMessage: '{count, plural, one {# comment here} other {# comments here}}',
22
+ },
23
+ })
24
+
17
25
  /**
18
26
  * The count badge standing in for several threads folded together at the current zoom. Hovering it
19
27
  * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.
@@ -99,7 +107,7 @@ export const ClusterBadge = memo(function ClusterBadge({
99
107
  type="button"
100
108
  className="tlui-cmt-button tlui-cmt-canvas-cluster"
101
109
  style={{ left: point.x, top: point.y }}
102
- aria-label={msg('comments.cluster-label').replace('{count}', String(node.count))}
110
+ aria-label={msg(messages.commentsClusterLabel, { count: node.count })}
103
111
  onPointerDown={(e) => {
104
112
  if (isCanvasPanGesture(editor, e)) {
105
113
  forwardPointerEventToCanvas(container, e)
@@ -1,11 +1,13 @@
1
1
  import { useMemo } from 'react'
2
2
  import {
3
+ createCommentReaction,
4
+ createCommentReactionId,
5
+ defineMessages,
3
6
  Editor,
4
7
  TLComment,
5
8
  TLCommentReaction,
6
- createCommentReaction,
7
- createCommentReactionId,
8
9
  useEditor,
10
+ useTranslation,
9
11
  useValue,
10
12
  } from 'tldraw'
11
13
  import { RenderReaction } from '../ui/reaction'
@@ -16,6 +18,10 @@ import { UNKNOWN_AUTHOR } from './comment-render'
16
18
  import { getCommentReactions } from './comment-store'
17
19
  import { getCommentingOptions, useCommentingOptions } from './options'
18
20
 
21
+ const messages = defineMessages({
22
+ commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
23
+ })
24
+
19
25
  /**
20
26
  * One comment's reactions, oldest first, reactively.
21
27
  *
@@ -52,15 +58,18 @@ export interface ReactionSummaryInput {
52
58
  * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so
53
59
  * the row stays stable as later reactions arrive. `active` marks the current user's emoji and
54
60
  * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't
55
- * name falls back to a generic "Someone", never the raw user id.
61
+ * name falls back to `unknownAuthorName`, never the raw user id. Pass a translated name for that:
62
+ * this is a plain function, so it can't reach the translations itself.
56
63
  *
57
64
  * @public
58
65
  */
59
66
  export function summarizeReactions(
60
67
  reactions: readonly ReactionSummaryInput[],
61
68
  currentUserId?: string | null,
62
- resolveName?: (userId: string) => string | undefined
69
+ resolveName?: (userId: string) => string | undefined,
70
+ opts?: { unknownAuthorName?: string }
63
71
  ): ReactionSummary[] {
72
+ const unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR
64
73
  const groups = new Map<
65
74
  string,
66
75
  { count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }
@@ -68,7 +77,7 @@ export function summarizeReactions(
68
77
  for (const reaction of reactions) {
69
78
  const mine = currentUserId != null && reaction.userId === currentUserId
70
79
  // Fall back to a generic name, never the raw user id, when the id can't be resolved.
71
- const reactor = { name: resolveName?.(reaction.userId) ?? UNKNOWN_AUTHOR, you: mine }
80
+ const reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine }
72
81
  const group = groups.get(reaction.emoji)
73
82
  if (group) {
74
83
  group.count++
@@ -177,9 +186,11 @@ export function CommentReactions({ comment, currentUserId, resolveName }: Commen
177
186
  const renderReaction = useReactionRenderer()
178
187
  const { components } = useCommentingOptions()
179
188
  const reactions = useCommentReactions(editor, comment.id)
189
+ const msg = useTranslation()
190
+ const unknownAuthorName = msg(messages.commentsUnknownAuthor)
180
191
  const summaries = useMemo(
181
- () => summarizeReactions(reactions, currentUserId, resolveName),
182
- [reactions, currentUserId, resolveName]
192
+ () => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),
193
+ [reactions, currentUserId, resolveName, unknownAuthorName]
183
194
  )
184
195
  // Suppress the hover list while any menu is open (the reaction picker, an overflow menu…) so it
185
196
  // doesn't compete with the menu the user is actually working in. Edit mode already hides the
@@ -4,10 +4,12 @@ import { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'
4
4
  import { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'
5
5
 
6
6
  /**
7
- * The author name shown in a byline when no source can name an id (e.g. a deleted account, or a
8
- * member with no comment and no live presence). The toolkit's one generic default, applied where a
9
- * byline needs a name; hosts pre-empt it by resolving the id from `resolveAuthor`. Not a
10
- * translation key — a single English literal.
7
+ * The author name used when no source can name an id (e.g. a deleted account, or a member with no
8
+ * comment and no live presence). Hosts pre-empt it by resolving the id from `resolveAuthor`.
9
+ *
10
+ * The English fallback, for the places that can't translate: it's a module constant, so the UI
11
+ * reads `comments.unknown-author` instead and only lands here when there are no translations at
12
+ * all. Pass a translated name to {@link summarizeReactions} for the same reason.
11
13
  */
12
14
  export const UNKNOWN_AUTHOR = 'Someone'
13
15
 
@@ -10,9 +10,11 @@ import {
10
10
  Editor,
11
11
  TLShapeId,
12
12
  TLStateNodeConstructor,
13
+ TLUiOverrideHelpers,
14
+ TLUiToolsContextType,
13
15
  } from 'tldraw'
14
16
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
15
- import { CommentTool, startCommentAt } from './comment-tool'
17
+ import { CommentTool, commentToolOverrides, startCommentAt } from './comment-tool'
16
18
  import { commentsSidebarOpen, pendingComment, regionDraft } from './state'
17
19
 
18
20
  /**
@@ -197,6 +199,37 @@ describe('startCommentAt', () => {
197
199
  })
198
200
  })
199
201
 
202
+ describe('commentToolOverrides', () => {
203
+ let driver: Driver
204
+
205
+ function getCommentTool() {
206
+ const tools = commentToolOverrides.tools!(
207
+ editor,
208
+ {} as TLUiToolsContextType,
209
+ {} as TLUiOverrideHelpers
210
+ )
211
+ return tools.comment
212
+ }
213
+
214
+ beforeEach(() => {
215
+ driver = makeEditor()
216
+ })
217
+
218
+ it('opens the composer where the context menu was opened, not at the live pointer', () => {
219
+ driver.pointerDown(400, 300).pointerUp(400, 300)
220
+ driver.pointerMove(600, 500)
221
+ getCommentTool().onSelect('context-menu')
222
+ expect(editor.isIn('comment.idle')).toBe(true)
223
+ expect(pendingComment.get(editor)?.point).toEqual({ x: 400, y: 300 })
224
+ })
225
+
226
+ it('only enters the tool from other sources', () => {
227
+ getCommentTool().onSelect('quick-actions')
228
+ expect(editor.isIn('comment.idle')).toBe(true)
229
+ expect(pendingComment.get(editor)).toBeNull()
230
+ })
231
+ })
232
+
200
233
  describe('comment tool placement hints with regions enabled', () => {
201
234
  it('clears the hint when a drag becomes a region', () => {
202
235
  const driver = makeEditor(CommentTool.configure({ enableRegions: true }))
@@ -297,3 +330,14 @@ describe('comment tool anchoring to frames', () => {
297
330
  expect(editor.getHintingShapeIds()).toEqual([])
298
331
  })
299
332
  })
333
+
334
+ describe('commentToolOverrides', () => {
335
+ // The label is data the toolbar puts through `msg()`, so free text here renders as itself in
336
+ // every locale and the extractor never sees it — which is how this one shipped as 'Comment'
337
+ // and stayed English everywhere. It has to be a declared id.
338
+ it('labels the tool with a translatable id', () => {
339
+ makeEditor()
340
+ const tools = commentToolOverrides.tools!(editor, {}, {} as never)
341
+ expect(tools.comment.label).toBe('tool.comment')
342
+ })
343
+ })
@@ -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'
@@ -308,17 +309,27 @@ class CommentDragging extends StateNode {
308
309
  /** @public */
309
310
  export const commentTools = [CommentTool]
310
311
 
312
+ const messages = defineMessages({
313
+ toolComment: { id: 'tool.comment', defaultMessage: 'Comment' },
314
+ })
315
+
311
316
  /** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.
312
- * 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.
313
320
  * @public */
314
321
  export const commentToolOverrides: TLUiOverrides = {
315
322
  tools(editor, tools) {
316
323
  tools.comment = {
317
324
  id: 'comment',
318
325
  icon: 'comment',
319
- label: 'Comment',
326
+ label: messages.toolComment.id,
320
327
  kbd: 'c',
321
- 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
+ },
322
333
  }
323
334
  return tools
324
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
  /**
@@ -140,8 +146,8 @@ export function PendingComposer({
140
146
  {canComment ? (
141
147
  <CommentComposer
142
148
  author={me ?? UNKNOWN_COMMENT_AUTHOR}
143
- placeholder={msg('comments.add-placeholder')}
144
- sendLabel={msg('comments.send')}
149
+ placeholder={msg(messages.commentsAddPlaceholder)}
150
+ sendLabel={msg(messages.commentsSend)}
145
151
  value={text}
146
152
  onChange={(value) => {
147
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)} />