@multicomma/arkcn 0.1.2 → 0.1.3

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 (148) hide show
  1. package/package.json +1 -1
  2. package/registry/index.json +1 -1
  3. package/registry/items/accordion.json +302 -0
  4. package/registry/items/activity-feed.json +408 -0
  5. package/registry/items/alert-dialog.json +676 -0
  6. package/registry/items/alert.json +113 -0
  7. package/registry/items/angle-slider.json +378 -0
  8. package/registry/items/app-shell.json +2654 -1
  9. package/registry/items/aspect-ratio.json +36 -0
  10. package/registry/items/attachment.json +299 -1
  11. package/registry/items/avatar.json +213 -0
  12. package/registry/items/badge.json +44 -0
  13. package/registry/items/breadcrumb.json +191 -0
  14. package/registry/items/bubble.json +146 -0
  15. package/registry/items/button-group.json +110 -0
  16. package/registry/items/button.json +52 -0
  17. package/registry/items/calendar.json +499 -1
  18. package/registry/items/canvas.json +446 -1
  19. package/registry/items/card.json +192 -0
  20. package/registry/items/carousel.json +208 -1
  21. package/registry/items/cascader.json +797 -1
  22. package/registry/items/chart.json +1764 -1
  23. package/registry/items/chat.json +993 -0
  24. package/registry/items/checkbox.json +503 -0
  25. package/registry/items/clipboard.json +308 -0
  26. package/registry/items/collapsible.json +226 -0
  27. package/registry/items/color-picker.json +1261 -0
  28. package/registry/items/combobox.json +1123 -1
  29. package/registry/items/command.json +837 -1
  30. package/registry/items/comment-thread.json +952 -1
  31. package/registry/items/context-menu.json +1344 -1
  32. package/registry/items/data-grid.json +449 -1
  33. package/registry/items/data-table.json +1310 -1
  34. package/registry/items/date-input.json +476 -0
  35. package/registry/items/dialog.json +592 -0
  36. package/registry/items/direction.json +29 -1
  37. package/registry/items/drawer.json +797 -0
  38. package/registry/items/dropdown-menu.json +1311 -1
  39. package/registry/items/editable.json +503 -0
  40. package/registry/items/empty.json +166 -0
  41. package/registry/items/field.json +286 -1
  42. package/registry/items/file-upload.json +638 -0
  43. package/registry/items/floating-panel.json +720 -0
  44. package/registry/items/floating-toolbar.json +43 -0
  45. package/registry/items/format.json +109 -0
  46. package/registry/items/gantt.json +773 -1
  47. package/registry/items/hooks/use-mobile.json +1 -0
  48. package/registry/items/hotkeys.json +99 -1
  49. package/registry/items/hover-card.json +357 -0
  50. package/registry/items/image-cropper.json +431 -0
  51. package/registry/items/input-group.json +189 -0
  52. package/registry/items/input-otp.json +181 -0
  53. package/registry/items/input.json +28 -0
  54. package/registry/items/item.json +308 -0
  55. package/registry/items/json-tree-view.json +336 -0
  56. package/registry/items/kanban.json +624 -1
  57. package/registry/items/kbd.json +54 -0
  58. package/registry/items/label.json +28 -0
  59. package/registry/items/lib/controllable.json +1 -0
  60. package/registry/items/lib/data-table-adapter.json +1 -0
  61. package/registry/items/lib/drag-preview.json +1 -0
  62. package/registry/items/lib/form.json +1 -0
  63. package/registry/items/lib/history.json +1 -0
  64. package/registry/items/lib/row-layout.json +1 -0
  65. package/registry/items/lib/table.json +1 -0
  66. package/registry/items/lib/time.json +1 -0
  67. package/registry/items/lib/utils.json +1 -0
  68. package/registry/items/live-region.json +43 -0
  69. package/registry/items/marker.json +96 -0
  70. package/registry/items/marquee.json +300 -0
  71. package/registry/items/menubar.json +1282 -1
  72. package/registry/items/message.json +166 -0
  73. package/registry/items/navigation-menu.json +561 -1
  74. package/registry/items/node-graph.json +1037 -1
  75. package/registry/items/number-input.json +519 -0
  76. package/registry/items/pagination.json +244 -1
  77. package/registry/items/password-input.json +311 -0
  78. package/registry/items/popconfirm.json +566 -0
  79. package/registry/items/popover.json +615 -0
  80. package/registry/items/progress.json +516 -0
  81. package/registry/items/qr-code.json +249 -0
  82. package/registry/items/query-builder.json +1368 -1
  83. package/registry/items/radio-group.json +362 -0
  84. package/registry/items/rating-group.json +304 -0
  85. package/registry/items/resizable.json +242 -1
  86. package/registry/items/rich-text-editor.json +934 -1
  87. package/registry/items/scheduler.json +1013 -1
  88. package/registry/items/scroll-area.json +227 -0
  89. package/registry/items/segment-group.json +363 -0
  90. package/registry/items/select.json +956 -0
  91. package/registry/items/separator.json +45 -1
  92. package/registry/items/sheet.json +592 -0
  93. package/registry/items/sidebar.json +785 -1
  94. package/registry/items/signature-pad.json +330 -0
  95. package/registry/items/skeleton.json +28 -0
  96. package/registry/items/slider.json +799 -0
  97. package/registry/items/sonner.json +135 -0
  98. package/registry/items/spinner.json +28 -0
  99. package/registry/items/steps.json +483 -0
  100. package/registry/items/swap.json +107 -0
  101. package/registry/items/switch.json +362 -0
  102. package/registry/items/table.json +210 -0
  103. package/registry/items/tabs.json +326 -0
  104. package/registry/items/tags-input.json +640 -0
  105. package/registry/items/textarea.json +28 -0
  106. package/registry/items/timer.json +289 -0
  107. package/registry/items/toc.json +372 -0
  108. package/registry/items/toggle-group.json +214 -1
  109. package/registry/items/toggle.json +124 -0
  110. package/registry/items/tooltip.json +417 -0
  111. package/registry/items/tour.json +498 -0
  112. package/registry/items/transfer-list.json +609 -1
  113. package/registry/items/tree-select.json +742 -1
  114. package/registry/items/tree-view.json +794 -0
  115. package/registry/items/virtual-list.json +214 -1
  116. package/src/components/ui/app-shell.tsx +11 -0
  117. package/src/components/ui/attachment.tsx +1 -0
  118. package/src/components/ui/calendar.tsx +1 -0
  119. package/src/components/ui/canvas.tsx +8 -0
  120. package/src/components/ui/carousel.tsx +3 -0
  121. package/src/components/ui/cascader.tsx +1 -0
  122. package/src/components/ui/chart.tsx +10 -0
  123. package/src/components/ui/combobox.tsx +3 -0
  124. package/src/components/ui/command.tsx +1 -0
  125. package/src/components/ui/comment-thread.tsx +11 -0
  126. package/src/components/ui/context-menu.tsx +6 -0
  127. package/src/components/ui/data-grid.tsx +8 -0
  128. package/src/components/ui/data-table.tsx +6 -0
  129. package/src/components/ui/direction.tsx +2 -0
  130. package/src/components/ui/dropdown-menu.tsx +6 -0
  131. package/src/components/ui/field.tsx +1 -0
  132. package/src/components/ui/gantt.tsx +15 -0
  133. package/src/components/ui/hotkeys.tsx +3 -0
  134. package/src/components/ui/kanban.tsx +3 -0
  135. package/src/components/ui/menubar.tsx +6 -0
  136. package/src/components/ui/navigation-menu.tsx +1 -0
  137. package/src/components/ui/node-graph.tsx +23 -0
  138. package/src/components/ui/pagination.tsx +1 -0
  139. package/src/components/ui/query-builder.tsx +7 -0
  140. package/src/components/ui/resizable.tsx +1 -0
  141. package/src/components/ui/rich-text-editor.tsx +54 -0
  142. package/src/components/ui/scheduler.tsx +26 -0
  143. package/src/components/ui/separator.tsx +1 -0
  144. package/src/components/ui/sidebar.tsx +5 -0
  145. package/src/components/ui/toggle-group.tsx +3 -0
  146. package/src/components/ui/transfer-list.tsx +11 -0
  147. package/src/components/ui/tree-select.tsx +5 -0
  148. package/src/components/ui/virtual-list.tsx +7 -0
@@ -34,6 +34,957 @@
34
34
  "extractMentions",
35
35
  "renderMentions"
36
36
  ],
37
+ "parts": [
38
+ {
39
+ "name": "CommentThread",
40
+ "props": [
41
+ {
42
+ "name": "currentUserId",
43
+ "type": "string",
44
+ "required": false,
45
+ "description": "Id of the signed-in person; their comments get `data-own` and mentions of them `data-self`.",
46
+ "from": "arkcn"
47
+ },
48
+ {
49
+ "name": "onReplyToChange",
50
+ "type": "((id: string | null) => void)",
51
+ "required": false,
52
+ "description": "Called when the reply target changes.",
53
+ "from": "arkcn"
54
+ },
55
+ {
56
+ "name": "people",
57
+ "type": "Person[]",
58
+ "required": false,
59
+ "default": "[]",
60
+ "description": "People who can be mentioned and whose names and initials are rendered.",
61
+ "from": "arkcn"
62
+ },
63
+ {
64
+ "name": "replyTo",
65
+ "type": "string | null",
66
+ "required": false,
67
+ "description": "Id of the comment being replied to (controlled).",
68
+ "from": "arkcn"
69
+ },
70
+ {
71
+ "name": "children",
72
+ "type": "ReactNode",
73
+ "required": false,
74
+ "description": "Content rendered inside the part.",
75
+ "from": "@types/react"
76
+ },
77
+ {
78
+ "name": "className",
79
+ "type": "string",
80
+ "required": false,
81
+ "description": "Extra classes merged into the part's own classes with `cn`.",
82
+ "from": "@types/react"
83
+ },
84
+ {
85
+ "name": "style",
86
+ "type": "CSSProperties",
87
+ "required": false,
88
+ "description": "Inline styles applied to the rendered element.",
89
+ "from": "@types/react"
90
+ }
91
+ ]
92
+ },
93
+ {
94
+ "name": "CommentList",
95
+ "props": [
96
+ {
97
+ "name": "children",
98
+ "type": "ReactNode",
99
+ "required": false,
100
+ "description": "Content rendered inside the part.",
101
+ "from": "@types/react"
102
+ },
103
+ {
104
+ "name": "className",
105
+ "type": "string",
106
+ "required": false,
107
+ "description": "Extra classes merged into the part's own classes with `cn`.",
108
+ "from": "@types/react"
109
+ },
110
+ {
111
+ "name": "style",
112
+ "type": "CSSProperties",
113
+ "required": false,
114
+ "description": "Inline styles applied to the rendered element.",
115
+ "from": "@types/react"
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "name": "Comment",
121
+ "props": [
122
+ {
123
+ "name": "authorId",
124
+ "type": "string",
125
+ "required": false,
126
+ "description": "Id of the person who wrote the comment.",
127
+ "from": "arkcn"
128
+ },
129
+ {
130
+ "name": "children",
131
+ "type": "ReactNode",
132
+ "required": false,
133
+ "description": "Content rendered inside the part.",
134
+ "from": "@types/react"
135
+ },
136
+ {
137
+ "name": "className",
138
+ "type": "string",
139
+ "required": false,
140
+ "description": "Extra classes merged into the part's own classes with `cn`.",
141
+ "from": "@types/react"
142
+ },
143
+ {
144
+ "name": "style",
145
+ "type": "CSSProperties",
146
+ "required": false,
147
+ "description": "Inline styles applied to the rendered element.",
148
+ "from": "@types/react"
149
+ }
150
+ ]
151
+ },
152
+ {
153
+ "name": "CommentAvatar",
154
+ "props": [
155
+ {
156
+ "name": "size",
157
+ "type": "\"default\" | \"sm\" | \"lg\"",
158
+ "required": false,
159
+ "description": "Size variant; sets a `data-size` attribute.",
160
+ "from": "arkcn"
161
+ },
162
+ {
163
+ "name": "asChild",
164
+ "type": "boolean",
165
+ "required": false,
166
+ "description": "Use the provided child element as the default rendered element, combining their props and behavior.",
167
+ "from": "@ark-ui/react"
168
+ },
169
+ {
170
+ "name": "children",
171
+ "type": "ReactNode",
172
+ "required": false,
173
+ "description": "Content rendered inside the part.",
174
+ "from": "@types/react"
175
+ },
176
+ {
177
+ "name": "className",
178
+ "type": "string",
179
+ "required": false,
180
+ "description": "Extra classes merged into the part's own classes with `cn`.",
181
+ "from": "@types/react"
182
+ },
183
+ {
184
+ "name": "ids",
185
+ "type": "ElementIds",
186
+ "required": false,
187
+ "description": "The ids of the elements in the avatar. Useful for composition.",
188
+ "from": "@zag-js/avatar"
189
+ },
190
+ {
191
+ "name": "onStatusChange",
192
+ "type": "((details: StatusChangeDetails) => void)",
193
+ "required": false,
194
+ "description": "Functional called when the image loading status changes.",
195
+ "from": "@zag-js/avatar"
196
+ },
197
+ {
198
+ "name": "style",
199
+ "type": "CSSProperties",
200
+ "required": false,
201
+ "description": "Inline styles applied to the rendered element.",
202
+ "from": "@types/react"
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "name": "CommentContent",
208
+ "props": [
209
+ {
210
+ "name": "children",
211
+ "type": "ReactNode",
212
+ "required": false,
213
+ "description": "Content rendered inside the part.",
214
+ "from": "@types/react"
215
+ },
216
+ {
217
+ "name": "className",
218
+ "type": "string",
219
+ "required": false,
220
+ "description": "Extra classes merged into the part's own classes with `cn`.",
221
+ "from": "@types/react"
222
+ },
223
+ {
224
+ "name": "style",
225
+ "type": "CSSProperties",
226
+ "required": false,
227
+ "description": "Inline styles applied to the rendered element.",
228
+ "from": "@types/react"
229
+ }
230
+ ]
231
+ },
232
+ {
233
+ "name": "CommentHeader",
234
+ "props": [
235
+ {
236
+ "name": "children",
237
+ "type": "ReactNode",
238
+ "required": false,
239
+ "description": "Content rendered inside the part.",
240
+ "from": "@types/react"
241
+ },
242
+ {
243
+ "name": "className",
244
+ "type": "string",
245
+ "required": false,
246
+ "description": "Extra classes merged into the part's own classes with `cn`.",
247
+ "from": "@types/react"
248
+ },
249
+ {
250
+ "name": "style",
251
+ "type": "CSSProperties",
252
+ "required": false,
253
+ "description": "Inline styles applied to the rendered element.",
254
+ "from": "@types/react"
255
+ }
256
+ ]
257
+ },
258
+ {
259
+ "name": "CommentAuthor",
260
+ "props": [
261
+ {
262
+ "name": "children",
263
+ "type": "ReactNode",
264
+ "required": false,
265
+ "description": "Content rendered inside the part.",
266
+ "from": "@types/react"
267
+ },
268
+ {
269
+ "name": "className",
270
+ "type": "string",
271
+ "required": false,
272
+ "description": "Extra classes merged into the part's own classes with `cn`.",
273
+ "from": "@types/react"
274
+ },
275
+ {
276
+ "name": "style",
277
+ "type": "CSSProperties",
278
+ "required": false,
279
+ "description": "Inline styles applied to the rendered element.",
280
+ "from": "@types/react"
281
+ }
282
+ ]
283
+ },
284
+ {
285
+ "name": "CommentTime",
286
+ "props": [
287
+ {
288
+ "name": "date",
289
+ "type": "Date",
290
+ "required": true,
291
+ "description": "The date this part represents.",
292
+ "from": "arkcn"
293
+ },
294
+ {
295
+ "name": "now",
296
+ "type": "Date",
297
+ "required": false,
298
+ "description": "Reference time for relative labels.",
299
+ "from": "arkcn"
300
+ },
301
+ {
302
+ "name": "children",
303
+ "type": "ReactNode",
304
+ "required": false,
305
+ "description": "Content rendered inside the part.",
306
+ "from": "@types/react"
307
+ },
308
+ {
309
+ "name": "className",
310
+ "type": "string",
311
+ "required": false,
312
+ "description": "Extra classes merged into the part's own classes with `cn`.",
313
+ "from": "@types/react"
314
+ },
315
+ {
316
+ "name": "style",
317
+ "type": "CSSProperties",
318
+ "required": false,
319
+ "description": "Inline styles applied to the rendered element.",
320
+ "from": "@types/react"
321
+ }
322
+ ]
323
+ },
324
+ {
325
+ "name": "CommentMeta",
326
+ "props": [
327
+ {
328
+ "name": "children",
329
+ "type": "ReactNode",
330
+ "required": false,
331
+ "description": "Content rendered inside the part.",
332
+ "from": "@types/react"
333
+ },
334
+ {
335
+ "name": "className",
336
+ "type": "string",
337
+ "required": false,
338
+ "description": "Extra classes merged into the part's own classes with `cn`.",
339
+ "from": "@types/react"
340
+ },
341
+ {
342
+ "name": "style",
343
+ "type": "CSSProperties",
344
+ "required": false,
345
+ "description": "Inline styles applied to the rendered element.",
346
+ "from": "@types/react"
347
+ }
348
+ ]
349
+ },
350
+ {
351
+ "name": "CommentBody",
352
+ "props": [
353
+ {
354
+ "name": "text",
355
+ "type": "string",
356
+ "required": false,
357
+ "description": "Plain text with `@Full Name` mentions.",
358
+ "from": "arkcn"
359
+ },
360
+ {
361
+ "name": "children",
362
+ "type": "ReactNode",
363
+ "required": false,
364
+ "description": "Content rendered inside the part.",
365
+ "from": "@types/react"
366
+ },
367
+ {
368
+ "name": "className",
369
+ "type": "string",
370
+ "required": false,
371
+ "description": "Extra classes merged into the part's own classes with `cn`.",
372
+ "from": "@types/react"
373
+ },
374
+ {
375
+ "name": "style",
376
+ "type": "CSSProperties",
377
+ "required": false,
378
+ "description": "Inline styles applied to the rendered element.",
379
+ "from": "@types/react"
380
+ }
381
+ ]
382
+ },
383
+ {
384
+ "name": "CommentMention",
385
+ "props": [
386
+ {
387
+ "name": "personId",
388
+ "type": "string",
389
+ "required": false,
390
+ "description": "Id of the mentioned person.",
391
+ "from": "arkcn"
392
+ },
393
+ {
394
+ "name": "children",
395
+ "type": "ReactNode",
396
+ "required": false,
397
+ "description": "Content rendered inside the part.",
398
+ "from": "@types/react"
399
+ },
400
+ {
401
+ "name": "className",
402
+ "type": "string",
403
+ "required": false,
404
+ "description": "Extra classes merged into the part's own classes with `cn`.",
405
+ "from": "@types/react"
406
+ },
407
+ {
408
+ "name": "style",
409
+ "type": "CSSProperties",
410
+ "required": false,
411
+ "description": "Inline styles applied to the rendered element.",
412
+ "from": "@types/react"
413
+ }
414
+ ]
415
+ },
416
+ {
417
+ "name": "CommentReactions",
418
+ "props": [
419
+ {
420
+ "name": "children",
421
+ "type": "ReactNode",
422
+ "required": false,
423
+ "description": "Content rendered inside the part.",
424
+ "from": "@types/react"
425
+ },
426
+ {
427
+ "name": "className",
428
+ "type": "string",
429
+ "required": false,
430
+ "description": "Extra classes merged into the part's own classes with `cn`.",
431
+ "from": "@types/react"
432
+ },
433
+ {
434
+ "name": "style",
435
+ "type": "CSSProperties",
436
+ "required": false,
437
+ "description": "Inline styles applied to the rendered element.",
438
+ "from": "@types/react"
439
+ }
440
+ ]
441
+ },
442
+ {
443
+ "name": "CommentReaction",
444
+ "props": [
445
+ {
446
+ "name": "emoji",
447
+ "type": "string",
448
+ "required": true,
449
+ "description": "The reaction emoji.",
450
+ "from": "arkcn"
451
+ },
452
+ {
453
+ "name": "active",
454
+ "type": "boolean",
455
+ "required": false,
456
+ "default": "false",
457
+ "description": "Whether the part is the active one; sets `data-active`.",
458
+ "from": "arkcn"
459
+ },
460
+ {
461
+ "name": "count",
462
+ "type": "number",
463
+ "required": false,
464
+ "default": "0",
465
+ "description": "How many people reacted.",
466
+ "from": "arkcn"
467
+ },
468
+ {
469
+ "name": "asChild",
470
+ "type": "boolean",
471
+ "required": false,
472
+ "description": "Use the provided child element as the default rendered element, combining their props and behavior.",
473
+ "from": "@ark-ui/react"
474
+ },
475
+ {
476
+ "name": "children",
477
+ "type": "ReactNode",
478
+ "required": false,
479
+ "description": "Content rendered inside the part.",
480
+ "from": "@types/react"
481
+ },
482
+ {
483
+ "name": "className",
484
+ "type": "string",
485
+ "required": false,
486
+ "description": "Extra classes merged into the part's own classes with `cn`.",
487
+ "from": "@types/react"
488
+ },
489
+ {
490
+ "name": "style",
491
+ "type": "CSSProperties",
492
+ "required": false,
493
+ "description": "Inline styles applied to the rendered element.",
494
+ "from": "@types/react"
495
+ }
496
+ ]
497
+ },
498
+ {
499
+ "name": "CommentActions",
500
+ "props": [
501
+ {
502
+ "name": "children",
503
+ "type": "ReactNode",
504
+ "required": false,
505
+ "description": "Content rendered inside the part.",
506
+ "from": "@types/react"
507
+ },
508
+ {
509
+ "name": "className",
510
+ "type": "string",
511
+ "required": false,
512
+ "description": "Extra classes merged into the part's own classes with `cn`.",
513
+ "from": "@types/react"
514
+ },
515
+ {
516
+ "name": "style",
517
+ "type": "CSSProperties",
518
+ "required": false,
519
+ "description": "Inline styles applied to the rendered element.",
520
+ "from": "@types/react"
521
+ }
522
+ ]
523
+ },
524
+ {
525
+ "name": "CommentReplyTrigger",
526
+ "props": [
527
+ {
528
+ "name": "asChild",
529
+ "type": "boolean",
530
+ "required": false,
531
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
532
+ "from": "arkcn"
533
+ },
534
+ {
535
+ "name": "size",
536
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
537
+ "required": false,
538
+ "description": "Size variant; sets a `data-size` attribute.",
539
+ "from": "arkcn"
540
+ },
541
+ {
542
+ "name": "variant",
543
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
544
+ "required": false,
545
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
546
+ "from": "arkcn"
547
+ },
548
+ {
549
+ "name": "children",
550
+ "type": "ReactNode",
551
+ "required": false,
552
+ "description": "Content rendered inside the part.",
553
+ "from": "@types/react"
554
+ },
555
+ {
556
+ "name": "className",
557
+ "type": "string",
558
+ "required": false,
559
+ "description": "Extra classes merged into the part's own classes with `cn`.",
560
+ "from": "@types/react"
561
+ },
562
+ {
563
+ "name": "style",
564
+ "type": "CSSProperties",
565
+ "required": false,
566
+ "description": "Inline styles applied to the rendered element.",
567
+ "from": "@types/react"
568
+ }
569
+ ]
570
+ },
571
+ {
572
+ "name": "CommentEditTrigger",
573
+ "props": [
574
+ {
575
+ "name": "asChild",
576
+ "type": "boolean",
577
+ "required": false,
578
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
579
+ "from": "arkcn"
580
+ },
581
+ {
582
+ "name": "size",
583
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
584
+ "required": false,
585
+ "description": "Size variant; sets a `data-size` attribute.",
586
+ "from": "arkcn"
587
+ },
588
+ {
589
+ "name": "variant",
590
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
591
+ "required": false,
592
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
593
+ "from": "arkcn"
594
+ },
595
+ {
596
+ "name": "children",
597
+ "type": "ReactNode",
598
+ "required": false,
599
+ "description": "Content rendered inside the part.",
600
+ "from": "@types/react"
601
+ },
602
+ {
603
+ "name": "className",
604
+ "type": "string",
605
+ "required": false,
606
+ "description": "Extra classes merged into the part's own classes with `cn`.",
607
+ "from": "@types/react"
608
+ },
609
+ {
610
+ "name": "style",
611
+ "type": "CSSProperties",
612
+ "required": false,
613
+ "description": "Inline styles applied to the rendered element.",
614
+ "from": "@types/react"
615
+ }
616
+ ]
617
+ },
618
+ {
619
+ "name": "CommentReplies",
620
+ "props": [
621
+ {
622
+ "name": "children",
623
+ "type": "ReactNode",
624
+ "required": false,
625
+ "description": "Content rendered inside the part.",
626
+ "from": "@types/react"
627
+ },
628
+ {
629
+ "name": "className",
630
+ "type": "string",
631
+ "required": false,
632
+ "description": "Extra classes merged into the part's own classes with `cn`.",
633
+ "from": "@types/react"
634
+ },
635
+ {
636
+ "name": "style",
637
+ "type": "CSSProperties",
638
+ "required": false,
639
+ "description": "Inline styles applied to the rendered element.",
640
+ "from": "@types/react"
641
+ }
642
+ ]
643
+ },
644
+ {
645
+ "name": "CommentEmpty",
646
+ "props": [
647
+ {
648
+ "name": "children",
649
+ "type": "ReactNode",
650
+ "required": false,
651
+ "description": "Content rendered inside the part.",
652
+ "from": "@types/react"
653
+ },
654
+ {
655
+ "name": "className",
656
+ "type": "string",
657
+ "required": false,
658
+ "description": "Extra classes merged into the part's own classes with `cn`.",
659
+ "from": "@types/react"
660
+ },
661
+ {
662
+ "name": "style",
663
+ "type": "CSSProperties",
664
+ "required": false,
665
+ "description": "Inline styles applied to the rendered element.",
666
+ "from": "@types/react"
667
+ }
668
+ ]
669
+ },
670
+ {
671
+ "name": "CommentComposer",
672
+ "props": [
673
+ {
674
+ "name": "defaultValue",
675
+ "type": "string",
676
+ "required": false,
677
+ "default": "\"\"",
678
+ "description": "The initial value when uncontrolled.",
679
+ "from": "arkcn"
680
+ },
681
+ {
682
+ "name": "maxSuggestions",
683
+ "type": "number",
684
+ "required": false,
685
+ "default": "6",
686
+ "description": "Maximum number of mention suggestions shown.",
687
+ "from": "arkcn"
688
+ },
689
+ {
690
+ "name": "onCancel",
691
+ "type": "(() => void)",
692
+ "required": false,
693
+ "description": "Called when the composer is cancelled with Escape or the cancel trigger.",
694
+ "from": "arkcn"
695
+ },
696
+ {
697
+ "name": "onSubmit",
698
+ "type": "((comment: { text: string; mentions: string[]; }) => void)",
699
+ "required": false,
700
+ "description": "Called with the trimmed text and the ids of mentioned people.",
701
+ "from": "arkcn"
702
+ },
703
+ {
704
+ "name": "onValueChange",
705
+ "type": "((value: string) => void)",
706
+ "required": false,
707
+ "description": "Called with the new value whenever it changes.",
708
+ "from": "arkcn"
709
+ },
710
+ {
711
+ "name": "people",
712
+ "type": "Person[]",
713
+ "required": false,
714
+ "default": "[]",
715
+ "description": "People who can be mentioned and whose names and initials are rendered.",
716
+ "from": "arkcn"
717
+ },
718
+ {
719
+ "name": "value",
720
+ "type": "string",
721
+ "required": false,
722
+ "description": "The controlled value.",
723
+ "from": "arkcn"
724
+ },
725
+ {
726
+ "name": "children",
727
+ "type": "ReactNode",
728
+ "required": false,
729
+ "description": "Content rendered inside the part.",
730
+ "from": "@types/react"
731
+ },
732
+ {
733
+ "name": "className",
734
+ "type": "string",
735
+ "required": false,
736
+ "description": "Extra classes merged into the part's own classes with `cn`.",
737
+ "from": "@types/react"
738
+ },
739
+ {
740
+ "name": "style",
741
+ "type": "CSSProperties",
742
+ "required": false,
743
+ "description": "Inline styles applied to the rendered element.",
744
+ "from": "@types/react"
745
+ }
746
+ ]
747
+ },
748
+ {
749
+ "name": "CommentComposerReplyingTo",
750
+ "props": [
751
+ {
752
+ "name": "children",
753
+ "type": "((details: { id: string; }) => ReactNode)",
754
+ "required": false,
755
+ "description": "Content rendered inside the part.",
756
+ "from": "arkcn"
757
+ },
758
+ {
759
+ "name": "className",
760
+ "type": "string",
761
+ "required": false,
762
+ "description": "Extra classes merged into the part's own classes with `cn`.",
763
+ "from": "@types/react"
764
+ },
765
+ {
766
+ "name": "style",
767
+ "type": "CSSProperties",
768
+ "required": false,
769
+ "description": "Inline styles applied to the rendered element.",
770
+ "from": "@types/react"
771
+ }
772
+ ]
773
+ },
774
+ {
775
+ "name": "CommentComposerInput",
776
+ "props": [
777
+ {
778
+ "name": "placeholder",
779
+ "type": "string",
780
+ "required": false,
781
+ "default": "\"Write a comment…\"",
782
+ "description": "Text shown while there is no value.",
783
+ "from": "arkcn"
784
+ },
785
+ {
786
+ "name": "children",
787
+ "type": "ReactNode",
788
+ "required": false,
789
+ "description": "Content rendered inside the part.",
790
+ "from": "@types/react"
791
+ },
792
+ {
793
+ "name": "className",
794
+ "type": "string",
795
+ "required": false,
796
+ "description": "Extra classes merged into the part's own classes with `cn`.",
797
+ "from": "@types/react"
798
+ },
799
+ {
800
+ "name": "style",
801
+ "type": "CSSProperties",
802
+ "required": false,
803
+ "description": "Inline styles applied to the rendered element.",
804
+ "from": "@types/react"
805
+ }
806
+ ]
807
+ },
808
+ {
809
+ "name": "CommentComposerMentionList",
810
+ "props": [
811
+ {
812
+ "name": "asChild",
813
+ "type": "boolean",
814
+ "required": false,
815
+ "description": "Use the provided child element as the default rendered element, combining their props and behavior.",
816
+ "from": "@ark-ui/react"
817
+ },
818
+ {
819
+ "name": "children",
820
+ "type": "ReactNode",
821
+ "required": false,
822
+ "description": "Content rendered inside the part.",
823
+ "from": "@types/react"
824
+ },
825
+ {
826
+ "name": "className",
827
+ "type": "string",
828
+ "required": false,
829
+ "description": "Extra classes merged into the part's own classes with `cn`.",
830
+ "from": "@types/react"
831
+ },
832
+ {
833
+ "name": "style",
834
+ "type": "CSSProperties",
835
+ "required": false,
836
+ "description": "Inline styles applied to the rendered element.",
837
+ "from": "@types/react"
838
+ }
839
+ ]
840
+ },
841
+ {
842
+ "name": "CommentComposerFooter",
843
+ "props": [
844
+ {
845
+ "name": "children",
846
+ "type": "ReactNode",
847
+ "required": false,
848
+ "description": "Content rendered inside the part.",
849
+ "from": "@types/react"
850
+ },
851
+ {
852
+ "name": "className",
853
+ "type": "string",
854
+ "required": false,
855
+ "description": "Extra classes merged into the part's own classes with `cn`.",
856
+ "from": "@types/react"
857
+ },
858
+ {
859
+ "name": "style",
860
+ "type": "CSSProperties",
861
+ "required": false,
862
+ "description": "Inline styles applied to the rendered element.",
863
+ "from": "@types/react"
864
+ }
865
+ ]
866
+ },
867
+ {
868
+ "name": "CommentComposerHint",
869
+ "props": [
870
+ {
871
+ "name": "children",
872
+ "type": "ReactNode",
873
+ "required": false,
874
+ "description": "Content rendered inside the part.",
875
+ "from": "@types/react"
876
+ },
877
+ {
878
+ "name": "className",
879
+ "type": "string",
880
+ "required": false,
881
+ "description": "Extra classes merged into the part's own classes with `cn`.",
882
+ "from": "@types/react"
883
+ },
884
+ {
885
+ "name": "style",
886
+ "type": "CSSProperties",
887
+ "required": false,
888
+ "description": "Inline styles applied to the rendered element.",
889
+ "from": "@types/react"
890
+ }
891
+ ]
892
+ },
893
+ {
894
+ "name": "CommentComposerSubmitTrigger",
895
+ "props": [
896
+ {
897
+ "name": "asChild",
898
+ "type": "boolean",
899
+ "required": false,
900
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
901
+ "from": "arkcn"
902
+ },
903
+ {
904
+ "name": "size",
905
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
906
+ "required": false,
907
+ "description": "Size variant; sets a `data-size` attribute.",
908
+ "from": "arkcn"
909
+ },
910
+ {
911
+ "name": "variant",
912
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
913
+ "required": false,
914
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
915
+ "from": "arkcn"
916
+ },
917
+ {
918
+ "name": "children",
919
+ "type": "ReactNode",
920
+ "required": false,
921
+ "description": "Content rendered inside the part.",
922
+ "from": "@types/react"
923
+ },
924
+ {
925
+ "name": "className",
926
+ "type": "string",
927
+ "required": false,
928
+ "description": "Extra classes merged into the part's own classes with `cn`.",
929
+ "from": "@types/react"
930
+ },
931
+ {
932
+ "name": "style",
933
+ "type": "CSSProperties",
934
+ "required": false,
935
+ "description": "Inline styles applied to the rendered element.",
936
+ "from": "@types/react"
937
+ }
938
+ ]
939
+ },
940
+ {
941
+ "name": "CommentComposerCancelTrigger",
942
+ "props": [
943
+ {
944
+ "name": "asChild",
945
+ "type": "boolean",
946
+ "required": false,
947
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
948
+ "from": "arkcn"
949
+ },
950
+ {
951
+ "name": "size",
952
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
953
+ "required": false,
954
+ "description": "Size variant; sets a `data-size` attribute.",
955
+ "from": "arkcn"
956
+ },
957
+ {
958
+ "name": "variant",
959
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
960
+ "required": false,
961
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
962
+ "from": "arkcn"
963
+ },
964
+ {
965
+ "name": "children",
966
+ "type": "ReactNode",
967
+ "required": false,
968
+ "description": "Content rendered inside the part.",
969
+ "from": "@types/react"
970
+ },
971
+ {
972
+ "name": "className",
973
+ "type": "string",
974
+ "required": false,
975
+ "description": "Extra classes merged into the part's own classes with `cn`.",
976
+ "from": "@types/react"
977
+ },
978
+ {
979
+ "name": "style",
980
+ "type": "CSSProperties",
981
+ "required": false,
982
+ "description": "Inline styles applied to the rendered element.",
983
+ "from": "@types/react"
984
+ }
985
+ ]
986
+ }
987
+ ],
37
988
  "dependencies": {
38
989
  "@ark-ui/react": "^5.39.1",
39
990
  "lucide-react": "^1.41.0"
@@ -50,7 +1001,7 @@
50
1001
  "files": [
51
1002
  {
52
1003
  "path": "components/ui/comment-thread.tsx",
53
- "content": "import * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { CornerDownRightIcon, PencilIcon, SendIcon } from \"lucide-react\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { Popover, PopoverContent } from \"@/components/ui/popover\"\nimport { formatRelativeTime } from \"@/lib/time\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\n\n/**\n * CommentThread — compositional, data-agnostic comments with replies,\n * reactions and a mentions composer. The consumer owns the comments and\n * the people list; the thread owns only which comment is being replied to or\n * edited. Mentions are plain text (`@Full Name`); `extractMentions` maps\n * them back to ids and `CommentBody` highlights them.\n *\n * Anatomy:\n * CommentThread { people, currentUserId }\n * CommentList > Comment value= authorId=\n * CommentAvatar + CommentContent\n * CommentHeader > CommentAuthor, CommentTime date=, CommentMeta\n * CommentBody highlights @mentions\n * CommentReactions > CommentReaction emoji= count= active=\n * CommentActions > CommentReplyTrigger, CommentEditTrigger, …\n * CommentReplies > Comment … nested, with a guide line\n * CommentComposer { people, onSubmit }\n * CommentComposerReplyingTo \"Replying to X\" strip\n * CommentComposerInput textarea; type @ to mention, ⌘↵ to send\n * CommentComposerMentionList suggestions under the input\n * CommentComposerFooter > CommentComposerHint, CommentComposerCancelTrigger, CommentComposerSubmitTrigger\n */\n\ntype Person = { id: string; name: string; initials?: string; avatar?: string }\n\ntype ThreadContextValue = {\n people: Person[]\n currentUserId?: string\n replyTo: string | null\n setReplyTo: (id: string | null) => void\n editing: string | null\n setEditing: (id: string | null) => void\n}\nconst ThreadContext = React.createContext<ThreadContextValue | null>(null)\nconst CommentContext = React.createContext<{ value: string; authorId?: string; depth: number } | null>(null)\n\nfunction useCommentThread() {\n const ctx = React.useContext(ThreadContext)\n if (!ctx) throw new Error(\"Comment parts must be used within <CommentThread>\")\n return ctx\n}\nfunction useComment() {\n const ctx = React.useContext(CommentContext)\n if (!ctx) throw new Error(\"Comment parts must be used within <Comment>\")\n return ctx\n}\n\nconst initialsOf = (person?: Person) =>\n person?.initials ??\n (person?.name ?? \"?\")\n .split(/\\s+/)\n .map((p) => p[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase()\n\n/* ---------------------------------------------------------------------------\n * Thread\n * ------------------------------------------------------------------------- */\n\nfunction CommentThread({\n people = [],\n currentUserId,\n replyTo: replyToProp,\n onReplyToChange,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n people?: Person[]\n currentUserId?: string\n replyTo?: string | null\n onReplyToChange?: (id: string | null) => void\n}) {\n const [replyTo, setReplyTo] = useControllable<string | null>(replyToProp, null, onReplyToChange)\n const [editing, setEditing] = React.useState<string | null>(null)\n const ctx = React.useMemo(\n () => ({ people, currentUserId, replyTo, setReplyTo, editing, setEditing }),\n [people, currentUserId, replyTo, setReplyTo, editing]\n )\n return (\n <ThreadContext.Provider value={ctx}>\n <div data-slot=\"comment-thread\" className={cn(\"flex flex-col gap-4\", className)} {...props} />\n </ThreadContext.Provider>\n )\n}\n\nfunction CommentList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return <ol data-slot=\"comment-list\" className={cn(\"flex flex-col gap-4\", className)} {...props} />\n}\n\nfunction Comment({\n value,\n authorId,\n className,\n ...props\n}: React.ComponentProps<\"li\"> & { value: string; authorId?: string }) {\n const thread = useCommentThread()\n const parent = React.useContext(CommentContext)\n const depth = parent ? parent.depth + 1 : 0\n const ctx = React.useMemo(() => ({ value, authorId, depth }), [value, authorId, depth])\n return (\n <CommentContext.Provider value={ctx}>\n <li\n data-slot=\"comment\"\n data-value={value}\n data-depth={depth}\n data-own={authorId && authorId === thread.currentUserId ? \"\" : undefined}\n data-replying={thread.replyTo === value ? \"\" : undefined}\n data-editing={thread.editing === value ? \"\" : undefined}\n className={cn(\"group/comment flex gap-3\", className)}\n {...props}\n />\n </CommentContext.Provider>\n )\n}\n\n/** Avatar for the comment's author (from `people`), or pass your own children. */\nfunction CommentAvatar({ className, children, ...props }: React.ComponentProps<typeof Avatar>) {\n const { people } = useCommentThread()\n const { authorId, depth } = useComment()\n const person = people.find((p) => p.id === authorId)\n return (\n <Avatar\n data-slot=\"comment-avatar\"\n size={depth > 0 ? \"sm\" : \"default\"}\n className={cn(\"shrink-0\", className)}\n {...props}\n >\n {children ?? (\n <>\n {person?.avatar && <AvatarImage src={person.avatar} alt=\"\" />}\n <AvatarFallback>{initialsOf(person)}</AvatarFallback>\n </>\n )}\n </Avatar>\n )\n}\n\nfunction CommentContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"comment-content\" className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", className)} {...props} />\n}\n\nfunction CommentHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-header\"\n className={cn(\"flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm\", className)}\n {...props}\n />\n )\n}\n\n/** Author name from `people` by default. */\nfunction CommentAuthor({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const { people } = useCommentThread()\n const { authorId } = useComment()\n return (\n <span data-slot=\"comment-author\" className={cn(\"font-medium\", className)} {...props}>\n {children ?? people.find((p) => p.id === authorId)?.name ?? \"Unknown\"}\n </span>\n )\n}\n\nfunction CommentTime({\n date,\n now,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"time\">, \"dateTime\"> & { date: Date; now?: Date }) {\n return (\n <time\n data-slot=\"comment-time\"\n dateTime={date.toISOString()}\n title={date.toLocaleString()}\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n >\n {children ?? formatRelativeTime(date, { now })}\n </time>\n )\n}\n\n/** Small trailing note, e.g. \"edited\" or a role badge. */\nfunction CommentMeta({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"comment-meta\" className={cn(\"text-xs text-muted-foreground\", className)} {...props} />\n}\n\n/** Split text into runs, turning `@Full Name` for known people into mention nodes. */\nfunction renderMentions(text: string, people: Person[]) {\n if (!people.length || !text.includes(\"@\")) return [text]\n const names = [...people].sort((a, b) => b.name.length - a.name.length)\n const pattern = new RegExp(\n `@(${names.map((p) => p.name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")).join(\"|\")})(?![\\\\w])`,\n \"g\"\n )\n const nodes: React.ReactNode[] = []\n let last = 0\n for (const match of text.matchAll(pattern)) {\n const index = match.index ?? 0\n if (index > last) nodes.push(text.slice(last, index))\n const person = people.find((p) => p.name === match[1])\n nodes.push(\n <CommentMention key={`${index}-${match[1]}`} personId={person?.id}>\n @{match[1]}\n </CommentMention>\n )\n last = index + match[0].length\n }\n if (last < text.length) nodes.push(text.slice(last))\n return nodes\n}\n\nfunction CommentMention({ personId, className, ...props }: React.ComponentProps<\"span\"> & { personId?: string }) {\n const { currentUserId } = useCommentThread()\n return (\n <span\n data-slot=\"comment-mention\"\n data-person={personId}\n data-self={personId && personId === currentUserId ? \"\" : undefined}\n className={cn(\n \"rounded-sm bg-primary/10 px-1 font-medium text-primary data-self:bg-amber-500/20 data-self:text-amber-700 dark:data-self:text-amber-300\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** Plain text with mentions highlighted; pass `children` to render anything else. */\nfunction CommentBody({ text, className, children, ...props }: React.ComponentProps<\"div\"> & { text?: string }) {\n const { people } = useCommentThread()\n return (\n <div\n data-slot=\"comment-body\"\n className={cn(\"text-sm/relaxed wrap-break-word whitespace-pre-wrap\", className)}\n {...props}\n >\n {children ?? (text !== undefined ? renderMentions(text, people) : null)}\n </div>\n )\n}\n\nfunction CommentReactions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-reactions\"\n className={cn(\"flex flex-wrap items-center gap-1 pt-0.5\", className)}\n {...props}\n />\n )\n}\n\nfunction CommentReaction({\n emoji,\n count = 0,\n active = false,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ark.button> & { emoji: string; count?: number; active?: boolean }) {\n return (\n <ark.button\n type=\"button\"\n data-slot=\"comment-reaction\"\n data-active={active ? \"\" : undefined}\n aria-pressed={active}\n aria-label={`${emoji} ${count}`}\n className={cn(\n \"inline-flex h-6 items-center gap-1 rounded-full border bg-background px-1.5 text-xs transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none data-active:border-primary/40 data-active:bg-primary/10\",\n className\n )}\n {...props}\n >\n <span aria-hidden>{emoji}</span>\n {children ?? <span className=\"tabular-nums\">{count}</span>}\n </ark.button>\n )\n}\n\n/** Row of small text buttons; revealed on hover/focus, always visible when the comment is active. */\nfunction CommentActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-actions\"\n className={cn(\n \"flex items-center gap-1 text-xs text-muted-foreground opacity-0 transition-opacity group-focus-within/comment:opacity-100 group-hover/comment:opacity-100 group-data-editing/comment:opacity-100 group-data-replying/comment:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommentReplyTrigger({ asChild, children, onClick, className, ...props }: React.ComponentProps<typeof Button>) {\n const { replyTo, setReplyTo } = useCommentThread()\n const { value } = useComment()\n return (\n <Button\n data-slot=\"comment-reply-trigger\"\n variant=\"ghost\"\n size=\"xs\"\n asChild={asChild}\n aria-pressed={replyTo === value}\n className={cn(\"h-6 px-1.5 text-xs\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setReplyTo(replyTo === value ? null : value)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <CornerDownRightIcon /> Reply\n </>\n ))}\n </Button>\n )\n}\n\nfunction CommentEditTrigger({ asChild, children, onClick, className, ...props }: React.ComponentProps<typeof Button>) {\n const { editing, setEditing } = useCommentThread()\n const { value } = useComment()\n return (\n <Button\n data-slot=\"comment-edit-trigger\"\n variant=\"ghost\"\n size=\"xs\"\n asChild={asChild}\n aria-pressed={editing === value}\n className={cn(\"h-6 px-1.5 text-xs\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setEditing(editing === value ? null : value)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <PencilIcon /> Edit\n </>\n ))}\n </Button>\n )\n}\n\n/** Nested replies with a guide line; put `Comment`s inside. */\nfunction CommentReplies({ className, ...props }: React.ComponentProps<\"ol\">) {\n return (\n <ol data-slot=\"comment-replies\" className={cn(\"mt-2 flex flex-col gap-3 border-s-2 ps-3\", className)} {...props} />\n )\n}\n\nfunction CommentEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-empty\"\n className={cn(\"rounded-lg border border-dashed px-3 py-6 text-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Composer with mentions\n * ------------------------------------------------------------------------- */\n\ntype MentionState = { query: string; start: number; index: number } | null\n\ntype ComposerContextValue = {\n people: Person[]\n value: string\n setValue: (value: string) => void\n mention: MentionState\n setMention: (mention: MentionState) => void\n suggestions: Person[]\n pick: (person: Person) => void\n /** The input registers how a pick is applied to its DOM. */\n registerPicker: (fn: (person: Person, mention: NonNullable<MentionState>) => void) => () => void\n submit: () => void\n cancel: () => void\n canSubmit: boolean\n inputRef: React.RefObject<HTMLDivElement | null>\n listId: string\n}\nconst ComposerContext = React.createContext<ComposerContextValue | null>(null)\nfunction useComposer() {\n const ctx = React.useContext(ComposerContext)\n if (!ctx) throw new Error(\"Composer parts must be used within <CommentComposer>\")\n return ctx\n}\n\n/** Ids of people whose `@Full Name` appears in the text. */\nfunction extractMentions(text: string, people: Person[]) {\n return people\n .filter((p) => new RegExp(`@${p.name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w])`).test(text))\n .map((p) => p.id)\n}\n\nfunction CommentComposer({\n people = [],\n value: valueProp,\n defaultValue = \"\",\n onValueChange,\n onSubmit,\n onCancel,\n maxSuggestions = 6,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onSubmit\" | \"defaultValue\"> & {\n people?: Person[]\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n /** Called with the trimmed text and the ids of mentioned people. */\n onSubmit?: (comment: { text: string; mentions: string[] }) => void\n onCancel?: () => void\n maxSuggestions?: number\n}) {\n const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange)\n const [mention, setMention] = React.useState<MentionState>(null)\n const inputRef = React.useRef<HTMLDivElement>(null)\n const listId = React.useId()\n const pickerRef = React.useRef<((person: Person, mention: NonNullable<MentionState>) => void) | null>(null)\n const registerPicker = React.useCallback((fn: (person: Person, mention: NonNullable<MentionState>) => void) => {\n pickerRef.current = fn\n return () => {\n if (pickerRef.current === fn) pickerRef.current = null\n }\n }, [])\n\n const suggestions = React.useMemo(() => {\n if (!mention) return []\n const q = mention.query.toLowerCase()\n return people.filter((p) => p.name.toLowerCase().includes(q)).slice(0, maxSuggestions)\n }, [mention, people, maxSuggestions])\n\n const pick = React.useCallback(\n (person: Person) => {\n if (!mention) return\n pickerRef.current?.(person, mention)\n setMention(null)\n },\n [mention]\n )\n\n const canSubmit = value.trim().length > 0\n const submit = React.useCallback(() => {\n if (!canSubmit) return\n onSubmit?.({ text: value.trim(), mentions: extractMentions(value, people) })\n setValue(\"\")\n setMention(null)\n }, [canSubmit, onSubmit, value, people, setValue])\n const cancel = React.useCallback(() => {\n setValue(\"\")\n setMention(null)\n onCancel?.()\n }, [setValue, onCancel])\n\n const ctx = React.useMemo<ComposerContextValue>(\n () => ({\n people,\n value,\n setValue,\n mention,\n setMention,\n suggestions,\n pick,\n registerPicker,\n submit,\n cancel,\n canSubmit,\n inputRef,\n listId,\n }),\n [people, value, setValue, mention, suggestions, pick, registerPicker, submit, cancel, canSubmit, listId]\n )\n\n return (\n <ComposerContext.Provider value={ctx}>\n <div\n data-slot=\"comment-composer\"\n data-mentioning={mention ? \"\" : undefined}\n className={cn(\n \"relative flex flex-col gap-2 rounded-xl border bg-background p-2 transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </ComposerContext.Provider>\n )\n}\n\n/** \"Replying to …\" strip; renders nothing when not replying. The render prop gets the comment id. */\nfunction CommentComposerReplyingTo({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (details: { id: string }) => React.ReactNode }) {\n const { replyTo, setReplyTo } = useCommentThread()\n if (!replyTo) return null\n return (\n <div\n data-slot=\"comment-composer-replying-to\"\n className={cn(\"flex items-center gap-2 px-1 text-xs text-muted-foreground\", className)}\n {...props}\n >\n <CornerDownRightIcon className=\"size-3.5\" />\n {children ? children({ id: replyTo }) : <span>Replying to a comment</span>}\n <Button variant=\"ghost\" size=\"xs\" className=\"ms-auto h-5 px-1.5\" onClick={() => setReplyTo(null)}>\n Cancel\n </Button>\n </div>\n )\n}\n\n/* ----- contenteditable helpers ----- */\n\nconst CHIP = \"comment-mention-chip\"\nconst escapeRe = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\n/** Serialise the editable DOM to plain text; chips become `@Full Name`, blocks and <br> become newlines. */\nfunction serialize(root: HTMLElement): string {\n let out = \"\"\n const walk = (node: Node) => {\n if (node.nodeType === Node.TEXT_NODE) {\n out += (node.textContent ?? \"\").replace(/ /g, \" \")\n return\n }\n if (!(node instanceof HTMLElement)) return\n if (node.dataset.slot === CHIP) {\n out += `@${node.dataset.name ?? \"\"}`\n return\n }\n if (node.tagName === \"BR\") {\n out += \"\\n\"\n return\n }\n const block = node.tagName === \"DIV\" || node.tagName === \"P\"\n if (block && out.length && !out.endsWith(\"\\n\")) out += \"\\n\"\n node.childNodes.forEach(walk)\n }\n root.childNodes.forEach(walk)\n return out.replace(/\\n$/, \"\")\n}\n\nfunction createChip(person: Person) {\n const chip = document.createElement(\"span\")\n chip.dataset.slot = CHIP\n chip.dataset.person = person.id\n chip.dataset.name = person.name\n chip.contentEditable = \"false\"\n chip.className =\n \"mx-px inline-flex items-center rounded-full bg-muted px-1.5 py-px align-baseline text-[0.9em] font-medium text-foreground select-none\"\n chip.textContent = `@${person.name}`\n return chip\n}\n\n/** Build editable DOM from plain text, turning known `@Full Name`s into chips. */\nfunction hydrate(root: HTMLElement, text: string, people: Person[]) {\n root.replaceChildren()\n const names = [...people].sort((a, b) => b.name.length - a.name.length)\n const pattern = names.length ? new RegExp(`@(${names.map((p) => escapeRe(p.name)).join(\"|\")})(?![\\\\w])`, \"g\") : null\n text.split(\"\\n\").forEach((line, lineIndex) => {\n if (lineIndex > 0) root.appendChild(document.createElement(\"br\"))\n let last = 0\n if (pattern) {\n for (const match of line.matchAll(pattern)) {\n const index = match.index ?? 0\n if (index > last) root.appendChild(document.createTextNode(line.slice(last, index)))\n const person = people.find((p) => p.name === match[1])\n if (person) root.appendChild(createChip(person))\n last = index + match[0].length\n }\n }\n if (last < line.length) root.appendChild(document.createTextNode(line.slice(last)))\n })\n}\n\n/** The `@query` immediately before the caret inside a text node, if any. */\nfunction mentionAtCaret(root: HTMLElement): { node: Text; start: number; end: number; query: string } | null {\n const selection = window.getSelection()\n if (!selection || !selection.rangeCount || !selection.isCollapsed) return null\n const range = selection.getRangeAt(0)\n const node = range.startContainer\n if (node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return null\n const text = node.textContent ?? \"\"\n const caret = range.startOffset\n const before = text.slice(0, caret)\n const at = before.lastIndexOf(\"@\")\n if (at === -1) return null\n if (at > 0 && /\\S/.test(before[at - 1])) return null\n const query = before.slice(at + 1)\n if (query.length > 30) return null\n return { node: node as Text, start: at, end: caret, query }\n}\n\nfunction placeCaretAfter(node: Node) {\n const selection = window.getSelection()\n if (!selection) return\n const range = document.createRange()\n range.setStartAfter(node)\n range.collapse(true)\n selection.removeAllRanges()\n selection.addRange(range)\n}\n\n/**\n * Contenteditable input: picked mentions become atomic grey chips (one\n * Backspace removes a whole chip); `value` stays plain `@Full Name` text.\n * Arrows/Enter/Tab pick from the list, Escape closes it, ⌘/Ctrl+Enter submits.\n */\nfunction CommentComposerInput({\n className,\n placeholder = \"Write a comment…\",\n autoFocus,\n onKeyDown,\n onBlur,\n onInput,\n onPaste,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\"> & { placeholder?: string; autoFocus?: boolean }) {\n const c = useComposer()\n const lastSerialized = React.useRef<string | null>(null)\n\n // Reflect outside changes to `value` (initial text, edits, reset after submit) without disturbing typing.\n React.useLayoutEffect(() => {\n const el = c.inputRef.current\n if (!el || c.value === lastSerialized.current) return\n hydrate(el, c.value, c.people)\n lastSerialized.current = c.value\n }, [c.value, c.people, c.inputRef])\n\n React.useEffect(() => {\n if (autoFocus) c.inputRef.current?.focus()\n }, [autoFocus, c.inputRef])\n\n const { registerPicker, setValue, inputRef } = c\n React.useEffect(\n () =>\n registerPicker((person) => {\n const el = inputRef.current\n if (!el) return\n const hit = mentionAtCaret(el)\n const chip = createChip(person)\n const space = document.createTextNode(\" \")\n if (hit) {\n const range = document.createRange()\n range.setStart(hit.node, hit.start)\n range.setEnd(hit.node, hit.end)\n range.deleteContents()\n range.insertNode(space)\n range.insertNode(chip)\n } else {\n el.append(chip, space)\n }\n placeCaretAfter(space)\n el.focus()\n const next = serialize(el)\n lastSerialized.current = next\n setValue(next)\n }),\n [registerPicker, inputRef, setValue]\n )\n\n const sync = () => {\n const el = c.inputRef.current\n if (!el) return\n if (el.innerHTML === \"<br>\") el.replaceChildren()\n const next = serialize(el)\n lastSerialized.current = next\n c.setValue(next)\n const hit = mentionAtCaret(el)\n c.setMention(hit ? { query: hit.query, start: hit.start, index: 0 } : null)\n }\n\n return (\n <div\n ref={c.inputRef}\n data-slot=\"comment-composer-input\"\n role={c.mention ? \"combobox\" : \"textbox\"}\n contentEditable\n suppressContentEditableWarning\n aria-multiline={c.mention ? undefined : true}\n aria-expanded={c.mention ? true : undefined}\n aria-controls={c.mention ? c.listId : undefined}\n aria-autocomplete={c.mention ? \"list\" : undefined}\n aria-activedescendant={\n c.mention && c.suggestions[c.mention.index] ? `${c.listId}-${c.suggestions[c.mention.index].id}` : undefined\n }\n data-placeholder={placeholder}\n className={cn(\n \"min-h-20 px-2 py-1.5 text-sm wrap-break-word whitespace-pre-wrap outline-none\",\n \"empty:before:pointer-events-none empty:before:text-muted-foreground empty:before:content-[attr(data-placeholder)]\",\n className\n )}\n onInput={(event) => {\n onInput?.(event)\n sync()\n }}\n onKeyUp={(event) => {\n if ([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"].includes(event.key)) sync()\n }}\n onClick={() => sync()}\n onPaste={(event) => {\n onPaste?.(event)\n if (event.defaultPrevented) return\n event.preventDefault()\n document.execCommand(\"insertText\", false, event.clipboardData.getData(\"text/plain\"))\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if ((event.metaKey || event.ctrlKey) && event.key === \"Enter\") {\n event.preventDefault()\n c.submit()\n return\n }\n if (!c.mention) {\n if (event.key === \"Escape\") c.cancel()\n return\n }\n const count = c.suggestions.length\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault()\n if (!count) return\n const delta = event.key === \"ArrowDown\" ? 1 : -1\n c.setMention({ ...c.mention, index: (c.mention.index + delta + count) % count })\n } else if ((event.key === \"Enter\" || event.key === \"Tab\") && count) {\n event.preventDefault()\n c.pick(c.suggestions[c.mention.index])\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n c.setMention(null)\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n setTimeout(() => {\n if (document.activeElement !== event.target) c.setMention(null)\n }, 120)\n }}\n {...props}\n />\n )\n}\n\n/** Floating suggestion list anchored under the input while an `@query` is active. Focus stays in the textarea. */\nfunction CommentComposerMentionList({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const c = useComposer()\n const open = !!c.mention\n return (\n <Popover\n open={open}\n onOpenChange={({ open }) => {\n if (!open) c.setMention(null)\n }}\n autoFocus={false}\n modal={false}\n positioning={{\n placement: \"bottom-start\",\n sameWidth: true,\n gutter: 4,\n getAnchorRect: () => {\n const rect = c.inputRef.current?.getBoundingClientRect()\n return rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null\n },\n }}\n >\n <PopoverContent\n id={c.listId}\n role=\"listbox\"\n aria-label=\"People\"\n data-slot=\"comment-composer-mention-list\"\n className={cn(\"max-h-56 w-auto overflow-y-auto p-1\", className)}\n onPointerDown={(event) => event.preventDefault()}\n {...props}\n >\n {c.suggestions.length === 0 ? (\n <div className=\"px-2 py-1.5 text-sm text-muted-foreground\">No one matches \"{c.mention?.query}\"</div>\n ) : (\n c.suggestions.map((person, index) => (\n <div\n key={person.id}\n id={`${c.listId}-${person.id}`}\n role=\"option\"\n aria-selected={index === c.mention?.index}\n data-slot=\"comment-composer-mention-item\"\n data-highlighted={index === c.mention?.index ? \"\" : undefined}\n className=\"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm data-highlighted:bg-accent data-highlighted:text-accent-foreground\"\n onMouseEnter={() => c.mention && c.setMention({ ...c.mention, index })}\n onClick={() => c.pick(person)}\n >\n <Avatar size=\"sm\" className=\"size-6 text-[10px]\">\n {person.avatar && <AvatarImage src={person.avatar} alt=\"\" />}\n <AvatarFallback>{initialsOf(person)}</AvatarFallback>\n </Avatar>\n <span className=\"truncate\">{person.name}</span>\n </div>\n ))\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction CommentComposerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"comment-composer-footer\" className={cn(\"flex items-center gap-2 px-1\", className)} {...props} />\n )\n}\n\nfunction CommentComposerHint({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"comment-composer-hint\"\n className={cn(\"me-auto text-xs text-muted-foreground\", className)}\n {...props}\n >\n {children ?? (\n <>\n <Kbd>@</Kbd> to mention · <Kbd>⌘↵</Kbd> to send\n </>\n )}\n </span>\n )\n}\n\nfunction CommentComposerSubmitTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { submit, canSubmit } = useComposer()\n return (\n <Button\n data-slot=\"comment-composer-submit-trigger\"\n size=\"sm\"\n asChild={asChild}\n disabled={!canSubmit}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) submit()\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <SendIcon /> Comment\n </>\n ))}\n </Button>\n )\n}\n\nfunction CommentComposerCancelTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { cancel } = useComposer()\n return (\n <Button\n data-slot=\"comment-composer-cancel-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) cancel()\n }}\n {...props}\n >\n {asChild ? children : (children ?? \"Cancel\")}\n </Button>\n )\n}\n\nexport {\n CommentThread,\n CommentList,\n Comment,\n CommentAvatar,\n CommentContent,\n CommentHeader,\n CommentAuthor,\n CommentTime,\n CommentMeta,\n CommentBody,\n CommentMention,\n CommentReactions,\n CommentReaction,\n CommentActions,\n CommentReplyTrigger,\n CommentEditTrigger,\n CommentReplies,\n CommentEmpty,\n CommentComposer,\n CommentComposerReplyingTo,\n CommentComposerInput,\n CommentComposerMentionList,\n CommentComposerFooter,\n CommentComposerHint,\n CommentComposerSubmitTrigger,\n CommentComposerCancelTrigger,\n useCommentThread,\n useComment,\n extractMentions,\n renderMentions,\n type Person as CommentPerson,\n}\n"
1004
+ "content": "import * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { CornerDownRightIcon, PencilIcon, SendIcon } from \"lucide-react\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { Popover, PopoverContent } from \"@/components/ui/popover\"\nimport { formatRelativeTime } from \"@/lib/time\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\n\n/**\n * CommentThread — compositional, data-agnostic comments with replies,\n * reactions and a mentions composer. The consumer owns the comments and\n * the people list; the thread owns only which comment is being replied to or\n * edited. Mentions are plain text (`@Full Name`); `extractMentions` maps\n * them back to ids and `CommentBody` highlights them.\n *\n * Anatomy:\n * CommentThread { people, currentUserId }\n * CommentList > Comment value= authorId=\n * CommentAvatar + CommentContent\n * CommentHeader > CommentAuthor, CommentTime date=, CommentMeta\n * CommentBody highlights @mentions\n * CommentReactions > CommentReaction emoji= count= active=\n * CommentActions > CommentReplyTrigger, CommentEditTrigger, …\n * CommentReplies > Comment … nested, with a guide line\n * CommentComposer { people, onSubmit }\n * CommentComposerReplyingTo \"Replying to X\" strip\n * CommentComposerInput textarea; type @ to mention, ⌘↵ to send\n * CommentComposerMentionList suggestions under the input\n * CommentComposerFooter > CommentComposerHint, CommentComposerCancelTrigger, CommentComposerSubmitTrigger\n */\n\ntype Person = { id: string; name: string; initials?: string; avatar?: string }\n\ntype ThreadContextValue = {\n /** People who can be mentioned and whose names and initials are rendered. */\n people: Person[]\n /** Id of the signed-in person; their comments get `data-own` and mentions of them `data-self`. */\n currentUserId?: string\n /** Id of the comment being replied to (controlled). */\n replyTo: string | null\n setReplyTo: (id: string | null) => void\n editing: string | null\n setEditing: (id: string | null) => void\n}\nconst ThreadContext = React.createContext<ThreadContextValue | null>(null)\nconst CommentContext = React.createContext<{ value: string; authorId?: string; depth: number } | null>(null)\n\nfunction useCommentThread() {\n const ctx = React.useContext(ThreadContext)\n if (!ctx) throw new Error(\"Comment parts must be used within <CommentThread>\")\n return ctx\n}\nfunction useComment() {\n const ctx = React.useContext(CommentContext)\n if (!ctx) throw new Error(\"Comment parts must be used within <Comment>\")\n return ctx\n}\n\nconst initialsOf = (person?: Person) =>\n person?.initials ??\n (person?.name ?? \"?\")\n .split(/\\s+/)\n .map((p) => p[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase()\n\n/* ---------------------------------------------------------------------------\n * Thread\n * ------------------------------------------------------------------------- */\n\nfunction CommentThread({\n people = [],\n currentUserId,\n replyTo: replyToProp,\n onReplyToChange,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** People who can be mentioned and whose names and initials are rendered. */\n people?: Person[]\n /** Id of the signed-in person; their comments get `data-own` and mentions of them `data-self`. */\n currentUserId?: string\n /** Id of the comment being replied to (controlled). */\n replyTo?: string | null\n /** Called when the reply target changes. */\n onReplyToChange?: (id: string | null) => void\n}) {\n const [replyTo, setReplyTo] = useControllable<string | null>(replyToProp, null, onReplyToChange)\n const [editing, setEditing] = React.useState<string | null>(null)\n const ctx = React.useMemo(\n () => ({ people, currentUserId, replyTo, setReplyTo, editing, setEditing }),\n [people, currentUserId, replyTo, setReplyTo, editing]\n )\n return (\n <ThreadContext.Provider value={ctx}>\n <div data-slot=\"comment-thread\" className={cn(\"flex flex-col gap-4\", className)} {...props} />\n </ThreadContext.Provider>\n )\n}\n\nfunction CommentList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return <ol data-slot=\"comment-list\" className={cn(\"flex flex-col gap-4\", className)} {...props} />\n}\n\nfunction Comment({\n value,\n authorId,\n className,\n ...props\n}: React.ComponentProps<\"li\"> & { value: string; authorId?: string }) {\n const thread = useCommentThread()\n const parent = React.useContext(CommentContext)\n const depth = parent ? parent.depth + 1 : 0\n const ctx = React.useMemo(() => ({ value, authorId, depth }), [value, authorId, depth])\n return (\n <CommentContext.Provider value={ctx}>\n <li\n data-slot=\"comment\"\n data-value={value}\n data-depth={depth}\n data-own={authorId && authorId === thread.currentUserId ? \"\" : undefined}\n data-replying={thread.replyTo === value ? \"\" : undefined}\n data-editing={thread.editing === value ? \"\" : undefined}\n className={cn(\"group/comment flex gap-3\", className)}\n {...props}\n />\n </CommentContext.Provider>\n )\n}\n\n/** Avatar for the comment's author (from `people`), or pass your own children. */\nfunction CommentAvatar({ className, children, ...props }: React.ComponentProps<typeof Avatar>) {\n const { people } = useCommentThread()\n const { authorId, depth } = useComment()\n const person = people.find((p) => p.id === authorId)\n return (\n <Avatar\n data-slot=\"comment-avatar\"\n size={depth > 0 ? \"sm\" : \"default\"}\n className={cn(\"shrink-0\", className)}\n {...props}\n >\n {children ?? (\n <>\n {person?.avatar && <AvatarImage src={person.avatar} alt=\"\" />}\n <AvatarFallback>{initialsOf(person)}</AvatarFallback>\n </>\n )}\n </Avatar>\n )\n}\n\nfunction CommentContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"comment-content\" className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", className)} {...props} />\n}\n\nfunction CommentHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-header\"\n className={cn(\"flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm\", className)}\n {...props}\n />\n )\n}\n\n/** Author name from `people` by default. */\nfunction CommentAuthor({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const { people } = useCommentThread()\n const { authorId } = useComment()\n return (\n <span data-slot=\"comment-author\" className={cn(\"font-medium\", className)} {...props}>\n {children ?? people.find((p) => p.id === authorId)?.name ?? \"Unknown\"}\n </span>\n )\n}\n\nfunction CommentTime({\n date,\n now,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"time\">, \"dateTime\"> & { date: Date; now?: Date }) {\n return (\n <time\n data-slot=\"comment-time\"\n dateTime={date.toISOString()}\n title={date.toLocaleString()}\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n >\n {children ?? formatRelativeTime(date, { now })}\n </time>\n )\n}\n\n/** Small trailing note, e.g. \"edited\" or a role badge. */\nfunction CommentMeta({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"comment-meta\" className={cn(\"text-xs text-muted-foreground\", className)} {...props} />\n}\n\n/** Split text into runs, turning `@Full Name` for known people into mention nodes. */\nfunction renderMentions(text: string, people: Person[]) {\n if (!people.length || !text.includes(\"@\")) return [text]\n const names = [...people].sort((a, b) => b.name.length - a.name.length)\n const pattern = new RegExp(\n `@(${names.map((p) => p.name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")).join(\"|\")})(?![\\\\w])`,\n \"g\"\n )\n const nodes: React.ReactNode[] = []\n let last = 0\n for (const match of text.matchAll(pattern)) {\n const index = match.index ?? 0\n if (index > last) nodes.push(text.slice(last, index))\n const person = people.find((p) => p.name === match[1])\n nodes.push(\n <CommentMention key={`${index}-${match[1]}`} personId={person?.id}>\n @{match[1]}\n </CommentMention>\n )\n last = index + match[0].length\n }\n if (last < text.length) nodes.push(text.slice(last))\n return nodes\n}\n\nfunction CommentMention({ personId, className, ...props }: React.ComponentProps<\"span\"> & { personId?: string }) {\n const { currentUserId } = useCommentThread()\n return (\n <span\n data-slot=\"comment-mention\"\n data-person={personId}\n data-self={personId && personId === currentUserId ? \"\" : undefined}\n className={cn(\n \"rounded-sm bg-primary/10 px-1 font-medium text-primary data-self:bg-amber-500/20 data-self:text-amber-700 dark:data-self:text-amber-300\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** Plain text with mentions highlighted; pass `children` to render anything else. */\nfunction CommentBody({ text, className, children, ...props }: React.ComponentProps<\"div\"> & { text?: string }) {\n const { people } = useCommentThread()\n return (\n <div\n data-slot=\"comment-body\"\n className={cn(\"text-sm/relaxed wrap-break-word whitespace-pre-wrap\", className)}\n {...props}\n >\n {children ?? (text !== undefined ? renderMentions(text, people) : null)}\n </div>\n )\n}\n\nfunction CommentReactions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-reactions\"\n className={cn(\"flex flex-wrap items-center gap-1 pt-0.5\", className)}\n {...props}\n />\n )\n}\n\nfunction CommentReaction({\n emoji,\n count = 0,\n active = false,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ark.button> & { emoji: string; count?: number; active?: boolean }) {\n return (\n <ark.button\n type=\"button\"\n data-slot=\"comment-reaction\"\n data-active={active ? \"\" : undefined}\n aria-pressed={active}\n aria-label={`${emoji} ${count}`}\n className={cn(\n \"inline-flex h-6 items-center gap-1 rounded-full border bg-background px-1.5 text-xs transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none data-active:border-primary/40 data-active:bg-primary/10\",\n className\n )}\n {...props}\n >\n <span aria-hidden>{emoji}</span>\n {children ?? <span className=\"tabular-nums\">{count}</span>}\n </ark.button>\n )\n}\n\n/** Row of small text buttons; revealed on hover/focus, always visible when the comment is active. */\nfunction CommentActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-actions\"\n className={cn(\n \"flex items-center gap-1 text-xs text-muted-foreground opacity-0 transition-opacity group-focus-within/comment:opacity-100 group-hover/comment:opacity-100 group-data-editing/comment:opacity-100 group-data-replying/comment:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommentReplyTrigger({ asChild, children, onClick, className, ...props }: React.ComponentProps<typeof Button>) {\n const { replyTo, setReplyTo } = useCommentThread()\n const { value } = useComment()\n return (\n <Button\n data-slot=\"comment-reply-trigger\"\n variant=\"ghost\"\n size=\"xs\"\n asChild={asChild}\n aria-pressed={replyTo === value}\n className={cn(\"h-6 px-1.5 text-xs\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setReplyTo(replyTo === value ? null : value)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <CornerDownRightIcon /> Reply\n </>\n ))}\n </Button>\n )\n}\n\nfunction CommentEditTrigger({ asChild, children, onClick, className, ...props }: React.ComponentProps<typeof Button>) {\n const { editing, setEditing } = useCommentThread()\n const { value } = useComment()\n return (\n <Button\n data-slot=\"comment-edit-trigger\"\n variant=\"ghost\"\n size=\"xs\"\n asChild={asChild}\n aria-pressed={editing === value}\n className={cn(\"h-6 px-1.5 text-xs\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setEditing(editing === value ? null : value)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <PencilIcon /> Edit\n </>\n ))}\n </Button>\n )\n}\n\n/** Nested replies with a guide line; put `Comment`s inside. */\nfunction CommentReplies({ className, ...props }: React.ComponentProps<\"ol\">) {\n return (\n <ol data-slot=\"comment-replies\" className={cn(\"mt-2 flex flex-col gap-3 border-s-2 ps-3\", className)} {...props} />\n )\n}\n\nfunction CommentEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"comment-empty\"\n className={cn(\"rounded-lg border border-dashed px-3 py-6 text-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Composer with mentions\n * ------------------------------------------------------------------------- */\n\ntype MentionState = { query: string; start: number; index: number } | null\n\ntype ComposerContextValue = {\n /** People who can be mentioned and whose names and initials are rendered. */\n people: Person[]\n value: string\n setValue: (value: string) => void\n mention: MentionState\n setMention: (mention: MentionState) => void\n suggestions: Person[]\n pick: (person: Person) => void\n /** The input registers how a pick is applied to its DOM. */\n registerPicker: (fn: (person: Person, mention: NonNullable<MentionState>) => void) => () => void\n submit: () => void\n cancel: () => void\n canSubmit: boolean\n inputRef: React.RefObject<HTMLDivElement | null>\n listId: string\n}\nconst ComposerContext = React.createContext<ComposerContextValue | null>(null)\nfunction useComposer() {\n const ctx = React.useContext(ComposerContext)\n if (!ctx) throw new Error(\"Composer parts must be used within <CommentComposer>\")\n return ctx\n}\n\n/** Ids of people whose `@Full Name` appears in the text. */\nfunction extractMentions(text: string, people: Person[]) {\n return people\n .filter((p) => new RegExp(`@${p.name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w])`).test(text))\n .map((p) => p.id)\n}\n\nfunction CommentComposer({\n people = [],\n value: valueProp,\n defaultValue = \"\",\n onValueChange,\n onSubmit,\n onCancel,\n maxSuggestions = 6,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onSubmit\" | \"defaultValue\"> & {\n /** People who can be mentioned and whose names and initials are rendered. */\n people?: Person[]\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n /** Called with the trimmed text and the ids of mentioned people. */\n onSubmit?: (comment: { text: string; mentions: string[] }) => void\n /** Called when the composer is cancelled with Escape or the cancel trigger. */\n onCancel?: () => void\n /** Maximum number of mention suggestions shown. */\n maxSuggestions?: number\n}) {\n const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange)\n const [mention, setMention] = React.useState<MentionState>(null)\n const inputRef = React.useRef<HTMLDivElement>(null)\n const listId = React.useId()\n const pickerRef = React.useRef<((person: Person, mention: NonNullable<MentionState>) => void) | null>(null)\n const registerPicker = React.useCallback((fn: (person: Person, mention: NonNullable<MentionState>) => void) => {\n pickerRef.current = fn\n return () => {\n if (pickerRef.current === fn) pickerRef.current = null\n }\n }, [])\n\n const suggestions = React.useMemo(() => {\n if (!mention) return []\n const q = mention.query.toLowerCase()\n return people.filter((p) => p.name.toLowerCase().includes(q)).slice(0, maxSuggestions)\n }, [mention, people, maxSuggestions])\n\n const pick = React.useCallback(\n (person: Person) => {\n if (!mention) return\n pickerRef.current?.(person, mention)\n setMention(null)\n },\n [mention]\n )\n\n const canSubmit = value.trim().length > 0\n const submit = React.useCallback(() => {\n if (!canSubmit) return\n onSubmit?.({ text: value.trim(), mentions: extractMentions(value, people) })\n setValue(\"\")\n setMention(null)\n }, [canSubmit, onSubmit, value, people, setValue])\n const cancel = React.useCallback(() => {\n setValue(\"\")\n setMention(null)\n onCancel?.()\n }, [setValue, onCancel])\n\n const ctx = React.useMemo<ComposerContextValue>(\n () => ({\n people,\n value,\n setValue,\n mention,\n setMention,\n suggestions,\n pick,\n registerPicker,\n submit,\n cancel,\n canSubmit,\n inputRef,\n listId,\n }),\n [people, value, setValue, mention, suggestions, pick, registerPicker, submit, cancel, canSubmit, listId]\n )\n\n return (\n <ComposerContext.Provider value={ctx}>\n <div\n data-slot=\"comment-composer\"\n data-mentioning={mention ? \"\" : undefined}\n className={cn(\n \"relative flex flex-col gap-2 rounded-xl border bg-background p-2 transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </ComposerContext.Provider>\n )\n}\n\n/** \"Replying to …\" strip; renders nothing when not replying. The render prop gets the comment id. */\nfunction CommentComposerReplyingTo({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (details: { id: string }) => React.ReactNode }) {\n const { replyTo, setReplyTo } = useCommentThread()\n if (!replyTo) return null\n return (\n <div\n data-slot=\"comment-composer-replying-to\"\n className={cn(\"flex items-center gap-2 px-1 text-xs text-muted-foreground\", className)}\n {...props}\n >\n <CornerDownRightIcon className=\"size-3.5\" />\n {children ? children({ id: replyTo }) : <span>Replying to a comment</span>}\n <Button variant=\"ghost\" size=\"xs\" className=\"ms-auto h-5 px-1.5\" onClick={() => setReplyTo(null)}>\n Cancel\n </Button>\n </div>\n )\n}\n\n/* ----- contenteditable helpers ----- */\n\nconst CHIP = \"comment-mention-chip\"\nconst escapeRe = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\n/** Serialise the editable DOM to plain text; chips become `@Full Name`, blocks and <br> become newlines. */\nfunction serialize(root: HTMLElement): string {\n let out = \"\"\n const walk = (node: Node) => {\n if (node.nodeType === Node.TEXT_NODE) {\n out += (node.textContent ?? \"\").replace(/ /g, \" \")\n return\n }\n if (!(node instanceof HTMLElement)) return\n if (node.dataset.slot === CHIP) {\n out += `@${node.dataset.name ?? \"\"}`\n return\n }\n if (node.tagName === \"BR\") {\n out += \"\\n\"\n return\n }\n const block = node.tagName === \"DIV\" || node.tagName === \"P\"\n if (block && out.length && !out.endsWith(\"\\n\")) out += \"\\n\"\n node.childNodes.forEach(walk)\n }\n root.childNodes.forEach(walk)\n return out.replace(/\\n$/, \"\")\n}\n\nfunction createChip(person: Person) {\n const chip = document.createElement(\"span\")\n chip.dataset.slot = CHIP\n chip.dataset.person = person.id\n chip.dataset.name = person.name\n chip.contentEditable = \"false\"\n chip.className =\n \"mx-px inline-flex items-center rounded-full bg-muted px-1.5 py-px align-baseline text-[0.9em] font-medium text-foreground select-none\"\n chip.textContent = `@${person.name}`\n return chip\n}\n\n/** Build editable DOM from plain text, turning known `@Full Name`s into chips. */\nfunction hydrate(root: HTMLElement, text: string, people: Person[]) {\n root.replaceChildren()\n const names = [...people].sort((a, b) => b.name.length - a.name.length)\n const pattern = names.length ? new RegExp(`@(${names.map((p) => escapeRe(p.name)).join(\"|\")})(?![\\\\w])`, \"g\") : null\n text.split(\"\\n\").forEach((line, lineIndex) => {\n if (lineIndex > 0) root.appendChild(document.createElement(\"br\"))\n let last = 0\n if (pattern) {\n for (const match of line.matchAll(pattern)) {\n const index = match.index ?? 0\n if (index > last) root.appendChild(document.createTextNode(line.slice(last, index)))\n const person = people.find((p) => p.name === match[1])\n if (person) root.appendChild(createChip(person))\n last = index + match[0].length\n }\n }\n if (last < line.length) root.appendChild(document.createTextNode(line.slice(last)))\n })\n}\n\n/** The `@query` immediately before the caret inside a text node, if any. */\nfunction mentionAtCaret(root: HTMLElement): { node: Text; start: number; end: number; query: string } | null {\n const selection = window.getSelection()\n if (!selection || !selection.rangeCount || !selection.isCollapsed) return null\n const range = selection.getRangeAt(0)\n const node = range.startContainer\n if (node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return null\n const text = node.textContent ?? \"\"\n const caret = range.startOffset\n const before = text.slice(0, caret)\n const at = before.lastIndexOf(\"@\")\n if (at === -1) return null\n if (at > 0 && /\\S/.test(before[at - 1])) return null\n const query = before.slice(at + 1)\n if (query.length > 30) return null\n return { node: node as Text, start: at, end: caret, query }\n}\n\nfunction placeCaretAfter(node: Node) {\n const selection = window.getSelection()\n if (!selection) return\n const range = document.createRange()\n range.setStartAfter(node)\n range.collapse(true)\n selection.removeAllRanges()\n selection.addRange(range)\n}\n\n/**\n * Contenteditable input: picked mentions become atomic grey chips (one\n * Backspace removes a whole chip); `value` stays plain `@Full Name` text.\n * Arrows/Enter/Tab pick from the list, Escape closes it, ⌘/Ctrl+Enter submits.\n */\nfunction CommentComposerInput({\n className,\n placeholder = \"Write a comment…\",\n autoFocus,\n onKeyDown,\n onBlur,\n onInput,\n onPaste,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\"> & { placeholder?: string; autoFocus?: boolean }) {\n const c = useComposer()\n const lastSerialized = React.useRef<string | null>(null)\n\n // Reflect outside changes to `value` (initial text, edits, reset after submit) without disturbing typing.\n React.useLayoutEffect(() => {\n const el = c.inputRef.current\n if (!el || c.value === lastSerialized.current) return\n hydrate(el, c.value, c.people)\n lastSerialized.current = c.value\n }, [c.value, c.people, c.inputRef])\n\n React.useEffect(() => {\n if (autoFocus) c.inputRef.current?.focus()\n }, [autoFocus, c.inputRef])\n\n const { registerPicker, setValue, inputRef } = c\n React.useEffect(\n () =>\n registerPicker((person) => {\n const el = inputRef.current\n if (!el) return\n const hit = mentionAtCaret(el)\n const chip = createChip(person)\n const space = document.createTextNode(\" \")\n if (hit) {\n const range = document.createRange()\n range.setStart(hit.node, hit.start)\n range.setEnd(hit.node, hit.end)\n range.deleteContents()\n range.insertNode(space)\n range.insertNode(chip)\n } else {\n el.append(chip, space)\n }\n placeCaretAfter(space)\n el.focus()\n const next = serialize(el)\n lastSerialized.current = next\n setValue(next)\n }),\n [registerPicker, inputRef, setValue]\n )\n\n const sync = () => {\n const el = c.inputRef.current\n if (!el) return\n if (el.innerHTML === \"<br>\") el.replaceChildren()\n const next = serialize(el)\n lastSerialized.current = next\n c.setValue(next)\n const hit = mentionAtCaret(el)\n c.setMention(hit ? { query: hit.query, start: hit.start, index: 0 } : null)\n }\n\n return (\n <div\n ref={c.inputRef}\n data-slot=\"comment-composer-input\"\n role={c.mention ? \"combobox\" : \"textbox\"}\n contentEditable\n suppressContentEditableWarning\n aria-multiline={c.mention ? undefined : true}\n aria-expanded={c.mention ? true : undefined}\n aria-controls={c.mention ? c.listId : undefined}\n aria-autocomplete={c.mention ? \"list\" : undefined}\n aria-activedescendant={\n c.mention && c.suggestions[c.mention.index] ? `${c.listId}-${c.suggestions[c.mention.index].id}` : undefined\n }\n data-placeholder={placeholder}\n className={cn(\n \"min-h-20 px-2 py-1.5 text-sm wrap-break-word whitespace-pre-wrap outline-none\",\n \"empty:before:pointer-events-none empty:before:text-muted-foreground empty:before:content-[attr(data-placeholder)]\",\n className\n )}\n onInput={(event) => {\n onInput?.(event)\n sync()\n }}\n onKeyUp={(event) => {\n if ([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"].includes(event.key)) sync()\n }}\n onClick={() => sync()}\n onPaste={(event) => {\n onPaste?.(event)\n if (event.defaultPrevented) return\n event.preventDefault()\n document.execCommand(\"insertText\", false, event.clipboardData.getData(\"text/plain\"))\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if ((event.metaKey || event.ctrlKey) && event.key === \"Enter\") {\n event.preventDefault()\n c.submit()\n return\n }\n if (!c.mention) {\n if (event.key === \"Escape\") c.cancel()\n return\n }\n const count = c.suggestions.length\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault()\n if (!count) return\n const delta = event.key === \"ArrowDown\" ? 1 : -1\n c.setMention({ ...c.mention, index: (c.mention.index + delta + count) % count })\n } else if ((event.key === \"Enter\" || event.key === \"Tab\") && count) {\n event.preventDefault()\n c.pick(c.suggestions[c.mention.index])\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n c.setMention(null)\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n setTimeout(() => {\n if (document.activeElement !== event.target) c.setMention(null)\n }, 120)\n }}\n {...props}\n />\n )\n}\n\n/** Floating suggestion list anchored under the input while an `@query` is active. Focus stays in the textarea. */\nfunction CommentComposerMentionList({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const c = useComposer()\n const open = !!c.mention\n return (\n <Popover\n open={open}\n onOpenChange={({ open }) => {\n if (!open) c.setMention(null)\n }}\n autoFocus={false}\n modal={false}\n positioning={{\n placement: \"bottom-start\",\n sameWidth: true,\n gutter: 4,\n getAnchorRect: () => {\n const rect = c.inputRef.current?.getBoundingClientRect()\n return rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null\n },\n }}\n >\n <PopoverContent\n id={c.listId}\n role=\"listbox\"\n aria-label=\"People\"\n data-slot=\"comment-composer-mention-list\"\n className={cn(\"max-h-56 w-auto overflow-y-auto p-1\", className)}\n onPointerDown={(event) => event.preventDefault()}\n {...props}\n >\n {c.suggestions.length === 0 ? (\n <div className=\"px-2 py-1.5 text-sm text-muted-foreground\">No one matches \"{c.mention?.query}\"</div>\n ) : (\n c.suggestions.map((person, index) => (\n <div\n key={person.id}\n id={`${c.listId}-${person.id}`}\n role=\"option\"\n aria-selected={index === c.mention?.index}\n data-slot=\"comment-composer-mention-item\"\n data-highlighted={index === c.mention?.index ? \"\" : undefined}\n className=\"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm data-highlighted:bg-accent data-highlighted:text-accent-foreground\"\n onMouseEnter={() => c.mention && c.setMention({ ...c.mention, index })}\n onClick={() => c.pick(person)}\n >\n <Avatar size=\"sm\" className=\"size-6 text-[10px]\">\n {person.avatar && <AvatarImage src={person.avatar} alt=\"\" />}\n <AvatarFallback>{initialsOf(person)}</AvatarFallback>\n </Avatar>\n <span className=\"truncate\">{person.name}</span>\n </div>\n ))\n )}\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction CommentComposerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"comment-composer-footer\" className={cn(\"flex items-center gap-2 px-1\", className)} {...props} />\n )\n}\n\nfunction CommentComposerHint({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"comment-composer-hint\"\n className={cn(\"me-auto text-xs text-muted-foreground\", className)}\n {...props}\n >\n {children ?? (\n <>\n <Kbd>@</Kbd> to mention · <Kbd>⌘↵</Kbd> to send\n </>\n )}\n </span>\n )\n}\n\nfunction CommentComposerSubmitTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { submit, canSubmit } = useComposer()\n return (\n <Button\n data-slot=\"comment-composer-submit-trigger\"\n size=\"sm\"\n asChild={asChild}\n disabled={!canSubmit}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) submit()\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <SendIcon /> Comment\n </>\n ))}\n </Button>\n )\n}\n\nfunction CommentComposerCancelTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { cancel } = useComposer()\n return (\n <Button\n data-slot=\"comment-composer-cancel-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) cancel()\n }}\n {...props}\n >\n {asChild ? children : (children ?? \"Cancel\")}\n </Button>\n )\n}\n\nexport {\n CommentThread,\n CommentList,\n Comment,\n CommentAvatar,\n CommentContent,\n CommentHeader,\n CommentAuthor,\n CommentTime,\n CommentMeta,\n CommentBody,\n CommentMention,\n CommentReactions,\n CommentReaction,\n CommentActions,\n CommentReplyTrigger,\n CommentEditTrigger,\n CommentReplies,\n CommentEmpty,\n CommentComposer,\n CommentComposerReplyingTo,\n CommentComposerInput,\n CommentComposerMentionList,\n CommentComposerFooter,\n CommentComposerHint,\n CommentComposerSubmitTrigger,\n CommentComposerCancelTrigger,\n useCommentThread,\n useComment,\n extractMentions,\n renderMentions,\n type Person as CommentPerson,\n}\n"
54
1005
  }
55
1006
  ],
56
1007
  "docs": "## Activity feed and comments\n`src/components/ui/activity-feed.tsx` is a compositional timeline of events on the WAI-ARIA feed\npattern (`role=\"feed\"`, focusable articles, PageUp/PageDown move, Ctrl+Home/End jump):\n`ActivityFeed` > `ActivityFeedGroup` > `ActivityFeedGroupLabel` + `ActivityFeedItems` (draws the\nconnector line) > `ActivityFeedItem value=` > `ActivityFeedItemMarker` (dot by default; pass an\nicon or an Avatar) + `ActivityFeedItemContent` > `ActivityFeedItemHeader` > `ActivityFeedItemActor`,\n`ActivityFeedItemTime date=` (`<time>` with a relative label from `src/lib/time.ts`\n`formatRelativeTime`; full date in the title), `ActivityFeedItemBody`, `ActivityFeedItemActions`;\nplus `ActivityFeedLoadMoreTrigger` and `ActivityFeedEmpty`. Data, grouping and read state are the\nconsumer's.\n`src/components/ui/comment-thread.tsx` is compositional comments with replies, reactions and a\nmentions composer. `CommentThread { people, currentUserId, replyTo/onReplyToChange }` owns only\nwhich comment is being replied to or edited (`useCommentThread()` → `replyTo`, `editing`, setters).\nParts: `CommentList` > `Comment value= authorId=` (`data-own/replying/editing/depth`) >\n`CommentAvatar` (initials from `people`) + `CommentContent` > `CommentHeader` > `CommentAuthor`,\n`CommentTime date=`, `CommentMeta`; `CommentBody text=` (plain text with `@Full Name` mentions\nhighlighted as `CommentMention`, `data-self` for the current user); `CommentReactions` >\n`CommentReaction emoji= count= active=`; `CommentActions` (hover-revealed) > `CommentReplyTrigger`,\n`CommentEditTrigger`; `CommentReplies` (nested list with a guide line); `CommentEmpty`.\n`CommentComposer { people, value/onValueChange, onSubmit({ text, mentions }), onCancel }` >\n`CommentComposerReplyingTo` (render prop gets the comment id), `CommentComposerInput` (a\ncontenteditable, not a textarea: picked mentions become atomic grey chips\n`[data-slot=comment-mention-chip]` that one Backspace removes, while `value` stays plain\n`@Full Name` text and `defaultValue`/`value` text is hydrated back into chips;\n`@query` at the caret opens `CommentComposerMentionList` (a portaled Popover anchored to the input, focus stays in the textarea, so no `overflow-hidden` ancestor clips it and nothing shifts), arrows/Enter/Tab pick, Escape closes,\n⌘/Ctrl+Enter submits, `aria-activedescendant` wired), `CommentComposerFooter` >\n`CommentComposerHint`, `CommentComposerCancelTrigger`, `CommentComposerSubmitTrigger`. Mentions are\nplain `@Full Name` text; `extractMentions(text, people)` returns ids. The same composer edits in\nplace (`defaultValue`, Save). `src/demo/activity/` is the reference (`/activity`): grouped feed\nwith All/Mentions/Comments filter, unread dots, load more, and a ticket thread with nested\nreplies, reactions, edit/delete of own comments (Popconfirm), and posting that also prepends a\nfeed item."