@multicomma/arkcn 0.1.0

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