Skip to content

Patterns · Collaboration

ChatDock

The Rook Chat launcher with an unread badge and the panel that holds Rook Chat in an iframe (rook-chat-embed v1).

@rookcom/monarch-react/shellPattern

Docked, with three unread

The page of the host app.

Usage examples

The example at the top of the page is the only one for this component.

Purpose

Every Rook app (except Rook Chat itself) docks Rook Chat in the bottom-right corner. The panel is the real Chat app in an iframe, so the host only draws the launcher and speaks the rook-chat-embed v1 protocol. ChatDock is the single shared implementation.

Use when

  • Once per signed-in app layout, when the person has Chat in this workspace.
  • With hidden on pages that have their own AI assistant on the right.

Avoid when

  • Help from the Rook team: that is SupportPanel, opened from the rail, never bottom-right.
  • Embedding another app’s settings page: use EmbeddedAppFrame.

Design principles

  • The host never fetches: the product passes the unread count it already polls and its own single-flight refreshSession; Chat asks for refreshes and the dock forwards them.
  • Never refreshes on its own: the frame is created on first open (or hover) and kept alive while closed, so calls, drafts and scroll position survive; it reloads only when src or the workspace changes.
  • Strict messaging: messages are read only from the frame’s own window at an allowed origin, validated field by field, and posted only to the embed’s exact origin.
  • Gets out of the way: hidden hides it where an assistant sits; a running call stays on screen.

API reference

import { CHAT_EMBED_PROTOCOL, CHAT_EMBED_VERSION, ChatDock, DEFAULT_CHAT_ALLOW, DEFAULT_CHAT_SANDBOX, chatPageContext, cleanOpenDetail, createChatDockBridge, decodeChatEmbedMessage, encodeChatEmbedMessage } from '@rookcom/monarch-react/shell';
ExportKindRenderingRoot class
CHAT_EMBED_PROTOCOLutilityserver-safe–
CHAT_EMBED_VERSIONutilityserver-safe–
ChatDockcomponentclient.mn-chat-dock
DEFAULT_CHAT_ALLOWutilityclient–
DEFAULT_CHAT_SANDBOXutilityclient–
chatPageContextutilityserver-safe–
cleanOpenDetailutilityserver-safe–
createChatDockBridgeutilityserver-safe–
decodeChatEmbedMessageutilityserver-safe–
encodeChatEmbedMessageutilityserver-safe–

Forwards its ref to the root element. controllerRef gives open(detail), close() and post(message) for "Share in Chat" buttons.

Also exported: CHAT_EMBED_PROTOCOL, CHAT_EMBED_VERSION, encodeChatEmbedMessage, decodeChatEmbedMessage, createChatDockBridge (framework-free host bridge), chatPageContext, cleanOpenDetail, DEFAULT_CHAT_SANDBOX, DEFAULT_CHAT_ALLOW.

Generated from the TypeScript declarations of packages/react/src/patterns/collaboration/chat-dock.

Properties

allowedOrigins is required: exact origins only (* and paths are dropped). The origin of src must be in it or nothing is posted.

unread is the count before the frame runs; once the frame reports rook-chat:unread, its count wins.

open + onOpenChange control the panel; defaultOpen leaves it uncontrolled.

ChatDock

The Rook Chat dock: a launcher with an unread badge and the panel that holds the real Rook Chat app in an iframe (`rook-chat-embed` v1). The frame is created on first open (or on hover) and then kept alive, so calls and drafts survive closing the panel. The dock never fetches: unread counts and session refreshes come from the product and the frame.

PropTypeDescription
allowedOrigins requiredreadonly string[]Exact origins the frame's messages are accepted from. Messages are posted only to the origin of `src`, and only when it is in this list.
appName requiredstringThe host app, e.g. "Rook Stride" (sent in `init`, shown in shared links).
refreshSession required() => Promise<unknown>The host app's own single-flight session refresh. The dock calls it only when the frame asks; it never refreshes the session or reloads the frame on its own.
src requiredstringThe Chat embed page (e.g. `https://chat.example.com/embed/dock?ws=…`).
workspaceId requiredstringThe Rook ID workspace id (sent in `init`).
allowreadonly string[]Permissions policy for the frame. Default `DEFAULT_CHAT_ALLOW`.
classNamestring
context(() => ChatHostContext)The page the person is on. Default: this URL without the fragment, and the document title.
controllerRefRef<ChatDockController>
defaultOpenboolean
hiddenbooleanHide the launcher and panel (the frame stays alive and connected), e.g. on pages with their own AI assistant. A call in progress stays on screen.
iconReactNodeLauncher icon. Default: a chat glyph. Pass the product's Chat mark. Decorative.
idstring
messagesPartial<ChatDockMessages>Interface text, for translation.
mountOnIntentbooleanCreate the frame on hover or focus of the launcher, so opening feels instant. Default true.
onEmbedMessage((message: ChatEmbedMessage) => void)Every valid message from the frame, after the dock handled it.
onLayoutChange((layout: ChatDockLayout) => void)
onLocationChange((conversationId: string | null) => void)The conversation now open in the frame (null: none).
onOpenChange((open: boolean) => void)
onStatusChange((status: ChatEmbedStatus) => void)
onUnreadChange((count: number) => void)
openbooleanPanel open (controlled).
ringTimeoutnumberHow long an unanswered ring stays. Default 30000 ms.
sandboxreadonly EmbedSandboxToken[]Sandbox tokens for the frame. Default `DEFAULT_CHAT_SANDBOX`.
unreadnumberUnread count known before the frame runs (the product's own poll). The frame's count wins once it reports.

Variants

PropValuesDescription
layout (from the frame)'panel''expanded''call''fullscreen'Chat asks for a size with rook-chat:layout; exposed as data-layout. Fullscreen hides the launcher and ignores Escape on the host.
hidden'false''true'Hides launcher and panel; the frame stays connected.

States

StateExposed asBehaviour
Closed–The launcher only (no frame until it is wanted).
Unread–A badge with the count (99+ above 99), hidden from assistive technology; the count is in the launcher’s name.
Loading–The panel shows a spinner until the frame says rook-chat:ready.
Opendata-openThe panel is shown and the launcher is aria-expanded="true".
Ringing–An incoming call while closed: an alert with Open and Dismiss, gone after 30 seconds.
Hidden / unavailabledata-hiddenhidden, or Chat reported unavailable: nothing on screen, frame kept.

Interaction behaviour

  • Hover or focus on the launcher creates the frame, so opening feels instant (mountOnIntent).
  • Opening moves focus into the frame; closing (launcher, Escape on the panel, or Chat’s own minimise) returns it to the launcher.
  • When the frame says ready, the dock sends init and the page context first, then anything queued (such as an open for a conversation).
  • The frame is told when it is shown or hidden (rook-chat:visibility).

Keyboard support

KeysAction
Enter / SpaceOpen or close the panel from the launcher.
EscapeClose the panel when focus is on it (not in fullscreen).
TabFrom the launcher into the open panel’s frame.

Inside the frame, keys belong to Rook Chat; its own minimise sends rook-chat:close and focus returns to the launcher.

Accessibility

Role
button (launcher) controlling a non-modal dialog that holds an iframe
Accessible name
The launcher is "Rook Chat" or "Rook Chat, 3 unread" (messages); the panel and the iframe are named "Rook Chat".
  • The launcher has aria-expanded and aria-controls; the badge is aria-hidden because the count is in the name.
  • The kept-alive panel is aria-hidden and out of the tab order while closed.
  • The incoming call is a role="alert" with labelled Open and Dismiss buttons.
  • Security: event.origin must be in allowedOrigins (exact) and event.source the frame’s window; every message is decoded against its exact shape (ids [A-Za-z0-9_-]{1,64}, counts integers under a million, known layouts and statuses) and dropped otherwise. Host messages to the frame go only to the origin of src, never to *.
  • Security: open details are cleaned before they are sent (bad ids and non-http URLs are dropped). Refresh requests are answered only with the host’s own refreshSession result (ok or not), never with a token.
  • Threat model: another frame or tab cannot drive the badge, layout or refresh (origin and source checks). The frame is sandboxed without top navigation; it gets camera, microphone, screen capture and clipboard for calls (DEFAULT_CHAT_ALLOW), so only point src at Rook Chat.

Responsive behaviour

  • The panel is 25rem wide and up to 40rem tall, never larger than the screen; below 30rem it spans the width above the launcher.
  • Expanded and call layouts grow to 67.5rem; fullscreen covers the viewport. Offsets respect safe-area insets.
  • Size and offset can be tuned with --mn-chat-dock-width, --mn-chat-dock-height and --mn-chat-dock-offset.

Theme support

  • The launcher uses the inverse surface (dark on light, light on dark) with a border so it stays visible on either; the badge uses the danger solid colour.
  • Hover lift, panel transitions and the ring pulse are removed under reduced motion.

Do and don't

Do

  • Render one dock in the signed-in app layout. Switching workspace changes workspaceId, and the dock starts a fresh frame for it.
  • Pass the app’s single-flight session refresh, the same one the rest of the app uses.
  • Set hidden on pages with an assistant panel on the right.

Don't

  • Do not remount the dock on navigation; that would end calls.
  • Do not refresh the session from the frame or on a timer for it.
  • Do not put the support chat here: Help & support lives in the rail.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsButton, Spinner

Testing coverage

  • Named launcher, no frame until wanted; mount on hover/focus, or not with mountOnIntent off.
  • Opens a sandboxed dialog panel, focus into the frame; Escape closes and returns focus.
  • The frame is kept across close and reopen (never reloads on its own).
  • Handshake: init and context to the exact origin, then queued messages; visibility.
  • Drops other origins, look-alikes, other windows, bad shapes, other versions and host-only types.
  • Unread badge in the name, 99+, follows the frame.
  • Refresh requests answered with the host refresh (ok and failed); never called on its own.
  • hidden keeps the frame; a call stays visible; close from inside; unavailable hides.
  • Incoming call alert opens the conversation; unsafe open details dropped; controlled; translation.
  • Protocol decoding; the bridge never posts outside the allow list.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/chat-dock; Cross-browser axe: /components/chat-dock

Tests: packages/react/src/patterns/collaboration/chat-dock