@tldraw/commenting 5.4.2 → 5.5.0-canary.0241f11f2e99

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 +0 -32
  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 +5 -5
  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 +0 -32
  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
  }
@@ -383,35 +383,6 @@
383
383
  user-select: none;
384
384
  }
385
385
 
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
386
  /* reactions */
416
387
  .tlui-cmt-reactions {
417
388
  display: flex;
@@ -648,9 +619,6 @@ button.tlui-cmt-reaction--active:hover {
648
619
  .tlui-button.tlui-cmt-thread__action:hover {
649
620
  color: var(--tl-color-text-1);
650
621
  }
651
- .tlui-button.tlui-cmt-thread__action--danger:hover {
652
- color: var(--tl-color-danger);
653
- }
654
622
  .tlui-cmt-thread__resolved {
655
623
  margin: 4px 12px;
656
624
  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
  }