@tldraw/commenting 5.5.0-next.f9308d324e1c → 5.6.0-canary.5bfc37740468

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 (95) hide show
  1. package/commenting.css +1 -38
  2. package/dist-cjs/canvas/anchor-lifecycle.js +4 -5
  3. package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
  4. package/dist-cjs/canvas/cluster-fade.js +1 -1
  5. package/dist-cjs/canvas/cluster-fade.js.map +2 -2
  6. package/dist-cjs/canvas/cluster-model.js +23 -31
  7. package/dist-cjs/canvas/cluster-model.js.map +2 -2
  8. package/dist-cjs/canvas/comment-render.js +19 -19
  9. package/dist-cjs/canvas/comment-render.js.map +2 -2
  10. package/dist-cjs/canvas/comment-store.js +10 -5
  11. package/dist-cjs/canvas/comment-store.js.map +2 -2
  12. package/dist-cjs/canvas/comments-overlay.js +12 -16
  13. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  14. package/dist-cjs/canvas/comments-sidebar.js +1 -7
  15. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  16. package/dist-cjs/canvas/hooks.js.map +1 -1
  17. package/dist-cjs/canvas/thread-pin.js +14 -26
  18. package/dist-cjs/canvas/thread-pin.js.map +2 -2
  19. package/dist-cjs/canvas/thread-stack.js +2 -6
  20. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  21. package/dist-cjs/canvas/thread-state.js +13 -0
  22. package/dist-cjs/canvas/thread-state.js.map +2 -2
  23. package/dist-cjs/canvas/thread-view.js +67 -78
  24. package/dist-cjs/canvas/thread-view.js.map +2 -2
  25. package/dist-cjs/clustering/computeClusterTable.js +2 -9
  26. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  27. package/dist-cjs/clustering/replay.js +10 -23
  28. package/dist-cjs/clustering/replay.js.map +2 -2
  29. package/dist-cjs/clustering/runtime.js +9 -15
  30. package/dist-cjs/clustering/runtime.js.map +2 -2
  31. package/dist-cjs/index.js +1 -1
  32. package/dist-cjs/ui/comment-composer.js +2 -4
  33. package/dist-cjs/ui/comment-composer.js.map +2 -2
  34. package/dist-cjs/ui/format-time.js +20 -8
  35. package/dist-cjs/ui/format-time.js.map +2 -2
  36. package/dist-cjs/ui/reaction.js +5 -15
  37. package/dist-cjs/ui/reaction.js.map +2 -2
  38. package/dist-esm/canvas/anchor-lifecycle.mjs +5 -6
  39. package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
  40. package/dist-esm/canvas/cluster-fade.mjs +1 -1
  41. package/dist-esm/canvas/cluster-fade.mjs.map +2 -2
  42. package/dist-esm/canvas/cluster-model.mjs +24 -32
  43. package/dist-esm/canvas/cluster-model.mjs.map +2 -2
  44. package/dist-esm/canvas/comment-render.mjs +19 -19
  45. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  46. package/dist-esm/canvas/comment-store.mjs +10 -5
  47. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  48. package/dist-esm/canvas/comments-overlay.mjs +13 -17
  49. package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
  50. package/dist-esm/canvas/comments-sidebar.mjs +3 -14
  51. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  52. package/dist-esm/canvas/hooks.mjs.map +1 -1
  53. package/dist-esm/canvas/thread-pin.mjs +16 -26
  54. package/dist-esm/canvas/thread-pin.mjs.map +2 -2
  55. package/dist-esm/canvas/thread-stack.mjs +3 -7
  56. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  57. package/dist-esm/canvas/thread-state.mjs +13 -0
  58. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  59. package/dist-esm/canvas/thread-view.mjs +67 -78
  60. package/dist-esm/canvas/thread-view.mjs.map +2 -2
  61. package/dist-esm/clustering/computeClusterTable.mjs +2 -9
  62. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  63. package/dist-esm/clustering/replay.mjs +10 -23
  64. package/dist-esm/clustering/replay.mjs.map +2 -2
  65. package/dist-esm/clustering/runtime.mjs +9 -15
  66. package/dist-esm/clustering/runtime.mjs.map +2 -2
  67. package/dist-esm/index.mjs +1 -1
  68. package/dist-esm/ui/comment-composer.mjs +2 -4
  69. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  70. package/dist-esm/ui/format-time.mjs +20 -8
  71. package/dist-esm/ui/format-time.mjs.map +2 -2
  72. package/dist-esm/ui/reaction.mjs +7 -15
  73. package/dist-esm/ui/reaction.mjs.map +2 -2
  74. package/package.json +16 -20
  75. package/src/canvas/anchor-lifecycle.ts +5 -6
  76. package/src/canvas/cluster-fade.ts +6 -6
  77. package/src/canvas/cluster-model.test.ts +64 -1
  78. package/src/canvas/cluster-model.ts +28 -33
  79. package/src/canvas/comment-render.ts +30 -28
  80. package/src/canvas/comment-store.ts +16 -6
  81. package/src/canvas/comments-overlay.tsx +18 -19
  82. package/src/canvas/comments-sidebar.tsx +3 -18
  83. package/src/canvas/hooks.ts +1 -1
  84. package/src/canvas/thread-pin.tsx +21 -31
  85. package/src/canvas/thread-stack.tsx +3 -7
  86. package/src/canvas/thread-state.test.ts +35 -0
  87. package/src/canvas/thread-state.ts +27 -0
  88. package/src/canvas/thread-view.tsx +93 -95
  89. package/src/clustering/computeClusterTable.ts +3 -20
  90. package/src/clustering/replay.ts +10 -23
  91. package/src/clustering/runtime.ts +9 -15
  92. package/src/ui/comment-composer.tsx +2 -4
  93. package/src/ui/comments.css +1 -34
  94. package/src/ui/format-time.ts +24 -11
  95. package/src/ui/reaction.tsx +11 -23
@@ -114,8 +114,7 @@ export function CommentComposer({
114
114
  if (!expandedRef.current) setExpanded(true)
115
115
  return
116
116
  }
117
- const field = wrap.parentElement
118
- const send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null
117
+ const send = wrap.parentElement?.querySelector<HTMLElement>('.tlui-cmt-send')
119
118
  const input = wrap.querySelector('.tlui-cmt-input')
120
119
  if (!send || !input) return
121
120
  mirror.innerHTML = input.innerHTML
@@ -263,8 +262,7 @@ export function CommentComposer({
263
262
  // space beside/below a short line) focuses the editor rather than only the text glyphs being
264
263
  // clickable. The input (caret placement) and the send button keep their own click handling.
265
264
  const focusEditorFromField = (e: ReactMouseEvent<HTMLDivElement>) => {
266
- const target = e.target as HTMLElement
267
- if (target.closest('.tlui-cmt-input') || target.closest('.tlui-cmt-send')) return
265
+ if ((e.target as HTMLElement).closest('.tlui-cmt-input, .tlui-cmt-send')) return
268
266
  e.preventDefault()
269
267
  editor?.commands.focus('end')
270
268
  }
@@ -136,8 +136,7 @@
136
136
  text-decoration: underline;
137
137
  }
138
138
 
139
- /* Avatar and mention styles live in @tldraw/mentions (src/mention.css) and are bundled into
140
- commenting.css by copy-css-files.mjs. */
139
+ /* Avatar and mention styles live in @tldraw/mentions and are imported by commenting.css. */
141
140
 
142
141
  /* composer — an avatar beside a rounded-rect field holding the rich-text input and the send button.
143
142
  Formatting is applied via markdown and keyboard shortcuts (no toolbar). The avatar aligns to the
@@ -383,35 +382,6 @@
383
382
  user-select: none;
384
383
  }
385
384
 
386
- /* thread */
387
- .tlui-cmt-thread {
388
- display: flex;
389
- flex-direction: column;
390
- width: 300px;
391
- }
392
-
393
- .tlui-cmt-thread__head {
394
- display: flex;
395
- align-items: center;
396
- gap: 8px;
397
- font-size: 12px;
398
- font-weight: 600;
399
- color: var(--tl-color-text-3);
400
- text-transform: uppercase;
401
- letter-spacing: 0.04em;
402
- }
403
-
404
- .tlui-cmt-badge {
405
- font-size: 10px;
406
- font-weight: 600;
407
- text-transform: none;
408
- letter-spacing: 0;
409
- color: #fff;
410
- background: var(--tl-color-muted-1);
411
- padding: 1px 6px;
412
- border-radius: 999px;
413
- }
414
-
415
385
  /* reactions */
416
386
  .tlui-cmt-reactions {
417
387
  display: flex;
@@ -648,9 +618,6 @@ button.tlui-cmt-reaction--active:hover {
648
618
  .tlui-button.tlui-cmt-thread__action:hover {
649
619
  color: var(--tl-color-text-1);
650
620
  }
651
- .tlui-button.tlui-cmt-thread__action--danger:hover {
652
- color: var(--tl-color-danger);
653
- }
654
621
  .tlui-cmt-thread__resolved {
655
622
  margin: 4px 12px;
656
623
  padding: 6px 10px;
@@ -8,6 +8,20 @@ const DIVISIONS = [
8
8
  { amount: Number.POSITIVE_INFINITY, unit: 'year' },
9
9
  ] as const
10
10
 
11
+ // Both formatters are cached per locale — comment cards re-render on every reply keystroke, and
12
+ // constructing an Intl formatter is far more expensive than formatting with one.
13
+ const relativeTimeFormatters = new Map<string, Intl.RelativeTimeFormat>()
14
+ const fullDateTimeFormatters = new Map<string, Intl.DateTimeFormat>()
15
+
16
+ function getCached<T>(cache: Map<string, T>, locale: string, create: () => T): T {
17
+ let formatter = cache.get(locale)
18
+ if (!formatter) {
19
+ formatter = create()
20
+ cache.set(locale, formatter)
21
+ }
22
+ return formatter
23
+ }
24
+
11
25
  /**
12
26
  * Format an ISO datetime as compact relative time ("2h ago", "yesterday", "last wk.").
13
27
  * Locale-aware via Intl.RelativeTimeFormat.
@@ -17,7 +31,11 @@ export function formatRelativeTime(iso: string, locale = 'en'): string {
17
31
  const then = new Date(iso).getTime()
18
32
  if (Number.isNaN(then)) return ''
19
33
 
20
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' })
34
+ const rtf = getCached(
35
+ relativeTimeFormatters,
36
+ locale,
37
+ () => new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' })
38
+ )
21
39
  let duration = (then - Date.now()) / 1000
22
40
  // Under a minute is just "now" — with second granularity the label visibly ticks while
23
41
  // typing a reply (every keystroke re-renders the card).
@@ -33,10 +51,6 @@ export function formatRelativeTime(iso: string, locale = 'en'): string {
33
51
  return ''
34
52
  }
35
53
 
36
- // Cached per locale — comment cards re-render on every reply keystroke, and constructing a
37
- // DateTimeFormat is far more expensive than formatting with one.
38
- const fullDateTimeFormatters = new Map<string, Intl.DateTimeFormat>()
39
-
40
54
  /**
41
55
  * Format an ISO datetime as a full date and time ("Tuesday, July 22, 2025 at 4:44 PM").
42
56
  * Locale-aware via Intl.DateTimeFormat.
@@ -45,10 +59,9 @@ const fullDateTimeFormatters = new Map<string, Intl.DateTimeFormat>()
45
59
  export function formatFullDateTime(iso: string, locale = 'en'): string {
46
60
  const date = new Date(iso)
47
61
  if (Number.isNaN(date.getTime())) return ''
48
- let formatter = fullDateTimeFormatters.get(locale)
49
- if (!formatter) {
50
- formatter = new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeStyle: 'short' })
51
- fullDateTimeFormatters.set(locale, formatter)
52
- }
53
- return formatter.format(date)
62
+ return getCached(
63
+ fullDateTimeFormatters,
64
+ locale,
65
+ () => new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeStyle: 'short' })
66
+ ).format(date)
54
67
  }
@@ -108,28 +108,16 @@ export function DefaultReactionTooltipContent({ reactors }: { reactors: Reaction
108
108
  const names = reactors.map((reactor) =>
109
109
  reactor.you ? msg('comments.mention-you') : reactor.name
110
110
  )
111
+ if (names.length === 0) return null
111
112
  const [a, b, c] = names
112
- switch (names.length) {
113
- case 0:
114
- return null
115
- case 1:
116
- return <>{msg('comments.reacted-1').replace('{a}', a)}</>
117
- case 2:
118
- return <>{msg('comments.reacted-2').replace('{a}', a).replace('{b}', b)}</>
119
- case 3:
120
- return <>{msg('comments.reacted-3').replace('{a}', a).replace('{b}', b).replace('{c}', c)}</>
121
- default: {
122
- const others = names.length - 3
123
- const template = others === 1 ? 'comments.reacted-more-one' : 'comments.reacted-more'
124
- return (
125
- <>
126
- {msg(template)
127
- .replace('{a}', a)
128
- .replace('{b}', b)
129
- .replace('{c}', c)
130
- .replace('{count}', String(others))}
131
- </>
132
- )
133
- }
134
- }
113
+ const others = names.length - 3
114
+ const key =
115
+ names.length <= 3
116
+ ? `comments.reacted-${names.length}`
117
+ : others === 1
118
+ ? 'comments.reacted-more-one'
119
+ : 'comments.reacted-more'
120
+ // Single pass, so a substituted name containing a placeholder can't be re-substituted.
121
+ const vars: Record<string, string | undefined> = { a, b, c, count: String(others) }
122
+ return <>{msg(key).replace(/\{(a|b|c|count)\}/g, (_, k) => vars[k] ?? '')}</>
135
123
  }