Feature docs

Telephony

TECHNOLOGY

Every live call runs in one floating card that stays on top of the whole app — drag it anywhere and it stays there for the next call. A connected call adds Hold and Transfer: ask a colleague first, transfer now, or send the caller to that colleague's voicemail, and the timeline records where the call went.

The call card is v3.1, the approved redesign engineering builds next (PRESCRIPTIVE): one 360 × 128 card in every state, icon keys that never move, and an audio orb that follows the caller's voice. The transfer sheet, the composer's Call mode and the timeline rows mirror the QA build (space_crm origin/qa 1395973). The composer's Call mode is one green button that dials; the call never lives inside the composer.

Rulings ledger

DateRuling
09-30Call card v3.2: the Transfer panel and a header keypad (CPO) — Transfer opens as a 424 details panel in the right column; the call card is never part of it and steps aside only if it overlaps. A Keypad key at the right end of the card header turns the name area into the digit entry line. The timer leads line 2. PRESCRIPTIVE (comp-transfer-panel, comp-call-keypad).
09-30The docs show one call card (CPO) — today's card leaves this page (comp-phone-call-states stays as the record of QA), and the transfer sheet's colleague rows take the standard user-list padding: 8, 56 tall.
09-29Call card v3.1 (CPO) — one 360 × 128 card in every state; a tone tint plus an audio orb (the thinking-orbs "listening" orb, driven by the other side's voice) in the header; icon-only keys in fixed slots with tooltips; no outline; red only ends a call; Ask first's Cancel is a neutral ×. PRESCRIPTIVE: same logic and callbacks as today's card.
09-29The design system mirrors QA (CPO) — the Call Pill, the in-composer live call and its waveform, the audio-device picker, the Reconnecting state and the busy-disabled Call tab retire (supersedes ds_v276 / ds_v280). The mirror is CANONICAL (mirrors the QA build).
09-28Hold and Transfer on placed calls; the sheet closes when the call ends; the colleague sees who asked first; one red button during Ask first (#2076, #2080, #2081, #2085).
09-22Telephony v2 (#2021) — Hold, Transfer and Ask first on the call card; the Transfer sheet lists colleagues with presence; the timeline records the transfer line and 'Voicemail for'.
09-04The composer's Call mode is one green Call button (#1994) — it states the number it dials; no fields, no toolbar, no send.

Anatomy map

Call card v3.1The redesign — one size, icon keys with tooltips, the audio orb, every state, the Flutter handoff. Transfer sheetPick a colleague — presence, the three verbs, loading and empty states. Transfer panel v3.2The right-column panel that replaces the sheet; the card steps aside. Keypad v3.2The header key, the entry line, the desktop pad and the mobile dock. Composer Call modeOne green Call button; Do Not Contact and No Phone. Timeline call rowsThe transfer line and Voicemail for. ToastThe toast atom the call toasts use.

The call card

Call card v3.1 (PRESCRIPTIVE): one size in every state, icon keys that never move, and an audio orb that follows the caller's voice. Same logic and callbacks as the card in QA today.

1 · One card, one size (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
Today (CANONICAL, QA)
02:41Shane Dusang
v3.1 (PRESCRIPTIVE)
Shane Dusang02:41
Connected
  • One size in every state: 360 × 128 (max-width 100%, so 358 in a 390 phone with 16 gutters). Nothing grows 72 → 120 at connect or shrinks after a refusal; a dragged card never jumps.
  • State lives in the header: its tone tint plus the audio orb. The 1px outline is gone; the card floats on --elevation-popover.
  • Icon keys in fixed slots: desktop Mute · Hold · Transfer · End; mobile adds Speaker in slot 0. A key that cannot act dims to 40% in place and line 2 says why.
  • Red only ends or declines a call. During Ask first the End slot is a neutral × Cancel; a green ✓ completes the transfer.
  • Header B (v3.2, CPO 2026-09-30): line 1 is the name alone; line 2 is the timer, then “ · ” and one plain phrase. The timer never truncates; the phrase ellipsizes (171 px after the timer), so lock reasons are short: “Can't hold or transfer”. No glyphs.
  • v3.2 adds a Keypad key at the right end of the header on every call state, and Transfer opens as a details panel beside the card. Specs: the keypad · the Transfer panel.
  • Same logic as the CANONICAL card (comp-phone-call-states): every key calls the callback it calls today. The only new data is one audio-level stream for the orb.
5 · The orb on a live call — Mute and Hold work on this card (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
Shane Dusang02:41
Connected
SilenceThe same orb at 3.3×.
Customerraw levelCustomerorb energyYouraw levelYoumic energy
  • Bars: twilio.js Call volume, every 50 ms (0–0.6). Dashed: the noise floor the orb finds. Green: orb energy (0–1). Black: your mic energy, with the 0.25 green-glyph line. Click the chart to jump.
  • The levels are a synthesized two-voice call run through the exact analyser twilio.js 2.4.1 uses for its volume event (32-sample spectrum, −100 to −30 dB, averaged, every 50 ms). The trace is real; the voices are not.
  • Why it smooths: the raw value jumps every 50 ms and never reaches zero — line noise alone reads about 0.15, speech 0.2–0.5. Wired straight in, the orb would twitch in silence and flicker in speech.
  • Level → energy: floor = min(raw over the last 2.5 s), eased (falls in 0.15 s, rises in 0.6 s) · target = clamp((raw − floor − 0.04) / 0.30) · energy = target with a 50 ms attack and a 280 ms release. Speech sits around 0.6–0.95; silence around 0.04.
  • Calibrate on real calls before shipping: the floor window, gate and range come from this emulation (UNVERIFIED on real line noise, headsets, speakerphone).
2 · Anatomy (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
Desktop · 4 slots
Shane Dusang02:41
Connected
Mobile · 5 slots · 390 phone
Shane Dusang02:41
Connected
  • Card 360 × 128 · r7 · --gray-07 · --elevation-popover · no border, no outline · the whole card drags (drag, 16px clamp and saved position unchanged).
  • Header 64 · pad 8 / 16 / 8 / 12 · tone tint (-02) · orb 48 · 12 gap · name column 220 × 40 (218 in a 390 phone, 148 on a 320 phone) · 12 gap · Keypad key 40 × 40 at x 304–344, y 12–52 (hit area 48) on every call state, dimmed when it can't act (v3.2, keypad).
  • Line 1 24 · the name alone, --t-paragraph-sb gray-00, ellipsis, tap → customer.
  • Line 2 16 · --t-tiny gray-02 · the timer first (tabular, only after connect, never truncates), then “ · ” and one phrase that ellipsizes: transfer phase › On hold › Sending digits · N left › lock reason › note › Connected. Before connect there is no timer and the phrase starts at the left edge.
  • Bar 64 · pad 8 · gap 4 · gray-07 · key = 1 fr × 48, r5, one 20px FA Regular glyph, no label.
  • Slots desktop 1 Mute · 2 Hold · 3 Transfer · 4 End (≈83 each) · mobile 0 Speaker · 1 Mute · 2 Hold · 3 Transfer · 4 End (≈65 each; ≈51 on a 320 phone).
  • Slot swaps Hold ↔ Resume · Transfer → ✓ (Ask first) · End → × Cancel (Ask first) · incoming: Accept spans 1–3 (0–3 mobile), Decline takes 4.
  • Tones LIVE green-02 / green-01 (connected, muted, speaker, returned, 409, Ask first failed) · WAITING orange (incoming, on hold) · TRANSFER blue (Ask first, transferring, the colleague's card) · DIALING gray (calling).
  • Position desktop: centred over the 424 detail column, top 98 (same formula, cardW 360) · mobile: centred. While Transfer is open the card steps aside only if it overlaps the panel, and is never saved there (Transfer panel).
3 · Keys and tooltips (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
Neutral · gray-06 / gray-00
End · Decline · red-01 / global-white
Accept · ✓ · green-01 / white
Unmute · red-02 / red-01
Resume · orange-02 / -01
Speaker on · panel open · blue-02 / -01
× Cancel (Ask first) · neutral
Can't act · opacity .4
You speaking · glyph green-01
  • No labels. Every key is one 20px glyph. Its name is the tooltip (hover and keyboard focus on desktop, the DS .sb-tooltip via [data-tip]) and a long-press Tooltip on mobile, and always the screen-reader label.
  • Names: Mute / Unmute · Hold / Resume · Transfer / Close transfer (panel open: .c3-key--blue, expanded, live even during isBusy) · Complete transfer · End call · Cancel Ask first · Accept · Decline · Speaker. The header's Keypad key: Keypad / Close keypad (fa-table-cells, 16px).
  • Glyphs (FA Regular): fa-microphone, fa-microphone-slash, fa-pause, fa-play, fa-right-left, fa-check, fa-xmark, fa-phone-slash, fa-phone-flip, fa-volume-low, fa-volume-high.
  • Hover: neutral → gray-05; End / Decline → red-02 fill, red-01 glyph, 1px inset red-01; Accept / ✓ → green-02 fill, green-01 glyph, 1px inset green-01. Toggled tones keep their fill.
  • Focus-visible: inset 2px blue-01 · focused + pressed: inset 2px gray-03 · disabled: opacity .4, no hover, default cursor.
  • Flag for a11y (light, measured): white on green-01 2.0:1 (as the composer Call button) · global-white on red-01 2.9:1 · toggled red 2.6, orange 2.3, blue 2.9 · green-01 mic glyph on gray-06 1.7. All pass in dark except blue toggled (4.2).
4 · The audio orb (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
Customer speaking
Rings swell and roll faster with their voice.
Silence
A slow, shallow roll: alive, not reacting.
Incoming
Two pulses every 2 s.
Calling
Swells 2 s, rests 4 s, like ringback.
On hold
Energy 0, rolls at 0.2×.
Ask first ringing · transferring
The ringback rhythm on blue.
Ask first talking · colleague
Follows the colleague's voice.
No level stream
The library's own loop (iOS / Android for now).
Reduced motion
One still frame.
  • What it is: the thinking-orbs “listening” orb (libraries.dev/orbs, MIT, Jakub Antalik) — a dotted sphere of latitude rings rolling a slow wave, plain circle fills drawn far to near. 48 × 48 canvas.sb-callorb, ~102 dots (8 rings × 20), drawn by sb-callorb.js.
  • Colour: near dots in the tone accent (--c3-accent, -01); far dots fade 70% of the way to the header tint (--c3-hd, -02). No fill, no badge, no pause sign.
  • Connected, it follows OUTPUT (the other side): louder rings swell outward (+7%), draw bigger (+35%) and stronger dots and roll faster (4.3 × (0.5 + 0.9·energy)); each ring reads the level 60 ms later than the one nearer the equator, so a syllable rolls through it.
  • You speaking turns the Mute glyph green-01 (INPUT energy above 0.25 and not muted).
  • Drivers: level (connected, Ask first talking, the colleague's side) · ringing (incoming) · ringback (calling, Ask first ringing, transferring) · hold (energy 0, 0.2×) · none (no level stream: the library's own loop) · reduced motion (one still frame).
6 · Every state · desktop · [1 Mute] [2 Hold] [3 Transfer] [4 End] (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
S1 · Incoming · WAITING
Shane Dusang
Incoming Call
Orange tint; the orb pulses twice every 2 s (not audio). Accept (green, wide) spans slots 1–3; Decline (red) takes the End slot. Same height as in-call, so answering only recolours the header and swaps the bar.
S1 · Incoming · returned · WAITING
Shane Dusang
Returned · Karla Sellers didn't pick up
As S1; line 2 carries the returned note, ellipsis if long.
S1 · Incoming · asks first · WAITING
Shane Dusang
Devon Park asks first
The colleague's phone ringing during an Ask first: line 2 “{rep} asks first”.
S2 · Calling · DIALING
Shane Dusang
Calling...
Gray; the orb swells with the ringback rhythm (2 s on, 4 s off). No timer before connect. Mute works where the platform allows (canToggleMute); Hold and Transfer dim until connect, so nothing moves when it does.
S3 · Connected · legacy / v1 · LIVE
Shane Dusang02:41
Can't hold or transfer
Hold and Transfer dim in place and line 2 gives the reason. The ≤3 s v2 probe window uses the same dimmed keys with “Connected”.
S4 · Connected · v2 · LIVE
Shane Dusang02:41
Connected
The green orb follows the customer's voice; the Mute glyph turns green while you talk. Every key live. Ask first failed (S11) looks identical, plus the existing warning toast.
S5 · Muted · LIVE
Shane Dusang02:41
Connected
Mute → Unmute (red-02 / red-01). Nothing on the orb, which keeps following the customer. Nothing else moves.
S7 · On hold · WAITING
Shane Dusang02:41
On hold
Orange tint; the orb slows almost to a stop. Line 2 “On hold”; Hold → Resume (orange-02 / orange-01). No glyph on the timer.
S8 · Ask first · ringing · TRANSFER
Shane Dusang02:41
Calling Karla Sellers...
Blue; the orb follows the ringback while the colleague's phone rings. Timer = total call time. Transfer slot → ✓ (green, dim until the colleague answers). End slot → neutral × Cancel (cancelConsult): red never cancels.
S9 · Ask first · talking · TRANSFER
Shane Dusang03:05
Talking to Karla Sellers
The blue orb follows the colleague's voice. ✓ (completeConsult) beside × reads as confirm / cancel.
S10 · Transferring · TRANSFER
Shane Dusang03:12
Transferring to Karla Sellers...
Blue ringback; line 2 is why Hold and Transfer are dimmed. End still hangs up.
S13 · Returned · connected · LIVE
Shane Dusang00:37
Returned · Karla Sellers didn't pick up
Line 2 keeps “Returned · …” while idle; hold, Ask first and transferring phrases override it while they run.
S12 · Colleague's side · connected · TRANSFER
Shane Dusang00:12
Devon Park asks first
The colleague's card during an Ask first: the orb follows the rep who asked. Hold/Resume and Transfer dim (followsConsult); End hangs up. Turns LIVE when the consult ends.
409 · Controls refused · LIVE
Shane Dusang02:41
Can't hold or transfer
The card never shrinks: Hold and Transfer dim in place with the line-2 reason; the existing info toast still fires. (Since #2087 a probe 409 looks like this; an action 409 keeps the keys live.)
7 · Every state · mobile · [0 Speaker] [1 Mute] [2 Hold] [3 Transfer] [4 End] · names on long-press (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
S1 · Incoming · WAITING
Shane Dusang
Incoming Call
Mobile web only: native iOS/Android keep the OS ring screen and first show the card at connect. Accept spans slots 0–3.
S1 · Incoming · returned · WAITING
Shane Dusang
Returned · Karla Sellers didn't pick up
Mobile web only. Line 2: the returned note.
S1 · Incoming · asks first · WAITING
Shane Dusang
Devon Park asks first
Mobile web only. Line 2: “{rep} asks first”.
S2 · Calling · DIALING
Shane Dusang
Calling...
Speaker is dimmed too until connect.
S3 · Connected · legacy / v1 · LIVE
Shane Dusang02:41
Can't hold or transfer
As desktop; Speaker stays live in slot 0.
S4 · Connected · v2 · LIVE
Shane Dusang02:41
Connected
Five keys; Speaker off = neutral fa-volume-low.
S5 · Muted · LIVE
Shane Dusang02:41
Connected
Mute → Unmute (red-02 / red-01). Nothing on the orb, which keeps following the customer. Nothing else moves.
S6 · Speaker on · LIVE
Shane Dusang02:41
Connected
Mobile only. Slot 0 is a toggle: blue-02 / blue-01 with fa-volume-high while the speaker is on.
S6 · Speaker off · LIVE
Shane Dusang02:41
Connected
Mobile only. Neutral with fa-volume-low, as on every other mobile row.
S7 · On hold · WAITING
Shane Dusang02:41
On hold
Orange tint; the orb slows almost to a stop. Line 2 “On hold”; Hold → Resume (orange-02 / orange-01). No glyph on the timer.
S8 · Ask first · ringing · TRANSFER
Shane Dusang02:41
Calling Karla Sellers...
Blue; the orb follows the ringback while the colleague's phone rings. Timer = total call time. Transfer slot → ✓ (green, dim until the colleague answers). End slot → neutral × Cancel (cancelConsult): red never cancels.
S9 · Ask first · talking · TRANSFER
Shane Dusang03:05
Talking to Karla Sellers
The blue orb follows the colleague's voice. ✓ (completeConsult) beside × reads as confirm / cancel.
S10 · Transferring · TRANSFER
Shane Dusang03:12
Transferring to Karla Sellers...
Blue ringback; line 2 is why Hold and Transfer are dimmed. End still hangs up.
S13 · Returned · connected · LIVE
Shane Dusang00:37
Returned · Karla Sellers didn't pick up
Line 2 keeps “Returned · …” while idle; hold, Ask first and transferring phrases override it while they run.
S12 · Colleague's side · connected · TRANSFER
Shane Dusang00:12
Devon Park asks first
The colleague's card during an Ask first: the orb follows the rep who asked. Hold/Resume and Transfer dim (followsConsult); End hangs up. Turns LIVE when the consult ends.
409 · Controls refused · LIVE
Shane Dusang02:41
Can't hold or transfer
The card never shrinks: Hold and Transfer dim in place with the line-2 reason; the existing info toast still fires. (Since #2087 a probe 409 looks like this; an action 409 keeps the keys live.)
8 · Engineering handoff (PRESCRIPTIVE — call card v3.1, CPO 2026-09-29)
  • The orb in Flutter is written and tested: docs/engineering/call-orb in this repo — call_orb_engine.dart (plain Dart: geometry + level follower), call_orb.dart (Ticker + CustomPainter, ~102 drawCircle calls in a RepaintBoundary), the tests and their fixtures, and the thinking-orbs MIT notice that must travel with them.
  • Verified 2026-09-29 (Flutter 3.44.6 / Dart 3.12.2): with no audio the engine reproduces the library's golden frames (8 cases, 4,224 values, ≤1e-4); the follower and audio frames equal sb-callorb.js (2,118 energies + 3 frames, ≤1e-9); line noise < 0.05 vs speech > 0.7; every driver runs; reduced motion schedules no frames; ~20 µs geometry per frame; flutter analyze clean.
  • Level stream: PhoneCallService.audioLevel, a ValueListenable<CallAudioLevel?> {input, output} (raw 0..1), only while connected. Web: add call.on("volume", …) beside the listeners in packages/twilio_voice/lib/_internal/twilio_voice_web.dart:1019–1028. macOS (WKWebView) and Windows (WebView2) run the same twilio.js 2.4.1: add volume to TVCallEvent / attachEventListeners (TVCall.swift:151) and the Windows bridge. iOS / Android: no level API wired (no getStats in the plugin) → driver none until added.
  • Card: _CallInProgressCard → Column[_CallHeader(64), _CallBar(64)] at a fixed 360×128; foregroundDecoration removed, popover shadow added; _IconButton → _CallKey (48 tall, r5, 20px FaIcon, no label, Tooltip + Semantics, opacity .4 when onPressed == null); slotsFor(status, phase, platform) always returns 4 slots (5 on mobile); the End slot is a neutral × Cancel for consultRinging / consultTalking (it already calls cancelConsult).
  • Mic glyph: a second CallLevelFollower on INPUT; the Mute glyph is green-01 while its energy is above 0.25 and the call is not muted.
  • Unchanged: PhoneCallService and CallControlNotifier logic (probe, 409, isBusy, polling, auto-fail), every toast, transfer_sheet.dart, drag and clamp, the hidden incoming card on native mobile.
  • [CLOSED 09-30] The in-call keypad (#2087) is placed in v3.2: the header Keypad key, the entry line, the desktop pad and the mobile dock (comp-call-keypad). [UNVERIFIED] Floor / gate / range on real calls; the macOS / Windows bridge at 20 messages a second; desktop_app.dart:197-207 reads cardW.
StateDriverWhat it doesTint
Incomingringingtwo 0.4 s pulses every 2 s (not audio)orange
Calling · Ask first ringing · Transferringringbackswells 2 s, rests 4 sgray · blue · blue
Connected · muted · speaker · returned · 409levelfollows the customer's voicegreen
Ask first talking · colleague's sidelevelfollows the colleague's voiceblue
On holdholdenergy 0, rolls at 0.2×orange
No level stream (iOS / Android for now)nonethe library's listening loop, unchangedby state
Reduced motion (any state)—one still frame (t = 0.6)by state

Call card v3.2 — Transfer panel and keypad

PRESCRIPTIVE (CPO 2026-09-30): Transfer opens as a details panel in the right column, and a Keypad key in the card header turns the name area into the digit entry line. The card is never part of the panel; it steps aside only if it overlaps. Full specs: Transfer panel · Keypad.

1 · The panel and the card are two surfaces (PRESCRIPTIVE — call card v3.2, CPO 2026-09-30)
T4 · 1440 × 900 · Karla picked · Transfer hovered
Shane Dusang02:41
Connected
Send the call to Karla now
The panel fills the 424 right column from y 51 to the window bottom. The card is not part of it: it overlapped the column at its default spot, so it stepped aside to x 640, y 98.
  • Transfer opens as a details panel in the right column, in the same grammar as Customer Details: title bar with ×, search, edge-to-edge user rows, a footer with the verbs. Replaces the Transfer sheet (comp-transfer-sheet, CANONICAL until this ships).
  • The call card is never inside or docked to the panel. While the panel is open, a card that overlaps it steps aside to the left, and comes back on close (§2). A card that doesn't overlap never moves.
  • Modal, without a dim. A click outside the panel or the card closes it and does nothing else (the dropdown rule). The page stays visible but can't be clicked while it is open.
  • Layer: above the page and any detail panel; below GlobalOverlay (the card, toasts, DAISI). It covers Customer Details, a SlideOut or a task pane without closing them; on close they come back as they were, scroll position and unsaved input included.
  • Opens only from the card's Transfer key, which turns .c3-key--blue “Close transfer”. Keypad: comp-call-keypad · card: comp-call-card-v3.
2 · The call card while Transfer is open: it steps aside (PRESCRIPTIVE — call card v3.2, CPO 2026-09-30)
Closed · default spot x 1048, y 98
Shane Dusang02:41
Connected
Open · steps aside to x 640, y 98
Shane Dusang02:41
Connected
Closed · dragged to x 1000, y 520
Shane Dusang02:41
Connected
Open · steps aside to x 640, y 520
Shane Dusang02:41
Connected
Open · dragged clear to x 320, y 600 · doesn't move
Shane Dusang02:41
Connected
  • Rule: while the panel is open, if the card's rect overlaps the panel's rect, the card moves to x = panel left − 16 − 360 and keeps its y. At 1440 × 900 the panel is x 1016–1440, so the card goes to x 640.
  • Motion: the card moves with the panel route's own duration and curve, so both land together, and returns on close. Reduced motion: it jumps.
  • Never saved: the move never writes phone_call_card_position; the saved spot is where it returns. Drag is off while the panel is open (default cursor).
  • The default anchor stays at top 98, centred over the 424 column (x 1048 at 1440). ENGINEER_HANDOFF T.3 stays open.
  • Header unchanged (tone, orb, name, timer · line 2). Keys: Mute, Hold/Resume, End and Speaker work · the name tap closes Transfer, then opens the customer · the Keypad key closes Transfer (the card returns) and opens the keypad · Transfer is .c3-key--blue, name “Close transfer”, Semantics expanded, live and blue even during isBusy.
  • [OPEN] Windows under 816 wide: panel left − 376 falls inside the 16 margin. Proposed: clamp at x 16, so the card overlaps the panel's left edge. UNVERIFIED; needs a call.
1 · The header becomes the keypad (PRESCRIPTIVE — call card v3.2, CPO 2026-09-30)
Closed · the key trails the header
Shane Dusang02:41
Connected
Open by pointer · the pad hangs under the card
Shane Dusang02:41
Connected
Type digits, * or #
Shane Dusang · 02:41
  • A Keypad key (.c3-hkey) sits at the right end of the card header on every call state. Pressing it turns the name area into a digit entry line (.c3-entry). The bar you approved does not change.
  • Desktop: open, then type. One click, or Enter on the key, then type 0–9, * and #, paste, and Backspace. Mouse users also get a 12-key pad attached under the card. It stays armed until you click somewhere else; clicking the card re-arms it. Needs Spike A (focus into GlobalOverlay).
  • Mobile: an in-app dial pad docks at the bottom of the screen, where the OS keyboard would sit — * and # on its face, Paste and Done, a haptic on every press, End always visible above it.
  • No red anywhere. Hold dims the keypad in place instead of closing it; a refused press is never drawn; no failure toast while it is open. Digits show only while it is open, wipe on close, on any auto-close and after 2 min idle, and are never stored or logged.
  • Opening the keypad closes Transfer, and the other way round (comp-transfer-panel). Card: comp-call-card-v3.

Call toasts

What the rep sees after each hold and transfer action. Unchanged in v3.1.

12 · Toasts (CANONICAL — mirrors QA 1395973)
success · 2s · Done after Ask firstTransferred to Karla Sellers
info · 5s · TransferTransferring to Karla Sellers
info · 5s · VoicemailSent to voicemail for Karla Sellers
warning · 3s · no answerKarla Sellers didn't answer
warning · 3s · the colleague leftKarla Sellers left the call
info · 5s · controls refused (409)Call controls are not available for this call
error · 3s · any other control errorWe couldn't complete that requestWe apologize for the inconvenience. Our development team has been made aware of this issue.
error · 3s · offlineNo Network ConnectionPlease connect to a wifi or cellular network to access this content.
error · 3s · a second call while one is liveYou are already on a call. Please end the call before making a new one.
placement · desktop
You are already on a call. Please end the call before making a new one.
placement · mobile (wraps, stays centred)
You are already on a call. Please end the call before making a new one.
  • One floating toast at the bottom centre, 16px from the edges, shrink-wrapped, at most window − 32 wide.
  • A new toast replaces the current one. Tapping a toast dismisses it.
  • Durations: success 2s · info 5s · warning 3s · error 3s.
  • Fills: success green-01 · info blue-01 · warning orange-01 · error red-01.
  • Text and icon use --white, which is dark text on the colour in dark mode. Every line is centred, and a two-line toast has no gap between its lines.
  • In "Transferred to {name}", "{name} didn't answer" and "{name} left the call", the name falls back to "a colleague". "Transferring to" and "Sent to voicemail for" use the colleague's full name, with no fallback in the code.
  • Error text comes from the server. Only the default and offline strings are known.
  • No toast for: starting Ask first, hold / unhold, the rep's own cancel, or anything on the colleague's side.
  • One call at a time: starting a second call shows the error toast "You are already on a call. Please end the call before making a new one." (phone_call_service.dart:500-510). The old busy-disabled Call tab is retired.
  • Where they overlap, the call card sits above toasts (the card is above the app's navigator). UNVERIFIED on device.
  • Motion: a 250ms fade + height reveal (Material 3 floating SnackBar). UNVERIFIED, not drawn.
  • The toast glyph is fa-solid (the atom specimen's weight; the app's weight is UNVERIFIED).

The transfer sheet

Where the rep picks who gets the call today (CANONICAL, the QA build). v3.2 replaces it with the Transfer panel.

1 · Anatomy (CANONICAL — mirrors QA 1395973)
desktop
Transfer call
KSKarla SellersSales ManagerOn shift
DPDevon ParkBDC RepresentativeOn a call
RARita AlvarezFinance ManagerOff shift
MHMarcus HillSales ConsultantOn shift
JWJen WuN/AOn shift
Shane Dusang02:41
Connected
mobile
Transfer call
KSKarla SellersSales ManagerOn shift
DPDevon ParkBDC RepresentativeOn a call
RARita AlvarezFinance ManagerOff shift
MHMarcus HillSales ConsultantOn shift
JWJen WuN/AOn shift
Shane Dusang02:41
Connected
  • v3.2 replaces this sheet (PRESCRIPTIVE, CPO 2026-09-30): Transfer opens as a 424 details panel in the right column and the call card is never part of it — comp-transfer-panel. This page stays CANONICAL until that ships.
  • Opened by Transfer on the call card. Transfer is disabled while a transfer is in progress and on the colleague's card during an Ask first, and hidden during the rep's own Ask first. Only one sheet can be open at a time.
  • The canon action sheet: 424 wide, centred on the whole window, anchored to the bottom, 16 outer padding, over the --overlay-trans-64 scrim. Under 424 it takes the full width.
  • ONE body card (gray-07, 1px gray-06, r7, pad 16) and no title card or button card. The card is 392 wide and its content 358 on desktop.
  • Inside the card, top to bottom, 16 apart: header — title "Transfer call" in --t-title gray-00, left-aligned; close × = the 32 primary icon button with a 12px gray-00 glyph · search · list (max 320, then it scrolls) · actions.
  • Loaded with 5+ rows and one selected, the card is 490 tall and sits 16 above the bottom.
  • The call card stays visible and usable above the scrim.
  • The whole sheet rides up with the soft keyboard. The search autofocuses, so on phones the keyboard opens at once.
  • No safe-area inset (the iOS home indicator is UNVERIFIED).
  • The sheet slides up from the bottom (Cupertino modal popup). Timing UNVERIFIED, not drawn.
3 · Rows and presence (CANONICAL — mirrors QA 1395973)
KSKarla SellersSales ManagerOn shift
DPDevon ParkBDC RepresentativeOn a call
RARita AlvarezFinance ManagerOff shift
MHMarcus HillSales ConsultantOn shift
JWJen WuN/AOn shift
On shiftOn a callOff shift
  • A row is 56 tall: 8 inset, 8 gap — the standard user-list row, as every other people list in the app (PRESCRIPTIVE, CPO 2026-09-30). QA's _CandidateRow keeps ListItemView's default 8 on top of UserItemScaffold's 8 (72 tall); passing padding: EdgeInsets.zero, as UserInfoItemView does, fixes it.
  • Rows are white on the gray-07 card, with square corners.
  • Row states: hover gray-06, pressed gray-05, selected blue-03 (no blue border).
  • 1px gray-06 bottom line, gone on hover and selected.
  • Avatar: a 32 slot drawn with .sb-avatar.s32 initials.
  • Production draws every loaded row's avatar with the online dot (isWorking) and the round-robin ring: both come from the /dealers user store, not the telephony directory, and every loaded user carries them, so the photo shrinks to 24 inside the 32 slot (avatar_view.dart:247-248). The DS draws plain initials here; v3.2 hides both.
  • Name: --t-tiny-bold gray-00, "First Last", falling back to email or phone.
  • Subtitle: the job title only, or "N/A", in --t-tiny gray-02.
  • The presence chip sits on the right.
  • The signed-in user is not listed.
  • Order: alphabetical by full name, from the dealership user store (dealership_repository.dart:34-44); the sheet does not re-sort. The directory only supplies presence.
  • One selection at a time, kept across refreshes. On a call and Off shift rows stay selectable.
  • Presence colours — On shift: green-02 / green-01 · On a call: orange-02 / orange-01 · Off shift: gray-06 / gray-02.
  • Chip geometry: 24 min height, r7, pad 0 8, --t-tiny. This is the new .sb-chipitem atom (ChipItemView).
  • No directory entry → Off shift. On a call beats shift status.
  • While the sheet is open the list refreshes every 10s silently (no shimmer). Chips update in place, and the query and selection survive.
4 · Ask first · Transfer · Voicemail (CANONICAL — mirrors QA 1395973)
On shift selected
On a call selected
Devon is on a call: voicemail only
Off shift selected
Rita is off shift: voicemail only
Nothing selected
Pick a colleague to transfer the call to
  • The labels are exactly Ask first · Transfer · Voicemail. QA ships "Transfer", not "Transfer now".
  • Equal widths, 8 apart: (358 − 16)/3 = 114 on desktop, ~97.7 at 375.
  • Tertiary style: gray-06 fill, hover/pressed gray-05, --t-action gray-00.
  • Disabled: gray-04 text on the same fill, no opacity.
  • Padding 0 8 makes each verb 24 tall (derived from code; UNVERIFIED on device). One line each, with an ellipsis.
  • Hint: --t-paragraph gray-02, left-aligned, 8 above the verbs.
  • {First} is the colleague's trimmed first name, or "this colleague".
  • A verb closes the sheet first, then runs. The call card shows what happens next: Ask first → "Calling {name}..." · Transfer or Voicemail → "Transferring to {name}...".
  • The background of a disabled verb under hover is UNVERIFIED.
5 · States (CANONICAL — mirrors QA 1395973)
loading
Transfer call
Pick a colleague to transfer the call to
nobody
Transfer call
Nobody to transfer to
Pick a colleague to transfer the call to
no match
Transfer call
No one matches “zed”
Pick a colleague to transfer the call to
  • Loading shows only on the first load: 4 rows, each 64 tall (pad 8, a 48×48 r5 block + a flexible 48-tall r5 block, gray-07).
  • These skeleton rows don't match the real 56px rows. That is how QA builds them.
  • QA's shimmer is a 2.5s back-and-forth sweep from gray-07 through white to gray-07. The DS .sb-skeleton sweep differs and is not changed here.
  • Empty and no-match: plain centred --t-paragraph gray-02 text in the 320 box (derived; UNVERIFIED on device).
  • A load error shows "We couldn't complete that request" / "We apologize for the inconvenience. Our development team has been made aware of this issue." with TRY AGAIN.
  • Offline, it shows "No Network Connection" / "Please connect to a wifi or cellular network to access this content."
  • Error styling is UNVERIFIED and not drawn.
6 · Closing (CANONICAL — mirrors QA 1395973)
  • The sheet closes on: the × · any verb · a tap on the scrim · Android back · the call ending (#2080).
  • Escape on desktop is UNVERIFIED.

Starting a call

Calls start from the composer's Call mode; Log records calls made off the system.

Call mode — one green button that dials; no fields, no toolbar, no send
Agent
hover / pressed
Agent
mobile width (390)
Agent
The label is "Call " + the number, masked +1 (###) ###-####. Runtime output is UNVERIFIED. Button: green-01 fill with a --white label (white in light, #1E1E1E in dark); 40 tall, pad 8/20, r5; a 12px phone glyph in a 24 box. Hover / pressed: green-02 fill + 1px green-01 border + green-01 text. The body is 80 (pad 20/16) on the green-02 wash, with no fields, toolbar or send. With Twilio, one tap starts the call in the floating call card (comp-phone-call-states.html). With GoTo on mobile, the tap opens the GoTo dialer. Otherwise a confirm opens: "CALL +1 (313) 555-0198?" / "This will open your device's phone app to make the call." / CALL · NEVERMIND, then tel:. With no dialer: "No apps found on your device that can make a phone call." The composer never changes after the tap: no live state, no wrap-up, no switch to Log. The only motion is the composer's 200ms height change when switching modes.
opt-out — the Call tab stays selectable; the body explains instead of dialing
Agent
Do Not ContactThis customer has opted out of phone calls.
The opt-out check runs before the phone check. The Call tab keeps its normal phone icon and stays selectable. The view is untinted: the host's background, presumed --white (UNVERIFIED per host). About 84 tall. Text and Email opt-out are not covered by this specimen; how the app handles them is UNVERIFIED (not audited).
no phone — Add Phone for users who can edit the customer
Agent
No PhoneTo call this customer add a phone number to their profile.
Add Phone shows only with permission to edit the customer; without it, it is hidden, not disabled. Tapping it loads the customer and opens Edit Customer Info. About 132 tall.
LOG — the manual backup (type=Call)
Agent
04/22/2026 05:03 PM
Confirmed the 36/10k terms — sending the quote after we hang up. Call 2m 41s.

On the timeline

Where the call went, after it ends.

5 · Calls — the transfer line (Telephony v2) (CANONICAL — mirrors QA 1395973)
No recording → the line is the subject
Lincoln Buyers@ 09/29/2026 9:12 am
Call Received by Devon Park
Transferred by Karla Sellers to Devon Park · answered
With a recording → the line is the first body line
Lincoln Buyers@ 09/29/2026 9:24 am
Call Received by Karla SellersHide
Transferred by Devon Park to Rita Alvarez · Rita Alvarez didn't pick up · back to Devon Park · then Devon Park asked Karla Sellers first · completed
0:002:38
View Transcript
Answered, no recording — an Ask first with no answer
Lincoln Buyers@ 09/29/2026 9:31 am
Call Received by Devon Park
Devon Park asked Rita Alvarez first · no answer
Mobile (390, 24px inset)
Lincoln Buyers@ 09/29/2026 9:24 am
Call Received by Karla SellersHide
Transferred by Devon Park to Rita Alvarez · Rita Alvarez didn't pick up · back to Devon Park · then Devon Park asked Karla Sellers first · completed
0:002:38
View Transcript
The line is built from the call's transfers, oldest first. A call with no transfers renders as before v2. No recording, summary or transcript: the line IS the subject. It is 13/500 italic in the card's accent colour (green, red or gray-04), always visible, with no Hide. With a body: the line is the first body line, 13/500 gray-02, not italic, 8 above the player. It collapses with Hide. Hidden while the call is in progress and during the 3-minute "DAISI is analyzing the call..." window. With no body and no transfers, the subject reads "No Data" in the same italic accent. Wording for one hop: "{By} asked {Target} first", then · completed, · canceled, · no answer, or · voicemail for {Target} — "{By} sent the caller to voicemail for {Target}" — "Transferred by {By} to {Target}", then · answered, · canceled, · {Target} didn't pick up, · {Target} didn't pick up · back to {By}, or · {Target} didn't pick up · voicemail for {Target}. Hops join with " · then ". The separator is U+00B7 with spaces. The apostrophe is ASCII. {By}: the colleague's full name, "Deleted User" if unknown, "a colleague" if missing. {Target}: a colleague's full name · an outside number as dialed (not masked), or "an outside number" · "voicemail" · "a group" · "a colleague". The opposite side insets 64px on desktop and 24px on mobile. These rows reuse the DS call row. Only the transfer line and the title are mirrored from QA; the bubble radius, header, player and play icon are the DS's.
6 · Calls — Voicemail for (CANONICAL — mirrors QA 1395973)
Voicemail for Rita Alvarez
Lincoln Buyers@ 09/29/2026 9:40 am
Voicemail for Rita AlvarezHide
Devon Park sent the caller to voicemail for Rita Alvarez
0:180:18
Voicemail for you
Lincoln Buyers@ 09/29/2026 9:52 am
Voicemail for youHide
Transferred by Devon Park to Karla Sellers · Karla Sellers didn't pick up · voicemail for Karla Sellers
0:180:18
The title replaces "Call Missed" only when all of these hold: the call is incoming · it was missed · a voicemail was left · the transfer named whose voicemail it is. It reads "Voicemail for you" for the signed-in user and "Voicemail for {Full Name}" otherwise ("Deleted User" if unknown). The row keeps the missed-with-voicemail visual: red, with the voicemail icon. The same title is used in the app's Notifications list, which the DS does not draw. Sales Inbox rows do not show it.

Engineering scope

What engineering should know before building the card and touching the sheet.

Full decision ledger: autopilot/TELEPHONY_PLAN.md (repo). The call card: v3.2 (PRESCRIPTIVE, 2026-09-30). Everything else: the QA build, space_crm origin/qa 1395973.