@coventia/softphone 0.2.0 → 0.2.1

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 (41) hide show
  1. package/dist/{call-DUVnDxsz.d.cts → call-VKq1I8yP.d.cts} +6 -0
  2. package/dist/{call-DUVnDxsz.d.ts → call-VKq1I8yP.d.ts} +6 -0
  3. package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
  4. package/dist/chunk-EYEDPD7Y.js.map +1 -0
  5. package/dist/element/index.d.ts +8 -2
  6. package/dist/element/index.js +334 -249
  7. package/dist/element/index.js.map +1 -1
  8. package/dist/{engine-CDjkrXHY.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
  9. package/dist/{engine-EPCKf-Bl.d.ts → engine-C0BOkIcW.d.ts} +1 -1
  10. package/dist/index.cjs +199 -23
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +90 -11
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/index.d.cts +2 -2
  17. package/dist/react/index.d.ts +2 -2
  18. package/dist/{softphone-BtdgZWkm.d.ts → softphone-Cyq3hiIM.d.ts} +1 -1
  19. package/dist/{softphone-DHG9MTNg.d.cts → softphone-DlGV12Xp.d.cts} +1 -1
  20. package/dist/softphone.css +82 -34
  21. package/dist/{sources-5L2R4HTV.js → sources-IRKNQPWW.js} +6 -2
  22. package/dist/sources-IRKNQPWW.js.map +1 -0
  23. package/dist/testing/index.d.cts +2 -2
  24. package/dist/testing/index.d.ts +2 -2
  25. package/dist/ui/index.cjs +332 -247
  26. package/dist/ui/index.cjs.map +1 -1
  27. package/dist/ui/index.d.cts +17 -6
  28. package/dist/ui/index.d.ts +17 -6
  29. package/dist/ui/index.js +332 -247
  30. package/dist/ui/index.js.map +1 -1
  31. package/dist/zadarma/index.cjs +106 -13
  32. package/dist/zadarma/index.cjs.map +1 -1
  33. package/dist/zadarma/index.d.cts +2 -2
  34. package/dist/zadarma/index.d.ts +2 -2
  35. package/dist/zadarma/index.js +1 -1
  36. package/dist/zadarma-engine-L5DEFXUS.js +5 -0
  37. package/dist/{zadarma-engine-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-YWI6EQYS.js.map +0 -1
  40. package/dist/sources-5L2R4HTV.js.map +0 -1
  41. package/dist/zadarma-engine-4VOPIBXG.js +0 -5
package/dist/ui/index.js CHANGED
@@ -550,21 +550,263 @@ function caretAfterDigits(formatted, digitsBefore) {
550
550
  function digitsBeforeCaret(text, caret) {
551
551
  return (text.slice(0, caret).match(/\d/g) ?? []).length;
552
552
  }
553
- var KEYS = [
554
- ["1", "", true],
555
- ["2", "ABC", true],
556
- ["3", "DEF", true],
557
- ["4", "GHI", true],
558
- ["5", "JKL", true],
559
- ["6", "MNO", true],
560
- ["7", "PQRS", true],
561
- ["8", "TUV", true],
562
- ["9", "WXYZ", true],
563
- ["*", "", false],
564
- ["0", "+", true],
565
- ["#", "", false]
553
+
554
+ // src/ui/dialing/keys.ts
555
+ var KEYPAD_KEYS = [
556
+ ["1", ""],
557
+ ["2", "ABC"],
558
+ ["3", "DEF"],
559
+ ["4", "GHI"],
560
+ ["5", "JKL"],
561
+ ["6", "MNO"],
562
+ ["7", "PQRS"],
563
+ ["8", "TUV"],
564
+ ["9", "WXYZ"],
565
+ ["*", ""],
566
+ ["0", "+"],
567
+ ["#", ""]
566
568
  ];
567
- function Dialpad({ outcome = null, onIntent } = {}) {
569
+
570
+ // src/ui/audio/audio.ts
571
+ var TONE_HZ = 425;
572
+ function createRemoteAudio() {
573
+ const el = document.createElement("audio");
574
+ el.autoplay = true;
575
+ el.style.display = "none";
576
+ document.body.appendChild(el);
577
+ return {
578
+ attach(stream) {
579
+ el.srcObject = stream;
580
+ void el.play().catch(() => {
581
+ });
582
+ },
583
+ detach() {
584
+ el.srcObject = null;
585
+ },
586
+ destroy() {
587
+ el.srcObject = null;
588
+ el.remove();
589
+ },
590
+ async setOutputDevice(deviceId) {
591
+ if (typeof el.setSinkId !== "function") return;
592
+ try {
593
+ await el.setSinkId(deviceId ?? "");
594
+ } catch {
595
+ }
596
+ }
597
+ };
598
+ }
599
+ var shared = null;
600
+ function sharedContext() {
601
+ if (shared) return shared;
602
+ const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
603
+ if (!Ctor) return null;
604
+ shared = new Ctor();
605
+ return shared;
606
+ }
607
+ function primeAudio() {
608
+ const ctx = sharedContext();
609
+ if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
610
+ });
611
+ }
612
+ function playRingtone() {
613
+ const ctx = sharedContext();
614
+ if (!ctx) return { stop() {
615
+ } };
616
+ const gain = ctx.createGain();
617
+ gain.gain.value = 0;
618
+ gain.connect(ctx.destination);
619
+ const osc = ctx.createOscillator();
620
+ osc.type = "sine";
621
+ osc.frequency.value = TONE_HZ;
622
+ osc.connect(gain);
623
+ osc.start();
624
+ const LEVEL = 0.8;
625
+ const RAMP = 0.02;
626
+ const burst = (at, dur) => {
627
+ gain.gain.setValueAtTime(0, at);
628
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
629
+ gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
630
+ gain.gain.linearRampToValueAtTime(0, at + dur);
631
+ };
632
+ const scheduleCycle = (at) => {
633
+ burst(at, 0.4);
634
+ burst(at + 0.6, 0.4);
635
+ return 3;
636
+ };
637
+ const LOOKAHEAD = 0.5;
638
+ const TICK_MS = 150;
639
+ const LEAD = 0.08;
640
+ let stopped = false;
641
+ let timer = null;
642
+ let nextAt = 0;
643
+ const pump = () => {
644
+ while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
645
+ };
646
+ void Promise.resolve(ctx.resume?.()).catch(() => {
647
+ }).finally(() => {
648
+ if (stopped) return;
649
+ nextAt = ctx.currentTime + LEAD;
650
+ pump();
651
+ timer = setInterval(pump, TICK_MS);
652
+ });
653
+ return {
654
+ stop() {
655
+ stopped = true;
656
+ if (timer) clearInterval(timer);
657
+ try {
658
+ osc.stop();
659
+ } catch {
660
+ }
661
+ try {
662
+ gain.disconnect();
663
+ } catch {
664
+ }
665
+ }
666
+ };
667
+ }
668
+ function playCallEndedTone() {
669
+ const ctx = sharedContext();
670
+ if (!ctx) return;
671
+ const gain = ctx.createGain();
672
+ gain.gain.value = 0;
673
+ gain.connect(ctx.destination);
674
+ const osc = ctx.createOscillator();
675
+ osc.type = "sine";
676
+ osc.frequency.value = TONE_HZ;
677
+ osc.connect(gain);
678
+ osc.start();
679
+ const LEVEL = 0.55;
680
+ const RAMP = 8e-3;
681
+ const BEEP = 0.25;
682
+ void Promise.resolve(ctx.resume?.()).catch(() => {
683
+ }).finally(() => {
684
+ const at = ctx.currentTime + 0.02;
685
+ gain.gain.setValueAtTime(0, at);
686
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
687
+ gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
688
+ gain.gain.linearRampToValueAtTime(0, at + BEEP);
689
+ const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
690
+ setTimeout(() => {
691
+ try {
692
+ osc.stop();
693
+ } catch {
694
+ }
695
+ try {
696
+ gain.disconnect();
697
+ } catch {
698
+ }
699
+ }, totalMs);
700
+ });
701
+ }
702
+ var DTMF_ROWS_HZ = [697, 770, 852, 941];
703
+ var DTMF_COLS_HZ = [1209, 1336, 1477];
704
+ var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
705
+ function dtmfFrequencies(key) {
706
+ if (key.length !== 1) return null;
707
+ for (let row = 0; row < DTMF_LAYOUT.length; row++) {
708
+ const col = DTMF_LAYOUT[row].indexOf(key);
709
+ if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
710
+ }
711
+ return null;
712
+ }
713
+ function playKeyTone(key) {
714
+ const hz = dtmfFrequencies(key);
715
+ if (!hz) return;
716
+ const ctx = sharedContext();
717
+ if (!ctx) return;
718
+ const LEVEL = 0.2;
719
+ const RAMP = 6e-3;
720
+ const DURATION = 0.12;
721
+ const LEAD = 0.01;
722
+ const gain = ctx.createGain();
723
+ gain.gain.value = 0;
724
+ gain.connect(ctx.destination);
725
+ const oscillators = hz.map((freq) => {
726
+ const osc = ctx.createOscillator();
727
+ osc.type = "sine";
728
+ osc.frequency.value = freq;
729
+ osc.connect(gain);
730
+ osc.start();
731
+ return osc;
732
+ });
733
+ const fire = () => {
734
+ const at = ctx.currentTime + LEAD;
735
+ gain.gain.setValueAtTime(0, at);
736
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
737
+ gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
738
+ gain.gain.linearRampToValueAtTime(0, at + DURATION);
739
+ for (const osc of oscillators) osc.stop(at + DURATION);
740
+ oscillators[0].onended = () => gain.disconnect();
741
+ };
742
+ if (ctx.state === "running") fire();
743
+ else void Promise.resolve(ctx.resume?.()).catch(() => {
744
+ }).finally(fire);
745
+ }
746
+ var ringbackUrl = null;
747
+ function ringbackSource() {
748
+ if (ringbackUrl) return ringbackUrl;
749
+ const sampleRate = 48e3;
750
+ const channels = 2;
751
+ const bytesPerSample = 2;
752
+ const amp = 0.5;
753
+ const toneSec = 1.5;
754
+ const silenceSec = 3;
755
+ const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
756
+ const toneFrames = Math.round(toneSec * sampleRate);
757
+ const ramp = Math.round(0.02 * sampleRate);
758
+ const blockAlign = channels * bytesPerSample;
759
+ const dataSize = totalFrames * blockAlign;
760
+ const buffer = new ArrayBuffer(44 + dataSize);
761
+ const view = new DataView(buffer);
762
+ const str = (off, s) => {
763
+ for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
764
+ };
765
+ str(0, "RIFF");
766
+ view.setUint32(4, 36 + dataSize, true);
767
+ str(8, "WAVE");
768
+ str(12, "fmt ");
769
+ view.setUint32(16, 16, true);
770
+ view.setUint16(20, 1, true);
771
+ view.setUint16(22, channels, true);
772
+ view.setUint32(24, sampleRate, true);
773
+ view.setUint32(28, sampleRate * blockAlign, true);
774
+ view.setUint16(32, blockAlign, true);
775
+ view.setUint16(34, bytesPerSample * 8, true);
776
+ str(36, "data");
777
+ view.setUint32(40, dataSize, true);
778
+ for (let i = 0; i < totalFrames; i++) {
779
+ let sample = 0;
780
+ if (i < toneFrames) {
781
+ let env = 1;
782
+ if (i < ramp) env = i / ramp;
783
+ else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
784
+ sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
785
+ }
786
+ const s16 = Math.round(sample * 32767);
787
+ const off = 44 + i * blockAlign;
788
+ for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
789
+ }
790
+ ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
791
+ return ringbackUrl;
792
+ }
793
+ function playRingback() {
794
+ const el = document.createElement("audio");
795
+ el.src = ringbackSource();
796
+ el.loop = true;
797
+ el.style.display = "none";
798
+ document.body.appendChild(el);
799
+ void el.play().catch(() => {
800
+ });
801
+ return {
802
+ stop() {
803
+ el.pause();
804
+ el.remove();
805
+ }
806
+ };
807
+ }
808
+ var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
809
+ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
568
810
  const softphone = useSoftphone();
569
811
  const state = useSoftphoneState();
570
812
  const [value, setValue] = useState("");
@@ -585,6 +827,7 @@ function Dialpad({ outcome = null, onIntent } = {}) {
585
827
  const append = (key) => {
586
828
  onIntent?.();
587
829
  setValue((v) => v + key);
830
+ if (keyTones) playKeyTone(key);
588
831
  };
589
832
  const backspace = () => {
590
833
  onIntent?.();
@@ -629,22 +872,25 @@ function Dialpad({ outcome = null, onIntent } = {}) {
629
872
  )
630
873
  ] }),
631
874
  /* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
632
- /* @__PURE__ */ jsx("div", { className: "cv-keypad", children: KEYS.map(([key, letters, dialable]) => /* @__PURE__ */ jsxs(
633
- "button",
634
- {
635
- type: "button",
636
- className: "cv-key",
637
- "data-dialable": dialable,
638
- onClick: () => dialable && append(key),
639
- "aria-disabled": !dialable,
640
- title: dialable ? void 0 : "Solo disponible durante una llamada",
641
- children: [
642
- /* @__PURE__ */ jsx("b", { children: key }),
643
- letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
644
- ]
645
- },
646
- key
647
- )) }),
875
+ /* @__PURE__ */ jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
876
+ const dialable = !NOT_DIALABLE.has(key);
877
+ return /* @__PURE__ */ jsxs(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ className: "cv-key",
882
+ "data-dialable": dialable,
883
+ onClick: () => dialable && append(key),
884
+ "aria-disabled": !dialable,
885
+ title: dialable ? void 0 : "Solo disponible durante una llamada",
886
+ children: [
887
+ /* @__PURE__ */ jsx("b", { children: key }),
888
+ letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
889
+ ]
890
+ },
891
+ key
892
+ );
893
+ }) }),
648
894
  /* @__PURE__ */ jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsx(
649
895
  "button",
650
896
  {
@@ -884,7 +1130,9 @@ function Control({
884
1130
  label,
885
1131
  pressed = false,
886
1132
  tone,
887
- onClick
1133
+ onClick,
1134
+ disabled = false,
1135
+ title
888
1136
  }) {
889
1137
  return /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
890
1138
  /* @__PURE__ */ jsx(
@@ -896,6 +1144,8 @@ function Control({
896
1144
  "aria-pressed": pressed,
897
1145
  "aria-label": label,
898
1146
  onClick,
1147
+ disabled,
1148
+ title,
899
1149
  children: /* @__PURE__ */ jsx(Icon3, { size: 19 })
900
1150
  }
901
1151
  ),
@@ -1034,18 +1284,22 @@ function TransferPicker({
1034
1284
  }
1035
1285
  function CallScreen({
1036
1286
  call,
1037
- incomingCallAction
1287
+ incomingCallAction,
1288
+ keyTones = true
1038
1289
  }) {
1039
1290
  const view = useCall(call);
1040
1291
  const softphone = useSoftphone();
1041
- const [showTransfer, setShowTransfer] = useState(false);
1292
+ const [panel, setPanel] = useState(null);
1293
+ const showTransfer = panel === "transfer";
1294
+ const showKeypad = panel === "keypad";
1295
+ const showDevices = panel === "devices";
1296
+ const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
1042
1297
  const [transferTo, setTransferTo] = useState("");
1043
1298
  const [consultCall, setConsultCall] = useState(null);
1044
1299
  const [consultOutcome, setConsultOutcome] = useState(null);
1045
1300
  const [myUserId, setMyUserId] = useState(null);
1046
1301
  const consultView = useCall(consultCall);
1047
1302
  const consultIntentRef = useRef(null);
1048
- const [showDevices, setShowDevices] = useState(false);
1049
1303
  const controlsRef = useRef(null);
1050
1304
  const now = useTicker(view?.state === "active");
1051
1305
  const consultNow = useTicker(consultView?.state === "active");
@@ -1102,20 +1356,31 @@ function CallScreen({
1102
1356
  useEffect(() => {
1103
1357
  if (!showDevices) return;
1104
1358
  const onKey = (e) => {
1105
- if (e.key === "Escape") setShowDevices(false);
1359
+ if (e.key === "Escape") setPanel(null);
1360
+ };
1361
+ document.addEventListener("keydown", onKey);
1362
+ return () => document.removeEventListener("keydown", onKey);
1363
+ }, [showDevices]);
1364
+ useEffect(() => {
1365
+ if (!showKeypad) return;
1366
+ const onKey = (e) => {
1367
+ if (e.key === "Escape") setPanel(null);
1106
1368
  };
1107
1369
  document.addEventListener("keydown", onKey);
1108
1370
  if (!isConnected) return () => document.removeEventListener("keydown", onKey);
1109
1371
  const root = controlsRef.current?.getRootNode() ?? document;
1110
1372
  const onDown = (e) => {
1111
- if (!controlsRef.current?.contains(e.target)) setShowDevices(false);
1373
+ if (!controlsRef.current?.contains(e.target)) setPanel(null);
1112
1374
  };
1113
1375
  root.addEventListener("pointerdown", onDown);
1114
1376
  return () => {
1115
1377
  document.removeEventListener("keydown", onKey);
1116
1378
  root.removeEventListener("pointerdown", onDown);
1117
1379
  };
1118
- }, [showDevices, isConnected]);
1380
+ }, [showKeypad, isConnected]);
1381
+ useEffect(() => {
1382
+ if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
1383
+ }, [view?.state]);
1119
1384
  if (!view || view.state === "ended") return null;
1120
1385
  const s = view.state;
1121
1386
  const isRinging = s === "ringing";
@@ -1141,9 +1406,9 @@ function CallScreen({
1141
1406
  const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
1142
1407
  const mm = rawM.padStart(2, "0");
1143
1408
  return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
1144
- /* @__PURE__ */ jsx("div", { className: "cv-call", children: !inCall && showDevices ? /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-inline", children: [
1409
+ /* @__PURE__ */ jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-inline", children: [
1145
1410
  /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-header", children: [
1146
- /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(false), children: [
1411
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
1147
1412
  /* @__PURE__ */ jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
1148
1413
  "Volver"
1149
1414
  ] }),
@@ -1151,7 +1416,7 @@ function CallScreen({
1151
1416
  ] }),
1152
1417
  /* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
1153
1418
  showNumber ? `${number} \xB7 ` : "",
1154
- STATUS_LABEL[s] ?? ""
1419
+ s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
1155
1420
  ] }),
1156
1421
  /* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
1157
1422
  ] }) : transferPickerActive ? (
@@ -1239,7 +1504,7 @@ function CallScreen({
1239
1504
  /* @__PURE__ */ jsx("i", { children: ":" }),
1240
1505
  /* @__PURE__ */ jsx("b", { children: ss })
1241
1506
  ] }) : /* @__PURE__ */ jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
1242
- !inCall ? /* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
1507
+ /* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
1243
1508
  incomingAction ? /* @__PURE__ */ jsxs(
1244
1509
  "button",
1245
1510
  {
@@ -1252,11 +1517,11 @@ function CallScreen({
1252
1517
  ]
1253
1518
  }
1254
1519
  ) : null,
1255
- /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(true), children: [
1520
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
1256
1521
  /* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
1257
1522
  "Dispositivos"
1258
1523
  ] })
1259
- ] }) : null
1524
+ ] })
1260
1525
  ] }) }),
1261
1526
  /* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
1262
1527
  /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
@@ -1315,17 +1580,19 @@ function CallScreen({
1315
1580
  label: "Transferir",
1316
1581
  pressed: showTransfer,
1317
1582
  tone: "accent",
1318
- onClick: () => setShowTransfer((v) => !v)
1583
+ onClick: () => togglePanel("transfer")
1319
1584
  }
1320
1585
  ),
1321
1586
  /* @__PURE__ */ jsx(
1322
1587
  Control,
1323
1588
  {
1324
- icon: Headphones,
1325
- label: "Dispositivos",
1326
- pressed: showDevices,
1589
+ icon: Grid3x3,
1590
+ label: "Teclado",
1591
+ pressed: showKeypad,
1327
1592
  tone: "accent",
1328
- onClick: () => setShowDevices((v) => !v)
1593
+ disabled: s !== "active",
1594
+ title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
1595
+ onClick: () => togglePanel("keypad")
1329
1596
  }
1330
1597
  )
1331
1598
  ] }) : null,
@@ -1343,7 +1610,22 @@ function CallScreen({
1343
1610
  ),
1344
1611
  /* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
1345
1612
  ] }) }),
1346
- inCall && showDevices ? /* @__PURE__ */ jsx("div", { className: "cv-device-popover cv-device-popover--above", role: "dialog", "aria-label": "Dispositivos de audio", children: /* @__PURE__ */ jsx(DevicePickers, {}) }) : null
1613
+ inCall && showKeypad ? /* @__PURE__ */ jsx("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ jsxs(
1614
+ "button",
1615
+ {
1616
+ type: "button",
1617
+ className: "cv-key",
1618
+ onClick: () => {
1619
+ call.sendDigits(key);
1620
+ if (keyTones) playKeyTone(key);
1621
+ },
1622
+ children: [
1623
+ /* @__PURE__ */ jsx("b", { children: key }),
1624
+ letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
1625
+ ]
1626
+ },
1627
+ key
1628
+ )) }) : null
1347
1629
  ] }) })
1348
1630
  ] });
1349
1631
  }
@@ -2108,203 +2390,6 @@ function useCallOutcome() {
2108
2390
  clear
2109
2391
  };
2110
2392
  }
2111
-
2112
- // src/ui/audio/audio.ts
2113
- var TONE_HZ = 425;
2114
- function createRemoteAudio() {
2115
- const el = document.createElement("audio");
2116
- el.autoplay = true;
2117
- el.style.display = "none";
2118
- document.body.appendChild(el);
2119
- return {
2120
- attach(stream) {
2121
- el.srcObject = stream;
2122
- void el.play().catch(() => {
2123
- });
2124
- },
2125
- detach() {
2126
- el.srcObject = null;
2127
- },
2128
- destroy() {
2129
- el.srcObject = null;
2130
- el.remove();
2131
- },
2132
- async setOutputDevice(deviceId) {
2133
- if (typeof el.setSinkId !== "function") return;
2134
- try {
2135
- await el.setSinkId(deviceId ?? "");
2136
- } catch {
2137
- }
2138
- }
2139
- };
2140
- }
2141
- var shared = null;
2142
- function sharedContext() {
2143
- if (shared) return shared;
2144
- const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
2145
- if (!Ctor) return null;
2146
- shared = new Ctor();
2147
- return shared;
2148
- }
2149
- function primeAudio() {
2150
- const ctx = sharedContext();
2151
- if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
2152
- });
2153
- }
2154
- function playRingtone() {
2155
- const ctx = sharedContext();
2156
- if (!ctx) return { stop() {
2157
- } };
2158
- const gain = ctx.createGain();
2159
- gain.gain.value = 0;
2160
- gain.connect(ctx.destination);
2161
- const osc = ctx.createOscillator();
2162
- osc.type = "sine";
2163
- osc.frequency.value = TONE_HZ;
2164
- osc.connect(gain);
2165
- osc.start();
2166
- const LEVEL = 0.8;
2167
- const RAMP = 0.02;
2168
- const burst = (at, dur) => {
2169
- gain.gain.setValueAtTime(0, at);
2170
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
2171
- gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
2172
- gain.gain.linearRampToValueAtTime(0, at + dur);
2173
- };
2174
- const scheduleCycle = (at) => {
2175
- burst(at, 0.4);
2176
- burst(at + 0.6, 0.4);
2177
- return 3;
2178
- };
2179
- const LOOKAHEAD = 0.5;
2180
- const TICK_MS = 150;
2181
- const LEAD = 0.08;
2182
- let stopped = false;
2183
- let timer = null;
2184
- let nextAt = 0;
2185
- const pump = () => {
2186
- while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
2187
- };
2188
- void Promise.resolve(ctx.resume?.()).catch(() => {
2189
- }).finally(() => {
2190
- if (stopped) return;
2191
- nextAt = ctx.currentTime + LEAD;
2192
- pump();
2193
- timer = setInterval(pump, TICK_MS);
2194
- });
2195
- return {
2196
- stop() {
2197
- stopped = true;
2198
- if (timer) clearInterval(timer);
2199
- try {
2200
- osc.stop();
2201
- } catch {
2202
- }
2203
- try {
2204
- gain.disconnect();
2205
- } catch {
2206
- }
2207
- }
2208
- };
2209
- }
2210
- function playCallEndedTone() {
2211
- const ctx = sharedContext();
2212
- if (!ctx) return;
2213
- const gain = ctx.createGain();
2214
- gain.gain.value = 0;
2215
- gain.connect(ctx.destination);
2216
- const osc = ctx.createOscillator();
2217
- osc.type = "sine";
2218
- osc.frequency.value = TONE_HZ;
2219
- osc.connect(gain);
2220
- osc.start();
2221
- const LEVEL = 0.55;
2222
- const RAMP = 8e-3;
2223
- const BEEP = 0.25;
2224
- void Promise.resolve(ctx.resume?.()).catch(() => {
2225
- }).finally(() => {
2226
- const at = ctx.currentTime + 0.02;
2227
- gain.gain.setValueAtTime(0, at);
2228
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
2229
- gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
2230
- gain.gain.linearRampToValueAtTime(0, at + BEEP);
2231
- const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
2232
- setTimeout(() => {
2233
- try {
2234
- osc.stop();
2235
- } catch {
2236
- }
2237
- try {
2238
- gain.disconnect();
2239
- } catch {
2240
- }
2241
- }, totalMs);
2242
- });
2243
- }
2244
- var ringbackUrl = null;
2245
- function ringbackSource() {
2246
- if (ringbackUrl) return ringbackUrl;
2247
- const sampleRate = 48e3;
2248
- const channels = 2;
2249
- const bytesPerSample = 2;
2250
- const amp = 0.5;
2251
- const toneSec = 1.5;
2252
- const silenceSec = 3;
2253
- const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
2254
- const toneFrames = Math.round(toneSec * sampleRate);
2255
- const ramp = Math.round(0.02 * sampleRate);
2256
- const blockAlign = channels * bytesPerSample;
2257
- const dataSize = totalFrames * blockAlign;
2258
- const buffer = new ArrayBuffer(44 + dataSize);
2259
- const view = new DataView(buffer);
2260
- const str = (off, s) => {
2261
- for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
2262
- };
2263
- str(0, "RIFF");
2264
- view.setUint32(4, 36 + dataSize, true);
2265
- str(8, "WAVE");
2266
- str(12, "fmt ");
2267
- view.setUint32(16, 16, true);
2268
- view.setUint16(20, 1, true);
2269
- view.setUint16(22, channels, true);
2270
- view.setUint32(24, sampleRate, true);
2271
- view.setUint32(28, sampleRate * blockAlign, true);
2272
- view.setUint16(32, blockAlign, true);
2273
- view.setUint16(34, bytesPerSample * 8, true);
2274
- str(36, "data");
2275
- view.setUint32(40, dataSize, true);
2276
- for (let i = 0; i < totalFrames; i++) {
2277
- let sample = 0;
2278
- if (i < toneFrames) {
2279
- let env = 1;
2280
- if (i < ramp) env = i / ramp;
2281
- else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
2282
- sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
2283
- }
2284
- const s16 = Math.round(sample * 32767);
2285
- const off = 44 + i * blockAlign;
2286
- for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
2287
- }
2288
- ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
2289
- return ringbackUrl;
2290
- }
2291
- function playRingback() {
2292
- const el = document.createElement("audio");
2293
- el.src = ringbackSource();
2294
- el.loop = true;
2295
- el.style.display = "none";
2296
- document.body.appendChild(el);
2297
- void el.play().catch(() => {
2298
- });
2299
- return {
2300
- stop() {
2301
- el.pause();
2302
- el.remove();
2303
- }
2304
- };
2305
- }
2306
-
2307
- // src/ui/audio/useCallAudio.ts
2308
2393
  function useCallAudio(enabled) {
2309
2394
  const softphone = useSoftphone();
2310
2395
  const remote = useRef(null);
@@ -2522,7 +2607,7 @@ function SoftphoneWidget(props) {
2522
2607
  ...theme === "system" ? {} : { "data-cv-theme": theme },
2523
2608
  children: [
2524
2609
  screenIsFree ? /* @__PURE__ */ jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
2525
- /* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
2610
+ /* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
2526
2611
  screenIsFree ? /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
2527
2612
  "button",
2528
2613
  {