@coventia/softphone 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/{call-BTYlT2SU.d.cts → call-DUVnDxsz.d.cts} +152 -5
  2. package/dist/{call-BTYlT2SU.d.ts → call-DUVnDxsz.d.ts} +152 -5
  3. package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
  4. package/dist/chunk-7R6LOOAI.js.map +1 -0
  5. package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
  6. package/dist/chunk-LMJXX4YM.js.map +1 -0
  7. package/dist/{chunk-5BGEHMPP.js → chunk-YWI6EQYS.js} +40 -7
  8. package/dist/chunk-YWI6EQYS.js.map +1 -0
  9. package/dist/element/index.d.ts +154 -5
  10. package/dist/element/index.js +512 -152
  11. package/dist/element/index.js.map +1 -1
  12. package/dist/{engine-D9nbeY08.d.cts → engine-CDjkrXHY.d.cts} +1 -1
  13. package/dist/{engine-Doa6Brkm.d.ts → engine-EPCKf-Bl.d.ts} +1 -1
  14. package/dist/index.cjs +396 -14
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +5 -5
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.js +256 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/react/index.cjs +15 -0
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +14 -3
  23. package/dist/react/index.d.ts +14 -3
  24. package/dist/react/index.js +2 -2
  25. package/dist/{softphone-DhuhcKCy.d.cts → softphone-BtdgZWkm.d.ts} +17 -2
  26. package/dist/{softphone-DHmhotbf.d.ts → softphone-DHG9MTNg.d.cts} +17 -2
  27. package/dist/softphone.css +395 -1
  28. package/dist/{sources-N4BOEDRL.js → sources-5L2R4HTV.js} +104 -5
  29. package/dist/sources-5L2R4HTV.js.map +1 -0
  30. package/dist/testing/index.cjs +41 -5
  31. package/dist/testing/index.cjs.map +1 -1
  32. package/dist/testing/index.d.cts +8 -2
  33. package/dist/testing/index.d.ts +8 -2
  34. package/dist/testing/index.js +41 -5
  35. package/dist/testing/index.js.map +1 -1
  36. package/dist/ui/index.cjs +490 -139
  37. package/dist/ui/index.cjs.map +1 -1
  38. package/dist/ui/index.d.cts +2 -2
  39. package/dist/ui/index.d.ts +2 -2
  40. package/dist/ui/index.js +478 -141
  41. package/dist/ui/index.js.map +1 -1
  42. package/dist/zadarma/index.cjs +38 -5
  43. package/dist/zadarma/index.cjs.map +1 -1
  44. package/dist/zadarma/index.d.cts +2 -2
  45. package/dist/zadarma/index.d.ts +2 -2
  46. package/dist/zadarma/index.js +1 -1
  47. package/dist/zadarma-engine-4VOPIBXG.js +5 -0
  48. package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-4VOPIBXG.js.map} +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-3V2L6UTK.js.map +0 -1
  51. package/dist/chunk-5BGEHMPP.js.map +0 -1
  52. package/dist/chunk-N2N2EQQJ.js.map +0 -1
  53. package/dist/sources-N4BOEDRL.js.map +0 -1
  54. package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as Softphone, A as AudioDevices } from '../softphone-DHmhotbf.js';
4
- import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, o as SoftphoneState } from '../call-BTYlT2SU.js';
3
+ import { S as Softphone, A as AudioDevices } from '../softphone-BtdgZWkm.js';
4
+ import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, o as SoftphoneState } from '../call-DUVnDxsz.js';
5
5
 
6
6
  interface SoftphoneProviderProps {
7
7
  readonly softphone: Softphone;
@@ -37,6 +37,17 @@ declare function useIsLeader(): boolean;
37
37
  * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.
38
38
  */
39
39
  declare function useActiveCall(): Call | null;
40
+ /**
41
+ * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.
42
+ * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket
43
+ * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own
44
+ * always-connected decision), so passing `false` here does not close it — it only stops this
45
+ * particular component from re-rendering on every update while it has no reason to care, e.g. the
46
+ * transfer step being closed (spec §4.5: "subscribe when the step opens... unsubscribe when it
47
+ * closes"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`
48
+ * never shows a stale snapshot.
49
+ */
50
+ declare function usePresence(active: boolean): readonly PresenceEntry[];
40
51
  /** An immutable snapshot of a call's reactive fields. */
41
52
  interface CallView {
42
53
  readonly state: CallState;
@@ -65,4 +76,4 @@ declare function useCall(call: Call | null): CallView | null;
65
76
  */
66
77
  declare function useDevices(): AudioDevices;
67
78
 
68
- export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useSoftphone, useSoftphoneState };
79
+ export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
@@ -1,4 +1,4 @@
1
- export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useSoftphone, useSoftphoneState } from '../chunk-3V2L6UTK.js';
2
- import '../chunk-N2N2EQQJ.js';
1
+ export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState } from '../chunk-LMJXX4YM.js';
2
+ import '../chunk-7R6LOOAI.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,19 @@
1
- import { u as ContactsSource, v as HistorySource, g as Contact, A as AgentsDirectory, q as TypedEmitter, n as SoftphoneEventMap, o as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call } from './call-BTYlT2SU.cjs';
1
+ import { u as ContactsSource, v as HistorySource, g as Contact, A as AgentsDirectory, w as TransferOutcomesPage, x as PresenceSource, q as TypedEmitter, n as SoftphoneEventMap, o as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-DUVnDxsz.js';
2
2
 
3
- /** Contacts, history, caller ID and the agents directory, all from one config. */
3
+ /** Contacts, history, caller ID, the agents directory, transfer outcomes and presence, all from one
4
+ * config. */
4
5
  interface BackendSources {
5
6
  readonly contacts: ContactsSource;
6
7
  readonly history: HistorySource;
7
8
  readonly resolveContact: (number: string) => Promise<Contact | null>;
8
9
  /** The Historial tab's "filter by person" directory. Always Coventia's own org membership. */
9
10
  readonly listAgents: () => Promise<AgentsDirectory>;
11
+ /** Phase A (transfer outcome relay) — polled by the transfer watch, never by UI directly. */
12
+ readonly transferOutcomes: (params: {
13
+ readonly since?: string;
14
+ }) => Promise<TransferOutcomesPage>;
15
+ /** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */
16
+ readonly presence: PresenceSource;
10
17
  }
11
18
 
12
19
  /**
@@ -114,6 +121,14 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
114
121
  */
115
122
  readonly sources: BackendSources | null;
116
123
  readonly audio: AudioDevices;
124
+ /**
125
+ * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
126
+ * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
127
+ * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
128
+ * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
129
+ * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
130
+ */
131
+ readonly presence: readonly PresenceEntry[];
117
132
  /**
118
133
  * False when the browser has blocked audio playback.
119
134
  *
@@ -1,12 +1,19 @@
1
- import { u as ContactsSource, v as HistorySource, g as Contact, A as AgentsDirectory, q as TypedEmitter, n as SoftphoneEventMap, o as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call } from './call-BTYlT2SU.js';
1
+ import { u as ContactsSource, v as HistorySource, g as Contact, A as AgentsDirectory, w as TransferOutcomesPage, x as PresenceSource, q as TypedEmitter, n as SoftphoneEventMap, o as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-DUVnDxsz.cjs';
2
2
 
3
- /** Contacts, history, caller ID and the agents directory, all from one config. */
3
+ /** Contacts, history, caller ID, the agents directory, transfer outcomes and presence, all from one
4
+ * config. */
4
5
  interface BackendSources {
5
6
  readonly contacts: ContactsSource;
6
7
  readonly history: HistorySource;
7
8
  readonly resolveContact: (number: string) => Promise<Contact | null>;
8
9
  /** The Historial tab's "filter by person" directory. Always Coventia's own org membership. */
9
10
  readonly listAgents: () => Promise<AgentsDirectory>;
11
+ /** Phase A (transfer outcome relay) — polled by the transfer watch, never by UI directly. */
12
+ readonly transferOutcomes: (params: {
13
+ readonly since?: string;
14
+ }) => Promise<TransferOutcomesPage>;
15
+ /** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */
16
+ readonly presence: PresenceSource;
10
17
  }
11
18
 
12
19
  /**
@@ -114,6 +121,14 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
114
121
  */
115
122
  readonly sources: BackendSources | null;
116
123
  readonly audio: AudioDevices;
124
+ /**
125
+ * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
126
+ * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
127
+ * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
128
+ * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
129
+ * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
130
+ */
131
+ readonly presence: readonly PresenceEntry[];
117
132
  /**
118
133
  * False when the browser has blocked audio playback.
119
134
  *
@@ -219,6 +219,10 @@
219
219
 
220
220
  background: var(--cv-bg);
221
221
  overflow: hidden;
222
+ /* The containing block for `.cv-transfer-notices` — it overlays this whole shell, top edge to top
223
+ edge, not just `.cv-body`, so it reads as one seamless panel rather than a strip that starts a few
224
+ pixels short of the shell's own rounded corner. */
225
+ position: relative;
222
226
 
223
227
  /* The frame is the HOST's call. The CRM's panel already draws `border rounded-xl shadow-2xl`, so
224
228
  drawing our own put a card inside a card — nested chrome, which is always wrong. Defaulting to
@@ -293,6 +297,140 @@
293
297
  .cv-tab:disabled { opacity: 0.35; cursor: not-allowed; }
294
298
  .cv-tab span { font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.01em; }
295
299
 
300
+ /* ── Transfer outcome notices (Phase A — transfer-outcome-relay.md §5.5) ──────── */
301
+ /*
302
+ A persistent, dismissable banner OVER `.cv-body`, never a toast and never auto-dismissed — it carries
303
+ an action, and WCAG 2.2.1 requires a time limit to be removable, adjustable or extended.
304
+
305
+ Overlaps rather than reserving its own height, the same reasoning `.cv-dial-outcome`'s own comment
306
+ already gives for why the widget never lets a line of text resize the keypad under a moving finger —
307
+ here that principle cuts the other way: appearing costs nothing but a repaint, not a reflow, so the
308
+ widget's overall height stays identical whether a notice is showing or not, exactly like `.cv-detail`
309
+ overlaying `.cv-body`'s own list rather than pushing it down. It sits over whatever the current tab
310
+ happens to be showing (most often the dialer's country picker and number field — an acceptable loss:
311
+ the agent is not expected to start a fresh manual dial before dealing with a transfer that just failed
312
+ to connect), which is also why it outranks `.cv-detail` in `z-index`.
313
+
314
+ `[data-has-notices='false']` collapses to zero footprint: the element stays mounted (so its
315
+ `role="status"` region exists before the FIRST notice ever arrives — an aria-live region inserted at
316
+ the same moment as its own content is not reliably announced), but an idle widget, the common case,
317
+ covers nothing and costs no paint either.
318
+ */
319
+ .cv-transfer-notices {
320
+ position: absolute;
321
+ inset: 0 0 auto 0;
322
+ z-index: 3; /* above `.cv-detail`'s 2 — this is the higher-priority thing on screen */
323
+ }
324
+ .cv-transfer-notices[data-has-notices='true'] {
325
+ display: flex;
326
+ flex-direction: column;
327
+ gap: 0.5rem;
328
+ padding: 0.6rem 0.75rem;
329
+ background: var(--cv-bg);
330
+ box-shadow: var(--cv-shadow);
331
+ animation: cv-transfer-notices-in 200ms var(--cv-ease-out) both;
332
+ }
333
+ @keyframes cv-transfer-notices-in {
334
+ from { opacity: 0; transform: translateY(-6px); }
335
+ to { opacity: 1; transform: none; }
336
+ }
337
+ @media (prefers-reduced-motion: reduce) {
338
+ .cv-transfer-notices[data-has-notices='true'] { animation: none; }
339
+ }
340
+ .cv-transfer-notices-count {
341
+ margin: 0;
342
+ font: 600 0.72rem var(--cv-font);
343
+ color: var(--cv-muted);
344
+ text-transform: uppercase;
345
+ letter-spacing: 0.04em;
346
+ }
347
+ .cv-transfer-notice {
348
+ display: flex;
349
+ align-items: flex-start;
350
+ gap: 0.6rem;
351
+ padding: 0.65rem 0.7rem;
352
+ border-radius: var(--cv-radius-key);
353
+ border: 1px solid var(--cv-danger-soft);
354
+ background: var(--cv-surface);
355
+ }
356
+ /* Same shape as `.cv-gate-mark`, smaller — one visual language for "here is a status", not two. */
357
+ .cv-transfer-notice-mark {
358
+ flex-shrink: 0;
359
+ display: grid;
360
+ place-items: center;
361
+ width: 30px;
362
+ height: 30px;
363
+ border-radius: 999px;
364
+ background: var(--cv-danger-soft);
365
+ color: var(--cv-danger);
366
+ }
367
+ .cv-transfer-notice-body { flex: 1; min-width: 0; }
368
+ /*
369
+ Title shares its row with only the dismiss button (24px) — not the whole card's width, and not the
370
+ "Llamar de vuelta" button, which gets its own row below (`.cv-transfer-notice-footer`). The original
371
+ layout put mark + title + subtitle + a full button all in one flex row, leaving the title as little as
372
+ ~80px to wrap into at the real ~340px embed width — nearly every word landed on its own line.
373
+ `align-items: flex-start` keeps the dismiss pinned to the first line even when the title wraps to two.
374
+ */
375
+ .cv-transfer-notice-header { display: flex; align-items: flex-start; gap: 0.4rem; }
376
+ .cv-transfer-notice-title {
377
+ flex: 1;
378
+ min-width: 0;
379
+ margin: 0;
380
+ font: 600 0.82rem var(--cv-font);
381
+ color: var(--cv-text);
382
+ line-height: 1.35;
383
+ }
384
+ .cv-transfer-notice-subtitle {
385
+ margin: 0.15rem 0 0;
386
+ font-size: 0.78rem;
387
+ color: var(--cv-muted);
388
+ line-height: 1.35;
389
+ }
390
+ /* Age and the callback button share a row too, same reasoning as the header — a short fixed-width
391
+ timestamp beside a content-sized button, neither one fighting the title for space above. */
392
+ .cv-transfer-notice-footer {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: space-between;
396
+ gap: 0.5rem;
397
+ margin-top: 0.35rem;
398
+ }
399
+ .cv-transfer-notice-age { margin: 0; font-size: 0.7rem; color: var(--cv-faint); flex-shrink: 0; }
400
+ /*
401
+ `.cv-btn.cv-transfer-notice-callback` — TWO classes, not one. Equal-specificity selectors resolve by
402
+ source order, and this rule used to lose its `padding`/`font-size` to the shared `.cv-btn` rule below
403
+ simply because that one appears later in the file; the button rendered at full default CTA size
404
+ instead of this smaller, secondary one, which is what actually forced the title into that ~80px
405
+ column above. The extra class is what makes this override win regardless of source order.
406
+ */
407
+ .cv-btn.cv-transfer-notice-callback {
408
+ flex-shrink: 0;
409
+ padding: 0.35rem 0.6rem;
410
+ font-size: 0.75rem;
411
+ display: inline-flex;
412
+ align-items: center;
413
+ gap: 0.3rem;
414
+ white-space: nowrap;
415
+ }
416
+ /* Same round icon-only shape as `.cv-search-clear` — one dismiss affordance, not a new one. */
417
+ .cv-transfer-notice-dismiss {
418
+ flex-shrink: 0;
419
+ width: 24px;
420
+ height: 24px;
421
+ display: inline-flex;
422
+ align-items: center;
423
+ justify-content: center;
424
+ border: none;
425
+ border-radius: 999px;
426
+ background: none;
427
+ color: var(--cv-muted);
428
+ cursor: pointer;
429
+ transition: color 0.12s ease, background 0.12s ease, transform 0.1s var(--cv-ease-out);
430
+ }
431
+ .cv-transfer-notice-dismiss:hover { color: var(--cv-text); background: var(--cv-surface-2); }
432
+ .cv-transfer-notice-dismiss:active { transform: scale(0.88); }
433
+
296
434
  /* ── Buttons ─────────────────────────────────────────────────────────────────── */
297
435
  .cv-btn {
298
436
  font: 600 0.875rem var(--cv-font);
@@ -494,6 +632,16 @@
494
632
  gap: 0.15rem;
495
633
  text-align: center;
496
634
  padding: 1rem;
635
+ /*
636
+ A REAL bug, found live (plan T7): the transfer picker's agent list can make this box's content
637
+ taller than the space `flex: 1` actually allocates it — the avatar/name/timer/status ABOVE the
638
+ picker are still shown while transferring, unlike `.cv-call-devices-inline`'s own "swap the
639
+ content" fix for the pre-connect devices screen. Without this, the overflow painted straight past
640
+ `.cv-call`'s own shrunk box and onto `.cv-controls` below it — not scroll, actual overlap, controls
641
+ rendered ON TOP of list rows. `overflow-y: auto` keeps any excess inside THIS box, scrollable,
642
+ exactly like `.cv-presence-list`'s own internal scroll already does for the list itself.
643
+ */
644
+ overflow-y: auto;
497
645
  }
498
646
 
499
647
  .cv-avatar-wrap { position: relative; margin-bottom: 0.85rem; }
@@ -752,11 +900,250 @@
752
900
  border-color: var(--cv-text);
753
901
  }
754
902
 
903
+ /*
904
+ The attended-transfer target-entry panel (`docs/specs/transfer-availability.md` §5): an optional
905
+ outcome line, then a row of whatever's actionable (input+button today). Once a consult exists,
906
+ `ConsultScreen` takes over the whole screen instead — this panel is ONLY ever the pre-consult /
907
+ retry-after-failure state now; it used to also cover the consulting/answered phases, which is why
908
+ its rule is a column container built for more than one child even though there's only ever one row
909
+ here today. (History: was two near-duplicate rules — `.cv-transfer` as a bare ROW,
910
+ `.cv-transfer-consult` as a column — until the row version turned out to be a real,
911
+ reproduced-in-browser bug: the optional `.cv-dial-outcome` message rendered as a THIRD item in the
912
+ same horizontal row as the input and button, squeezing the input to its content-minimum. Merged
913
+ rather than patched.)
914
+ */
755
915
  .cv-transfer {
916
+ display: flex;
917
+ flex-direction: column;
918
+ gap: 0.3rem;
919
+ width: 100%;
920
+ padding: 0.2rem 0.25rem 0;
921
+ /* `.cv-call` centres and centre-aligns its normal (avatar) content — same reasoning
922
+ `.cv-call-devices-inline` already opts out of for the OTHER inline swap in this box (its own
923
+ comment): rows read left-aligned everywhere else in the widget, and a centred context line
924
+ sitting directly above a left-aligned input/toggle/list read as two layouts stacked on top of
925
+ each other, found live (2026-09-29). */
926
+ text-align: left;
927
+ /* Found live (2026-09-29): `.cv-call`'s avatar/name/timer block used to sit ABOVE this,
928
+ unconditionally, so this panel only ever got whatever was left over — see the JSX call site's own
929
+ comment. Now that this IS the whole of `.cv-call`'s content while a transfer is in progress
930
+ (a swap, not an addition), it should CLAIM `.cv-call`'s available height, not just sit in it —
931
+ `flex: 1` does that; `min-height: 0` is what lets a flex child actually shrink to fit instead of
932
+ forcing its parent to grow past it (the same reason `.cv-call`/`.cv-view`/`.cv-body` all set it,
933
+ see their own comments). This is what lets `.cv-presence-list` below flex-grow into real room
934
+ instead of needing its own hand-tuned `max-height` guess. */
935
+ flex: 1;
936
+ min-height: 0;
937
+ /* Same entrance `.cv-call-devices-inline` already uses for the analogous inline swap (its own
938
+ comment) — reused, not a second animation invented for this widget's second inline-swap view. */
939
+ animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);
940
+ }
941
+ @media (prefers-reduced-motion: reduce) {
942
+ .cv-transfer { animation: none; }
943
+ }
944
+ /* The compact "who am I still on the phone with" line while the picker is open — reuses
945
+ `.cv-call-devices-inline`'s own two classes verbatim (`.cv-call-devices-title` for the name,
946
+ `.cv-call-devices-context` for the number/timer line) rather than inventing a second "quiet header"
947
+ style for the same widget; only the tiny stacking gap between them is new.
948
+
949
+ Found live (2026-09-29): `.cv-transfer`'s own `gap` alone gave every section — context, input row,
950
+ mode toggle, results list — the exact same spacing, so nothing read as its own group; the eye had
951
+ no cue for where "who I'm on the phone with" ends and "pick a transfer target" begins. This extra
952
+ `margin-bottom`, on top of the base `gap`, is that cue — the toggle stays close to the list it
953
+ controls (unchanged), only the boundary above the interactive controls opened up. */
954
+ .cv-transfer-context {
955
+ display: flex;
956
+ flex-direction: column;
957
+ gap: 2px;
958
+ flex-shrink: 0;
959
+ margin-bottom: 0.35rem;
960
+ }
961
+ /* Plan T3: the mode toggle reuses `.cv-segmented` (HistoryTab's own filter control) as-is, but its
962
+ `margin-bottom` (needed THERE, where the surrounding layout has no gap of its own) is redundant
963
+ here — `.cv-transfer`'s own `gap` above already spaces every child, so the two would stack. Scoped
964
+ to this context only; HistoryTab's own copy is untouched. */
965
+ .cv-transfer .cv-segmented { margin-bottom: 0; flex-shrink: 0; }
966
+ .cv-transfer .cv-presence-legend { flex-shrink: 0; }
967
+ /* Same treatment as `.cv-call-status` (the avatar's own "Llamando…"/"En espera" line) — one muted
968
+ status-line vocabulary for the whole call screen, not a second one invented for this panel. */
969
+ .cv-transfer-status {
970
+ margin: 0;
971
+ font-size: 0.78rem;
972
+ color: var(--cv-muted);
973
+ letter-spacing: 0.04em;
974
+ }
975
+ /* The target-entry/retry row: an input plus one button. */
976
+ .cv-transfer-row {
756
977
  display: flex;
757
978
  gap: 0.5rem;
979
+ }
980
+ /*
981
+ Flex items default to `min-width: auto`, which floors them at their CONTENT size rather than letting
982
+ them shrink or grow to fill the row — `.cv-search`'s own `width: 100%` never actually took effect
983
+ here, because `.cv-btn`'s text ("Llamar") set an unshrinkable floor and the input, having no
984
+ `flex-grow`, never reclaimed the space left over. `flex: 1` + `min-width: 0` is what actually makes
985
+ an input-plus-button row behave like one; `flex: none` keeps the button at its natural size.
986
+ */
987
+ .cv-transfer-row .cv-search {
988
+ flex: 1;
989
+ min-width: 0;
990
+ }
991
+ /* `max-width` + ellipsis: the label now includes whatever was typed ("Llamar a {typed}", spec §4.5)
992
+ and `truncateForButton` already bounds that in JS — this is the CSS backstop, so a value that
993
+ slips past it (a very wide character set, a future copy change) still can't push `.cv-search` down
994
+ to an unusable sliver the way the pre-truncation button did before this was noticed. */
995
+ .cv-transfer-row .cv-btn {
996
+ flex: none;
997
+ max-width: 60%;
998
+ overflow: hidden;
999
+ text-overflow: ellipsis;
1000
+ white-space: nowrap;
1001
+ }
1002
+ /* The consult-answered row: one or two buttons, no input. */
1003
+ .cv-transfer-actions {
1004
+ display: flex;
1005
+ gap: 0.5rem;
1006
+ }
1007
+ .cv-transfer-actions .cv-btn {
1008
+ flex: 1;
1009
+ }
1010
+ /* The ringing phase has only "Cancelar" — a secondary, low-stakes action. Letting the row's own
1011
+ `flex: 1` stretch a LONE button edge to edge reads as oversized for what it is, and nothing else in
1012
+ this widget sizes a single button to the full row; every other `.cv-btn` sizes to its content. */
1013
+ .cv-transfer-actions .cv-btn:only-child {
1014
+ flex: none;
1015
+ }
1016
+
1017
+ /*
1018
+ The agent picker (spec §4.5, plan T7) — an inline, always-visible filtered list under the
1019
+ input+button row, not a click-to-open popover like `.cv-person-filter-pop`: the whole point is that
1020
+ typing already filters it, so hiding it behind a second interaction would undo that.
1021
+
1022
+ Redesigned live (2026-09-29): T7 (72px) and T3 (30px) both hand-tuned a `max-height` in PIXELS
1023
+ against whatever else happened to be on screen at the time, and both were broken by the very next
1024
+ addition above the list (T3's own toggle broke T7's number the same way this would keep breaking on
1025
+ the next change) — down to "barely proves the list is scrollable" at 30px, which is what the user
1026
+ was actually seeing: "users cannot almost see any of the listed users." The real fix wasn't a bigger
1027
+ magic number, it was `.cv-transfer`'s own move to `flex: 1` (see its comment) freeing real height
1028
+ from the block that used to sit above this whole panel — `.cv-presence` and this list now flex-grow
1029
+ into whatever's ACTUALLY left after the context line, input row, toggle and legend take their own
1030
+ natural size, on whatever real height the host panel gives `.cv-call` (this SDK's fixed ~310×520
1031
+ embed today, but also whatever a different host gives it tomorrow — no more re-tuning a constant
1032
+ every time something above the list changes). `.cv-call`'s own `overflow-y: auto` (its own comment)
1033
+ is still the last-resort net if content ever genuinely doesn't fit; this just means that net should
1034
+ now rarely, if ever, have to catch anything for a normal agent list.
1035
+ */
1036
+ .cv-presence {
1037
+ display: flex;
1038
+ flex-direction: column;
1039
+ gap: 0.35rem;
1040
+ flex: 1;
1041
+ min-height: 0;
1042
+ }
1043
+ .cv-presence-list {
1044
+ display: flex;
1045
+ flex-direction: column;
1046
+ gap: 2px;
1047
+ flex: 1;
1048
+ min-height: 0;
1049
+ overflow-y: auto;
1050
+ padding: 0.25rem;
1051
+ border: 1px solid var(--cv-border);
1052
+ border-radius: var(--cv-radius-key);
1053
+ background: var(--cv-surface);
1054
+ }
1055
+ .cv-presence-empty {
1056
+ margin: 0;
1057
+ padding: 0.5rem;
1058
+ text-align: center;
1059
+ font-size: 0.78rem;
1060
+ color: var(--cv-muted);
1061
+ }
1062
+ .cv-presence-item {
758
1063
  width: 100%;
759
- padding: 0.85rem 0.25rem 0;
1064
+ display: flex;
1065
+ align-items: center;
1066
+ gap: 0.5rem;
1067
+ padding: 0.4rem 0.5rem;
1068
+ border: none;
1069
+ border-radius: calc(var(--cv-radius) - 4px);
1070
+ background: none;
1071
+ color: var(--cv-text);
1072
+ font: 400 0.8rem var(--cv-font);
1073
+ text-align: left;
1074
+ cursor: pointer;
1075
+ }
1076
+ .cv-presence-item:hover:not(:disabled) { background: var(--cv-surface-2); }
1077
+ /* En llamada rows are a hard block, not just a hint (CallScreen.tsx's own comment on the row has why)
1078
+ — same `:disabled` treatment `.cv-btn`/`.cv-tab` already use elsewhere in this file, not a new pattern. */
1079
+ .cv-presence-item:disabled { opacity: 0.45; cursor: not-allowed; }
1080
+ .cv-presence-dot {
1081
+ flex-shrink: 0;
1082
+ width: 8px;
1083
+ height: 8px;
1084
+ border-radius: 999px;
1085
+ }
1086
+ /* Reuses existing intent tokens rather than inventing new ones — `--cv-accent` already means
1087
+ "good, reachable" (the answer button), `--cv-hold` already means "occupied elsewhere" (the hold
1088
+ toggle), and `--cv-faint` is this file's own DECORATIVE-only tier (see the `--cv-*` legend near the
1089
+ top) — exactly what "no signal for this person" is, no signal being the whole point of that tier. */
1090
+ .cv-presence-dot[data-state='disponible'] { background: var(--cv-accent); }
1091
+ .cv-presence-dot[data-state='en_llamada'] { background: var(--cv-hold); }
1092
+ .cv-presence-dot[data-state='desconectado'] { background: var(--cv-faint); }
1093
+ .cv-presence-name {
1094
+ flex: 1;
1095
+ min-width: 0;
1096
+ overflow: hidden;
1097
+ text-overflow: ellipsis;
1098
+ white-space: nowrap;
1099
+ }
1100
+ .cv-presence-meta {
1101
+ flex-shrink: 0;
1102
+ font-size: 0.72rem;
1103
+ color: var(--cv-muted);
1104
+ }
1105
+ /* Spec §4.6: Desconectado is a HINT, never "can't transfer" — this is what tells the agent so, right
1106
+ under the list it describes. `--cv-muted`, NOT `--cv-faint`: this text is informational (the reader
1107
+ needs it to not misread the state), and `--cv-faint` is reserved for decorative-only content that
1108
+ fails AA on purpose — using it here would repeat the exact tier mistake this file's own `--cv-*`
1109
+ comment block warns against. */
1110
+ .cv-presence-legend {
1111
+ margin: 0;
1112
+ font-size: 0.7rem;
1113
+ color: var(--cv-muted);
1114
+ line-height: 1.35;
1115
+ }
1116
+
1117
+ /*
1118
+ `ConsultScreen`'s "someone is still waiting" banner (`docs/specs/transfer-availability.md` §5, the
1119
+ cross-talk fix) — full-width and tappable, not a passive status line, because the entire reason this
1120
+ screen exists is to make "go back to the caller" impossible to miss. Neutral surface + the same
1121
+ `--cv-hold` colour `.cv-badge-hold` already uses for the same fact elsewhere, not a new soft-fill
1122
+ token: a text/icon tint on a plain surface is already proven legible here, and inventing an
1123
+ unmeasured background-tint pairing risks a real contrast regression this file takes seriously
1124
+ elsewhere (see the `-soft`/`-strong` split's own comments).
1125
+ */
1126
+ .cv-consult-hold-banner {
1127
+ display: flex;
1128
+ align-items: center;
1129
+ justify-content: center;
1130
+ gap: 0.4rem;
1131
+ width: 100%;
1132
+ padding: 0.5rem 0.75rem;
1133
+ border-radius: 999px;
1134
+ border: 1px solid var(--cv-border);
1135
+ background: var(--cv-surface);
1136
+ color: var(--cv-hold);
1137
+ font: 600 0.78rem var(--cv-font);
1138
+ cursor: pointer;
1139
+ transition: background 0.15s ease, border-color 0.15s ease;
1140
+ }
1141
+ .cv-consult-hold-banner:hover {
1142
+ background: var(--cv-surface-2);
1143
+ border-color: var(--cv-hold);
1144
+ }
1145
+ .cv-consult-hold-banner:active {
1146
+ transform: scale(0.98);
760
1147
  }
761
1148
 
762
1149
  /*
@@ -814,6 +1201,13 @@
814
1201
  color: var(--cv-text);
815
1202
  font: inherit;
816
1203
  transition: border-color 0.15s ease, background 0.15s ease;
1204
+ /* Found live (2026-09-29): in the transfer row specifically, `.cv-btn`'s own `max-width: 60%`
1205
+ (needed for the dynamic "Llamar a {typed}" label) can leave the input narrower than its
1206
+ placeholder text — an `<input>` has no native ellipsis, so it hard-clipped mid-word
1207
+ ("...o núm") instead of trailing off cleanly. A real, if rare, risk for ANY `.cv-search` usage
1208
+ wherever it's squeezed, not just this one row — the fix belongs on the base rule. */
1209
+ overflow: hidden;
1210
+ text-overflow: ellipsis;
817
1211
  }
818
1212
  .cv-search::placeholder { color: var(--cv-muted); }
819
1213
  .cv-search:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }