@tldraw/commenting 5.3.0-internal.c5c7f1d817d0 → 5.3.0-next.2fa9c61a8de6

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 (268) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1040 -230
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-drafts.js +54 -0
  8. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  9. package/dist-cjs/canvas/comment-mutations.js +123 -0
  10. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  11. package/dist-cjs/canvas/comment-reactions.js +163 -0
  12. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  13. package/dist-cjs/canvas/comment-render.js +5 -3
  14. package/dist-cjs/canvas/comment-render.js.map +2 -2
  15. package/dist-cjs/canvas/comment-store.js +19 -9
  16. package/dist-cjs/canvas/comment-store.js.map +2 -2
  17. package/dist-cjs/canvas/comment-tool.js +64 -14
  18. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  19. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  20. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  21. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  22. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overlay.js +377 -437
  24. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  25. package/dist-cjs/canvas/comments-sidebar.js +34 -31
  26. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  27. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  28. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  29. package/dist-cjs/canvas/context.js +17 -0
  30. package/dist-cjs/canvas/context.js.map +7 -0
  31. package/dist-cjs/canvas/hooks.js +22 -3
  32. package/dist-cjs/canvas/hooks.js.map +2 -2
  33. package/dist-cjs/canvas/options.js +19 -2
  34. package/dist-cjs/canvas/options.js.map +2 -2
  35. package/dist-cjs/canvas/pin-stacking.js +56 -0
  36. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  37. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  38. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  39. package/dist-cjs/canvas/state.js +28 -14
  40. package/dist-cjs/canvas/state.js.map +2 -2
  41. package/dist-cjs/canvas/thread-preview.js +185 -0
  42. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  43. package/dist-cjs/canvas/thread-stack.js +199 -0
  44. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  45. package/dist-cjs/canvas/thread-state.js +67 -21
  46. package/dist-cjs/canvas/thread-state.js.map +2 -2
  47. package/dist-cjs/canvas/thread-view.js +397 -0
  48. package/dist-cjs/canvas/thread-view.js.map +7 -0
  49. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  50. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  51. package/dist-cjs/clustering/runtime.js +9 -9
  52. package/dist-cjs/clustering/runtime.js.map +2 -2
  53. package/dist-cjs/clustering/types.js.map +1 -1
  54. package/dist-cjs/index.d.ts +763 -353
  55. package/dist-cjs/index.js +46 -28
  56. package/dist-cjs/index.js.map +2 -2
  57. package/dist-cjs/ui/byline.js +3 -1
  58. package/dist-cjs/ui/byline.js.map +2 -2
  59. package/dist-cjs/ui/comment-card.js +12 -5
  60. package/dist-cjs/ui/comment-card.js.map +2 -2
  61. package/dist-cjs/ui/comment-composer.js +112 -17
  62. package/dist-cjs/ui/comment-composer.js.map +2 -2
  63. package/dist-cjs/ui/comment-pin.js +20 -2
  64. package/dist-cjs/ui/comment-pin.js.map +2 -2
  65. package/dist-cjs/ui/comment-thread.js +4 -2
  66. package/dist-cjs/ui/comment-thread.js.map +2 -2
  67. package/dist-cjs/ui/comments-list.js +22 -10
  68. package/dist-cjs/ui/comments-list.js.map +2 -2
  69. package/dist-cjs/ui/count-badge.js +3 -2
  70. package/dist-cjs/ui/count-badge.js.map +2 -2
  71. package/dist-cjs/ui/emoji-picker.js +54 -0
  72. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  73. package/dist-cjs/ui/format-time.js +4 -1
  74. package/dist-cjs/ui/format-time.js.map +2 -2
  75. package/dist-cjs/ui/reaction-picker.js +73 -0
  76. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  77. package/dist-cjs/ui/reaction.js +50 -4
  78. package/dist-cjs/ui/reaction.js.map +2 -2
  79. package/dist-cjs/ui/reactions.js +20 -15
  80. package/dist-cjs/ui/reactions.js.map +2 -2
  81. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  82. package/dist-cjs/ui/reply-count.js.map +7 -0
  83. package/dist-cjs/ui/send-button.js +20 -1
  84. package/dist-cjs/ui/send-button.js.map +2 -2
  85. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  86. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  87. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  89. package/dist-esm/canvas/cluster-input.mjs +2 -2
  90. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  91. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  92. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  93. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  94. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  95. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  96. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  97. package/dist-esm/canvas/comment-render.mjs +5 -3
  98. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  99. package/dist-esm/canvas/comment-store.mjs +19 -9
  100. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  101. package/dist-esm/canvas/comment-tool.mjs +67 -17
  102. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  103. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  104. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  105. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  106. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  107. package/dist-esm/canvas/comments-overlay.mjs +398 -442
  108. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  109. package/dist-esm/canvas/comments-sidebar.mjs +37 -34
  110. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  112. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  113. package/dist-esm/canvas/context.mjs +1 -0
  114. package/dist-esm/canvas/context.mjs.map +7 -0
  115. package/dist-esm/canvas/hooks.mjs +28 -5
  116. package/dist-esm/canvas/hooks.mjs.map +2 -2
  117. package/dist-esm/canvas/options.mjs +21 -3
  118. package/dist-esm/canvas/options.mjs.map +2 -2
  119. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  120. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  121. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  122. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  123. package/dist-esm/canvas/state.mjs +29 -19
  124. package/dist-esm/canvas/state.mjs.map +2 -2
  125. package/dist-esm/canvas/thread-preview.mjs +170 -0
  126. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  127. package/dist-esm/canvas/thread-stack.mjs +190 -0
  128. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  129. package/dist-esm/canvas/thread-state.mjs +71 -22
  130. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-view.mjs +402 -0
  132. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  133. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  134. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  135. package/dist-esm/clustering/runtime.mjs +9 -9
  136. package/dist-esm/clustering/runtime.mjs.map +2 -2
  137. package/dist-esm/index.d.mts +763 -353
  138. package/dist-esm/index.mjs +87 -41
  139. package/dist-esm/index.mjs.map +2 -2
  140. package/dist-esm/ui/byline.mjs +3 -1
  141. package/dist-esm/ui/byline.mjs.map +2 -2
  142. package/dist-esm/ui/comment-card.mjs +12 -5
  143. package/dist-esm/ui/comment-card.mjs.map +2 -2
  144. package/dist-esm/ui/comment-composer.mjs +122 -16
  145. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  146. package/dist-esm/ui/comment-pin.mjs +20 -2
  147. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  148. package/dist-esm/ui/comment-thread.mjs +4 -2
  149. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  150. package/dist-esm/ui/comments-list.mjs +39 -25
  151. package/dist-esm/ui/comments-list.mjs.map +2 -2
  152. package/dist-esm/ui/count-badge.mjs +3 -2
  153. package/dist-esm/ui/count-badge.mjs.map +2 -2
  154. package/dist-esm/ui/emoji-picker.mjs +34 -0
  155. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  156. package/dist-esm/ui/format-time.mjs +4 -1
  157. package/dist-esm/ui/format-time.mjs.map +2 -2
  158. package/dist-esm/ui/reaction-picker.mjs +60 -0
  159. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  160. package/dist-esm/ui/reaction.mjs +51 -5
  161. package/dist-esm/ui/reaction.mjs.map +2 -2
  162. package/dist-esm/ui/reactions.mjs +21 -16
  163. package/dist-esm/ui/reactions.mjs.map +2 -2
  164. package/dist-esm/ui/reply-count.mjs +9 -0
  165. package/dist-esm/ui/reply-count.mjs.map +7 -0
  166. package/dist-esm/ui/send-button.mjs +20 -1
  167. package/dist-esm/ui/send-button.mjs.map +2 -2
  168. package/dist-esm/ui/tooltip-button.mjs +12 -0
  169. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  170. package/package.json +6 -6
  171. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  172. package/src/canvas/anchor-lifecycle.ts +205 -0
  173. package/src/canvas/canvas.css +439 -24
  174. package/src/canvas/cluster-input.test.ts +31 -23
  175. package/src/canvas/cluster-input.ts +3 -4
  176. package/src/canvas/comment-drafts.test.ts +55 -0
  177. package/src/canvas/comment-drafts.ts +46 -0
  178. package/src/canvas/comment-mutations.test.ts +410 -0
  179. package/src/canvas/comment-mutations.ts +288 -0
  180. package/src/canvas/comment-reactions.test.ts +132 -0
  181. package/src/canvas/comment-reactions.tsx +245 -0
  182. package/src/canvas/comment-render.ts +9 -5
  183. package/src/canvas/comment-store.ts +67 -27
  184. package/src/canvas/comment-tool.test.ts +162 -0
  185. package/src/canvas/comment-tool.tsx +105 -23
  186. package/src/canvas/comments-filter-menu.tsx +23 -32
  187. package/src/canvas/comments-overflow-menu.tsx +12 -19
  188. package/src/canvas/comments-overlay.tsx +602 -562
  189. package/src/canvas/comments-sidebar.test.ts +45 -0
  190. package/src/canvas/comments-sidebar.tsx +75 -57
  191. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  192. package/src/canvas/context.ts +47 -0
  193. package/src/canvas/hooks.ts +52 -11
  194. package/src/canvas/options.test.ts +52 -2
  195. package/src/canvas/options.ts +133 -9
  196. package/src/canvas/pin-stacking.test.ts +114 -0
  197. package/src/canvas/pin-stacking.ts +57 -0
  198. package/src/canvas/sidebar-filters.ts +4 -3
  199. package/src/canvas/state.ts +92 -35
  200. package/src/canvas/thread-preview.test.ts +91 -0
  201. package/src/canvas/thread-preview.tsx +304 -0
  202. package/src/canvas/thread-stack.tsx +252 -0
  203. package/src/canvas/thread-state.test.ts +209 -0
  204. package/src/canvas/thread-state.ts +137 -36
  205. package/src/canvas/thread-view.tsx +516 -0
  206. package/src/clustering/computeClusterTable.test.ts +24 -23
  207. package/src/clustering/computeClusterTable.ts +5 -5
  208. package/src/clustering/runtime.test.ts +34 -0
  209. package/src/clustering/runtime.ts +20 -15
  210. package/src/clustering/types.ts +9 -9
  211. package/src/index.ts +76 -40
  212. package/src/ui/byline.tsx +4 -2
  213. package/src/ui/comment-card.tsx +17 -7
  214. package/src/ui/comment-composer.tsx +160 -13
  215. package/src/ui/comment-pin.tsx +22 -2
  216. package/src/ui/comment-thread.tsx +6 -1
  217. package/src/ui/comments-list.test.ts +31 -0
  218. package/src/ui/comments-list.tsx +32 -12
  219. package/src/ui/comments.css +489 -204
  220. package/src/ui/count-badge.tsx +9 -2
  221. package/src/ui/emoji-picker.test.ts +25 -0
  222. package/src/ui/emoji-picker.tsx +76 -0
  223. package/src/ui/format-time.ts +7 -2
  224. package/src/ui/reaction-picker.tsx +109 -0
  225. package/src/ui/reaction.tsx +116 -4
  226. package/src/ui/reactions.tsx +68 -13
  227. package/src/ui/reply-count.ts +16 -0
  228. package/src/ui/send-button.tsx +18 -3
  229. package/src/ui/tooltip-button.tsx +20 -0
  230. package/dist-cjs/canvas/region-options.js +0 -49
  231. package/dist-cjs/canvas/region-options.js.map +0 -7
  232. package/dist-cjs/ui/avatar.js +0 -51
  233. package/dist-cjs/ui/avatar.js.map +0 -7
  234. package/dist-cjs/ui/comment-mention.js +0 -42
  235. package/dist-cjs/ui/comment-mention.js.map +0 -7
  236. package/dist-cjs/ui/comment-text.js.map +0 -7
  237. package/dist-cjs/ui/mention-list.js +0 -70
  238. package/dist-cjs/ui/mention-list.js.map +0 -7
  239. package/dist-cjs/ui/mention-suggestion.js +0 -206
  240. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  241. package/dist-cjs/ui/mention.js.map +0 -7
  242. package/dist-cjs/ui/render-markdown.js +0 -63
  243. package/dist-cjs/ui/render-markdown.js.map +0 -7
  244. package/dist-esm/canvas/region-options.mjs +0 -29
  245. package/dist-esm/canvas/region-options.mjs.map +0 -7
  246. package/dist-esm/ui/avatar.mjs +0 -31
  247. package/dist-esm/ui/avatar.mjs.map +0 -7
  248. package/dist-esm/ui/comment-mention.mjs +0 -22
  249. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  250. package/dist-esm/ui/comment-text.mjs +0 -9
  251. package/dist-esm/ui/comment-text.mjs.map +0 -7
  252. package/dist-esm/ui/mention-list.mjs +0 -50
  253. package/dist-esm/ui/mention-list.mjs.map +0 -7
  254. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  255. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  256. package/dist-esm/ui/mention.mjs +0 -11
  257. package/dist-esm/ui/mention.mjs.map +0 -7
  258. package/dist-esm/ui/render-markdown.mjs +0 -45
  259. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  260. package/src/canvas/region-options.ts +0 -57
  261. package/src/ui/avatar.tsx +0 -38
  262. package/src/ui/comment-mention.ts +0 -47
  263. package/src/ui/comment-text.tsx +0 -12
  264. package/src/ui/mention-list.tsx +0 -106
  265. package/src/ui/mention-suggestion.test.ts +0 -18
  266. package/src/ui/mention-suggestion.tsx +0 -285
  267. package/src/ui/mention.tsx +0 -9
  268. package/src/ui/render-markdown.tsx +0 -72
@@ -1,6 +1,6 @@
1
1
  import type { ClusterNode, ClusterTable, MergeEvent } from './types'
2
2
 
3
- /** @public */
3
+ /** @internal */
4
4
  export interface ClusterRuntime {
5
5
  /** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */
6
6
  readonly k: number
@@ -42,7 +42,7 @@ export interface ClusterRuntime {
42
42
  getVisible(): ReadonlyMap<string, ClusterNode>
43
43
  }
44
44
 
45
- /** @public */
45
+ /** @internal */
46
46
  export function createClusterRuntime(table: ClusterTable): ClusterRuntime {
47
47
  return new ClusterRuntimeImpl(table)
48
48
  }
@@ -141,6 +141,24 @@ class ClusterRuntimeImpl implements ClusterRuntime {
141
141
  }
142
142
 
143
143
  let changed = false
144
+ // Heal suppressed events at their own merge threshold BEFORE the merge walk: a zoom-out
145
+ // past zMerge merges a held-out band event exactly as if it had still been ahead of the
146
+ // cursor. Healing must precede the walk because a single zoom jump can cross both a
147
+ // suppressed event and an event that consumes its result; applying the consumer first
148
+ // would leave the unapplied producer's children in `visible` (its delete is a no-op) and
149
+ // then re-add the producer on top, double-counting those leaves. Threshold monotonicity
150
+ // guarantees a suppressed producer's zMerge is >= its consumer's, so anything the walk
151
+ // needs has already healed. Set iteration is insertion order (ascending index), so a
152
+ // healed event's suppressed children (larger zMerge, smaller index) heal before it.
153
+ if (this.suppressed.size > 0) {
154
+ for (const i of [...this.suppressed]) {
155
+ if (zoom <= this.table.events[i].zMerge) {
156
+ this.suppressed.delete(i)
157
+ applyEvent(this.visible, this.table.events[i])
158
+ changed = true
159
+ }
160
+ }
161
+ }
144
162
  while (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {
145
163
  applyEvent(this.visible, this.table.events[this.k])
146
164
  this.k++
@@ -154,19 +172,6 @@ class ClusterRuntimeImpl implements ClusterRuntime {
154
172
  changed = true
155
173
  }
156
174
  }
157
- // Heal suppressed events at their own merge threshold: a zoom-out past zMerge merges a
158
- // held-out band event exactly as if it had still been ahead of the cursor. Set iteration
159
- // is insertion order (ascending index), so a healed event's suppressed children (larger
160
- // zMerge, smaller index) always heal before it.
161
- if (this.suppressed.size > 0) {
162
- for (const i of [...this.suppressed]) {
163
- if (zoom <= this.table.events[i].zMerge) {
164
- this.suppressed.delete(i)
165
- applyEvent(this.visible, this.table.events[i])
166
- changed = true
167
- }
168
- }
169
- }
170
175
  if (changed) this.version++
171
176
  }
172
177
 
@@ -2,7 +2,7 @@ import type { VecLike } from 'tldraw'
2
2
 
3
3
  /**
4
4
  * One comment thread's pin, already resolved to a page-space anchor point.
5
- * @public
5
+ * @internal
6
6
  */
7
7
  export interface LeafInput {
8
8
  /** Unique. Thread id. Uniqueness is a precondition — throw on duplicates. */
@@ -23,7 +23,7 @@ export interface MstEdge {
23
23
 
24
24
  /**
25
25
  * A cluster in the merge tree: a leaf (one thread) or a merged group.
26
- * @public
26
+ * @internal
27
27
  */
28
28
  export interface ClusterNode {
29
29
  /** Leaves: the thread id verbatim. Merged nodes: `cluster:${count}:${minMemberId}`. */
@@ -58,7 +58,7 @@ export interface ContractedEvent {
58
58
 
59
59
  /**
60
60
  * A finalized merge event, ready for the runtime.
61
- * @public
61
+ * @internal
62
62
  */
63
63
  export interface MergeEvent {
64
64
  zMerge: number
@@ -70,7 +70,7 @@ export interface MergeEvent {
70
70
 
71
71
  /**
72
72
  * The precomputed clustering schedule for one page's comments.
73
- * @public
73
+ * @internal
74
74
  */
75
75
  export interface ClusterTable {
76
76
  /** Sorted non-increasing by zMerge; satisfies the invariants of CLUSTERING.md §7.6. */
@@ -79,15 +79,15 @@ export interface ClusterTable {
79
79
  leaves: readonly ClusterNode[]
80
80
  }
81
81
 
82
- /** @public */
82
+ /** @internal */
83
83
  export interface ClusterOptions {
84
- /** Cluster (merge) distance, screen px. Default 40. */
84
+ /** Cluster (merge) distance, screen px. Default 22. */
85
85
  Tc?: number
86
- /** Uncluster (split) distance, screen px. Must be \> Tc. Default 1.5 · Tc. */
86
+ /** Uncluster (split) distance, screen px. Must be \> Tc. Default 1.2 · Tc. */
87
87
  Tu?: number
88
- /** Contraction window ratio. Default 0.12. */
88
+ /** Contraction window ratio. Default 0.7. */
89
89
  eps?: number
90
- /** Max cluster screen extent at birth, screen px. Must be \>= Tc. Default 3 · Tc. */
90
+ /** Max cluster screen extent at birth, screen px. Must be \>= Tc. Default 3.75 · Tc. */
91
91
  Dmax?: number
92
92
  /** Camera zoom bounds — pass the editor's camera constraints. Required. */
93
93
  minZoom: number
package/src/index.ts CHANGED
@@ -1,58 +1,93 @@
1
1
  import { registerTldrawLibraryVersion } from '@tldraw/utils'
2
2
 
3
3
  // Presentational commenting components. These are tldraw-independent and can be used to build
4
- // custom commenting UI.
5
- export { Avatar, type AvatarProps } from './ui/avatar'
4
+ // custom commenting UI. The mention picker, pill, and avatar are re-exported from @tldraw/mentions
5
+ // so the commenting public API is unchanged.
6
+ export {
7
+ Avatar,
8
+ type AvatarProps,
9
+ type CommentAuthor,
10
+ createMentionSuggestion,
11
+ filterMentionMembers,
12
+ Mention,
13
+ MentionList,
14
+ type MentionListProps,
15
+ type MentionMember,
16
+ type MentionProps,
17
+ type MentionSuggestionOptions,
18
+ } from '@tldraw/mentions'
6
19
  export { Byline, type BylineProps } from './ui/byline'
7
20
  export { CommentCard, type CommentCardProps } from './ui/comment-card'
8
21
  export { CommentComposer, type CommentComposerProps } from './ui/comment-composer'
9
22
  export { CountBadge, type CountBadgeProps } from './ui/count-badge'
10
23
  export { CommentPin, type CommentPinProps } from './ui/comment-pin'
11
- export { CommentText, type CommentTextProps } from './ui/comment-text'
12
24
  export { CommentThread, type CommentThreadProps } from './ui/comment-thread'
13
- export { CommentsList, type CommentListItemProps, type CommentsListProps } from './ui/comments-list'
25
+ export {
26
+ CommentsList,
27
+ isOpenInNewTabClick,
28
+ type CommentListItemProps,
29
+ type CommentsListProps,
30
+ } from './ui/comments-list'
14
31
  export { EmptyState, type EmptyStateProps } from './ui/empty-state'
15
32
  export { formatRelativeTime } from './ui/format-time'
16
- export { Mention, type MentionProps } from './ui/mention'
17
- export { MentionList, type MentionListProps, type MentionMember } from './ui/mention-list'
18
33
  export {
19
- createMentionSuggestion,
20
- filterMentionMembers,
21
- type MentionSuggestionOptions,
22
- } from './ui/mention-suggestion'
23
- export { Reaction, type ReactionProps } from './ui/reaction'
24
- export { Reactions } from './ui/reactions'
25
- export { renderMarkdown } from './ui/render-markdown'
34
+ DEFAULT_REACTION_EMOJI,
35
+ EmojiPicker,
36
+ type EmojiPickerProps,
37
+ isAllowedReactionEmoji,
38
+ } from './ui/emoji-picker'
39
+ export {
40
+ Reaction,
41
+ type ReactionProps,
42
+ type RenderReaction,
43
+ defaultRenderReaction,
44
+ DefaultReactionTooltip,
45
+ DefaultReactionTooltipContent,
46
+ type ReactionTooltipProps,
47
+ } from './ui/reaction'
48
+ export { ReactionPicker, type ReactionPickerProps } from './ui/reaction-picker'
49
+ export {
50
+ Reactions,
51
+ type ReactionReactor,
52
+ type ReactionsProps,
53
+ type ReactionSummary,
54
+ } from './ui/reactions'
26
55
  export { SendButton, type SendButtonProps } from './ui/send-button'
27
56
 
28
57
  // The tldraw-coupled commenting layer: the comment tool, reactive hooks over the comment
29
58
  // records, a rich-text body renderer, and a batteries-included <CanvasComments> overlay. Pairs
30
59
  // with the presentational components above.
60
+ export { registerCommentAnchorLifecycle } from './canvas/anchor-lifecycle'
31
61
  export { CommentBody, type CommentBodyProps } from './canvas/comment-body'
32
62
  export {
33
- CommentTool,
34
- commentToolOverrides,
35
- commentTools,
36
- type PendingComment,
37
- } from './canvas/comment-tool'
38
- export { collectClusterLeaves } from './canvas/cluster-input'
39
- export { computeClusterTable } from './clustering/computeClusterTable'
63
+ deleteComment,
64
+ deleteThread,
65
+ editComment,
66
+ putCommentRecords,
67
+ removeCommentRecords,
68
+ reopenThread,
69
+ resolveThread,
70
+ } from './canvas/comment-mutations'
71
+ export {
72
+ CommentReactionPicker,
73
+ type CommentReactionPickerProps,
74
+ CommentReactions,
75
+ type CommentReactionsProps,
76
+ summarizeReactions,
77
+ toggleCommentReaction,
78
+ useCommentReactions,
79
+ } from './canvas/comment-reactions'
80
+ export { CommentTool, commentToolOverrides, commentTools } from './canvas/comment-tool'
40
81
  export {
82
+ getCommentReactions,
41
83
  getCommentRecord,
42
84
  getComments,
43
85
  getCommentThreads,
44
- putCommentRecords,
45
- removeCommentRecords,
86
+ getLiveComments,
87
+ getLiveCommentThreads,
46
88
  type TLCommentRecord,
47
89
  } from './canvas/comment-store'
48
- export { createClusterRuntime, type ClusterRuntime } from './clustering/runtime'
49
- export type {
50
- ClusterNode,
51
- ClusterOptions,
52
- ClusterTable,
53
- LeafInput,
54
- MergeEvent,
55
- } from './clustering/types'
90
+ export { type CommentingContext } from './canvas/context'
56
91
  export { CommentsFilterMenu, type CommentsFilterMenuProps } from './canvas/comments-filter-menu'
57
92
  export { CommentsMenuItem } from './canvas/comments-menu-item'
58
93
  export { CanvasComments, type CanvasCommentsProps } from './canvas/comments-overlay'
@@ -60,34 +95,35 @@ export {
60
95
  type CommentingComponents,
61
96
  type CommentingOptions,
62
97
  defaultCommentingOptions,
98
+ getCanComment,
63
99
  getCommentingOptions,
100
+ type ShapeCommentPrecisionContext,
101
+ useCanComment,
64
102
  useCommentingOptions,
65
103
  } from './canvas/options'
66
104
  export { CommentsOverflowMenu } from './canvas/comments-overflow-menu'
105
+ export { CommentsVisibilityToggle } from './canvas/comments-visibility-toggle'
67
106
  export { CanvasCommentsSidebar, type CanvasCommentsSidebarProps } from './canvas/comments-sidebar'
68
107
  export { useComments, useCommentThreads, useThreadComments } from './canvas/hooks'
69
108
  export { useCommentingEnabled } from './canvas/license'
70
- export { DEFAULT_REGION_COMMENT_OPTIONS, type RegionCommentOptions } from './canvas/region-options'
71
109
  export { richTextToPlaintext } from './canvas/rich-text'
72
110
  export { DEFAULT_SIDEBAR_FILTERS, type SidebarFilters } from './canvas/sidebar-filters'
73
111
  export {
74
112
  commentsHidden,
75
- commitCommentMutation,
113
+ commentsSidebarOpen,
114
+ getRevealThreadPending,
76
115
  openThreadId,
77
- pendingComment,
116
+ revealThread,
78
117
  sidebarFilters,
79
118
  toggleCommentsHidden,
119
+ toggleCommentsSidebar,
80
120
  useCommentsHidden,
121
+ useCommentsSidebarOpen,
81
122
  useOpenThreadId,
82
- usePendingComment,
123
+ useRevealThreadPending,
83
124
  useSidebarFilters,
84
125
  } from './canvas/state'
85
- export {
86
- anchorPagePoint,
87
- DEFAULT_IMPRECISE_SHAPE_ANCHOR,
88
- focusThread,
89
- shapeAnchorAt,
90
- } from './canvas/thread-state'
126
+ export { anchorPagePoint, focusThread, shapeAnchorAt } from './canvas/thread-state'
91
127
 
92
128
  registerTldrawLibraryVersion(
93
129
  (globalThis as any).TLDRAW_LIBRARY_NAME,
package/src/ui/byline.tsx CHANGED
@@ -1,8 +1,9 @@
1
+ import { Avatar, type CommentAuthor } from '@tldraw/mentions'
1
2
  import { formatRelativeTime } from './format-time'
2
3
 
3
4
  /** @public */
4
5
  export interface BylineProps {
5
- author: string
6
+ author: CommentAuthor
6
7
  /** ISO datetime; formatted to relative time by the component. */
7
8
  date: string
8
9
  /** Shows an "edited" marker when the comment has been edited. */
@@ -13,7 +14,8 @@ export interface BylineProps {
13
14
  export function Byline({ author, date, edited }: BylineProps) {
14
15
  return (
15
16
  <div className="tlui-cmt-head">
16
- <span className="tlui-cmt-author">{author}</span>
17
+ <Avatar author={author} />
18
+ <span className="tlui-cmt-author">{author.name}</span>
17
19
  <span className="tlui-cmt-time">
18
20
  {formatRelativeTime(date)}
19
21
  {edited && <span className="tlui-cmt-edited"> · edited</span>}
@@ -1,12 +1,12 @@
1
+ import { type CommentAuthor } from '@tldraw/mentions'
1
2
  import { ReactNode } from 'react'
2
- import { Avatar } from './avatar'
3
3
  import { Byline } from './byline'
4
4
 
5
5
  /** @public */
6
6
  export interface CommentCardProps {
7
- author: string
8
- /** The rendered comment body. The card doesn't dictate a format — pass a `<CommentText>`
9
- * for markdown, a rich-text render, or any node. */
7
+ author: CommentAuthor
8
+ /** The rendered comment body. The card doesn't dictate a format — pass a `<CommentBody>`
9
+ * for a comment's rich text, or any node of your own. */
10
10
  body: ReactNode
11
11
  /** ISO datetime; formatted to relative time by the component. */
12
12
  date: string
@@ -15,16 +15,26 @@ export interface CommentCardProps {
15
15
  edited?: boolean
16
16
  /** Hover-revealed controls at the card's top-right (e.g. an edit affordance). */
17
17
  actions?: ReactNode
18
+ /** Content under the body, aligned with it rather than the avatar (e.g. a `<Reactions>` row). */
19
+ footer?: ReactNode
18
20
  }
19
21
 
20
22
  /** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
21
- export function CommentCard({ author, body, date, you, edited, actions }: CommentCardProps) {
23
+ export function CommentCard({
24
+ author,
25
+ body,
26
+ date,
27
+ you,
28
+ edited,
29
+ actions,
30
+ footer,
31
+ }: CommentCardProps) {
22
32
  return (
23
33
  <div className={you ? 'tlui-cmt-card tlui-cmt-card--you' : 'tlui-cmt-card'}>
24
- <Avatar name={author} />
34
+ <Byline author={author} date={date} edited={edited} />
25
35
  <div className="tlui-cmt-body">
26
- <Byline author={author} date={date} edited={edited} />
27
36
  {body}
37
+ {footer}
28
38
  </div>
29
39
  {actions !== undefined && <div className="tlui-cmt-card__actions">{actions}</div>}
30
40
  </div>
@@ -1,17 +1,29 @@
1
1
  import { JSONContent } from '@tiptap/core'
2
2
  import { EditorContent, useEditor } from '@tiptap/react'
3
- import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
3
+ import {
4
+ Avatar,
5
+ type CommentAuthor,
6
+ createMentionExtension,
7
+ createMentionSuggestion,
8
+ isMentionPickerOpen,
9
+ MentionMember,
10
+ } from '@tldraw/mentions'
11
+ import {
12
+ type MouseEvent as ReactMouseEvent,
13
+ ReactNode,
14
+ useEffect,
15
+ useLayoutEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'react'
4
20
  import { isEqual, TLRichText, useMaybeEditor } from 'tldraw'
5
- import { Avatar } from './avatar'
6
21
  import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'
7
- import { commentMention } from './comment-mention'
8
- import { MentionMember } from './mention-list'
9
- import { createMentionSuggestion, isMentionPickerOpen } from './mention-suggestion'
10
22
  import { SendButton } from './send-button'
11
23
 
12
24
  /** @public */
13
25
  export interface CommentComposerProps {
14
- author: string
26
+ author: CommentAuthor
15
27
  placeholder: string
16
28
  /** Controlled rich-text value. Omit for the presentational (display-only) composer. */
17
29
  value?: TLRichText
@@ -19,6 +31,9 @@ export interface CommentComposerProps {
19
31
  /** Called on Send click or Enter. When set, the composer is interactive. */
20
32
  onSubmit?(): void
21
33
  sendLabel?: string
34
+ /** Called when Up is pressed in an empty composer — e.g. to start editing the comment above,
35
+ * the way chat apps edit your last message. With content in the field, Up moves the cursor. */
36
+ onArrowUpWhenEmpty?(): void
22
37
  disabled?: boolean
23
38
  autoFocus?: boolean
24
39
  /** The leading element before the field. Defaults to the author's avatar. */
@@ -43,6 +58,7 @@ export function CommentComposer({
43
58
  onChange,
44
59
  onSubmit,
45
60
  sendLabel = 'Send',
61
+ onArrowUpWhenEmpty,
46
62
  disabled,
47
63
  autoFocus,
48
64
  leading,
@@ -60,6 +76,8 @@ export function CommentComposer({
60
76
  onChangeRef.current = onChange
61
77
  const onSubmitRef = useRef(onSubmit)
62
78
  onSubmitRef.current = onSubmit
79
+ const onArrowUpWhenEmptyRef = useRef(onArrowUpWhenEmpty)
80
+ onArrowUpWhenEmptyRef.current = onArrowUpWhenEmpty
63
81
  const disabledRef = useRef(disabled)
64
82
  disabledRef.current = disabled
65
83
  const getMentionSuggestionsRef = useRef(getMentionSuggestions)
@@ -68,6 +86,60 @@ export function CommentComposer({
68
86
  renderMentionSuggestionRef.current = renderMentionSuggestion
69
87
 
70
88
  const [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))
89
+ // Whether the field has grown to two rows: input across the full width, send button below.
90
+ // Flips when the (single-line) content width reaches the space left beside the send button.
91
+ const [expanded, setExpanded] = useState(false)
92
+ const expandedRef = useRef(expanded)
93
+ expandedRef.current = expanded
94
+ const inputWrapRef = useRef<HTMLDivElement>(null)
95
+ const mirrorRef = useRef<HTMLDivElement>(null)
96
+
97
+ // Measure whether the content still fits on one line beside the send button. The mirror is a
98
+ // hidden, nowrap clone of the editor's rendered content, so marks and mention chips measure at
99
+ // their true width. A small dead zone between the expand and collapse thresholds keeps the
100
+ // layout from flapping while typing at the boundary.
101
+ const remeasure = () => {
102
+ const wrap = inputWrapRef.current
103
+ const mirror = mirrorRef.current
104
+ const editor = editorRef.current
105
+ if (!wrap || !mirror || !editor) return
106
+ // Empty always fits — and measuring before TipTap's DOM has laid out reads zero widths,
107
+ // which would flash the expanded layout for a frame on mount.
108
+ if (editor.isEmpty) {
109
+ if (expandedRef.current) setExpanded(false)
110
+ return
111
+ }
112
+ if (wrap.clientWidth === 0) return
113
+ const doc = editor.state.doc
114
+ const multiBlock =
115
+ doc.childCount > 1 || (doc.firstChild !== null && doc.firstChild.type.name !== 'paragraph')
116
+ if (multiBlock) {
117
+ if (!expandedRef.current) setExpanded(true)
118
+ return
119
+ }
120
+ const field = wrap.parentElement
121
+ const send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null
122
+ const input = wrap.querySelector('.tlui-cmt-input')
123
+ if (!send || !input) return
124
+ mirror.innerHTML = input.innerHTML
125
+ const textWidth = mirror.offsetWidth
126
+ // The single-line space the input has beside the send button: the wrap's *content* box
127
+ // (clientWidth minus its own horizontal padding — the text wraps there, not at clientWidth),
128
+ // less the button plus the field's 6px gap when already expanded (the wrap then spans the
129
+ // full field). Without subtracting the padding, a padded wrap wraps to a second line ~padding
130
+ // px before expansion fires, so the input grows a line and then snaps to the expanded layout.
131
+ const wrapStyle = getComputedStyle(wrap)
132
+ const wrapPadX = parseFloat(wrapStyle.paddingLeft) + parseFloat(wrapStyle.paddingRight)
133
+ const collapsedAvailable =
134
+ wrap.clientWidth - wrapPadX - (expandedRef.current ? send.offsetWidth + 6 : 0)
135
+ if (!expandedRef.current && textWidth > collapsedAvailable - 8) {
136
+ setExpanded(true)
137
+ } else if (expandedRef.current && textWidth < collapsedAvailable - 24) {
138
+ setExpanded(false)
139
+ }
140
+ }
141
+ const remeasureRef = useRef(remeasure)
142
+ remeasureRef.current = remeasure
71
143
 
72
144
  // The editor instance, reachable from `handleKeyDown` (which is created before `useEditor`
73
145
  // returns). Updated on every render so the ref never points at a stale editor.
@@ -103,7 +175,7 @@ export function CommentComposer({
103
175
  ? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)
104
176
  : undefined
105
177
  list.push(
106
- commentMention({
178
+ createMentionExtension({
107
179
  suggestion: createMentionSuggestion(resolveSuggestions, {
108
180
  renderMember: renderRow,
109
181
  editor: tlEditor,
@@ -111,7 +183,7 @@ export function CommentComposer({
111
183
  })
112
184
  )
113
185
  } else {
114
- list.push(commentMention({ suggestion: { char: '@', allow: () => false } }))
186
+ list.push(createMentionExtension({ suggestion: { char: '@', allow: () => false } }))
115
187
  }
116
188
  return list
117
189
  }, [mentionsEnabled, hasCustomRow, tlEditor])
@@ -127,12 +199,30 @@ export function CommentComposer({
127
199
  enableCoreExtensions: { textDirection: false },
128
200
  textDirection: 'auto',
129
201
  editorProps: {
130
- attributes: { class: 'tlui-cmt-input' },
202
+ attributes: {
203
+ class: 'tlui-cmt-input',
204
+ 'aria-label': placeholder,
205
+ role: 'textbox',
206
+ 'aria-multiline': 'true',
207
+ },
131
208
  // Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter — an
132
209
  // `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.
133
210
  handleKeyDown: (_view, event) => {
134
211
  // Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.
135
212
  if (replayingEnter.current) return false
213
+ // Up in an empty composer hands off to the host (edit the comment above). With
214
+ // content, Up stays cursor movement; with the mention picker open, it navigates it.
215
+ if (event.key === 'ArrowUp' && !event.isComposing) {
216
+ if (
217
+ onArrowUpWhenEmptyRef.current &&
218
+ editorRef.current?.isEmpty &&
219
+ !isMentionPickerOpen()
220
+ ) {
221
+ onArrowUpWhenEmptyRef.current()
222
+ return true
223
+ }
224
+ return false
225
+ }
136
226
  if (event.key !== 'Enter' || event.isComposing) return false
137
227
  // While the @-mention picker is open, Enter selects the highlighted member — defer.
138
228
  if (isMentionPickerOpen()) return false
@@ -140,6 +230,9 @@ export function CommentComposer({
140
230
  // we don't re-enter and submit) to reuse the editor's list-aware Enter handling — a new
141
231
  // list item in a list, a new paragraph otherwise. tldraw doesn't do soft breaks.
142
232
  if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
233
+ // An empty field has nothing to break onto — swallow the keypress so it doesn't
234
+ // open the comment with a stray leading blank line.
235
+ if (editorRef.current?.isEmpty) return true
143
236
  replayingEnter.current = true
144
237
  try {
145
238
  editorRef.current?.commands.enter()
@@ -155,10 +248,13 @@ export function CommentComposer({
155
248
  },
156
249
  onUpdate: ({ editor }) => {
157
250
  setIsEmpty(editor.isEmpty)
251
+ // Same-value state sets don't re-render, so measure here — the editor's DOM is
252
+ // already updated when onUpdate fires.
253
+ remeasureRef.current()
158
254
  onChangeRef.current?.(editor.getJSON() as TLRichText)
159
255
  },
160
256
  },
161
- [interactive]
257
+ [interactive, placeholder]
162
258
  )
163
259
  editorRef.current = editor
164
260
 
@@ -171,6 +267,12 @@ export function CommentComposer({
171
267
  setIsEmpty(editor.isEmpty)
172
268
  }, [editor, value])
173
269
 
270
+ // Measure once the editor's content is in the DOM, so a composer that mounts pre-filled (the
271
+ // edit-in-place composer) starts in the right layout.
272
+ useLayoutEffect(() => {
273
+ remeasureRef.current()
274
+ }, [editor, value])
275
+
174
276
  // Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a
175
277
  // canvas pointer event whose default focus handling would otherwise steal it back.
176
278
  useEffect(() => {
@@ -179,17 +281,62 @@ export function CommentComposer({
179
281
  return () => cancelAnimationFrame(raf)
180
282
  }, [autoFocus, editor])
181
283
 
284
+ // tldraw's pass-through-wheel hook (on the floating composer's wrapper) forwards a wheel to the
285
+ // canvas unless ITS element is scrollable — but the scrollable element is this input, not that
286
+ // wrapper, so an overflowing input never scrolled: the wheel panned the canvas instead. Catch the
287
+ // wheel on the wrap first (a native listener here fires before the wrapper's, deeper in the tree)
288
+ // and stop it while the input can still scroll that way; at the scroll boundary let it through so
289
+ // the canvas still zooms.
290
+ useEffect(() => {
291
+ const wrap = inputWrapRef.current
292
+ if (!wrap) return
293
+ const onWheel = (e: WheelEvent) => {
294
+ const input = wrap.querySelector<HTMLElement>('.tlui-cmt-input')
295
+ if (!input || input.scrollHeight <= input.clientHeight) return
296
+ const atTop = input.scrollTop <= 0
297
+ const atBottom = input.scrollTop >= input.scrollHeight - input.clientHeight - 1
298
+ if ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)) {
299
+ e.stopPropagation()
300
+ }
301
+ }
302
+ wrap.addEventListener('wheel', onWheel)
303
+ return () => wrap.removeEventListener('wheel', onWheel)
304
+ }, [])
305
+
306
+ // The whole field behaves like the text input: clicking its empty area (the padding, or the
307
+ // space beside/below a short line) focuses the editor rather than only the text glyphs being
308
+ // clickable. The input (caret placement) and the send button keep their own click handling.
309
+ const focusEditorFromField = (e: ReactMouseEvent<HTMLDivElement>) => {
310
+ const target = e.target as HTMLElement
311
+ if (target.closest('.tlui-cmt-input') || target.closest('.tlui-cmt-send')) return
312
+ e.preventDefault()
313
+ editor?.commands.focus('end')
314
+ }
315
+
182
316
  return (
183
317
  <div className="tlui-cmt-composer">
184
- {leading ?? <Avatar name={author} />}
185
- <div className="tlui-cmt-composer__field">
186
- <div className="tlui-cmt-composer__input-wrap">
318
+ {leading ?? <Avatar author={author} />}
319
+ <div
320
+ className={[
321
+ 'tlui-cmt-composer__field',
322
+ interactive && expanded && 'tlui-cmt-composer__field--expanded',
323
+ ]
324
+ .filter(Boolean)
325
+ .join(' ')}
326
+ onMouseDown={interactive ? focusEditorFromField : undefined}
327
+ >
328
+ <div className="tlui-cmt-composer__input-wrap" ref={inputWrapRef}>
187
329
  <EditorContent editor={editor} />
188
330
  {isEmpty && (
189
331
  <div className="tlui-cmt-input__placeholder" aria-hidden="true">
190
332
  {placeholder}
191
333
  </div>
192
334
  )}
335
+ <div
336
+ className="tlui-cmt-composer__mirror tlui-cmt-input"
337
+ ref={mirrorRef}
338
+ aria-hidden="true"
339
+ />
193
340
  </div>
194
341
  {interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}
195
342
  </div>
@@ -10,16 +10,36 @@ export interface CommentPinProps {
10
10
  open?: boolean
11
11
  }
12
12
 
13
+ /* An inline check, not a text glyph — the '✓' character sits off-baseline and varies by font. */
14
+ const resolvedCheck = (
15
+ <svg
16
+ viewBox="0 0 24 24"
17
+ width="15"
18
+ height="15"
19
+ fill="none"
20
+ stroke="currentColor"
21
+ strokeWidth="2.5"
22
+ strokeLinecap="round"
23
+ strokeLinejoin="round"
24
+ aria-hidden="true"
25
+ >
26
+ <path d="M4 12.5l5 5L20 6.5" />
27
+ </svg>
28
+ )
29
+
13
30
  /** A canvas comment marker: shows its `children` (or a check when resolved). Purely
14
31
  * presentational — it reflects open/resolved state via CSS; wrap it to make it clickable.
15
32
  * @public @react */
16
33
  export function CommentPin({ children, resolved, open }: CommentPinProps) {
17
34
  const className = [
35
+ // `tlui-cmt-marker` carries the resting shadow and the hover lift — the same treatment a
36
+ // count badge wears, so the two markers behave alike.
37
+ 'tlui-cmt-marker',
18
38
  'tlui-cmt-pin',
19
39
  resolved && 'tlui-cmt-pin--resolved',
20
- open && 'tlui-cmt-pin--open',
40
+ open && 'tlui-cmt-marker--open',
21
41
  ]
22
42
  .filter(Boolean)
23
43
  .join(' ')
24
- return <div className={className}>{resolved ? '✓' : children}</div>
44
+ return <div className={className}>{resolved ? resolvedCheck : children}</div>
25
45
  }