ComponentsData display

Comment thread

Threaded discussion on a record — editorial notes on a post, CRM notes on a deal, feedback on a candidate, a partner ticket. One reply level, @people and #record mentions with a picker, attachments, reactions, edit and delete, resolve and collapse, and optimistic sending / failed states.

Plain textarea + combobox-pattern mention picker; ARIA feed components/comment-thread.tsx, components/attachment-list.tsxtheme · material · accent · density · font

Daniel Brooks,

@Ayşe Kaya Northwind asked for a 12-month price lock. Can we offer it if they sign before Dec 15?

Ayşe Kaya (You),

Yes, if the seat count stays at 40 or more. Looping in @Legal for the clause wording.

Mehmet Yılmaz,

· edited
SSO setup is included either way, so that's not a blocker.

Jane Cooper,

Our finance team needs the final quote as a PDF by Friday.

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { CommentThread } from "@vamos-ui/react";

<CommentThread
  comments={comments}
  currentUserId={me.id}
  currentUser={me}
  mentionSources={[{ search: (q) => api.people(q) }]}
  onSubmit={({ body, parentId, mentions }) => api.comment(dealId, { body, parentId })}
  onResolve={(id, resolved) => api.resolve(id, resolved)}
/>

// Stored bodies keep mentions as tokens:
// "@[Jane Cooper](user:42) can you check line 3?"

Keyboard: the list is an ARIA feed — PageDown / PageUp move between comments. Reply moves focus into the reply box and back after sending.

The mention picker follows the combobox pattern on the textarea (aria-autocomplete, aria-controls, aria-activedescendant) and announces the number of suggestions politely.

Examples

@people and #records

One source per trigger. ↑/↓ move, Enter or Tab insert, Esc closes; the submitted body holds @[Name](kind:id) tokens.

Try “@ilk” (Turkish case folding) or “#north”, then press ⌘/Ctrl+Enter.

Files in comments (HR)

allowAttachments adds a paperclip; files arrive in onSubmit and render as chips.

Sending and failed (PRM)

Add the comment with status "sending" right away; on error set "failed" and handle Retry / Discard.

Kadıköy Roasters,

The October commission statement shows 14 orders, our dashboard says 16.

Ayşe Kaya (You),

Checking now — two orders were refunded on Oct 30.

Resolve, collapse and deleted (CMS)

Resolved threads collapse their replies behind "Show"; a deleted comment with replies leaves a placeholder.

Meltem Öz,

Resolved by İlker Şahin
The intro paragraph promises “10 tips” but the post has 8.

Meltem Öz,

Hero image is 4.1 MB — please export it as WebP under 300 KB.

Side panel with sticky composer

layout="compact" and composer="sticky-bottom" inside a scrolling panel.

API

CommentThread

PropTypeDefault
comments
All comments; replies point to their top-level comment with parentId.
CommentData[]—
currentUserId / currentUser
Own comments get Edit and Delete; the avatar sits beside the composer.
string / CommentAuthor—
onSubmit
Post a comment or reply. Without it (or with readOnly) there is no composer.
({ body, parentId?, attachments, mentions }) => void | Promise<void>—
onEdit / onDelete
Inline edit (Esc cancels) and delete (confirm with an undo toast in your handler).
(id, body) => … / (id) => …—
onResolve
Resolve / reopen top-level comments.
(id, resolved) => void | Promise<void>—
onRetry / onDiscard
Actions on failed comments.
(comment) => void—
onReact / reactions
Reaction toggles and the picker; default set is icon-based (like, agree, love, question, seen).
(id, key, on) => void / ReactionOption[]—
mentionSources
{ search(q), trigger?: "@" | "#" } — async, locale-folded ranking.
MentionSource[]—
renderMention
Replace the default chip (e.g. link a #record).
(mention) => ReactNode—
getLink
Shows "Copy link".
(comment) => string—
allowAttachments
Paperclip in the composer.
booleanfalse
onPreviewAttachment
Open an attached file.
(file) => void—
order
Top-level order; replies are always oldest first.
"oldest-first" | "newest-first""oldest-first"
layout
Full or side-panel density.
"thread" | "compact""thread"
composer
Where the new-comment box sits.
"inline" | "sticky-bottom" | "top""inline"
readOnly
No composer or actions.
booleanfalse
now
Pin relative times (docs, tests).
Date | string | number—
labels
All built-in strings (empty, placeholder, reply, resolve, deleted, sending, failed, …).
Partial<CommentThreadLabels>—

CommentData

PropTypeDefault
id / author / body / createdAt
Body is the stored form with @[Name](kind:id) tokens.
string / CommentAuthor / string / string—
parentId
Reply to this top-level comment.
string—
editedAt
Shows "edited" with the time as a tooltip.
string—
attachments
Rendered as chips.
AttachmentData[]—
resolved / resolvedBy
Collapsed state and who resolved it.
boolean / string—
status
Optimistic states.
"sending" | "failed"—
deleted
Placeholder when it has replies, hidden otherwise.
boolean—
reactions
Reaction counts.
{ key; count; reacted?; users? }[]—

Parts and helpers

PropTypeDefault
CommentComposer
The box on its own (⌘/Ctrl+Enter sends).
onSubmit, mentionSources, allowAttachments, defaultValue, onCancel, variant—
Comment
One <article> labelled by author and time.
comment, currentUserId, isReply, handlers—
CommentBody / MentionChip
Render a stored body; one mention token.
body / mention—
parseMentions(body)
Plain text plus ids and offsets.
{ text, mentions[] }—
serializeMention(item)
"@[Jane Cooper](user:42)".
string—