--- title: Chat UI description: Reusable Chat UI components, working demos, and a Cal integration guide for engineers and agents. --- This page turns the [Chat UI Primitives checklist](https://linear.app/calcom/project/chat-ui-primitives-aa8ff4101f53) into a working example and maps the pieces as generic, reusable UI. The [Cal Events Chat project](https://linear.app/calcom/project/cal-events-chat-ee6d3fcd92ce) is one planned consumer. ## Start here Use this page as the implementation entry point. Read the sections in this order: 1. [One Chat UI family](#one-chat-ui-family) — imports, public components, and state ownership. 2. [Minimal working example](#minimal-working-example) — copyable inline composition. 3. [Implement in the Cal app](#implement-in-the-cal-app) — data mapping, permissions, state transitions, and drawer integration. 4. [Integration checklist](#integration-checklist) — behavior to preserve and decisions to resolve before shipping. | Layer | Status and source | | --- | --- | | Reusable presentation | Implemented locally in `packages/coss-ui/src/chat/`; import directly from `@coss/ui/chat/*`. No chat provider or backend schema is required. | | Complete drawer | Working design-system example with synthetic data and local mutations. It is an integration reference, not a connected Cal chat. | | Cal integration | The application must provide authorization, persistence, live updates, eligibility, and delivery data. See the explicit [contract decisions](#contract-decisions). | | Coss distribution | Planned source-registry rebuild. The workspace package is private; no public Chat UI registry installation is available yet. | | Anatomy maps | Include exploration items beyond the working example. They are not an implementation checklist for the first release. | For agents, [raw Markdown](/api/raw/docs/composites/chat-ui) and **Copy Markdown** include the code and integration guidance below. Interactive previews alone do not expose their implementation in Markdown. ## Complete example Open the chat drawer to try one group conversation across multiple days. It rises from the bottom below `md` and opens from the right on wider screens. Select the member names below the title to view the group, mute a guest, remove them, or ban them. The current user is a co-host; host and co-host rows have no moderation menu. Scroll the timeline to see a date pill stay in view until the next day's messages arrive. Reply to a message, add a reaction, or type `@` to mention someone. Sending a message triggers a simulated response, so you can try the typing and read states. Messages, membership, and drafts stay in local demo state. Use the state triggers below the preview to open the drawer with a long message, pinned message, one or several people typing, first-message, loading, history-error, disconnected, access-lost, removed-from-chat, banned-from-chat, left-chat, unread, or delivery/retry example. The default conversation has no pinned message; choose **Pinned message** to see the pinned bar. The **Long message** state shows the expand and collapse control. The **First message** state introduces the event chat and shows who can access it; joining reveals the empty conversation and composer. The history-error, access-lost, removed-from-chat, banned-from-chat, and left-chat previews show a Coss Empty with no composer. **Reconnect** briefly shows its loading state before restoring the conversation, and **Rejoin chat** returns from the left-chat state. Opening a state resets sample messages and cancels pending replies, while keeping your draft. Loading and receipt examples stay in their illustrated states until you interact. Recovery and retry actions are local simulations. Choose **Message info** from one of your messages’ action menus to inspect read, delivered, and pending recipients in a nested drawer. It uses the [Coss nested-drawer pattern](https://coss.com/ui/docs/components/drawer#nested-drawers), keeping the conversation underneath. The selected message uses the same outgoing bubble and content as the conversation, and only recipient sections with people appear. The “Sounds good” message demonstrates partial receipts. Back, Escape, or dismissing the nested drawer returns to the conversation with your draft and reading position preserved. Choose **Leave chat** in the group members drawer footer to open a confirmation. Cancel keeps the conversation and draft; confirming shows “You left this chat,” clears the draft, and stops simulated replies. **Rejoin chat** restores the conversation. The event ticket stays valid. This is a local demo interaction. ### Member moderation **Remove from chat** kicks the person out by ending their membership. It does not create a ban: they can join again through their ticket invitation, subject to eligibility. The confirmation explains this, and the **Removed from chat** demo state includes Rejoin chat. **Ban from chat** also removes membership and creates a persistent ban. After confirmation, the person appears under Banned users and cannot join again until a moderator selects **Unban**. The **Banned from chat** state has no rejoin action. In the intended UI flow, Unban returns the person to Current members and restores their access. Their old receipt watermark is not restored. Both actions preserve the person's messages, quoted replies, reactions, and event ticket. A mute remains independent and is not cleared by leaving, removal, banning, or unbanning. Message info uses the current recipient list. Try the full moderator flow by muting Alex, banning them, and then selecting Unban. Alex returns to Current members with the Muted badge preserved, and their messages remain visible throughout. The Removed from chat and Banned from chat state triggers show the two different guest access screens. Muting currently demonstrates the moderator's member-list action and badge. A muted guest's own view, with readable history and a notice replacing the composer, is not yet demonstrated. For integration, supply the current roster and ban list independently. [PR #9727](https://github.com/calcom/cal/pull/9727) provides the persisted ban data; the consuming app must wire Remove to a moderator-authorized membership-removal operation and Ban to the ban operation. The PR's `unbanMember` only deletes the restriction: the intended Unban UI also requires a server operation that restores eligible membership. The backend must check eligibility and authorize the combined operation before the UI moves the person back. The demo simulates its successful result. The current [PRO-2343](https://linear.app/calcom/issue/PRO-2343/chat-moderation-mute-remove-and-unban-guests) description still uses “Remove” for a ban. That engineering contract needs to be reconciled with the distinct Remove and Ban actions demonstrated here. ### Message moderation The demo's current user, Sam, is an accepted co-host. Open the ellipsis menu beside **Alex Morgan’s message**, select **Delete message**, and confirm. Cancel preserves the message; confirmation replaces it with **Removed by a host**, removes its reactions and any pin, and updates the quote in Sam’s reply. You can try this in the Pinned message state to see the pin disappear too. Hosts and accepted co-hosts may delete guests’ messages, including messages from removed or banned guests. Everyone may delete their own messages, which show **Message deleted**. Other hosts’ and co-hosts’ messages are protected, so their menus omit Delete. Deletion erases the body and has no Undo. The consuming app must enforce these permissions server side, as specified in [PRO-2342](https://linear.app/calcom/issue/PRO-2342/chat-moderation-delete-messages-and-catch-up-on-reconnect). ## One Chat UI family The example is built from a cohesive family of ready-made chat components in `@coss/ui/chat/chat-*`. The components share layout, styling, and interaction conventions and can be composed in a drawer or directly in a page. They are being prepared for rebuilding in Coss; they are not published registry entries yet. | Component | Direct import | Owns | | --- | --- | --- | | `ChatBubble`, `ChatMessageLayout` | `@coss/ui/chat/chat-bubble`, `@coss/ui/chat/chat-message-layout` | Bubble surface and message layout parts for custom compositions | | `ChatMessage` | `@coss/ui/chat/chat-message` | Sender/avatar layout, grouped corners, bubble, and placement of supplied actions, reactions, and metadata | | `ChatConversation` | `@coss/ui/chat/chat-conversation` | Scroll area, edge fades, jump control, and focused-reply overlay placement | | `Composer` | `@coss/ui/components/composer` | Shared input, structured draft, submit policy, and optional mentions | | `ChatMessageActionToolbar` | `@coss/ui/chat/chat-message-actions` | Optional reaction, reply, and menu controls with keyboard navigation | | `ChatReactionChip`, `ChatReactionPicker` | `@coss/ui/chat/chat-reaction-chip`, `@coss/ui/chat/chat-reaction-picker` | Reaction interaction, popup anchoring, and scroll dismissal | | `ChatReplyPreview` | `@coss/ui/chat/chat-reply-preview` | Optional focused reply, source geometry, transition, and cancellation | | `ChatMemberList`, `ChatMemberSection` | `@coss/ui/chat/chat-members` | Participant rows with supplied badges and actions | | `ChatMessageInfo` | `@coss/ui/chat/chat-message-info` | Supplied message preview and receipt groups | | `ChatEmptyState`, `ChatLoadingState`, `ChatStatusBar` | `@coss/ui/chat/chat-states` | State presentation with consumer-supplied copy and actions | `ChatDatePill`, `ChatUnreadDivider`, `ChatJumpToLatestButton`, `ChatTypingIndicator`, `ChatDeliveryIndicator`, and `ChatReplyReference` each have a matching `chat-*` module. `ChatMessageBubble` is available for standalone previews, such as message info. `ChatBubble` and `ChatMessageLayout` live alongside the other chat components and can also be imported directly for custom compositions. `ChatMessage` formats plain string children automatically. Supply React content for quotes, attachments, or custom text formatting; use `ChatMessageText` when you need to highlight known mentions. Supply `actions`, `reactions`, and `footer` as needed. `grouped` controls consecutive-message layout, and `bubble={false}` supports unavailable-message content. `bubbleProps` forwards native bubble props, a ref, and styling. The deleted-message placeholder is currently composed in the demo using `bubble={false}`; it is not yet a separately exported Chat UI component. The consuming app owns message data, ordering/grouping, membership, permissions, translated labels, delivery calculations, and persistence. `useChatScroll` from `@coss/ui/chat/hooks/use-chat-scroll` owns following the conversation: it keeps the viewport at the end while content resizes, preserves reading position when scrolled up, and powers the jump control. Set `scroll.atBottomRef.current = true` before adding an outgoing message so sending returns to the latest message. Use `paused` while another panel or focused reply owns the viewport. Chat compositions use the shared [Coss Composer](https://coss.com/ui/docs/components/composer). The caller owns `value`, `onValueChange`, and `onSubmit`, and clears an accepted draft. Use `getComposerText(value).trim()` for a plain-text message. The inline example renders a native textarea; the drawer uses the default contenteditable input with mention triggers. Set `submitOnEnter="desktop"` so Return inserts a newline on touch devices and the send button submits. Supply accessible input and send-button labels. `ChatCollapsibleMessage` owns text-only preview/expansion, with controlled `open`/`onOpenChange` or an uncontrolled `defaultOpen`. `renderText` can style known mentions using `ChatMessageText`. Attachments and other arbitrary bubble children remain outside text truncation. `ChatMessage` accepts `animateEntrance` for live arrivals, `highlighted` for attention feedback, and `onAnimationComplete`; history defaults to no entrance animation. Motion respects reduced-motion preferences. `ChatBubble` and `ChatMessageLayout` have no Motion dependency. ### Minimal working example This smaller example uses `ChatConversation`, `ChatMessage`, and `Composer` with `useChatScroll`, without an event or drawer. Messages use plain string children and the composer has no mention configuration. All messages remain local to this example. The following is the complete source of `apps/design-system/components/chat-ui-inline-example.tsx`. It uses the existing workspace package and Coss theme setup. Add it to a client component; replace its local array and synthetic copy when connecting the app. ```tsx title="chat-ui-inline-example.tsx" "use client"; import { ChatConversation } from "@coss/ui/chat/chat-conversation"; import { ChatMessage } from "@coss/ui/chat/chat-message"; import { useChatScroll } from "@coss/ui/chat/hooks/use-chat-scroll"; import { Avatar, AvatarFallback } from "@coss/ui/components/avatar"; import { Button } from "@coss/ui/components/button"; import { Composer, ComposerActionGroup, ComposerActions, ComposerInput, ComposerSubmit, type ComposerValue, getComposerText, } from "@coss/ui/components/composer"; import { useMediaQuery } from "@coss/ui/hooks/use-media-query"; import { ArrowUpIcon } from "@coss/ui/icons"; import type { ReactElement } from "react"; import { useId, useState } from "react"; export function ChatUiInlineExample(): ReactElement { const [draft, setDraft] = useState([]); const hintId = useId(); const touchInput = useMediaQuery({ pointer: "coarse" }); const [messages, setMessages] = useState([ { id: 0, own: false, text: "The same chat components can live directly in a page. Try sending a message.", }, ]); const scroll = useChatScroll({ itemCount: messages.length }); return (
{messages.map((message) => ( ))}
{ const text = getComposerText(value).trim(); if (!text) return; scroll.atBottomRef.current = true; setMessages((current) => [...current, { id: current.length, own: true, text }]); setDraft([]); }}> } /> {!touchInput && (

Enter to send / Shift + Enter for a new line

)}
); } ``` This example deliberately demonstrates local sending only. Its array indexes, `own` flag, and immediate success are fixtures, not a production message contract. Use stable server IDs, reconcile optimistic messages, and translate labels in the application. Keep the existing Coss stylesheet and Tailwind source discovery; do not install another copy of Base UI or Motion for this composition. ## Implement in the Cal app ### 1. Keep data and policy in the application Build the client controller in the owning `apps/web/modules` feature when it depends on tRPC, authentication, or routing. Domain behavior belongs in the owning service; the Chat UI family receives view data and callbacks. Import family components directly from `@coss/ui/chat/*`; keep demo fixtures in the design-system app. Read `openspec/specs/cal-events-chat/spec.md` for event eligibility and service rules. Use the current API and DTO definitions from the backend work; the names in the following table describe data responsibilities, not new endpoint or DTO names. The demo's `CHAT_MEMBERS`, `MemberId`, scenario flags, timers, and receipt fixtures must not become production contracts. ### 2. Map data to the component contracts | Application supplies | Component contract | Integration responsibility | | --- | --- | --- | | Stable message ID, author, ordered body and timestamp | `ChatMessage`: React `key`, `side`, `author`, `avatar`, children, `footer` | Determine own messages from the authenticated user. Format localized times and group consecutive messages. Use `grouped` only within the same author/date group. | | Live arrival versus loaded history | `ChatMessage`: `animateEntrance` | Animate a new arrival once; do not animate fetched history or replay the entrance after a refetch. | | Visible timeline and viewport state | `ChatConversation`: `scroll`, `label`, `jumpLabel`; `useChatScroll` | Supply message order, date/unread markers, history pagination, and the policy for acknowledging reads. | | Draft and send callback | `Composer`: `value`, `onValueChange`, `onSubmit`; `ComposerInput` | `onSubmit` receives a structured draft. Convert it with `getComposerText(value).trim()` when sending text. Own pending/failed state and retry, and clear the accepted draft. | | Mention candidates | `Composer`: `triggers`; `ComposerPopup`, `ComposerList`, `ComposerItem` | Supply stable IDs and labels for each trigger. Selected mentions remain structured tokens; `getComposerText` converts them to displayed handles. Resolve authorization and membership in the application. | | Allowed message actions | `ChatMessage`: `actions`; `ChatMessageActionToolbar`: reply callbacks and `menuChildren` | Omit unauthorized menu items. There is no built-in `canDelete` or role system. Recheck permissions on every server mutation. | | Reaction counts, current-user selection, and reactors | `ChatReactionChip` and `ChatReactionPicker` | Pass a boolean `selected`, supply reactor content, and persist toggles. Counts and lists come from the application. | | Current members and banned people | `ChatMemberList` / `ChatMemberSection`: `members` with `id`, `name`, optional `avatar`, `badges`, `action`, `dimmed` | Keep membership, mute, and ban separate. Supply authorized controls and fetch the appropriate roster when the nested drawer opens. | | Per-message recipient state | `ChatMessageInfo`: `message`, `metadata`, `groups`; `ChatDeliveryIndicator` | Build mutually exclusive Read by / Delivered to / Pending delivery groups from actual receipt data. Empty groups hide automatically. | | Reply target and quoted content | `ChatReplyReference`, `ChatReplyPreview`, conversation `overlay` | Persist the reply-to ID. Own source geometry, presence, selection, and focus; see [focused reply](#focused-reply-and-nested-focus). | | Access, history, and connection state | `ChatEmptyState`, `ChatLoadingState`, `ChatStatusBar` | Choose which content and actions are allowed. A disabled composer is not an authorization boundary. | For receipts, distinguish “some recipients have read” from “everyone has read.” The demo shows blue double checks only when all current recipients have read, gray double checks when all have at least received it, and one check otherwise. Sending/failure state takes precedence. Confirm the recipient set and acknowledgement contract with the backend; never infer delivery or reading from a timer, connected socket, or scroll position alone. ### 3. Wire mutations and live updates 1. **Load and join:** resolve eligibility and membership before exposing history or member data. Join is an explicit server operation. Do not equate having a ticket with having joined the chat. 2. **Send and retry:** accept a draft into an optimistic message or await a successful send, then clear that accepted draft. Set `scroll.atBottomRef.current = true` before appending your outgoing message. Preserve failed content with a Retry action; reconcile server events with the pending message instead of appending it twice. 3. **Receive:** merge updates by stable ID and order by the backend's sequence contract. Update reactions, typing, receipts, and membership from their authoritative data. Expire stale typing indications; never present typing as online presence. 4. **Moderate:** confirm deletion, removal, and banning. Keep the dialog pending during the request and handle failure without reporting success. Reconcile the returned roster/message state, including other clients. Keep confirmation copy consistent with the operation's real semantics. 5. **Reconnect:** show loading on Reconnect, retain readable cached history only while access is still valid, and reauthorize. Refresh the latest history page as well as catching up newer messages: a deletion can affect an old sequence position. Refocus the composer only if sending is permitted and focus is not in another open panel. Deletion must clear all rendered copies of the body: the timeline, reply references, focused reply, and message-info preview. Remove reactions and a matching pin. Retain the message row, author, and time with the correct deletion reason. Do not keep erased text in the client cache or offer an Undo that the backend cannot perform. ### Moderation permissions This is the intended event-chat presentation. Treat server authorization as authoritative, including if permissions change while a menu is open. | Action | Sender / guest | Host or accepted co-host | | --- | --- | --- | | Delete own message | Allowed | Allowed | | Delete another guest's message | Not allowed | Allowed, including former or banned guests | | Delete another host/co-host's message | Not allowed | Not allowed | | Mute/unmute, remove, or ban a guest | Not allowed | Allowed | | Moderate another host/co-host | Not allowed | No controls in this demo | | Unban a guest | Not allowed | Allowed; membership restoration needs the contract below | Muting blocks sending and typing, while retaining reading and receipt acknowledgement. Removing or banning a member does **not** delete their historical messages. Only explicit message deletion produces “Message deleted” or “Removed by a host.” ### Contract decisions The visual flow is agreed; these backend differences must be resolved before connecting the corresponding controls. The observations below refer to the reviewed [PR #9727](https://github.com/calcom/cal/pull/9727) head `73423a57beafb97f44c0983879ed08ba354cfd8c` and the moderation ticket descriptions reviewed on October 9, 2026. Recheck the implementation before wiring it. | Topic | Intended UI | Required integration decision | | --- | --- | --- | | Remove versus Ban | Remove ends membership and permits an eligible explicit rejoin. Ban also prevents rejoining. | [PRO-2343](https://linear.app/calcom/issue/PRO-2343/chat-moderation-mute-remove-and-unban-guests) still calls a ban “Remove.” Agree separate authorized operations and matching copy. Do not wire both buttons to `banMember`. | | Unban | Returns the eligible person to Current members, keeping their mute. | The reviewed `unbanMember` only removes the ban. Coordinate an authorized membership restoration on the server before showing the person as a member; do not restore old receipt watermarks. | | Persistent mute | Leave/rejoin, removal, and ban/unban do not clear it. | Read mute independently of membership and enforce it for send/typing. Do not derive it solely from the current roster. | | Message deletion | Sender/moderator placeholders, erased body, no Undo. | [PRO-2342](https://linear.app/calcom/issue/PRO-2342/chat-moderation-delete-messages-and-catch-up-on-reconnect) defines authorization, deletion events, and reconnect refresh. A repository method alone does not deliver those application guarantees. | ### 4. Compose the responsive drawer Use `apps/design-system/components/chat-ui-demo.tsx` as the shell reference. Port its composition into the owning feature; replace its controller and fixture data. Keep these settings together: | Part | Existing composition and reason | | --- | --- | | Root | Controlled Coss `Drawer`, `position={useMediaQuery("md") ? "right" : "bottom"}`. Retain its default modal behavior and page scroll lock. Keep scenario controls outside product UI. | | Keyboard | Bottom `DrawerPopup` provides `DrawerPrimitive.VirtualKeyboardProvider` automatically. Do not wrap it in another keyboard provider. | | Popup | `variant="inset"`, default close button, and the drawer's default initial focus behavior. Opening the drawer must not focus the composer or open the software keyboard. | | Mobile geometry | Main popup uses `[--drawer-height:100dvh]` and keyboard/safe-area bottom padding; portal viewport uses `max-md:[&_[data-slot=drawer-viewport]]:pt-2`. Preserve these deliberate keyboard adjustments when porting. | | Header | Default `DrawerHeader`, `DrawerTitle`, and `DrawerDescription`. Member text opens the nested roster. | | Panel | `DrawerPanel scrollable={false}` with `relative flex min-h-0 flex-1 flex-col overflow-clip p-0 pt-0!`. The conversation owns scrolling, and the panel clips the focused-reply transition. | | Conversation | `ChatConversation` inside the panel. Keep `min-h-0` through the flex chain; header and composer stay outside its scroll surface. | | Composer | Sibling after the panel, with `px-4` and `pb-[max(1rem,env(safe-area-inset-bottom,0px))]`. Do not give the whole popup another scrolling container. | | Tooltips | The complete example supplies a Coss `TooltipProvider`; preserve it when porting its action toolbar and tooltips. | Coss styles and the current package dependencies are already part of the Cal workspace. Keep the existing primitives intact. The architecture handoff in `openspec/specs/design-system/design/chat-ui.md` records intentional overrides and the later Coss distribution work. ### Focused reply and nested focus The focused reply is an optional composition; it is not enabled by merely rendering `ChatReplyPreview`. Preserve the existing interaction when implementing the full demo: 1. Capture the original bubble element through `bubbleProps.ref`, its width, grouping, and expanded state before selecting it. Keep the message/avatar in their original layout slot while the source is `lifted`. 2. Render `ChatReplyPreview` in `ChatConversation.overlay`, inside `AnimatePresence`. Supply the same message content and source geometry. Mark the underlying timeline `inactive` and pause `useChatScroll` while the overlay owns the viewport. 3. On cancel or send, set the dismissal ref to the corresponding value and start the presence exit. Retain the reply geometry until `onExitComplete`, then clear it and restore scroll/focus. On main-drawer dismissal, wait for `onOpenChangeComplete` before resetting the reply. 4. Keep member and message-info drawers nested in the parent drawer context. Return focus to their initiating controls and preserve the main draft/reading position. Confirmation dialogs must also restore focus on cancel; after deletion, focus the retained placeholder rather than its removed menu trigger. See `chat-ui-demo-reply-focus.tsx`, `chat-ui-demo-message.tsx`, and the presence/focus callbacks in `chat-ui-demo.tsx` together. Copying only the animation component omits the lifecycle needed for a stable return transition. ### Access and recovery states | State | Conversation and composer | Action | | --- | --- | --- | | Not joined | Join introduction; no history or composer | Join after eligibility succeeds | | Joined, no messages | Empty conversation with composer | Send the first message | | Initial loading | Shared skeleton; disabled composer | Wait for history | | History error | Error empty state; no composer | Retry history | | Disconnected | Cached conversation; disabled composer | Reconnect with a pending state, then refocus when permitted | | Muted | Readable history; replace composer with a mute notice | No send or typing; this own-user view is not yet demonstrated | | Access lost | Access empty state; no composer | Do not offer rejoin without renewed eligibility | | Removed | Removed empty state; no composer | Eligible explicit rejoin through the ticket invitation | | Banned | Banned empty state; no composer | No rejoin while the ban exists | | Left | Left empty state; no composer | Eligible explicit rejoin | The demo states are isolated examples, not a precedence resolver. In the app, access denial takes precedence over cached history; a ban takes precedence over a rejoin action; mute takes precedence over enabling send after reconnect. Dispose of old-room subscriptions and pending callbacks when switching rooms. ### Accessible receipts and popup dismissal Delivery indicators expose a labelled image, not a live region. Keep important send failures in one application-owned status region; do not announce the same failure from each receipt. Supply labels for sending, sent, delivered, read, and failed states. Message menus and reactors popovers dismiss on deliberate input scrolling their conversation. Automatic scrolling, incoming messages, menu keyboard navigation, and scrolling unrelated containers must keep them open. Reaction inspection keeps the 500ms touch hold and 8px movement tolerance; a short tap still toggles the reaction. Preserve reduced-motion behavior. ### Integration checklist - [ ] Resolve the [contract decisions](#contract-decisions); enforce authorization and eligibility on the server, including stale-menu requests. - [ ] Use stable IDs and reconcile optimistic sends, retries, duplicate live events, and deletion updates across every message preview. - [ ] Implement and demonstrate the muted guest's own view. The moderator's Muted badge does not cover this state. - [ ] Supply translated labels, accessible action names, localized dates/times, and empty/error copy. Demo English is synthetic. - [ ] Keep `useChatScroll` active only for the visible conversation, pause it for focused/nested UI, and reset it only when changing rooms. Implement older-history anchoring separately: capture the viewport before prepending and preserve the visible message afterward. The hook does not fetch, paginate, virtualize, or compensate for prepended pages. - [ ] Use actual recipient acknowledgements. `onReachEnd` only reports scrolling near the bottom; it is not proof the user read every message. Confirm the recipient set after membership changes. - [ ] Exercise long drafts with the mobile keyboard, desktop Enter/Shift+Enter, IME composition, mention selection, nested focus return, and reduced motion. Preserve the approved animation and drawer defaults. - [ ] Verify failure as well as success for join, reconnect, send/retry, deletion, and moderation. Keep focused reply cancellation and draft preservation working throughout. ### Source map Paths below are relative to the Cal repository. Import reusable components from the package; consult demo files for composition without introducing a dependency on `apps/design-system`. | Source | Read it for | | --- | --- | | `packages/coss-ui/src/chat/chat-*.tsx` | Public prop contracts and rendering behavior | | `packages/coss-ui/src/chat/hooks/use-chat-scroll.ts` | End-following, pause/reset, viewport refs | | `apps/design-system/components/chat-ui-inline-example.tsx` | Minimal local composition, reproduced above | | `apps/design-system/components/chat-ui-demo.tsx` | Main/nested drawers, membership controls, confirmations, focused-reply lifecycle | | `apps/design-system/components/chat-ui-demo-message.tsx` | Message slots, action permissions, quote/deletion presentation | | `apps/design-system/components/chat-ui-demo-reply-focus.tsx` | Focused reply adapter and source geometry | | `apps/design-system/components/chat-ui-demo-message-info.tsx` | Nested receipts drawer and message preview | | `apps/design-system/components/chat-ui-demo-scenarios.tsx` | Empty, loading, access, and recovery presentations | | `apps/design-system/components/chat-ui-demo-state.ts`, `chat-ui-demo-receipts.ts` | Demo policy and synthetic data only; do not copy as backend logic | | `openspec/specs/cal-events-chat/spec.md` | Event-chat eligibility and domain contract | | `openspec/specs/design-system/spec.md` | Accepted Chat UI behavior and documentation requirements | | `openspec/specs/design-system/design/chat-ui.md` | Family ownership, primitive overrides, and Coss rebuild boundary | ## Single conversation drawer The drawer has a header, a scrolling message timeline, and a composer. The middle is a repeater, so the map labels the components that can appear inside it. It combines conditional states and actions for reference; they do not all appear at once. ### Drawer and timeline - **Conversation header** — a plain title and a short list of member names that opens the nested member drawer, using the Coss header, title, and description defaults. - **Chat panel layout** — the drawer shell positions the header, scrolling history, and composer; it also needs narrow and wide layouts. - **Message timeline** — repeats grouped messages and keeps scrolling stable as history loads. - **Older history loading** — feedback while fetching earlier messages. - **Timeline separators** — sticky date pills and new-message markers. - **Jump to latest** — returns to recent messages when the reader has scrolled up. - **Timeline states** — first-message/join, initial loading, history error, disconnected with reconnecting feedback, access lost, removed from chat, banned from chat, and left chat. ### Message parts - **Message bubble** — incoming and outgoing messages, sender, time, wrapping, grouping, a smaller top corner on the sender's side for the first bubble only, and slots for actions and reactions. - **Delivery indicator** — clock while sending, one check when sent, gray double checks when delivered to everyone, blue double checks when read by everyone, and failure/retry. - **Message info** — a nested drawer with the selected message and mutually exclusive Read by, Delivered to, and Pending delivery lists, reached from your own message’s action menu. - **Reply reference** — a quoted message inside a bubble, including an unavailable-message fallback; hover and focus use the primary color on the reply arrow and author without dimming the text. - **Deleted message placeholder** — preserves the sender, time, and timeline position; displays “Message deleted” or “Removed by a host” according to the deletion reason, with matching quoted-reply text and no Undo. - **Mention text** — uses medium font weight for a mentioned participant, including current-user and former-member states. - **Translated message treatment** — exploratory presentation of a translation and its source. ### Actions and activity - **Message actions** — a toolbar sits beside the bubble, on the left for your messages and the right for others, with reaction and reply buttons and an ellipsis menu. The menu offers Copy, Pin/Unpin, Message info on your messages, and Delete where permitted. Controls support pointer, keyboard, and touch activation; a message context menu, long-press message menu, and forwarding action remain exploratory. - **Reaction picker** — chooses a reaction. - **Reaction strip** — shows reactions attached to a message. - **Swipe action treatment** — exploratory gesture affordance for replying. - **Forward and pin presentation** — pin/unpin is interactive and the Pinned message state shows the pinned bar. The Forwarded marker is sample content; forwarding is not implemented. - **Edit-message concept** — exploratory; editing is not yet a decided chat feature. - **Typing indicator** — one three-dot bubble names one or several people typing, with a soft entrance and exit and a staggered dot pulse. Keep it mounted and use its `visible` prop to animate its exit. Reduced motion disables these animations. ### Composer - **Focused reply** — lifts the selected bubble above the composer while the conversation blurs. Cancel, Escape, or clicking the blurred background returns it to its original position and preserves the draft. Sending restores the conversation at the new reply. - **Mention suggestions** — a participant picker shown after typing `@`, filtered from the supplied options, with avatars, names, and keyboard selection. No popup appears when no members match. Loading and empty-state presentations are not implemented. - **Message composer** — text entry, multiline input, send, disabled, sending, failed and retry states. ## Conversation list The dashboard may need discovery across chats. The following map is an exploration inventory; working conversation-list, search, and list-state components are not implemented in this example or exported by the Chat UI family. - **Conversation search input** — filters the list. - **Conversation list** — repeats list items and supports keyboard selection. - **Conversation list item** — avatar, title, preview, timestamp, unread state, and typing, deleted-message, and mention variants. - **Conversation list states** — loading, no conversations, no search results, and recoverable error. ## Exploration notes - **Editor approach** — both examples use the shared Coss Composer. The drawer enables @ mentions in its contenteditable input; the inline example renders a plain textarea. The agent UI uses Lexical; replacing that editor remains a separate investigation. - **Motion details** — the demo animates arriving messages, focused replies, quote navigation, reactions, and typing indicators, with reduced-motion support. Swipe interactions remain exploratory. The anatomy maps also show exploratory components and states that are not part of the working example. They do not decide package ownership or commit every feature to the first product release.