@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.cjs CHANGED
@@ -13721,21 +13721,263 @@ function caretAfterDigits(formatted, digitsBefore) {
13721
13721
  function digitsBeforeCaret(text, caret) {
13722
13722
  return (text.slice(0, caret).match(/\d/g) ?? []).length;
13723
13723
  }
13724
- var KEYS = [
13725
- ["1", "", true],
13726
- ["2", "ABC", true],
13727
- ["3", "DEF", true],
13728
- ["4", "GHI", true],
13729
- ["5", "JKL", true],
13730
- ["6", "MNO", true],
13731
- ["7", "PQRS", true],
13732
- ["8", "TUV", true],
13733
- ["9", "WXYZ", true],
13734
- ["*", "", false],
13735
- ["0", "+", true],
13736
- ["#", "", false]
13724
+
13725
+ // src/ui/dialing/keys.ts
13726
+ var KEYPAD_KEYS = [
13727
+ ["1", ""],
13728
+ ["2", "ABC"],
13729
+ ["3", "DEF"],
13730
+ ["4", "GHI"],
13731
+ ["5", "JKL"],
13732
+ ["6", "MNO"],
13733
+ ["7", "PQRS"],
13734
+ ["8", "TUV"],
13735
+ ["9", "WXYZ"],
13736
+ ["*", ""],
13737
+ ["0", "+"],
13738
+ ["#", ""]
13737
13739
  ];
13738
- function Dialpad({ outcome = null, onIntent } = {}) {
13740
+
13741
+ // src/ui/audio/audio.ts
13742
+ var TONE_HZ = 425;
13743
+ function createRemoteAudio() {
13744
+ const el = document.createElement("audio");
13745
+ el.autoplay = true;
13746
+ el.style.display = "none";
13747
+ document.body.appendChild(el);
13748
+ return {
13749
+ attach(stream) {
13750
+ el.srcObject = stream;
13751
+ void el.play().catch(() => {
13752
+ });
13753
+ },
13754
+ detach() {
13755
+ el.srcObject = null;
13756
+ },
13757
+ destroy() {
13758
+ el.srcObject = null;
13759
+ el.remove();
13760
+ },
13761
+ async setOutputDevice(deviceId) {
13762
+ if (typeof el.setSinkId !== "function") return;
13763
+ try {
13764
+ await el.setSinkId(deviceId ?? "");
13765
+ } catch {
13766
+ }
13767
+ }
13768
+ };
13769
+ }
13770
+ var shared = null;
13771
+ function sharedContext() {
13772
+ if (shared) return shared;
13773
+ const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
13774
+ if (!Ctor) return null;
13775
+ shared = new Ctor();
13776
+ return shared;
13777
+ }
13778
+ function primeAudio() {
13779
+ const ctx = sharedContext();
13780
+ if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
13781
+ });
13782
+ }
13783
+ function playRingtone() {
13784
+ const ctx = sharedContext();
13785
+ if (!ctx) return { stop() {
13786
+ } };
13787
+ const gain = ctx.createGain();
13788
+ gain.gain.value = 0;
13789
+ gain.connect(ctx.destination);
13790
+ const osc = ctx.createOscillator();
13791
+ osc.type = "sine";
13792
+ osc.frequency.value = TONE_HZ;
13793
+ osc.connect(gain);
13794
+ osc.start();
13795
+ const LEVEL = 0.8;
13796
+ const RAMP = 0.02;
13797
+ const burst = (at, dur) => {
13798
+ gain.gain.setValueAtTime(0, at);
13799
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
13800
+ gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
13801
+ gain.gain.linearRampToValueAtTime(0, at + dur);
13802
+ };
13803
+ const scheduleCycle = (at) => {
13804
+ burst(at, 0.4);
13805
+ burst(at + 0.6, 0.4);
13806
+ return 3;
13807
+ };
13808
+ const LOOKAHEAD = 0.5;
13809
+ const TICK_MS = 150;
13810
+ const LEAD = 0.08;
13811
+ let stopped = false;
13812
+ let timer = null;
13813
+ let nextAt = 0;
13814
+ const pump = () => {
13815
+ while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
13816
+ };
13817
+ void Promise.resolve(ctx.resume?.()).catch(() => {
13818
+ }).finally(() => {
13819
+ if (stopped) return;
13820
+ nextAt = ctx.currentTime + LEAD;
13821
+ pump();
13822
+ timer = setInterval(pump, TICK_MS);
13823
+ });
13824
+ return {
13825
+ stop() {
13826
+ stopped = true;
13827
+ if (timer) clearInterval(timer);
13828
+ try {
13829
+ osc.stop();
13830
+ } catch {
13831
+ }
13832
+ try {
13833
+ gain.disconnect();
13834
+ } catch {
13835
+ }
13836
+ }
13837
+ };
13838
+ }
13839
+ function playCallEndedTone() {
13840
+ const ctx = sharedContext();
13841
+ if (!ctx) return;
13842
+ const gain = ctx.createGain();
13843
+ gain.gain.value = 0;
13844
+ gain.connect(ctx.destination);
13845
+ const osc = ctx.createOscillator();
13846
+ osc.type = "sine";
13847
+ osc.frequency.value = TONE_HZ;
13848
+ osc.connect(gain);
13849
+ osc.start();
13850
+ const LEVEL = 0.55;
13851
+ const RAMP = 8e-3;
13852
+ const BEEP = 0.25;
13853
+ void Promise.resolve(ctx.resume?.()).catch(() => {
13854
+ }).finally(() => {
13855
+ const at = ctx.currentTime + 0.02;
13856
+ gain.gain.setValueAtTime(0, at);
13857
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
13858
+ gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
13859
+ gain.gain.linearRampToValueAtTime(0, at + BEEP);
13860
+ const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
13861
+ setTimeout(() => {
13862
+ try {
13863
+ osc.stop();
13864
+ } catch {
13865
+ }
13866
+ try {
13867
+ gain.disconnect();
13868
+ } catch {
13869
+ }
13870
+ }, totalMs);
13871
+ });
13872
+ }
13873
+ var DTMF_ROWS_HZ = [697, 770, 852, 941];
13874
+ var DTMF_COLS_HZ = [1209, 1336, 1477];
13875
+ var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
13876
+ function dtmfFrequencies(key) {
13877
+ if (key.length !== 1) return null;
13878
+ for (let row = 0; row < DTMF_LAYOUT.length; row++) {
13879
+ const col = DTMF_LAYOUT[row].indexOf(key);
13880
+ if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
13881
+ }
13882
+ return null;
13883
+ }
13884
+ function playKeyTone(key) {
13885
+ const hz = dtmfFrequencies(key);
13886
+ if (!hz) return;
13887
+ const ctx = sharedContext();
13888
+ if (!ctx) return;
13889
+ const LEVEL = 0.2;
13890
+ const RAMP = 6e-3;
13891
+ const DURATION = 0.12;
13892
+ const LEAD = 0.01;
13893
+ const gain = ctx.createGain();
13894
+ gain.gain.value = 0;
13895
+ gain.connect(ctx.destination);
13896
+ const oscillators = hz.map((freq) => {
13897
+ const osc = ctx.createOscillator();
13898
+ osc.type = "sine";
13899
+ osc.frequency.value = freq;
13900
+ osc.connect(gain);
13901
+ osc.start();
13902
+ return osc;
13903
+ });
13904
+ const fire = () => {
13905
+ const at = ctx.currentTime + LEAD;
13906
+ gain.gain.setValueAtTime(0, at);
13907
+ gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
13908
+ gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
13909
+ gain.gain.linearRampToValueAtTime(0, at + DURATION);
13910
+ for (const osc of oscillators) osc.stop(at + DURATION);
13911
+ oscillators[0].onended = () => gain.disconnect();
13912
+ };
13913
+ if (ctx.state === "running") fire();
13914
+ else void Promise.resolve(ctx.resume?.()).catch(() => {
13915
+ }).finally(fire);
13916
+ }
13917
+ var ringbackUrl = null;
13918
+ function ringbackSource() {
13919
+ if (ringbackUrl) return ringbackUrl;
13920
+ const sampleRate = 48e3;
13921
+ const channels = 2;
13922
+ const bytesPerSample = 2;
13923
+ const amp = 0.5;
13924
+ const toneSec = 1.5;
13925
+ const silenceSec = 3;
13926
+ const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
13927
+ const toneFrames = Math.round(toneSec * sampleRate);
13928
+ const ramp = Math.round(0.02 * sampleRate);
13929
+ const blockAlign = channels * bytesPerSample;
13930
+ const dataSize = totalFrames * blockAlign;
13931
+ const buffer = new ArrayBuffer(44 + dataSize);
13932
+ const view = new DataView(buffer);
13933
+ const str = (off, s) => {
13934
+ for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
13935
+ };
13936
+ str(0, "RIFF");
13937
+ view.setUint32(4, 36 + dataSize, true);
13938
+ str(8, "WAVE");
13939
+ str(12, "fmt ");
13940
+ view.setUint32(16, 16, true);
13941
+ view.setUint16(20, 1, true);
13942
+ view.setUint16(22, channels, true);
13943
+ view.setUint32(24, sampleRate, true);
13944
+ view.setUint32(28, sampleRate * blockAlign, true);
13945
+ view.setUint16(32, blockAlign, true);
13946
+ view.setUint16(34, bytesPerSample * 8, true);
13947
+ str(36, "data");
13948
+ view.setUint32(40, dataSize, true);
13949
+ for (let i = 0; i < totalFrames; i++) {
13950
+ let sample = 0;
13951
+ if (i < toneFrames) {
13952
+ let env = 1;
13953
+ if (i < ramp) env = i / ramp;
13954
+ else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
13955
+ sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
13956
+ }
13957
+ const s16 = Math.round(sample * 32767);
13958
+ const off = 44 + i * blockAlign;
13959
+ for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
13960
+ }
13961
+ ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
13962
+ return ringbackUrl;
13963
+ }
13964
+ function playRingback() {
13965
+ const el = document.createElement("audio");
13966
+ el.src = ringbackSource();
13967
+ el.loop = true;
13968
+ el.style.display = "none";
13969
+ document.body.appendChild(el);
13970
+ void el.play().catch(() => {
13971
+ });
13972
+ return {
13973
+ stop() {
13974
+ el.pause();
13975
+ el.remove();
13976
+ }
13977
+ };
13978
+ }
13979
+ var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
13980
+ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
13739
13981
  const softphone = useSoftphone();
13740
13982
  const state = useSoftphoneState();
13741
13983
  const [value, setValue] = React.useState("");
@@ -13756,6 +13998,7 @@ function Dialpad({ outcome = null, onIntent } = {}) {
13756
13998
  const append = (key) => {
13757
13999
  onIntent?.();
13758
14000
  setValue((v) => v + key);
14001
+ if (keyTones) playKeyTone(key);
13759
14002
  };
13760
14003
  const backspace = () => {
13761
14004
  onIntent?.();
@@ -13800,22 +14043,25 @@ function Dialpad({ outcome = null, onIntent } = {}) {
13800
14043
  )
13801
14044
  ] }),
13802
14045
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
13803
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children: KEYS.map(([key, letters, dialable]) => /* @__PURE__ */ jsxRuntime.jsxs(
13804
- "button",
13805
- {
13806
- type: "button",
13807
- className: "cv-key",
13808
- "data-dialable": dialable,
13809
- onClick: () => dialable && append(key),
13810
- "aria-disabled": !dialable,
13811
- title: dialable ? void 0 : "Solo disponible durante una llamada",
13812
- children: [
13813
- /* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
13814
- letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
13815
- ]
13816
- },
13817
- key
13818
- )) }),
14046
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
14047
+ const dialable = !NOT_DIALABLE.has(key);
14048
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14049
+ "button",
14050
+ {
14051
+ type: "button",
14052
+ className: "cv-key",
14053
+ "data-dialable": dialable,
14054
+ onClick: () => dialable && append(key),
14055
+ "aria-disabled": !dialable,
14056
+ title: dialable ? void 0 : "Solo disponible durante una llamada",
14057
+ children: [
14058
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
14059
+ letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
14060
+ ]
14061
+ },
14062
+ key
14063
+ );
14064
+ }) }),
13819
14065
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsxRuntime.jsx(
13820
14066
  "button",
13821
14067
  {
@@ -14055,7 +14301,9 @@ function Control({
14055
14301
  label,
14056
14302
  pressed = false,
14057
14303
  tone,
14058
- onClick
14304
+ onClick,
14305
+ disabled = false,
14306
+ title
14059
14307
  }) {
14060
14308
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
14061
14309
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14067,6 +14315,8 @@ function Control({
14067
14315
  "aria-pressed": pressed,
14068
14316
  "aria-label": label,
14069
14317
  onClick,
14318
+ disabled,
14319
+ title,
14070
14320
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 19 })
14071
14321
  }
14072
14322
  ),
@@ -14205,18 +14455,22 @@ function TransferPicker({
14205
14455
  }
14206
14456
  function CallScreen({
14207
14457
  call,
14208
- incomingCallAction
14458
+ incomingCallAction,
14459
+ keyTones = true
14209
14460
  }) {
14210
14461
  const view = useCall(call);
14211
14462
  const softphone = useSoftphone();
14212
- const [showTransfer, setShowTransfer] = React.useState(false);
14463
+ const [panel, setPanel] = React.useState(null);
14464
+ const showTransfer = panel === "transfer";
14465
+ const showKeypad = panel === "keypad";
14466
+ const showDevices = panel === "devices";
14467
+ const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
14213
14468
  const [transferTo, setTransferTo] = React.useState("");
14214
14469
  const [consultCall, setConsultCall] = React.useState(null);
14215
14470
  const [consultOutcome, setConsultOutcome] = React.useState(null);
14216
14471
  const [myUserId, setMyUserId] = React.useState(null);
14217
14472
  const consultView = useCall(consultCall);
14218
14473
  const consultIntentRef = React.useRef(null);
14219
- const [showDevices, setShowDevices] = React.useState(false);
14220
14474
  const controlsRef = React.useRef(null);
14221
14475
  const now = useTicker(view?.state === "active");
14222
14476
  const consultNow = useTicker(consultView?.state === "active");
@@ -14273,20 +14527,31 @@ function CallScreen({
14273
14527
  React.useEffect(() => {
14274
14528
  if (!showDevices) return;
14275
14529
  const onKey = (e) => {
14276
- if (e.key === "Escape") setShowDevices(false);
14530
+ if (e.key === "Escape") setPanel(null);
14531
+ };
14532
+ document.addEventListener("keydown", onKey);
14533
+ return () => document.removeEventListener("keydown", onKey);
14534
+ }, [showDevices]);
14535
+ React.useEffect(() => {
14536
+ if (!showKeypad) return;
14537
+ const onKey = (e) => {
14538
+ if (e.key === "Escape") setPanel(null);
14277
14539
  };
14278
14540
  document.addEventListener("keydown", onKey);
14279
14541
  if (!isConnected) return () => document.removeEventListener("keydown", onKey);
14280
14542
  const root = controlsRef.current?.getRootNode() ?? document;
14281
14543
  const onDown = (e) => {
14282
- if (!controlsRef.current?.contains(e.target)) setShowDevices(false);
14544
+ if (!controlsRef.current?.contains(e.target)) setPanel(null);
14283
14545
  };
14284
14546
  root.addEventListener("pointerdown", onDown);
14285
14547
  return () => {
14286
14548
  document.removeEventListener("keydown", onKey);
14287
14549
  root.removeEventListener("pointerdown", onDown);
14288
14550
  };
14289
- }, [showDevices, isConnected]);
14551
+ }, [showKeypad, isConnected]);
14552
+ React.useEffect(() => {
14553
+ if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
14554
+ }, [view?.state]);
14290
14555
  if (!view || view.state === "ended") return null;
14291
14556
  const s = view.state;
14292
14557
  const isRinging = s === "ringing";
@@ -14312,9 +14577,9 @@ function CallScreen({
14312
14577
  const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
14313
14578
  const mm = rawM.padStart(2, "0");
14314
14579
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
14315
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children: !inCall && showDevices ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-inline", children: [
14580
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-inline", children: [
14316
14581
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-header", children: [
14317
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(false), children: [
14582
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
14318
14583
  /* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
14319
14584
  "Volver"
14320
14585
  ] }),
@@ -14322,7 +14587,7 @@ function CallScreen({
14322
14587
  ] }),
14323
14588
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-call-devices-context", children: [
14324
14589
  showNumber ? `${number} \xB7 ` : "",
14325
- STATUS_LABEL[s] ?? ""
14590
+ s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
14326
14591
  ] }),
14327
14592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) })
14328
14593
  ] }) : transferPickerActive ? (
@@ -14410,7 +14675,7 @@ function CallScreen({
14410
14675
  /* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
14411
14676
  /* @__PURE__ */ jsxRuntime.jsx("b", { children: ss })
14412
14677
  ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
14413
- !inCall ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-chips", children: [
14678
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-chips", children: [
14414
14679
  incomingAction ? /* @__PURE__ */ jsxRuntime.jsxs(
14415
14680
  "button",
14416
14681
  {
@@ -14423,11 +14688,11 @@ function CallScreen({
14423
14688
  ]
14424
14689
  }
14425
14690
  ) : null,
14426
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(true), children: [
14691
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
14427
14692
  /* @__PURE__ */ jsxRuntime.jsx(Headphones, { size: 13, "aria-hidden": true }),
14428
14693
  "Dispositivos"
14429
14694
  ] })
14430
- ] }) : null
14695
+ ] })
14431
14696
  ] }) }),
14432
14697
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls-primary", children: [
14433
14698
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
@@ -14486,17 +14751,19 @@ function CallScreen({
14486
14751
  label: "Transferir",
14487
14752
  pressed: showTransfer,
14488
14753
  tone: "accent",
14489
- onClick: () => setShowTransfer((v) => !v)
14754
+ onClick: () => togglePanel("transfer")
14490
14755
  }
14491
14756
  ),
14492
14757
  /* @__PURE__ */ jsxRuntime.jsx(
14493
14758
  Control,
14494
14759
  {
14495
- icon: Headphones,
14496
- label: "Dispositivos",
14497
- pressed: showDevices,
14760
+ icon: Grid3x3,
14761
+ label: "Teclado",
14762
+ pressed: showKeypad,
14498
14763
  tone: "accent",
14499
- onClick: () => setShowDevices((v) => !v)
14764
+ disabled: s !== "active",
14765
+ title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
14766
+ onClick: () => togglePanel("keypad")
14500
14767
  }
14501
14768
  )
14502
14769
  ] }) : null,
@@ -14514,7 +14781,22 @@ function CallScreen({
14514
14781
  ),
14515
14782
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
14516
14783
  ] }) }),
14517
- inCall && showDevices ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-device-popover cv-device-popover--above", role: "dialog", "aria-label": "Dispositivos de audio", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) }) : null
14784
+ inCall && showKeypad ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ jsxRuntime.jsxs(
14785
+ "button",
14786
+ {
14787
+ type: "button",
14788
+ className: "cv-key",
14789
+ onClick: () => {
14790
+ call.sendDigits(key);
14791
+ if (keyTones) playKeyTone(key);
14792
+ },
14793
+ children: [
14794
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
14795
+ letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
14796
+ ]
14797
+ },
14798
+ key
14799
+ )) }) : null
14518
14800
  ] }) })
14519
14801
  ] });
14520
14802
  }
@@ -15289,203 +15571,6 @@ function useCallOutcome() {
15289
15571
  clear
15290
15572
  };
15291
15573
  }
15292
-
15293
- // src/ui/audio/audio.ts
15294
- var TONE_HZ = 425;
15295
- function createRemoteAudio() {
15296
- const el = document.createElement("audio");
15297
- el.autoplay = true;
15298
- el.style.display = "none";
15299
- document.body.appendChild(el);
15300
- return {
15301
- attach(stream) {
15302
- el.srcObject = stream;
15303
- void el.play().catch(() => {
15304
- });
15305
- },
15306
- detach() {
15307
- el.srcObject = null;
15308
- },
15309
- destroy() {
15310
- el.srcObject = null;
15311
- el.remove();
15312
- },
15313
- async setOutputDevice(deviceId) {
15314
- if (typeof el.setSinkId !== "function") return;
15315
- try {
15316
- await el.setSinkId(deviceId ?? "");
15317
- } catch {
15318
- }
15319
- }
15320
- };
15321
- }
15322
- var shared = null;
15323
- function sharedContext() {
15324
- if (shared) return shared;
15325
- const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
15326
- if (!Ctor) return null;
15327
- shared = new Ctor();
15328
- return shared;
15329
- }
15330
- function primeAudio() {
15331
- const ctx = sharedContext();
15332
- if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
15333
- });
15334
- }
15335
- function playRingtone() {
15336
- const ctx = sharedContext();
15337
- if (!ctx) return { stop() {
15338
- } };
15339
- const gain = ctx.createGain();
15340
- gain.gain.value = 0;
15341
- gain.connect(ctx.destination);
15342
- const osc = ctx.createOscillator();
15343
- osc.type = "sine";
15344
- osc.frequency.value = TONE_HZ;
15345
- osc.connect(gain);
15346
- osc.start();
15347
- const LEVEL = 0.8;
15348
- const RAMP = 0.02;
15349
- const burst = (at, dur) => {
15350
- gain.gain.setValueAtTime(0, at);
15351
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
15352
- gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
15353
- gain.gain.linearRampToValueAtTime(0, at + dur);
15354
- };
15355
- const scheduleCycle = (at) => {
15356
- burst(at, 0.4);
15357
- burst(at + 0.6, 0.4);
15358
- return 3;
15359
- };
15360
- const LOOKAHEAD = 0.5;
15361
- const TICK_MS = 150;
15362
- const LEAD = 0.08;
15363
- let stopped = false;
15364
- let timer = null;
15365
- let nextAt = 0;
15366
- const pump = () => {
15367
- while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
15368
- };
15369
- void Promise.resolve(ctx.resume?.()).catch(() => {
15370
- }).finally(() => {
15371
- if (stopped) return;
15372
- nextAt = ctx.currentTime + LEAD;
15373
- pump();
15374
- timer = setInterval(pump, TICK_MS);
15375
- });
15376
- return {
15377
- stop() {
15378
- stopped = true;
15379
- if (timer) clearInterval(timer);
15380
- try {
15381
- osc.stop();
15382
- } catch {
15383
- }
15384
- try {
15385
- gain.disconnect();
15386
- } catch {
15387
- }
15388
- }
15389
- };
15390
- }
15391
- function playCallEndedTone() {
15392
- const ctx = sharedContext();
15393
- if (!ctx) return;
15394
- const gain = ctx.createGain();
15395
- gain.gain.value = 0;
15396
- gain.connect(ctx.destination);
15397
- const osc = ctx.createOscillator();
15398
- osc.type = "sine";
15399
- osc.frequency.value = TONE_HZ;
15400
- osc.connect(gain);
15401
- osc.start();
15402
- const LEVEL = 0.55;
15403
- const RAMP = 8e-3;
15404
- const BEEP = 0.25;
15405
- void Promise.resolve(ctx.resume?.()).catch(() => {
15406
- }).finally(() => {
15407
- const at = ctx.currentTime + 0.02;
15408
- gain.gain.setValueAtTime(0, at);
15409
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
15410
- gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
15411
- gain.gain.linearRampToValueAtTime(0, at + BEEP);
15412
- const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
15413
- setTimeout(() => {
15414
- try {
15415
- osc.stop();
15416
- } catch {
15417
- }
15418
- try {
15419
- gain.disconnect();
15420
- } catch {
15421
- }
15422
- }, totalMs);
15423
- });
15424
- }
15425
- var ringbackUrl = null;
15426
- function ringbackSource() {
15427
- if (ringbackUrl) return ringbackUrl;
15428
- const sampleRate = 48e3;
15429
- const channels = 2;
15430
- const bytesPerSample = 2;
15431
- const amp = 0.5;
15432
- const toneSec = 1.5;
15433
- const silenceSec = 3;
15434
- const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
15435
- const toneFrames = Math.round(toneSec * sampleRate);
15436
- const ramp = Math.round(0.02 * sampleRate);
15437
- const blockAlign = channels * bytesPerSample;
15438
- const dataSize = totalFrames * blockAlign;
15439
- const buffer = new ArrayBuffer(44 + dataSize);
15440
- const view = new DataView(buffer);
15441
- const str = (off, s) => {
15442
- for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
15443
- };
15444
- str(0, "RIFF");
15445
- view.setUint32(4, 36 + dataSize, true);
15446
- str(8, "WAVE");
15447
- str(12, "fmt ");
15448
- view.setUint32(16, 16, true);
15449
- view.setUint16(20, 1, true);
15450
- view.setUint16(22, channels, true);
15451
- view.setUint32(24, sampleRate, true);
15452
- view.setUint32(28, sampleRate * blockAlign, true);
15453
- view.setUint16(32, blockAlign, true);
15454
- view.setUint16(34, bytesPerSample * 8, true);
15455
- str(36, "data");
15456
- view.setUint32(40, dataSize, true);
15457
- for (let i = 0; i < totalFrames; i++) {
15458
- let sample = 0;
15459
- if (i < toneFrames) {
15460
- let env = 1;
15461
- if (i < ramp) env = i / ramp;
15462
- else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
15463
- sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
15464
- }
15465
- const s16 = Math.round(sample * 32767);
15466
- const off = 44 + i * blockAlign;
15467
- for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
15468
- }
15469
- ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
15470
- return ringbackUrl;
15471
- }
15472
- function playRingback() {
15473
- const el = document.createElement("audio");
15474
- el.src = ringbackSource();
15475
- el.loop = true;
15476
- el.style.display = "none";
15477
- document.body.appendChild(el);
15478
- void el.play().catch(() => {
15479
- });
15480
- return {
15481
- stop() {
15482
- el.pause();
15483
- el.remove();
15484
- }
15485
- };
15486
- }
15487
-
15488
- // src/ui/audio/useCallAudio.ts
15489
15574
  function useCallAudio(enabled) {
15490
15575
  const softphone = useSoftphone();
15491
15576
  const remote = React.useRef(null);
@@ -15703,7 +15788,7 @@ function SoftphoneWidget(props) {
15703
15788
  ...theme === "system" ? {} : { "data-cv-theme": theme },
15704
15789
  children: [
15705
15790
  screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
15706
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsxRuntime.jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsxRuntime.jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsxRuntime.jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsxRuntime.jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsxRuntime.jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsxRuntime.jsx(SettingsTab, {}) : /* @__PURE__ */ jsxRuntime.jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
15791
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsxRuntime.jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsxRuntime.jsx(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsxRuntime.jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsxRuntime.jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsxRuntime.jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsxRuntime.jsx(SettingsTab, {}) : /* @__PURE__ */ jsxRuntime.jsx(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
15707
15792
  screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
15708
15793
  "button",
15709
15794
  {