@coventia/softphone 0.2.1 → 0.4.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 (40) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-C6YpT6uR.d.cts} +107 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-C6YpT6uR.d.ts} +107 -1
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  6. package/dist/chunk-UHNLEBBO.js.map +1 -0
  7. package/dist/element/index.d.ts +124 -9
  8. package/dist/element/index.js +1837 -413
  9. package/dist/element/index.js.map +1 -1
  10. package/dist/{engine-BlQBo2Ys.d.cts → engine-CDdaSojx.d.cts} +1 -1
  11. package/dist/{engine-C0BOkIcW.d.ts → engine-DPft99Wj.d.ts} +1 -1
  12. package/dist/index.cjs +213 -13
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +16 -6
  15. package/dist/index.d.ts +16 -6
  16. package/dist/index.js +75 -10
  17. package/dist/index.js.map +1 -1
  18. package/dist/react/index.cjs +2 -0
  19. package/dist/react/index.cjs.map +1 -1
  20. package/dist/react/index.d.cts +2 -2
  21. package/dist/react/index.d.ts +2 -2
  22. package/dist/react/index.js +1 -1
  23. package/dist/{softphone-DlGV12Xp.d.cts → softphone-CY5d2WWY.d.cts} +28 -10
  24. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DXgmTyrX.d.ts} +28 -10
  25. package/dist/softphone.css +364 -35
  26. package/dist/{sources-IRKNQPWW.js → sources-ONYQW733.js} +125 -6
  27. package/dist/sources-ONYQW733.js.map +1 -0
  28. package/dist/testing/index.d.cts +2 -2
  29. package/dist/testing/index.d.ts +2 -2
  30. package/dist/ui/index.cjs +1450 -33
  31. package/dist/ui/index.cjs.map +1 -1
  32. package/dist/ui/index.d.cts +25 -3
  33. package/dist/ui/index.d.ts +25 -3
  34. package/dist/ui/index.js +1445 -36
  35. package/dist/ui/index.js.map +1 -1
  36. package/dist/zadarma/index.d.cts +2 -2
  37. package/dist/zadarma/index.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/chunk-LMJXX4YM.js.map +0 -1
  40. package/dist/sources-IRKNQPWW.js.map +0 -1
@@ -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; }
@@ -1952,6 +2012,137 @@
1952
2012
  }
1953
2013
  .cv-settings-select:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }
1954
2014
  .cv-settings-empty { font-size: 0.85rem; color: var(--cv-muted); }
2015
+
2016
+ /* `--cv-danger-strong` is the AS-TEXT danger token (see its definition): red-600 on the light `--cv-bg`,
2017
+ red-400 on the dark one. */
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); }
2031
+
2032
+ /*
2033
+ Ajustes as a list (custom-ringtone.md §4.6, revised 2026-10-14): each row names a setting, shows its CURRENT
2034
+ value under the name, and opens a page. Same icon circle and dividers as the device rows above (they live in
2035
+ the same `.cv-settings-section-body`, which supplies the padding and the dividers), same chevron as
2036
+ `.cv-detail-row--action` — one row vocabulary across the widget.
2037
+ */
2038
+ .cv-settings-nav {
2039
+ display: flex;
2040
+ align-items: center;
2041
+ gap: 0.7rem;
2042
+ width: 100%;
2043
+ border: none;
2044
+ background: none;
2045
+ font: inherit;
2046
+ color: inherit;
2047
+ text-align: left;
2048
+ }
2049
+ button.cv-settings-nav { cursor: pointer; transition: background 0.12s ease; }
2050
+ button.cv-settings-nav:hover { background: var(--cv-surface); }
2051
+ button.cv-settings-nav:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; }
2052
+ .cv-settings-nav-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
2053
+ .cv-settings-nav-title { font-size: 0.88rem; font-weight: 600; color: var(--cv-text); }
2054
+ /* One line, cut with an ellipsis — the full device name is on its page. `--cv-muted` on `--cv-bg` is the
2055
+ pairing already verified for `.cv-settings-note`. The static row (a note, no page) may wrap instead. */
2056
+ .cv-settings-nav-value {
2057
+ font-size: 0.8rem;
2058
+ color: var(--cv-muted);
2059
+ overflow: hidden;
2060
+ text-overflow: ellipsis;
2061
+ white-space: nowrap;
2062
+ }
2063
+ .cv-settings-nav--static .cv-settings-nav-value { white-space: normal; line-height: 1.45; }
2064
+ .cv-settings-nav-chevron { flex-shrink: 0; color: var(--cv-muted); }
2065
+
2066
+ /* A settings page: the `.cv-detail` overlay (entrance, back button, reduced motion all inherited) with a
2067
+ scrolling body of its own. */
2068
+ .cv-settings-page-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0.25rem 1rem 1rem; }
2069
+ .cv-settings-page-title {
2070
+ margin: 0.15rem 0.2rem 0.35rem;
2071
+ font-size: 1.05rem;
2072
+ font-weight: 700;
2073
+ letter-spacing: -0.01em;
2074
+ color: var(--cv-text);
2075
+ }
2076
+ /* Focus lands here on open (for keyboard and screen-reader users); a ring on a heading would only look
2077
+ like a stray box to everyone else. */
2078
+ .cv-settings-page-title:focus { outline: none; }
2079
+ .cv-settings-page-intro { margin: 0 0.2rem 0.85rem; font-size: 0.8rem; line-height: 1.45; color: var(--cv-muted); }
2080
+ .cv-settings-page-empty { margin: 0; padding: 0 0.2rem; font-size: 0.85rem; color: var(--cv-muted); }
2081
+
2082
+ /* The checkmark list: one card like `.cv-settings-section-body`, a row per choice. */
2083
+ .cv-choices {
2084
+ border: 1px solid var(--cv-border);
2085
+ border-radius: 14px;
2086
+ background: var(--cv-bg);
2087
+ overflow: hidden;
2088
+ }
2089
+ .cv-choice {
2090
+ position: relative;
2091
+ display: flex;
2092
+ align-items: center;
2093
+ gap: 0.6rem;
2094
+ min-height: 44px;
2095
+ padding: 0.6rem 0.85rem;
2096
+ cursor: pointer;
2097
+ transition: background 0.12s ease;
2098
+ }
2099
+ .cv-choice + .cv-choice { border-top: 1px solid var(--cv-border); }
2100
+ .cv-choice:hover { background: var(--cv-surface); }
2101
+ /* The real control: a native radio, kept in the accessibility tree and keyboard order, drawn by the row. */
2102
+ .cv-choice-input {
2103
+ position: absolute;
2104
+ opacity: 0;
2105
+ width: 1px;
2106
+ height: 1px;
2107
+ margin: 0;
2108
+ pointer-events: none;
2109
+ }
2110
+ .cv-choice:has(.cv-choice-input:focus-visible) { outline: 2px solid var(--cv-accent); outline-offset: -2px; }
2111
+ .cv-choice-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
2112
+ .cv-choice-label { font-size: 0.88rem; color: var(--cv-text); overflow-wrap: anywhere; }
2113
+ .cv-choice-sub { font-size: 0.78rem; color: var(--cv-muted); }
2114
+ .cv-choice[data-checked] .cv-choice-label { font-weight: 600; }
2115
+ .cv-choice-check { flex-shrink: 0; color: var(--cv-accent-strong); visibility: hidden; }
2116
+ .cv-choice[data-checked] .cv-choice-check { visibility: visible; }
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; }
2127
+
2128
+ .cv-settings-test,
2129
+ .cv-settings-retry {
2130
+ display: inline-flex;
2131
+ align-items: center;
2132
+ gap: 0.4rem;
2133
+ }
2134
+ .cv-settings-test { margin-top: 0.85rem; }
2135
+ .cv-settings-retry { margin-top: 0.6rem; }
2136
+
2137
+ /* Visually hidden, still read aloud. */
2138
+ .cv-sr-only {
2139
+ position: absolute;
2140
+ width: 1px;
2141
+ height: 1px;
2142
+ overflow: hidden;
2143
+ clip-path: inset(50%);
2144
+ white-space: nowrap;
2145
+ }
1955
2146
  /* `--cv-muted` on `--cv-bg` — 4.83:1 light / 6.97:1 dark, computed fresh for THIS pairing (the note
1956
2147
  used to sit on `--cv-surface` with its own card; now it sits directly on the section's `--cv-bg`,
1957
2148
  a different background that earns its own check rather than inheriting the old one's number). */
@@ -1961,18 +2152,6 @@
1961
2152
  font-size: 0.8rem;
1962
2153
  line-height: 1.5;
1963
2154
  }
1964
- /* Closing note for the tab as a whole, not any one section — sits directly on `.cv-settings-body`,
1965
- which shares the same `--cv-bg` as `.cv-settings-section-body` above, so the `--cv-muted`-on-
1966
- `--cv-bg` pairing already verified for `.cv-settings-note` carries over unchanged. Centred and
1967
- quieter than a row: this is a footnote about the tab, not a row's worth of content. */
1968
- .cv-settings-more {
1969
- margin: 0;
1970
- padding: 0 0.5rem;
1971
- text-align: center;
1972
- color: var(--cv-muted);
1973
- font-size: 0.78rem;
1974
- line-height: 1.5;
1975
- }
1976
2155
 
1977
2156
  /* ── Centred states (mic gate) ───────────────────────────────────────────────── */
1978
2157
  .cv-center {
@@ -2363,3 +2542,153 @@
2363
2542
  color: var(--cv-muted);
2364
2543
  font-size: 0.78rem;
2365
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
  }
@@ -206,6 +209,8 @@ function coventiaSources(config) {
206
209
  // row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
207
210
  // would just repeat the row under a wrong label.
208
211
  ...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
212
+ // And the other way round: which of the org's numbers an outbound call showed.
213
+ ...toDirection(row.direction) === "outbound" && row.from_number ? { from: row.from_number } : {},
209
214
  recorded,
210
215
  handledBy,
211
216
  // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
@@ -309,6 +314,118 @@ function coventiaSources(config) {
309
314
  })();
310
315
  });
311
316
  };
317
+ const fileBase = config.baseUrl.replace(/\/+$/, "");
318
+ const toLoop = (l) => l && Number.isFinite(l.start) && Number.isFinite(l.end) && l.start >= 0 && l.end > l.start ? { start: l.start, end: l.end } : null;
319
+ const toRingtoneSettings = (body) => {
320
+ const catalogue = (body.catalogue ?? []).map((r) => ({
321
+ id: r.id,
322
+ name: r.name,
323
+ url: r.path ? `${fileBase}${r.path}` : null,
324
+ loop: r.path ? toLoop(r.loop) : null
325
+ }));
326
+ const builtIn = catalogue.find((r) => r.url === null)?.id ?? "clasico";
327
+ const effective = catalogue.some((r) => r.id === body.effective) ? body.effective : builtIn;
328
+ return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };
329
+ };
330
+ const CACHE_PREFIX = "coventia-softphone:ringtones:";
331
+ const cacheKey = async (prefix = CACHE_PREFIX) => {
332
+ try {
333
+ const { identity } = await config.tokenSource.getToken();
334
+ return `${prefix}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
335
+ } catch {
336
+ return null;
337
+ }
338
+ };
339
+ const remember = async (settings) => {
340
+ const key = await cacheKey();
341
+ try {
342
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
343
+ } catch {
344
+ }
345
+ return settings;
346
+ };
347
+ const recall = async () => {
348
+ const key = await cacheKey();
349
+ try {
350
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
351
+ const s = raw ? JSON.parse(raw) : null;
352
+ const ok = s && Array.isArray(s.catalogue) && s.catalogue.every((r) => typeof r?.id === "string" && typeof r.name === "string" && (r.url === null || typeof r.url === "string")) && s.catalogue.some((r) => r.id === s.effective);
353
+ return ok ? { ...s, catalogue: s.catalogue.map((r) => ({ ...r, loop: toLoop(r.loop) })) } : null;
354
+ } catch {
355
+ return null;
356
+ }
357
+ };
358
+ const ringtones = {
359
+ get: async (signal) => remember(toRingtoneSettings(await get(`${base}/ringtones`, org, signal))),
360
+ // `null` clears: the empty id is dropped from the query by the request helper, and the backend reads a
361
+ // missing `ringtone_id` as "clear".
362
+ setMine: async (id) => remember(
363
+ toRingtoneSettings(await post(`${base}/ringtones/user`, { ...org, ringtone_id: id ?? "" }))
364
+ ),
365
+ cached: recall
366
+ };
367
+ const KINDS = /* @__PURE__ */ new Set(["landline", "mobile", "commercial", "other"]);
368
+ const STATES = /* @__PURE__ */ new Set(["applied", "pending", "failed", "unassigned"]);
369
+ const toOutboundSettings = (body) => {
370
+ const numbers = (body.numbers ?? []).map((n) => ({
371
+ id: n.id,
372
+ number: n.number,
373
+ label: n.label ?? null,
374
+ kind: KINDS.has(n.kind ?? "") ? n.kind : "other"
375
+ }));
376
+ const known = (id) => id && numbers.some((n) => n.id === id) ? id : null;
377
+ return {
378
+ numbers,
379
+ orgDefault: body.orgDefault ?? null,
380
+ mine: known(body.mine),
381
+ // Defensive: a malformed answer naming a number that is not in the list reads as the org default.
382
+ effective: known(body.effective) ?? known(body.orgDefault),
383
+ extension: body.extension ?? null,
384
+ state: STATES.has(body.state ?? "") ? body.state : "applied",
385
+ changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0))
386
+ };
387
+ };
388
+ const OUTBOUND_PREFIX = "coventia-softphone:outbound-number:";
389
+ const rememberOutbound = async (settings) => {
390
+ const key = await cacheKey(OUTBOUND_PREFIX);
391
+ try {
392
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
393
+ } catch {
394
+ }
395
+ return settings;
396
+ };
397
+ const recallOutbound = async () => {
398
+ const key = await cacheKey(OUTBOUND_PREFIX);
399
+ try {
400
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
401
+ const s = raw ? JSON.parse(raw) : null;
402
+ 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));
403
+ return ok ? { ...s, changeAvailableInSeconds: 0 } : null;
404
+ } catch {
405
+ return null;
406
+ }
407
+ };
408
+ const outboundNumbers = {
409
+ get: async (signal) => rememberOutbound(toOutboundSettings(await get(`${base}/outbound-numbers`, org, signal))),
410
+ // `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.
411
+ setMine: async (id) => {
412
+ try {
413
+ return rememberOutbound(
414
+ toOutboundSettings(await post(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? "" }))
415
+ );
416
+ } catch (error) {
417
+ const body = error instanceof ApiError ? error.body : null;
418
+ if (error instanceof ApiError && error.status === 429 && body?.code === "rate_limited") {
419
+ throw outboundNumberRefused({
420
+ reason: body.reason === "user" ? "user" : "platform",
421
+ retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1))
422
+ });
423
+ }
424
+ throw error;
425
+ }
426
+ },
427
+ cached: recallOutbound
428
+ };
312
429
  const contacts = config.contacts ?? coventiaContacts;
313
430
  const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
314
431
  const history = withNameResolution(rawHistory, resolve);
@@ -320,10 +437,12 @@ function coventiaSources(config) {
320
437
  resolveContact: resolve,
321
438
  listAgents: coventiaListAgents,
322
439
  transferOutcomes,
323
- presence
440
+ presence,
441
+ ringtones,
442
+ outboundNumbers
324
443
  };
325
444
  }
326
445
 
327
446
  export { coventiaSources, resolverFromContacts };
328
- //# sourceMappingURL=sources-IRKNQPWW.js.map
329
- //# sourceMappingURL=sources-IRKNQPWW.js.map
447
+ //# sourceMappingURL=sources-ONYQW733.js.map
448
+ //# sourceMappingURL=sources-ONYQW733.js.map