raft-ui
Version:
React UI components for Raft.
189 lines (137 loc) • 13.1 kB
Markdown
---
name: conversation
description: "Raft conversation surfaces: Sidebar, ConversationPanel vs ThreadPanel, MessageList/MessageItem satellite families, SystemMessage, Composer structure."
---
# Conversation surfaces
These encode the Raft product shape. Reach for them before assembling anything chat-like out of primitives.
## Contents
- Which surface
- ConversationPanel vs ThreadPanel
- Activity inbox and workspace search
- Composer vs Textarea
- Sidebar structure
- Message surfaces
- Composer structure
- What stays in your app
---
## Which surface
| Scenario | Component |
| ----------------------------------- | ----------------------------- |
| Channel and DM list | `SidebarRoot` |
| The main conversation column | `ConversationPanelRoot` |
| A thread beside the conversation | `ThreadPanelRoot` |
| The scrolling list of messages | `MessageList` + `MessageItem` |
| One human or agent message | `MessageItem` |
| Join/leave/rename event in the flow | `SystemMessage` |
| Message input | `ComposerRoot` + `Composer` |
| Notification inbox | `NotificationCenter` |
| Activity or saved-item inbox | `ActivityInboxPanel` |
| Workspace search results | `SearchShellRoot` |
| Live agent activity strip | `LiveAgentActivityBar` |
The sidebar family is named `Sidebar*`, not `ChatSidebar*` — it was generalized to shared navigation primitives. If you find `ChatSidebar` anywhere, it is pre-0.2 code.
---
## ConversationPanel vs ThreadPanel
They are not the same component with a different width.
| | `ConversationPanel` | `ThreadPanel` |
| --------------------- | ------------------------------ | ---------------------------------- |
| Shows | a channel or DM | replies to one message |
| Header | channel identity, status, tabs | the thread's title and reply count |
| Has an origin message | no | yes — `ThreadPanelOriginalMessage` |
`ThreadPanel` carries thread-specific parts that have no equivalent in the conversation column: `ThreadPanelOriginalMessage`, `ThreadPanelReplyCount`, `ThreadPanelReplySummary`, and an in-thread search (`ThreadPanelSearch`, `ThreadPanelSearchCount`, `ThreadPanelSearchTrailing`).
`ConversationPanel` keeps its own root, body, tabs, and empty state (`ConversationPanelRoot`, `ConversationPanelBody`, `ConversationPanelContent`, `ConversationPanelFooter`, `ConversationPanelTabs`, `ConversationPanelEmptyState` with `…Brand` / `…BrandMark`), but its **header chrome is the shared `Panel*` set owned by the `Panel` component**: `PanelHeader` with `PanelHeaderContent` / `PanelHeaderIcon` / `PanelHeading` / `PanelTitle` / `PanelMeta` / `PanelActions`, plus `PanelStatus` and `PanelActivity` — see the header guidance in [layout.md](./layout.md).
Use `ConversationPanelEmptyState` rather than the generic `EmptyState` inside a conversation.
---
## Activity inbox and workspace search
These are product-shaped presentation surfaces, not state managers.
| Surface | Compose | Keep in the app |
| ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Activity inbox | `ActivityInboxPanel`, sidebar, controls, selection-toolbar, viewport, and loading-more parts | filters, selection, bulk actions, pagination, loading policy |
| Reusable inbox row | `InboxItem` + `InboxItemTrigger`, content, metadata, trailing, and action parts | entity data, read/unread state, navigation, permissions |
| Workspace search | `SearchShellRoot`, filters, viewport, result sections and lists | query, filters, fetching, ranking, result selection |
| Search result | `SearchEntityResult*`, `SearchMessageResult*`, or `SearchThreadResult*` | result data, destination, domain-specific copy |
Use `NotificationCenter` for delivered notifications that persist until handled. Use
`ActivityInboxPanel` for a navigable activity or saved-item workflow. Use `SearchShellRoot` for
cross-entity search results; do not substitute `Combobox`, which is a value picker with collection
behavior.
`InboxItemTrigger` is the row's primary action and inherits the root's disabled state. Compose
secondary controls inside `InboxItemActions`; do not put navigation policy or bulk-selection state
into the component.
Search result parts own presentation and selected styling, not search semantics. Pick the result
family that matches the rendered entity rather than switching one result component with a `kind`
prop.
---
## Composer vs Textarea
`Textarea` is a form control. `Composer` is a message-sending surface.
Use the `ComposerRoot` / `Composer` family whenever the input sends a message. It provides the attachment, drop-overlay, toolbar, submit, and suggestion parts; `SortableComposerAttachments` and `useComposerSuggestions` own the reusable behavior they name. Your app still owns selected files, upload lifecycle, send state, and submit policy.
Use `Textarea` for a description field in a form.
---
## Sidebar structure
Deeply parted by design. Render the parts you need; do not pass item config objects.
Naming is regular at three levels — guess by pattern, then confirm against the declarations ([lookup.md](./lookup.md)):
| Level | Prefix | Anchor parts |
| ----------- | ----------------- | -------------------------------------------------------------------- |
| Frame | `Sidebar*` | `SidebarRoot`, `SidebarHeader`, `SidebarBody`, `SidebarLiveActivity` |
| Section | `SidebarSection*` | `…Header`, `…Title`, `…Count`, `…Chevron`, `…Disclosure`, `…Empty` |
| Row | `SidebarItem*` | `…Icon`, `…Title`, `…Subtitle`, `…Meta`, `…Count`, `…Status` |
| Footer menu | `SidebarMenu*` | `…Icon`, `…Title`, `…Count`, `…Aside` |
- Unread counts go in `SidebarItemCount`, not appended to the title string.
- Presence goes in `SidebarItemStatus`, not a hand-placed dot.
- `SidebarGroupLabel` labels a group of sections; `SidebarSectionTitle` labels one section.
- `SidebarLiveActivity` is the slot in the sidebar frame; the standalone strip is `LiveAgentActivityBar` (stable) with `…Row` / `…Avatar` / `…Status` / `…Text` parts.
- `SidebarItem`'s `variant` selects the active treatment. In Brutal, `default` is neutral white,
`primary` is yellow, and `accent` is pink; Elegant keeps one theme-owned active surface. Use
`variant="accent"` when migrating a row that relied on the previous pink active default.
- `LiveAgentActivityBar` keeps its enter and exit lifecycle when `beam={false}`; that option only
removes the Elegant animated border and glow. Brutal does not render the beam.
---
## Message surfaces
The message family is large and satellite-structured: `MessageItem` is the anchor, and each concern is its own part family with a regular prefix. Do not hand-roll any of these — every state below already has a dedicated part.
| Prefix | Owns |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `MessageList*` | the scroller: `MessageListDateDivider`, `…Notice`, `…HistoryLimited` |
| `MessageItem*` | one message: `…Header`, `…Sender`, `…Time`, `…Body`, `…Footer`, `…Gutter`, `…Toolbar`, `…ReadReceipt`, `…SavedIndicator`, `…SearchMatch` |
| `MessageReaction*` | reaction chips, quick row, picker, tooltip |
| `MessageReference*` | reference chips: `…Chip`, `…Pending`, `…Unavailable` |
| `MessageAttachment*` | attachment cards, diff summaries; plus `MessageImageGallery*` and `MessageVideoPreview*` |
| `MessageEmbed*` | link embeds |
| `MessageActionCard*` | agent action cards with scope and properties |
| `MessageQuotedPreview*` | quoted message preview, including `…Loading` and `…Unavailable` |
| `MessageReplies*` | the reply summary row under a message |
| `MessageForwardedBundle*` | forwarded message bundles |
| `MessageItemTranslation*` | translation status, original text, retry |
| `MessageMultiSelect*` | multi-select checkboxes and toolbar |
| `MessageTextSelectionToolbar*` | the toolbar that appears over selected text |
`SystemMessage` is separate from `MessageItem` — join/leave/rename events use `SystemMessage` + `SystemMessageTime`, and consecutive events collapse with the `SystemMessageGroup*` disclosure parts.
Product-state parts follow the library-wide pattern: the consumer decides which state part to render (`MessageReferencePending`, `MessageQuotedPreviewUnavailable`, `MessageItemTranslationFailedStatus`), the root never takes a `state` prop.
For message text content, the `Markdown*` renderer parts (see [display.md](./display.md)) style each markdown node.
---
## Composer structure
```
ComposerRoot
ComposerAttachments → ComposerAttachment → ComposerAttachmentImage | ComposerAttachmentFile,
ComposerAttachmentBody, ComposerAttachmentTitle,
ComposerAttachmentMeta, ComposerAttachmentRemove,
ComposerAttachmentUploadingOverlay,
ComposerAttachmentUploadProgressBar,
ComposerAttachmentFailedOverlay
ComposerInput
ComposerToolbar → ComposerToolbarLabel, ComposerIconButton
ComposerActions → ComposerSubmit
ComposerMeta
ComposerDropOverlay → ComposerDropOverlayLabel, ComposerDropOverlayPrompt
```
- `ComposerSubmit` is a `Button` locked to `variant="accent"` — the core-action color. Its props omit `variant`; do not try to restyle it into a yellow or default button.
- Upload state has dedicated overlay parts. Render `ComposerAttachmentUploadingOverlay` while uploading and `ComposerAttachmentFailedOverlay` on failure — do not dim the tile with a className.
- Reorderable attachments use `SortableComposerAttachments` / `SortableComposerAttachment` / `SortableComposerAttachmentsScroll`.
- Mentions and slash commands use the `useComposerSuggestions` hook with `ComposerSuggestionList`, `ComposerSuggestionGroup`, `ComposerSuggestionOption`, `ComposerSuggestionIcon`, `ComposerSuggestionTitle`, `ComposerSuggestionMeta`, `ComposerSuggestionAside`.
Set `ComposerSuggestionIcon variant` to `avatar`, `framed`, or `auxiliary`, and set `ComposerSuggestionMeta variant` to `text` or `code`. Pick `avatar` for people, `framed` for channels and commands, and `code` for anything the user types literally.
The suggestion parts are **styling slots**, not a combobox state machine. `useComposerSuggestions` owns matching and keyboard behavior; the parts render it.
---
## What stays in your app
The conversation components are business-agnostic shells. These belong to you, not to a prop on the component:
- Upload lifecycle and retry policy
- Send permissions, read-only channels, rate limits
- Notification and mute policy
- Read/unread tracking
- Which entities exist and what they are called
The pattern throughout is the same: the library exposes the parts, you choose which to render based on your state.