@coventia/softphone 0.3.0 → 0.5.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 (60) hide show
  1. package/dist/{call-CaPDWVk3.d.cts → call-FqcvDg1t.d.cts} +168 -12
  2. package/dist/{call-CaPDWVk3.d.ts → call-FqcvDg1t.d.ts} +168 -12
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
  6. package/dist/chunk-6RR6G35E.js.map +1 -0
  7. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  8. package/dist/chunk-FRXHEHQA.js.map +1 -0
  9. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  10. package/dist/chunk-I3RSC52R.js.map +1 -0
  11. package/dist/chunk-M76SAXXM.js +13 -0
  12. package/dist/chunk-M76SAXXM.js.map +1 -0
  13. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  14. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  15. package/dist/element/index.d.ts +198 -6
  16. package/dist/element/index.js +1160 -489
  17. package/dist/element/index.js.map +1 -1
  18. package/dist/{engine-BgBsIcEa.d.ts → engine-CsNRD7B7.d.ts} +1 -1
  19. package/dist/{engine-CU2rEo3g.d.cts → engine-CzOCrfY9.d.cts} +1 -1
  20. package/dist/index.cjs +675 -53
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +16 -6
  23. package/dist/index.d.ts +16 -6
  24. package/dist/index.js +453 -37
  25. package/dist/index.js.map +1 -1
  26. package/dist/react/index.cjs +26 -0
  27. package/dist/react/index.cjs.map +1 -1
  28. package/dist/react/index.d.cts +24 -5
  29. package/dist/react/index.d.ts +24 -5
  30. package/dist/react/index.js +2 -2
  31. package/dist/{softphone-DEt0TtQw.d.ts → softphone-By9sizlV.d.ts} +44 -3
  32. package/dist/{softphone-dZMkW8V1.d.cts → softphone-JZvJdzU3.d.cts} +44 -3
  33. package/dist/softphone.css +254 -23
  34. package/dist/{sources-M3GYMS6V.js → sources-QL2LRZVH.js} +93 -8
  35. package/dist/sources-QL2LRZVH.js.map +1 -0
  36. package/dist/testing/index.cjs +13 -0
  37. package/dist/testing/index.cjs.map +1 -1
  38. package/dist/testing/index.d.cts +2 -2
  39. package/dist/testing/index.d.ts +2 -2
  40. package/dist/testing/index.js +1 -1
  41. package/dist/ui/index.cjs +670 -20
  42. package/dist/ui/index.cjs.map +1 -1
  43. package/dist/ui/index.d.cts +2 -2
  44. package/dist/ui/index.d.ts +2 -2
  45. package/dist/ui/index.js +642 -22
  46. package/dist/ui/index.js.map +1 -1
  47. package/dist/zadarma/index.cjs +111 -14
  48. package/dist/zadarma/index.cjs.map +1 -1
  49. package/dist/zadarma/index.d.cts +5 -6
  50. package/dist/zadarma/index.d.ts +5 -6
  51. package/dist/zadarma/index.js +3 -2
  52. package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
  53. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  56. package/dist/chunk-7R6LOOAI.js.map +0 -1
  57. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  58. package/dist/chunk-UHNLEBBO.js.map +0 -1
  59. package/dist/sources-M3GYMS6V.js.map +0 -1
  60. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -237,16 +237,13 @@
237
237
  the gutters cost more than they give, and a cramped keypad is worse than a tight one. */
238
238
  @container cv (max-width: 300px) {
239
239
  .cv-keypad { padding: 0 0.6rem; gap: 6px; }
240
- .cv-dial-prefix { padding: 0.7rem 0.6rem 0; }
241
- .cv-dial-head { padding: 0.3rem 0.6rem 0.6rem; }
242
- .cv-display { font-size: 1.5rem; }
240
+ .cv-dial-head { margin: 0 0.6rem 0.6rem; }
243
241
  }
244
242
 
245
243
  /* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys
246
244
  stop being tappable — so buy the height back from the chrome instead of from the keys. */
247
245
  @container cv (max-height: 440px) {
248
- .cv-dial-prefix { padding-top: 0.5rem; }
249
- .cv-dial-head { padding-top: 0.2rem; padding-bottom: 0.4rem; }
246
+ .cv-dial-head { margin-bottom: 0.4rem; }
250
247
  .cv-dial-foot { padding-bottom: 0.5rem; }
251
248
  .cv-controls { gap: 0.4rem; padding-bottom: 0.6rem; }
252
249
  }
@@ -482,21 +479,51 @@
482
479
  .cv-round--hangup:hover:not(:disabled) { filter: brightness(0.94); }
483
480
 
484
481
  /* ── Dialpad ─────────────────────────────────────────────────────────────────── */
485
- /* The prefix chip, on its own row above the number.
486
- Sharing a line with the number starves it: chip + erase button eat the middle, and a formatted
487
- international number needs every pixel. The number is the content; the prefix is a setting. */
488
- .cv-dial-prefix {
489
- flex-shrink: 0;
490
- display: flex;
491
- justify-content: center;
492
- padding: 1rem 1rem 0;
493
- }
482
+ /* The number: ONE bordered field, the prefix inside it on the left and erase on the right — the pattern of the
483
+ providers' dialers (Talkdesk "[🇺🇸 +1 ▾ | Type or paste a number]", Zendesk, Aircall; outbound-number-per-agent.md
484
+ §5.5, 2026-10-26). The prefix is part of the number being dialled, so it sits inside the same box. The number
485
+ shrinks to fit (`DISPLAY_SIZES` in Dialpad.tsx). The field is the positioned ancestor of the country list, so the
486
+ list spans its width (`left: 0; right: 0`). */
494
487
  .cv-dial-head {
495
488
  flex-shrink: 0;
489
+ position: relative;
496
490
  display: flex;
497
491
  align-items: center;
498
- padding: 0.35rem 1rem 0.75rem;
499
- gap: 0.25rem;
492
+ height: 46px;
493
+ margin: 0 1rem 0.75rem;
494
+ padding: 0 0.2rem 0 0;
495
+ border: 1px solid var(--cv-border-strong);
496
+ border-radius: 10px;
497
+ background: var(--cv-bg);
498
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
499
+ }
500
+ /* With no "Llamar desde" line above it (an org with one number), the field opens the view. */
501
+ .cv-view > .cv-dial-head:first-child { margin-top: 1rem; }
502
+ /* Focus shows on the field, as on any input (WCAG 2.4.7): the accent border and a soft ring. */
503
+ .cv-dial-head:focus-within {
504
+ border-color: var(--cv-accent);
505
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-accent) 18%, transparent);
506
+ }
507
+ .cv-dial-head > .cv-cp {
508
+ position: static;
509
+ width: auto;
510
+ flex-shrink: 0;
511
+ align-self: stretch;
512
+ }
513
+ /* The prefix as the field's leading segment: the same chip, flush with the field, divided from the number. */
514
+ .cv-dial-head .cv-cp-chip {
515
+ height: 100%;
516
+ padding: 0 0.45rem 0 0.65rem;
517
+ border: 0;
518
+ border-right: 1px solid var(--cv-border);
519
+ border-radius: 9px 0 0 9px;
520
+ background: var(--cv-surface);
521
+ }
522
+ .cv-dial-head .cv-cp-chip:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; }
523
+ /* Following a number typed in full ("+44 …"): read-only — the flag alone, no caret, no hover. */
524
+ .cv-cp-chip[data-from-number] {
525
+ cursor: default;
526
+ padding: 0 0.6rem;
500
527
  }
501
528
  /*
502
529
  * The outcome of the last attempt, under the number.
@@ -538,20 +565,25 @@
538
565
  }
539
566
  }
540
567
 
568
+ /* Left-aligned after the prefix segment, as a phone field reads ("+34 | 600 11 12 22"). 1.375rem is the start of
569
+ `DISPLAY_SIZES`, which steps it down until a long number fits the field. */
541
570
  .cv-display {
542
571
  flex: 1;
543
572
  min-width: 0;
544
- font: 600 1.75rem var(--cv-font);
573
+ height: 100%;
574
+ font: 500 1.375rem var(--cv-font);
545
575
  font-variant-numeric: tabular-nums;
546
- letter-spacing: -0.02em;
547
- text-align: center;
576
+ letter-spacing: -0.01em;
577
+ text-align: left;
548
578
  color: var(--cv-text);
549
579
  border: none;
550
580
  background: none;
551
- padding: 0;
581
+ padding: 0 0.6rem;
552
582
  caret-color: var(--cv-accent);
553
583
  }
554
- .cv-display::placeholder { color: var(--cv-faint); font-weight: 400; }
584
+ /* `--cv-muted`, not `--cv-faint`: a placeholder is read like any text (4.5:1). */
585
+ .cv-display::placeholder { color: var(--cv-muted); font-weight: 400; }
586
+ /* The field around it shows focus (`.cv-dial-head:focus-within`). */
555
587
  .cv-display:focus { outline: none; }
556
588
 
557
589
  /* The spacer/erase pair keeps the number optically centred. */
@@ -607,8 +639,9 @@
607
639
  font-size: 9px;
608
640
  font-weight: 600;
609
641
  letter-spacing: 0.14em;
610
- /* Decorative: the letters carry no information the digit doesn't. `faint`, not `muted`. */
611
- color: var(--cv-faint);
642
+ /* `muted` (was `faint`, 2.5:1): the letters are read when dialling a number written in letters (900-FLORES),
643
+ so they are text, held to 4.5:1 like any other (design critique, 2026-10-26). */
644
+ color: var(--cv-muted);
612
645
  line-height: 1;
613
646
  }
614
647
 
@@ -687,6 +720,23 @@
687
720
 
688
721
  .cv-call-name { font-size: 1rem; font-weight: 600; line-height: 1.2; }
689
722
  .cv-call-number { font-size: 0.75rem; color: var(--cv-muted); margin-top: 2px; }
723
+ /* Which of the org's numbers this outbound call shows. Smaller than the callee: context, not identity — but muted,
724
+ not faint: --cv-faint is under 4.5:1 at this size in both themes, and the agent may need to read it mid-call. */
725
+ .cv-call-from {
726
+ display: inline-flex;
727
+ align-items: center;
728
+ gap: 0.25rem;
729
+ max-width: 100%;
730
+ margin-top: 4px;
731
+ font-size: 0.72rem;
732
+ color: var(--cv-muted);
733
+ }
734
+ .cv-call-from span {
735
+ min-width: 0;
736
+ overflow: hidden;
737
+ text-overflow: ellipsis;
738
+ white-space: nowrap;
739
+ }
690
740
 
691
741
  .cv-timer {
692
742
  display: flex;
@@ -1867,6 +1917,16 @@
1867
1917
  text-overflow: ellipsis;
1868
1918
  }
1869
1919
  .cv-detail-row-chevron { color: var(--cv-faint); flex-shrink: 0; }
1920
+ /* A second line under a row's value, for two facts that would each be cut off sharing one line ("Llamada desde":
1921
+ the name the org gave the number, then the number). */
1922
+ .cv-detail-row-sub {
1923
+ display: block;
1924
+ font-size: 0.75rem;
1925
+ font-weight: 400;
1926
+ color: var(--cv-muted);
1927
+ overflow: hidden;
1928
+ text-overflow: ellipsis;
1929
+ }
1870
1930
 
1871
1931
  .cv-detail-foot { flex-shrink: 0; padding: 0.85rem 1.25rem 1.1rem; }
1872
1932
  .cv-detail-call { width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
@@ -1956,6 +2016,18 @@
1956
2016
  /* `--cv-danger-strong` is the AS-TEXT danger token (see its definition): red-600 on the light `--cv-bg`,
1957
2017
  red-400 on the dark one. */
1958
2018
  .cv-settings-error { margin: 0.6rem 0.2rem 0; font-size: 0.78rem; line-height: 1.4; color: var(--cv-danger-strong); }
2019
+ /* Why the choice cannot change right now, and until when (Número de salida). Calm, like the dialpad's notice: the
2020
+ phone keeps working. Above the list, so it is in sight of the row just tapped. */
2021
+ .cv-settings-notice {
2022
+ margin: 0 0 0.75rem;
2023
+ padding: 0.6rem 0.75rem;
2024
+ border-radius: 10px;
2025
+ background: var(--cv-surface-2);
2026
+ color: var(--cv-text);
2027
+ font-size: 0.78rem;
2028
+ line-height: 1.4;
2029
+ }
2030
+ .cv-settings-notice[data-kind='error'] { color: var(--cv-danger-strong); }
1959
2031
 
1960
2032
  /*
1961
2033
  Ajustes as a list (custom-ringtone.md §4.6, revised 2026-10-14): each row names a setting, shows its CURRENT
@@ -2043,6 +2115,15 @@ button.cv-settings-nav:focus-visible { outline: 2px solid var(--cv-accent); outl
2043
2115
  .cv-choice-check { flex-shrink: 0; color: var(--cv-accent-strong); visibility: hidden; }
2044
2116
  .cv-choice[data-checked] .cv-choice-check { visibility: visible; }
2045
2117
  .cv-choice-playing { display: inline-flex; flex-shrink: 0; color: var(--cv-accent-strong); }
2118
+ /* A tagged choice (Número de salida): the label and its tag on one line, like the dialpad's list. */
2119
+ .cv-choice-line { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }
2120
+ /* Locked while a change is in flight or must wait: the chosen row stays clear, the others step back. */
2121
+ .cv-choice[data-disabled] { cursor: default; }
2122
+ .cv-choice[data-disabled]:not([data-checked]) { opacity: 0.55; }
2123
+ .cv-choice[data-disabled]:hover { background: none; }
2124
+ .cv-choice-applying { display: inline-flex; flex-shrink: 0; color: var(--cv-muted); }
2125
+ /* Exempt from reduced-motion like `.cv-empty-spinner`: the rotation is the information. */
2126
+ .cv-choice-applying-icon { animation: cv-spin 0.8s linear infinite; }
2046
2127
 
2047
2128
  .cv-settings-test,
2048
2129
  .cv-settings-retry {
@@ -2461,3 +2542,153 @@ button.cv-settings-nav:focus-visible { outline: 2px solid var(--cv-accent); outl
2461
2542
  color: var(--cv-muted);
2462
2543
  font-size: 0.78rem;
2463
2544
  }
2545
+
2546
+ /* ── "Llamar desde" (outbound-number-per-agent.md §5.5) ─────────────────────── */
2547
+ /* "Llamar desde: Oficina ▾" — a light line right above the number field, as the providers' dialers label their
2548
+ caller ID ("Caller ID: … ▾", "Outbound on: … ▾", "From: … ▾"; outbound-number-per-agent.md §5.5, 2026-10-26). Not a
2549
+ strip with a divider: under the host's own header that read as a second title bar. The line is the positioned
2550
+ ancestor, so its list and callout drop under it. */
2551
+ .cv-on {
2552
+ position: relative;
2553
+ flex-shrink: 0;
2554
+ display: flex;
2555
+ padding: 0.9rem 1rem 0.4rem;
2556
+ }
2557
+ .cv-on-chip {
2558
+ display: inline-flex;
2559
+ align-items: center;
2560
+ gap: 0.3rem;
2561
+ max-width: 100%;
2562
+ min-width: 0;
2563
+ height: 26px;
2564
+ margin-left: -0.3rem;
2565
+ padding: 0 0.3rem;
2566
+ border: 0;
2567
+ border-radius: 6px;
2568
+ background: none;
2569
+ color: var(--cv-text);
2570
+ font: 0.8rem var(--cv-font);
2571
+ text-align: left;
2572
+ cursor: pointer;
2573
+ transition: background 0.15s;
2574
+ }
2575
+ .cv-on-chip:hover { background: var(--cv-surface-2); }
2576
+ .cv-on-chip:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: 0; }
2577
+ .cv-on-label { flex-shrink: 0; color: var(--cv-muted); }
2578
+ .cv-on-chip .cv-on-current { font-weight: 600; }
2579
+ .cv-on-icon { color: var(--cv-accent-strong); flex-shrink: 0; }
2580
+ .cv-on-spin { animation: cv-spin 0.9s linear infinite; }
2581
+ @media (prefers-reduced-motion: reduce) {
2582
+ .cv-on-spin { animation: none; }
2583
+ }
2584
+ .cv-on-current {
2585
+ min-width: 0;
2586
+ overflow: hidden;
2587
+ text-overflow: ellipsis;
2588
+ white-space: nowrap;
2589
+ }
2590
+ /* Drops under the strip, over the number and the keypad, inset to the strip's edges. Capped so that in the CRM's
2591
+ 473px panel it never reaches the tab bar: the LIST gives way (it scrolls), never the title or a refusal message. */
2592
+ .cv-on-pop {
2593
+ top: calc(100% + 4px);
2594
+ left: 0.5rem;
2595
+ right: 0.5rem;
2596
+ max-width: none;
2597
+ max-height: 330px;
2598
+ }
2599
+ .cv-on-pop .cv-cp-list {
2600
+ flex: 1 1 auto;
2601
+ min-height: 0;
2602
+ max-height: none;
2603
+ }
2604
+ .cv-on-title {
2605
+ flex-shrink: 0;
2606
+ margin: 0;
2607
+ padding: 0.5rem 0.75rem 0.4rem;
2608
+ border-bottom: 1px solid var(--cv-border);
2609
+ font: 600 0.7rem var(--cv-font);
2610
+ letter-spacing: 0.06em;
2611
+ text-transform: uppercase;
2612
+ color: var(--cv-muted);
2613
+ }
2614
+ .cv-on-item { align-items: flex-start; }
2615
+ /* Locked while a change is applying or the limit says wait: still legible (the choice is visible), not usable. */
2616
+ .cv-on-item[aria-disabled='true'] { cursor: default; }
2617
+ .cv-on-item[aria-disabled='true']:not([aria-selected='true']) { opacity: 0.55; }
2618
+ .cv-on-item[aria-disabled='true']:not([aria-selected='true']):hover { background: none; }
2619
+ .cv-on-text {
2620
+ flex: 1;
2621
+ min-width: 0;
2622
+ display: flex;
2623
+ flex-direction: column;
2624
+ gap: 1px;
2625
+ }
2626
+ .cv-on-line {
2627
+ display: flex;
2628
+ align-items: center;
2629
+ gap: 0.35rem;
2630
+ min-width: 0;
2631
+ }
2632
+ .cv-on-sub {
2633
+ color: var(--cv-muted);
2634
+ font-size: 0.74rem;
2635
+ font-variant-numeric: tabular-nums;
2636
+ }
2637
+ /* A quiet, neutral pill: it tells the agent what the number is for, it is not a warning. Shared with Ajustes. */
2638
+ .cv-on-tag,
2639
+ .cv-choice-tag {
2640
+ flex-shrink: 0;
2641
+ padding: 0 0.35rem;
2642
+ border: 1px solid var(--cv-border-strong);
2643
+ border-radius: 999px;
2644
+ color: var(--cv-muted);
2645
+ font-size: 0.66rem;
2646
+ font-weight: 600;
2647
+ line-height: 1.4;
2648
+ }
2649
+ /* The usage rule IS information (when not to call from this number): --cv-muted, not --cv-faint, which is under
2650
+ 4.5:1 at this size in both themes (see `.cv-empty-sub`). */
2651
+ .cv-on-hint,
2652
+ .cv-choice-hint {
2653
+ color: var(--cv-muted);
2654
+ font-size: 0.7rem;
2655
+ line-height: 1.3;
2656
+ }
2657
+ .cv-on-item .cv-cp-check { margin-top: 2px; }
2658
+ /* Why a change cannot happen right now, and until when. Calm, not alarming: the phone keeps working. */
2659
+ .cv-on-notice {
2660
+ flex-shrink: 0;
2661
+ margin: 0;
2662
+ padding: 0.55rem 0.75rem;
2663
+ border-top: 1px solid var(--cv-border);
2664
+ background: var(--cv-surface-2);
2665
+ color: var(--cv-text);
2666
+ font-size: 0.74rem;
2667
+ line-height: 1.4;
2668
+ }
2669
+ .cv-on-notice[data-kind='error'] { color: var(--cv-danger-strong); }
2670
+ /* A change that did not go through, after the (optimistic) list already closed: the same words, where the list
2671
+ was (under the strip). Same place and surface as the list; dismissed by a tap elsewhere, when the wait is over,
2672
+ or after a few seconds. */
2673
+ .cv-on-callout {
2674
+ position: absolute;
2675
+ top: calc(100% + 4px);
2676
+ left: 0.5rem;
2677
+ right: 0.5rem;
2678
+ margin: 0;
2679
+ z-index: 20;
2680
+ padding: 0.6rem 0.75rem;
2681
+ background: var(--cv-surface);
2682
+ border: 1px solid var(--cv-border);
2683
+ border-radius: var(--cv-radius);
2684
+ box-shadow: var(--cv-shadow);
2685
+ color: var(--cv-text);
2686
+ font-size: 0.74rem;
2687
+ line-height: 1.4;
2688
+ transform-origin: top center;
2689
+ animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);
2690
+ }
2691
+ .cv-on-callout[data-kind='error'] { color: var(--cv-danger-strong); }
2692
+ @media (prefers-reduced-motion: reduce) {
2693
+ .cv-on-callout { animation: none; }
2694
+ }
@@ -1,3 +1,4 @@
1
+ import { outboundNumberRefused } from './chunk-5E5OEXS2.js';
1
2
  import { invalidateToken } from './chunk-MHOJHG5E.js';
2
3
 
3
4
  // src/sources.ts
@@ -39,10 +40,12 @@ function withNameResolution(source, resolve) {
39
40
  // src/coventia/api.ts
40
41
  var ApiError = class extends Error {
41
42
  status;
42
- constructor(status, message) {
43
+ body;
44
+ constructor(status, message, body = null) {
43
45
  super(message);
44
46
  this.name = "ApiError";
45
47
  this.status = status;
48
+ this.body = body;
46
49
  }
47
50
  };
48
51
  function createRequest(config, method) {
@@ -63,9 +66,9 @@ function createRequest(config, method) {
63
66
  });
64
67
  const body = await res.json().catch(() => null);
65
68
  const message = body?.message ?? `${path} \u2014 HTTP ${res.status}`;
66
- if (!res.ok) throw new ApiError(res.status, message);
69
+ if (!res.ok) throw new ApiError(res.status, message, body);
67
70
  if (body?.success === false) {
68
- throw new ApiError(res.status, message);
71
+ throw new ApiError(res.status, message, body);
69
72
  }
70
73
  return body;
71
74
  }
@@ -149,6 +152,7 @@ var toPresenceEntry = (row) => ({
149
152
  name: row.name ?? null,
150
153
  state: toPresenceState(row.state)
151
154
  });
155
+ var toLineLocation = (raw) => raw === "desktop" || raw === "browser" ? raw : null;
152
156
  var toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
153
157
  function coventiaSources(config) {
154
158
  const get = createGet(config);
@@ -206,6 +210,8 @@ function coventiaSources(config) {
206
210
  // row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
207
211
  // would just repeat the row under a wrong label.
208
212
  ...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
213
+ // And the other way round: which of the org's numbers an outbound call showed.
214
+ ...toDirection(row.direction) === "outbound" && row.from_number ? { from: row.from_number } : {},
209
215
  recorded,
210
216
  handledBy,
211
217
  // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
@@ -275,6 +281,16 @@ function coventiaSources(config) {
275
281
  if (ws.readyState !== WS.OPEN) return;
276
282
  ws.send(JSON.stringify({ type: "bye" }));
277
283
  },
284
+ // `line: true` is what makes the server treat this socket as taking part in the line owner;
285
+ // without it (or with an id it rejects) the `hello` is ignored, as an older SDK's would be.
286
+ hello: (clientId) => {
287
+ if (ws.readyState !== WS.OPEN) return;
288
+ ws.send(JSON.stringify({ type: "hello", clientId, line: true }));
289
+ },
290
+ claimLine: () => {
291
+ if (ws.readyState !== WS.OPEN) return;
292
+ ws.send(JSON.stringify({ type: "line-claim" }));
293
+ },
278
294
  close: () => {
279
295
  ws.onclose = null;
280
296
  ws.onerror = null;
@@ -294,6 +310,12 @@ function coventiaSources(config) {
294
310
  callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
295
311
  } else if (msg.type === "delta" && msg.agent) {
296
312
  callbacks.onDelta(toPresenceEntry(msg.agent));
313
+ } else if (msg.type === "line" && (msg.state === "here" || msg.state === "elsewhere")) {
314
+ callbacks.onLine?.({ state: msg.state, where: msg.state === "here" ? null : toLineLocation(msg.where) });
315
+ } else if (msg.type === "line-claim-result") {
316
+ callbacks.onClaimResult?.(
317
+ msg.ok === true ? { ok: true } : { ok: false, reason: typeof msg.reason === "string" ? msg.reason : "unavailable" }
318
+ );
297
319
  }
298
320
  };
299
321
  ws.onclose = () => {
@@ -323,10 +345,10 @@ function coventiaSources(config) {
323
345
  return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };
324
346
  };
325
347
  const CACHE_PREFIX = "coventia-softphone:ringtones:";
326
- const cacheKey = async () => {
348
+ const cacheKey = async (prefix = CACHE_PREFIX) => {
327
349
  try {
328
350
  const { identity } = await config.tokenSource.getToken();
329
- return `${CACHE_PREFIX}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
351
+ return `${prefix}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
330
352
  } catch {
331
353
  return null;
332
354
  }
@@ -359,6 +381,68 @@ function coventiaSources(config) {
359
381
  ),
360
382
  cached: recall
361
383
  };
384
+ const KINDS = /* @__PURE__ */ new Set(["landline", "mobile", "commercial", "other"]);
385
+ const STATES = /* @__PURE__ */ new Set(["applied", "pending", "failed", "unassigned"]);
386
+ const toOutboundSettings = (body) => {
387
+ const numbers = (body.numbers ?? []).map((n) => ({
388
+ id: n.id,
389
+ number: n.number,
390
+ label: n.label ?? null,
391
+ kind: KINDS.has(n.kind ?? "") ? n.kind : "other"
392
+ }));
393
+ const known = (id) => id && numbers.some((n) => n.id === id) ? id : null;
394
+ return {
395
+ numbers,
396
+ orgDefault: body.orgDefault ?? null,
397
+ mine: known(body.mine),
398
+ // Defensive: a malformed answer naming a number that is not in the list reads as the org default.
399
+ effective: known(body.effective) ?? known(body.orgDefault),
400
+ extension: body.extension ?? null,
401
+ state: STATES.has(body.state ?? "") ? body.state : "applied",
402
+ changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0))
403
+ };
404
+ };
405
+ const OUTBOUND_PREFIX = "coventia-softphone:outbound-number:";
406
+ const rememberOutbound = async (settings) => {
407
+ const key = await cacheKey(OUTBOUND_PREFIX);
408
+ try {
409
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
410
+ } catch {
411
+ }
412
+ return settings;
413
+ };
414
+ const recallOutbound = async () => {
415
+ const key = await cacheKey(OUTBOUND_PREFIX);
416
+ try {
417
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
418
+ const s = raw ? JSON.parse(raw) : null;
419
+ const ok = s && Array.isArray(s.numbers) && s.numbers.every((n) => typeof n?.id === "string" && typeof n.number === "string") && (s.effective === null || s.numbers.some((n) => n.id === s.effective));
420
+ return ok ? { ...s, changeAvailableInSeconds: 0 } : null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ };
425
+ const outboundNumbers = {
426
+ get: async (signal) => rememberOutbound(toOutboundSettings(await get(`${base}/outbound-numbers`, org, signal))),
427
+ // `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.
428
+ setMine: async (id) => {
429
+ try {
430
+ return rememberOutbound(
431
+ toOutboundSettings(await post(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? "" }))
432
+ );
433
+ } catch (error) {
434
+ const body = error instanceof ApiError ? error.body : null;
435
+ if (error instanceof ApiError && error.status === 429 && body?.code === "rate_limited") {
436
+ throw outboundNumberRefused({
437
+ reason: body.reason === "user" ? "user" : "platform",
438
+ retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1))
439
+ });
440
+ }
441
+ throw error;
442
+ }
443
+ },
444
+ cached: recallOutbound
445
+ };
362
446
  const contacts = config.contacts ?? coventiaContacts;
363
447
  const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
364
448
  const history = withNameResolution(rawHistory, resolve);
@@ -371,10 +455,11 @@ function coventiaSources(config) {
371
455
  listAgents: coventiaListAgents,
372
456
  transferOutcomes,
373
457
  presence,
374
- ringtones
458
+ ringtones,
459
+ outboundNumbers
375
460
  };
376
461
  }
377
462
 
378
463
  export { coventiaSources, resolverFromContacts };
379
- //# sourceMappingURL=sources-M3GYMS6V.js.map
380
- //# sourceMappingURL=sources-M3GYMS6V.js.map
464
+ //# sourceMappingURL=sources-QL2LRZVH.js.map
465
+ //# sourceMappingURL=sources-QL2LRZVH.js.map