Usage
@prepared911/ui/blocks/call-dock from its subpath. Load @prepared911/ui/styles.css once at the document root. In a Module Federation shell that is the shell's one stylesheet import, and a remote imports no stylesheet.
CallDock is the tray of the one active call, in a bar centered on the block-end edge of its shell. It is an inverted surface, light on a dark page and dark on a light page. The shell owns the frame and decides when it is there; a module (in the product, a module-federation remote) fills it. It slides in and out as a call starts and ends. There is no queue, and nothing in the API can express a minimized call or panel.
open is the whole presence rule. Derive it from call-taking's call-taking.call-plane runtime store, read through the stores domain: activeCallId is a string while a call is active. The tray shows while open is true (including while the module is still loading or showing a failure state), and renders nothing at all while it is false: no landmark, no reserved space, and no child mounted.RemoteComponent takes id, loading and children; every other prop is forwarded to the remote. There is no errorFallback: the mount's error boundary is built in (see Loading and failure). Pass CallDockSkeleton as loading.id is typed against mfe-core's ID_REGISTRY, so an unregistered id does not compile. The dock content is its own component view of the call-taking remote, call-taking/dock-view, which renders only the parts; its owners expose it with the add-module skill before a shell mounts it. Do not mount call-taking/view in the tray: it is the dashboard panel, a full surface rather than the dock parts.data-theme on an ancestor (the document element, as every shell's applyInitialAppearance writes it), which the inverted region reads; without one the page counts as light.open go false when the call ends; do not wrap it in call ? <CallDock … /> : null, which removes it without its exit.dir="rtl" with no prop.label is the landmark's name and stays the shell's, from its own formatMessage. Whoever renders CallDockError imports its glyph (@prepared911/ui/icons/error-circle); a block cannot import glyph modules.open turning true enters the tray from beyond the block-end edge on the glide spring with a short fade. open turning false makes the tray inert at once, holds its size, slides it back beyond the edge on the glide exit timing, and removes it from the tree after the exit. Opening again during the exit cancels it and releases the held size. A tray present on first render, or on the server, is at rest: nothing animates on first paint. Under reduced motion, from the operating system or from data-motion="reduced" on the html element, the slide and the width morph collapse to instant and only the fade remains.
Size follows content. When the call-state composition changes width (for example Incoming to Active), the tray's inline size springs on --motion-spring-morph when growing and --motion-spring-smooth when shrinking, through MorphWidth and the registered --ds-content-inline-size variable. Under reduced motion the width snaps with the motion tokens.
CallDockDetails and CallDockControls anywhere under the mount. The tray lays them out the same behind any number of display: contents wrappers (the mount wrapper, Suspense and error boundaries add none of their own), so the remote never needs to know what sits above it.CallDockControls; with only details, only CallDockDetails. The vertical rule leads the controls while the tray also holds details, and is not drawn otherwise.activeCallId goes null makes the controls vanish while the tray slides out. The tray holds its size for the exit, so an emptied remote cannot collapse it, but it still leaves empty. Hold the last call's data until the remote unmounts.open state or render the tray yourself; read the call from the call-plane store through the stores domain.Foreground.Default, Subtle, Muted). The content is a theme region in the opposite appearance of the page, so Button, Text, Icon, Counter and Badge resolve their ordinary tokens for the inverted surface. The Transient* foregrounds name the page's inverted values and would resolve to the wrong appearance inside the region.CallDockCallView in stories/fixtures.tsx, built only from shipped parts and typed props, with no className or style. call-dock-call-states.test.tsx, the Interactions and Composition stories and the browser test pass it changing state the way a remote would, and assert every documented behavior ([A1] to [A18] in CONTRACT.md).
| State | Details part | Controls part |
|---|---|---|
| Idle | none | A Field with type={FieldType.Tel} (InputSize.ExtraLarge, labelHidden, placeholder copy) and Place Call as direct children of CallDockControls. Place Call is disabled until the mask reports dialable digits and is ButtonIntent.Accent solid once enabled; Enter submits. onPlaceCall receives E.164 when the mask resolves it, otherwise raw digits. |
| Calling | none | The 16px phone glyph, "Calling" and the number, the caller line, End Call. |
| Incoming | none | The 16px phone glyph, the number and caller line, Answer with the ring timer, and a circular back-to-queue button whose tooltip reads "Back to queue". |
| Active | A pill of the number and the call timer, the caller line under it | Mute, Link, Hold, Park, Transfer in InlineSize.ControlColumn columns, the more menu, and End Call as direct CallDockControls children (--space-12 group gap only). |
| Muted | As Active | Mute becomes Unmute: an accent solid circle with the mic-off glyph. |
| On Hold | Accent pill with 24px between the number and a 12px timer-plus-chip group | Resume, Park, Transfer, the more menu, End Call; same fixed columns and group gap as Active. |
call-taking.call-plane), state.call carries the phase and the other party, state.canDial says whether Place Call can work, and the commands are dial, answer, decline, hangUp, hold, mute and park:
mm:ss from clockStartedAt and a hold start) and pass them as display text. A timer is a monospace Text (TextType.Code2), muted in the pill and TextType.Code1 inside the Answer button.CallDock mounted across states: the tray is one region for the whole call, so a state change never re-enters the tray and never moves focus.state.actions says which verbs are allowed now (for example answer is false while the queue auto-answers); pass disabled to the matching Button.decline; its owners decide whether that is the same thing (open question in CONTRACT.md).open true while the plane can be dialed instead of deriving it from activeCallId alone (open question in CONTRACT.md).call (Place Call, Answer), phone (the 16px glyph on Calling and Incoming), mic and mic-off (Mute, Unmute), paper-plane (Link), hold (Hold), circle-parking (Park), call-made (Transfer), call-received (Resume), arrow-turn-left (back to queue) and more-vertical (the menu). hold is a custom glyph made from the Figma vector (two vertical bars at a 1.5 stroke), because the published icon catalog has no such icon; a glyph module is imported by the module that renders the control, never by the block.
ButtonSize.ExtraLarge and InputSize.ExtraLarge (the 40px control tier), ButtonShape.Pill (the circular buttons), BlockSize.ExtraLargeControl (the pill's height) and Background.AccentTint (the hold pill's wash) fill the gaps the Call Dock needs. No composition uses className or style.
CallDockSkeleton is the loading content: placeholders at the tray's block size (one row at the control height), hidden from assistive technology, pulsing like every Skeleton and static under reduced motion. Pass it as the loading of the mount.
CallDockError is the failure content while a call is active: a glyph, a short message and, with onRetry and retryLabel, a Retry Button. The glyph and message form one polite status region, announced once when the state appears; Retry is a tab stop of its own. Without onRetry there is no button.
RemoteComponent's built-in boundary shows the platform's shared remote fallback, with its own retry, inside the tray. The tray stays, because open is still true. The shell passes nothing for this case.disconnected or a store read that errors. The remote renders CallDockError in place of the parts, with its own copy and retry, and imports the glyph once (@prepared911/ui/icons/error-circle).