@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
package/dist/ui/index.cjs CHANGED
@@ -13128,6 +13128,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
13128
13128
  ["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
13129
13129
  ]);
13130
13130
 
13131
+ // ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
13132
+ var BellRing = createLucideIcon("BellRing", [
13133
+ ["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
13134
+ ["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
13135
+ ["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
13136
+ ["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
13137
+ ]);
13138
+
13131
13139
  // ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
13132
13140
  var CalendarClock = createLucideIcon("CalendarClock", [
13133
13141
  ["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5", key: "1osxxc" }],
@@ -13242,6 +13250,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
13242
13250
  ["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
13243
13251
  ]);
13244
13252
 
13253
+ // ../../node_modules/lucide-react/dist/esm/icons/music.js
13254
+ var Music = createLucideIcon("Music", [
13255
+ ["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
13256
+ ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
13257
+ ["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
13258
+ ]);
13259
+
13245
13260
  // ../../node_modules/lucide-react/dist/esm/icons/pause.js
13246
13261
  var Pause = createLucideIcon("Pause", [
13247
13262
  ["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
@@ -13367,6 +13382,19 @@ var Users = createLucideIcon("Users", [
13367
13382
  ["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
13368
13383
  ]);
13369
13384
 
13385
+ // ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
13386
+ var Volume2 = createLucideIcon("Volume2", [
13387
+ [
13388
+ "path",
13389
+ {
13390
+ d: "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",
13391
+ key: "uqj9uw"
13392
+ }
13393
+ ],
13394
+ ["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
13395
+ ["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
13396
+ ]);
13397
+
13370
13398
  // ../../node_modules/lucide-react/dist/esm/icons/x.js
13371
13399
  var X = createLucideIcon("X", [
13372
13400
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -13493,7 +13521,9 @@ function useDevices() {
13493
13521
  outputs: audio.outputs,
13494
13522
  currentInputId: audio.currentInputId,
13495
13523
  currentOutputId: audio.currentOutputId,
13524
+ currentRingtoneOutputId: audio.currentRingtoneOutputId,
13496
13525
  canSelectOutput: audio.canSelectOutput,
13526
+ canSelectRingtoneOutput: audio.canSelectRingtoneOutput,
13497
13527
  setInput: (deviceId) => audio.setInput(deviceId),
13498
13528
  setOutput: (deviceId) => audio.setOutput(deviceId),
13499
13529
  setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
@@ -13510,6 +13540,36 @@ var PACKED = "AC247 AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU6
13510
13540
  var DIAL_CODES = new Map(
13511
13541
  PACKED.split(" ").map((token) => [token.slice(0, 2), Number(token.slice(2))])
13512
13542
  );
13543
+ var REGIONS_BY_CODE = (() => {
13544
+ const map = /* @__PURE__ */ new Map();
13545
+ for (const [region, code] of DIAL_CODES) map.set(code, [...map.get(code) ?? [], region]);
13546
+ return map;
13547
+ })();
13548
+ var MAIN_REGION = {
13549
+ 1: "US",
13550
+ 7: "RU",
13551
+ 39: "IT",
13552
+ 44: "GB",
13553
+ 47: "NO",
13554
+ 61: "AU",
13555
+ 212: "MA",
13556
+ 262: "RE",
13557
+ 290: "SH",
13558
+ 358: "FI",
13559
+ 590: "GP",
13560
+ 599: "CW"
13561
+ };
13562
+ function regionOfInternational(typed) {
13563
+ const text = String(typed ?? "").trim();
13564
+ if (!text.startsWith("+")) return null;
13565
+ const digits = text.slice(1).replace(/\D/g, "");
13566
+ for (let length = 1; length <= Math.min(3, digits.length); length++) {
13567
+ const code = Number(digits.slice(0, length));
13568
+ const regions = REGIONS_BY_CODE.get(code);
13569
+ if (regions) return MAIN_REGION[code] ?? regions[0] ?? null;
13570
+ }
13571
+ return null;
13572
+ }
13513
13573
  var DEFAULT_COUNTRY = "ES";
13514
13574
  var REGION_NAMES = (() => {
13515
13575
  try {
@@ -13561,12 +13621,16 @@ function CountryMark({ code }) {
13561
13621
  }
13562
13622
  function CountryPicker({
13563
13623
  value,
13564
- onChange
13624
+ onChange,
13625
+ fromNumber = false
13565
13626
  }) {
13566
13627
  const [open, setOpen] = React.useState(false);
13567
13628
  const [query, setQuery] = React.useState("");
13568
13629
  const rootRef = React.useRef(null);
13569
13630
  const inputRef = React.useRef(null);
13631
+ React.useEffect(() => {
13632
+ if (fromNumber) setOpen(false);
13633
+ }, [fromNumber]);
13570
13634
  const results = React.useMemo(() => {
13571
13635
  const q = query.trim().toLowerCase().replace(/^\+/, "");
13572
13636
  if (!q) return COUNTRIES;
@@ -13595,6 +13659,12 @@ function CountryPicker({
13595
13659
  else setQuery("");
13596
13660
  }, [open]);
13597
13661
  const dial = DIAL_CODES.get(value);
13662
+ if (fromNumber) {
13663
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-cp", ref: rootRef, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-cp-chip", "data-from-number": true, title: "El prefijo viene del n\xFAmero marcado", part: "country-chip", children: [
13664
+ /* @__PURE__ */ jsxRuntime.jsx(CountryMark, { code: value || "+" }),
13665
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: dial ? `Prefijo del n\xFAmero marcado: ${countryName(value)} (+${dial})` : "Prefijo del n\xFAmero marcado" })
13666
+ ] }) });
13667
+ }
13598
13668
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-cp", ref: rootRef, children: [
13599
13669
  /* @__PURE__ */ jsxRuntime.jsxs(
13600
13670
  "button",
@@ -13738,6 +13808,59 @@ var KEYPAD_KEYS = [
13738
13808
  ["#", ""]
13739
13809
  ];
13740
13810
 
13811
+ // src/ui/audio/classic-ring.ts
13812
+ var C6 = 1046.5;
13813
+ var E6 = 1318.51;
13814
+ var HARMONICITY = 4;
13815
+ var INDEX = 1.8;
13816
+ var CLASSIC_CYCLE_SECONDS = 3;
13817
+ var CLASSIC_GAIN = 0.3841;
13818
+ function classicRingGraph(ctx, dest) {
13819
+ const output = ctx.createGain();
13820
+ output.gain.value = CLASSIC_GAIN;
13821
+ const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
13822
+ const f = ctx.createBiquadFilter();
13823
+ f.type = type;
13824
+ f.frequency.value = type === "highpass" ? 250 : 7500;
13825
+ f.Q.value = 0.707;
13826
+ return f;
13827
+ });
13828
+ for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
13829
+ chain[chain.length - 1].connect(output);
13830
+ output.connect(dest);
13831
+ return { input: chain[0], output };
13832
+ }
13833
+ function strike(ctx, input, t, freq, vel, track) {
13834
+ const carrier = ctx.createOscillator();
13835
+ carrier.frequency.value = freq;
13836
+ const modulator = ctx.createOscillator();
13837
+ modulator.frequency.value = freq * HARMONICITY;
13838
+ const depth = ctx.createGain();
13839
+ depth.gain.setValueAtTime(0, t);
13840
+ depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
13841
+ depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
13842
+ modulator.connect(depth);
13843
+ depth.connect(carrier.frequency);
13844
+ const amp = ctx.createGain();
13845
+ amp.gain.setValueAtTime(0, t);
13846
+ amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
13847
+ amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
13848
+ amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
13849
+ carrier.connect(amp);
13850
+ amp.connect(input);
13851
+ for (const o of [carrier, modulator]) {
13852
+ o.start(t);
13853
+ o.stop(t + 0.7);
13854
+ track(o);
13855
+ }
13856
+ }
13857
+ function scheduleClassicCycle(ctx, input, t0, track) {
13858
+ for (const ring of [0, 0.6]) {
13859
+ for (let i = 0; i < 8; i++) strike(ctx, input, t0 + ring + i * 0.05, i % 2 ? E6 : C6, 0.55 + 0.1 * (i % 2), track);
13860
+ }
13861
+ return CLASSIC_CYCLE_SECONDS;
13862
+ }
13863
+
13741
13864
  // src/ui/audio/audio.ts
13742
13865
  var TONE_HZ = 425;
13743
13866
  function createRemoteAudio() {
@@ -13780,30 +13903,159 @@ function primeAudio() {
13780
13903
  if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
13781
13904
  });
13782
13905
  }
13783
- function playRingtone() {
13906
+ function playRingtone(options = {}) {
13784
13907
  const ctx = sharedContext();
13785
13908
  if (!ctx) return { stop() {
13786
13909
  } };
13910
+ const bus = ctx.createGain();
13911
+ const route = routeToDevice(ctx, bus, options.sinkId ?? null);
13912
+ const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
13913
+ return {
13914
+ stop() {
13915
+ voice.stop();
13916
+ route.stop();
13917
+ }
13918
+ };
13919
+ }
13920
+ function fileVoice(ctx, bus, buffer, loop) {
13921
+ const FILE_LEVEL = 1;
13922
+ const RAMP = 0.02;
13923
+ const LEAD = 0.08;
13787
13924
  const gain = ctx.createGain();
13788
13925
  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) => {
13926
+ gain.connect(bus);
13927
+ const src = ctx.createBufferSource();
13928
+ src.buffer = buffer;
13929
+ src.loop = true;
13930
+ const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
13931
+ if (points) {
13932
+ src.loopStart = points.start;
13933
+ src.loopEnd = points.end;
13934
+ }
13935
+ src.connect(gain);
13936
+ let stopped = false;
13937
+ void Promise.resolve(ctx.resume?.()).catch(() => {
13938
+ }).finally(() => {
13939
+ if (stopped) return;
13940
+ const at = ctx.currentTime + LEAD;
13798
13941
  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);
13942
+ gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
13943
+ src.start(at, points?.start ?? 0);
13944
+ });
13945
+ return {
13946
+ stop() {
13947
+ stopped = true;
13948
+ try {
13949
+ src.stop();
13950
+ } catch {
13951
+ }
13952
+ try {
13953
+ gain.disconnect();
13954
+ } catch {
13955
+ }
13956
+ }
13802
13957
  };
13803
- const scheduleCycle = (at) => {
13804
- burst(at, 0.4);
13805
- burst(at + 0.6, 0.4);
13806
- return 3;
13958
+ }
13959
+ function routeToDevice(ctx, bus, sinkId) {
13960
+ const toDefault = () => {
13961
+ bus.connect(ctx.destination);
13962
+ };
13963
+ const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
13964
+ const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
13965
+ const canRoute = el && dest && typeof el.setSinkId === "function";
13966
+ if (!canRoute || !sinkId) {
13967
+ toDefault();
13968
+ return { stop: () => safeDisconnect(bus) };
13969
+ }
13970
+ let stopped = false;
13971
+ el.style.display = "none";
13972
+ el.srcObject = dest.stream;
13973
+ document.body.appendChild(el);
13974
+ bus.connect(dest);
13975
+ void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
13976
+ if (stopped) return;
13977
+ console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
13978
+ safeDisconnect(bus);
13979
+ teardown(el);
13980
+ toDefault();
13981
+ });
13982
+ return {
13983
+ stop() {
13984
+ stopped = true;
13985
+ safeDisconnect(bus);
13986
+ teardown(el);
13987
+ }
13988
+ };
13989
+ }
13990
+ function safeDisconnect(node) {
13991
+ try {
13992
+ node.disconnect();
13993
+ } catch {
13994
+ }
13995
+ }
13996
+ function teardown(el) {
13997
+ el.pause();
13998
+ el.srcObject = null;
13999
+ el.remove();
14000
+ }
14001
+ var TEST_TONE_MS = 900;
14002
+ function playTestTone(sinkId) {
14003
+ const ctx = sharedContext();
14004
+ if (!ctx) return { stop() {
14005
+ } };
14006
+ const bus = ctx.createGain();
14007
+ const route = routeToDevice(ctx, bus, sinkId);
14008
+ const LEVEL = 0.5;
14009
+ const NOTES = [
14010
+ { hz: 660, at: 0.05 },
14011
+ { hz: 880, at: 0.4 }
14012
+ ];
14013
+ const oscs = [];
14014
+ let stopped = false;
14015
+ let timer = null;
14016
+ const stop = () => {
14017
+ if (stopped) return;
14018
+ stopped = true;
14019
+ if (timer) clearTimeout(timer);
14020
+ for (const o of oscs) {
14021
+ try {
14022
+ o.stop();
14023
+ } catch {
14024
+ }
14025
+ }
14026
+ route.stop();
14027
+ };
14028
+ void Promise.resolve(ctx.resume?.()).catch(() => {
14029
+ }).finally(() => {
14030
+ if (stopped) return;
14031
+ const t0 = ctx.currentTime;
14032
+ for (const { hz, at } of NOTES) {
14033
+ const gain = ctx.createGain();
14034
+ gain.gain.value = 0;
14035
+ gain.connect(bus);
14036
+ const osc = ctx.createOscillator();
14037
+ osc.type = "sine";
14038
+ osc.frequency.value = hz;
14039
+ osc.connect(gain);
14040
+ const start = t0 + at;
14041
+ gain.gain.setValueAtTime(0, start);
14042
+ gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
14043
+ gain.gain.setValueAtTime(LEVEL, start + 0.25);
14044
+ gain.gain.linearRampToValueAtTime(0, start + 0.32);
14045
+ osc.start(start);
14046
+ osc.stop(start + 0.34);
14047
+ oscs.push(osc);
14048
+ }
14049
+ timer = setTimeout(stop, TEST_TONE_MS);
14050
+ });
14051
+ return { stop };
14052
+ }
14053
+ function synthesisedVoice(ctx, bus) {
14054
+ const { input, output } = classicRingGraph(ctx, bus);
14055
+ const scheduled = /* @__PURE__ */ new Set();
14056
+ const track = (o) => {
14057
+ scheduled.add(o);
14058
+ o.onended = () => scheduled.delete(o);
13807
14059
  };
13808
14060
  const LOOKAHEAD = 0.5;
13809
14061
  const TICK_MS = 150;
@@ -13812,7 +14064,7 @@ function playRingtone() {
13812
14064
  let timer = null;
13813
14065
  let nextAt = 0;
13814
14066
  const pump = () => {
13815
- while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
14067
+ while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
13816
14068
  };
13817
14069
  void Promise.resolve(ctx.resume?.()).catch(() => {
13818
14070
  }).finally(() => {
@@ -13825,12 +14077,15 @@ function playRingtone() {
13825
14077
  stop() {
13826
14078
  stopped = true;
13827
14079
  if (timer) clearInterval(timer);
13828
- try {
13829
- osc.stop();
13830
- } catch {
14080
+ for (const o of scheduled) {
14081
+ try {
14082
+ o.stop();
14083
+ } catch {
14084
+ }
13831
14085
  }
14086
+ scheduled.clear();
13832
14087
  try {
13833
- gain.disconnect();
14088
+ output.disconnect();
13834
14089
  } catch {
13835
14090
  }
13836
14091
  }
@@ -13976,18 +14231,384 @@ function playRingback() {
13976
14231
  }
13977
14232
  };
13978
14233
  }
14234
+
14235
+ // src/core/outbound-numbers.ts
14236
+ function isOutboundNumberRefused(value) {
14237
+ const refusal = value?.refusal;
14238
+ return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
14239
+ }
14240
+ var stores = /* @__PURE__ */ new WeakMap();
14241
+ function outboundNumberStore(softphone) {
14242
+ const source = softphone.sources?.outboundNumbers;
14243
+ if (!source) return null;
14244
+ let store = stores.get(softphone);
14245
+ if (!store) {
14246
+ store = createOutboundNumberStore(source);
14247
+ stores.set(softphone, store);
14248
+ }
14249
+ return store;
14250
+ }
14251
+ function createOutboundNumberStore(source) {
14252
+ let settings = null;
14253
+ let inFlight = null;
14254
+ const listeners = /* @__PURE__ */ new Set();
14255
+ const apply = (next) => {
14256
+ settings = next;
14257
+ for (const l of listeners) l();
14258
+ };
14259
+ return {
14260
+ getSettings: () => settings,
14261
+ refresh() {
14262
+ inFlight ??= (async () => {
14263
+ if (!settings && source.cached) {
14264
+ const early = await source.cached().catch(() => null);
14265
+ if (early && !settings) apply(early);
14266
+ }
14267
+ try {
14268
+ apply(await source.get());
14269
+ } catch (error) {
14270
+ console.warn("[softphone] outbound numbers could not be loaded; keeping the previous ones", error);
14271
+ } finally {
14272
+ inFlight = null;
14273
+ }
14274
+ })();
14275
+ return inFlight;
14276
+ },
14277
+ async setMine(id) {
14278
+ apply(await source.setMine(id));
14279
+ },
14280
+ subscribe(listener) {
14281
+ listeners.add(listener);
14282
+ return () => listeners.delete(listener);
14283
+ }
14284
+ };
14285
+ }
14286
+ var noSubscribe = () => () => {
14287
+ };
14288
+ var noSettings = () => null;
14289
+ function useOutboundNumbers(store) {
14290
+ const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
14291
+ const [failed, setFailed] = React.useState(false);
14292
+ const load = React.useCallback(() => {
14293
+ if (!store) return () => {
14294
+ };
14295
+ let live = true;
14296
+ setFailed(false);
14297
+ void store.refresh().then(() => {
14298
+ if (live) setFailed(store.getSettings() === null);
14299
+ });
14300
+ return () => {
14301
+ live = false;
14302
+ };
14303
+ }, [store]);
14304
+ React.useEffect(load, [load]);
14305
+ return { settings, failed, retry: load };
14306
+ }
14307
+ function useKnownOutboundNumbers(store) {
14308
+ return React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
14309
+ }
14310
+ function useCountdown(until) {
14311
+ const [, setTick] = React.useState(0);
14312
+ React.useEffect(() => {
14313
+ if (!until) return void 0;
14314
+ const timer = setInterval(() => {
14315
+ setTick((n) => n + 1);
14316
+ if (secondsLeft(until) === 0) clearInterval(timer);
14317
+ }, 1e3);
14318
+ return () => clearInterval(timer);
14319
+ }, [until]);
14320
+ return secondsLeft(until);
14321
+ }
14322
+ function secondsLeft(until) {
14323
+ return until ? Math.max(0, Math.ceil((until - Date.now()) / 1e3)) : 0;
14324
+ }
14325
+ function numberName(n, format = formatIntl) {
14326
+ if (!n) return "N\xFAmero de la organizaci\xF3n";
14327
+ return n.label || format(`+${n.number}`);
14328
+ }
14329
+ function effectiveNumber(settings) {
14330
+ return settings?.numbers.find((n) => n.id === settings.effective);
14331
+ }
14332
+
14333
+ // src/ui/outbound/outbound-change.ts
14334
+ var KIND_TAG = {
14335
+ mobile: { tag: "M\xF3vil", hint: "No usar para atenci\xF3n al cliente ni para llamadas comerciales no solicitadas." },
14336
+ commercial: { tag: "Comercial (400)", hint: "Solo para llamadas comerciales. No recibe llamadas." }
14337
+ };
14338
+ function noticeText(situation, seconds, current) {
14339
+ switch (situation) {
14340
+ case "platform":
14341
+ return `El cambio de n\xFAmero de salida no est\xE1 disponible temporalmente. Vuelve a intentarlo en ${seconds} s. Hasta entonces, tus llamadas seguir\xE1n saliendo desde ${current}.`;
14342
+ case "user":
14343
+ return `Acabas de cambiar el n\xFAmero de salida. Podr\xE1s cambiarlo de nuevo en ${seconds} s. Hasta entonces, tus llamadas saldr\xE1n desde ${current}.`;
14344
+ case "wait":
14345
+ return `Podr\xE1s cambiar el n\xFAmero de salida en ${seconds} s.`;
14346
+ case "pending":
14347
+ return seconds > 0 ? `Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Podr\xE1s volver a elegirlo en ${seconds} s.` : "Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Vuelve a elegirlo para aplicarlo.";
14348
+ }
14349
+ }
14350
+ function useOutboundNumberChange(store, settings, current) {
14351
+ const [busy, setBusy] = React.useState(false);
14352
+ const [notice, setNotice] = React.useState(null);
14353
+ const [waitUntil, setWaitUntil] = React.useState(null);
14354
+ React.useEffect(() => {
14355
+ const s = settings?.changeAvailableInSeconds ?? 0;
14356
+ setWaitUntil(s > 0 ? Date.now() + s * 1e3 : null);
14357
+ }, [settings]);
14358
+ const refusedUntil = notice?.kind === "refused" ? notice.until : null;
14359
+ const blockedFor = useCountdown(refusedUntil ?? waitUntil);
14360
+ React.useEffect(() => {
14361
+ if (notice?.kind === "refused" && blockedFor === 0) setNotice(null);
14362
+ }, [notice, blockedFor]);
14363
+ const locked = busy || blockedFor > 0;
14364
+ const choose = async (id) => {
14365
+ if (!store || !settings || locked) return false;
14366
+ if (id === settings.mine) return true;
14367
+ setBusy(true);
14368
+ setNotice(null);
14369
+ try {
14370
+ await store.setMine(id);
14371
+ return true;
14372
+ } catch (error) {
14373
+ setNotice(
14374
+ isOutboundNumberRefused(error) ? { kind: "refused", reason: error.refusal.reason, until: Date.now() + error.refusal.retryAfterSeconds * 1e3 } : { kind: "error", text: errorText(error) }
14375
+ );
14376
+ return false;
14377
+ } finally {
14378
+ setBusy(false);
14379
+ }
14380
+ };
14381
+ return {
14382
+ busy,
14383
+ locked,
14384
+ choose,
14385
+ /** What to say about the choice right now, if anything. */
14386
+ message: settings ? currentMessage(notice, settings.state, blockedFor, current) : null,
14387
+ /** The message is an error (the change failed), not information (wait, pending). */
14388
+ isError: notice?.kind === "error"
14389
+ };
14390
+ }
14391
+ function currentMessage(notice, state, blockedFor, current) {
14392
+ if (notice?.kind === "refused") return blockedFor > 0 ? noticeText(notice.reason, blockedFor, current) : null;
14393
+ if (notice?.kind === "error") return notice.text;
14394
+ if (state === "pending" || state === "failed") return noticeText("pending", blockedFor, current);
14395
+ return blockedFor > 0 ? noticeText("wait", blockedFor, current) : null;
14396
+ }
14397
+ function errorText(error) {
14398
+ const message = error instanceof Error ? error.message : "";
14399
+ return message && !/HTTP \d+/.test(message) ? message : "No se ha podido cambiar el n\xFAmero de salida. Int\xE9ntalo de nuevo.";
14400
+ }
14401
+ var CALLOUT_MS = 6e3;
14402
+ function OutboundNumberPicker({ onBusyChange }) {
14403
+ const softphone = useSoftphone();
14404
+ const store = outboundNumberStore(softphone);
14405
+ const { settings } = useOutboundNumbers(store);
14406
+ const formatIntl2 = useIntlFormatter();
14407
+ const current = settings ? numberName(effectiveNumber(settings), formatIntl2) : "";
14408
+ const { busy, locked, choose: apply, message, isError } = useOutboundNumberChange(store, settings, current);
14409
+ const [open, setOpen] = React.useState(false);
14410
+ const [applying, setApplying] = React.useState(void 0);
14411
+ const [callout, setCallout] = React.useState(false);
14412
+ const rootRef = React.useRef(null);
14413
+ const chipRef = React.useRef(null);
14414
+ const keys = useListKeyboard(open);
14415
+ React.useEffect(() => onBusyChange?.(busy), [busy, onBusyChange]);
14416
+ React.useEffect(() => {
14417
+ if (!message) setCallout(false);
14418
+ }, [message]);
14419
+ React.useEffect(() => {
14420
+ if (!callout) return void 0;
14421
+ const timer = setTimeout(() => setCallout(false), CALLOUT_MS);
14422
+ return () => clearTimeout(timer);
14423
+ }, [callout]);
14424
+ const floating = open || callout;
14425
+ React.useEffect(() => {
14426
+ if (!floating) return;
14427
+ const close = () => {
14428
+ setOpen(false);
14429
+ setCallout(false);
14430
+ };
14431
+ const root = rootRef.current?.getRootNode() ?? document;
14432
+ const onDown = (e) => {
14433
+ if (!rootRef.current?.contains(e.target)) close();
14434
+ };
14435
+ const onKey = (e) => {
14436
+ if (e.key !== "Escape") return;
14437
+ if (rootRef.current?.contains(e.target)) chipRef.current?.focus();
14438
+ close();
14439
+ };
14440
+ root.addEventListener("pointerdown", onDown);
14441
+ document.addEventListener("keydown", onKey);
14442
+ return () => {
14443
+ root.removeEventListener("pointerdown", onDown);
14444
+ document.removeEventListener("keydown", onKey);
14445
+ };
14446
+ }, [floating]);
14447
+ if (!store || !settings || settings.numbers.length < 2) return null;
14448
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
14449
+ const applyingNumber = applying === null ? orgDefault : settings.numbers.find((n) => n.id === applying);
14450
+ const shown = applying === void 0 ? current : numberName(applyingNumber, formatIntl2);
14451
+ const choose = async (id) => {
14452
+ if (locked) return;
14453
+ setOpen(false);
14454
+ chipRef.current?.focus();
14455
+ if (id === settings.mine) return;
14456
+ setCallout(false);
14457
+ setApplying(id);
14458
+ const applied = await apply(id);
14459
+ setApplying(void 0);
14460
+ if (!applied) setCallout(true);
14461
+ };
14462
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-on", ref: rootRef, children: [
14463
+ /* @__PURE__ */ jsxRuntime.jsxs(
14464
+ "button",
14465
+ {
14466
+ ref: chipRef,
14467
+ type: "button",
14468
+ className: "cv-on-chip",
14469
+ onClick: () => {
14470
+ setCallout(false);
14471
+ setOpen((o) => !o);
14472
+ },
14473
+ "aria-haspopup": "listbox",
14474
+ "aria-expanded": open,
14475
+ "aria-label": `Llamar desde: ${shown}${busy ? " (aplicando)" : ""}. Cambiar n\xFAmero de salida`,
14476
+ "aria-busy": busy,
14477
+ part: "outbound-number-chip",
14478
+ children: [
14479
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-label", children: "Llamar desde:" }),
14480
+ busy ? /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { size: 13, className: "cv-on-icon cv-on-spin", "aria-hidden": true }) : null,
14481
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-current", children: shown }),
14482
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, { size: 13, className: "cv-cp-caret", "data-open": open, "aria-hidden": true })
14483
+ ]
14484
+ }
14485
+ ),
14486
+ open ? /* @__PURE__ */ jsxRuntime.jsxs(
14487
+ "div",
14488
+ {
14489
+ className: "cv-cp-pop cv-on-pop",
14490
+ role: "dialog",
14491
+ "aria-label": "Llamar desde",
14492
+ part: "outbound-number-list",
14493
+ onBlur: (e) => {
14494
+ if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
14495
+ },
14496
+ children: [
14497
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-title", children: "Llamar desde" }),
14498
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-cp-list", role: "listbox", "aria-disabled": locked, ref: keys.listRef, onKeyDown: keys.onKeyDown, children: [
14499
+ /* @__PURE__ */ jsxRuntime.jsx(
14500
+ OptionRow,
14501
+ {
14502
+ selected: settings.mine === null,
14503
+ disabled: locked,
14504
+ title: "Por defecto de la organizaci\xF3n",
14505
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
14506
+ onSelect: () => void choose(null)
14507
+ }
14508
+ ),
14509
+ settings.numbers.map((n) => /* @__PURE__ */ jsxRuntime.jsx(
14510
+ OptionRow,
14511
+ {
14512
+ selected: settings.mine === n.id,
14513
+ disabled: locked,
14514
+ title: numberName(n, formatIntl2),
14515
+ sub: n.label ? formatIntl2(`+${n.number}`) : null,
14516
+ tag: KIND_TAG[n.kind],
14517
+ onSelect: () => void choose(n.id)
14518
+ },
14519
+ n.id
14520
+ ))
14521
+ ] }),
14522
+ message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-notice", role: "status", "data-kind": isError ? "error" : "info", children: message }) : null
14523
+ ]
14524
+ }
14525
+ ) : null,
14526
+ callout && !open && message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-on-callout", role: "status", "data-kind": isError ? "error" : "info", part: "outbound-number-notice", children: message }) : null
14527
+ ] });
14528
+ }
14529
+ function OptionRow({
14530
+ selected,
14531
+ disabled,
14532
+ title,
14533
+ sub,
14534
+ tag,
14535
+ onSelect
14536
+ }) {
14537
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14538
+ "button",
14539
+ {
14540
+ type: "button",
14541
+ role: "option",
14542
+ "aria-selected": selected,
14543
+ "aria-disabled": disabled,
14544
+ className: "cv-cp-item cv-on-item",
14545
+ tabIndex: -1,
14546
+ onClick: onSelect,
14547
+ children: [
14548
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-on-text", children: [
14549
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-on-line", children: [
14550
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-cp-name", children: title }),
14551
+ tag ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-tag", children: tag.tag }) : null
14552
+ ] }),
14553
+ sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-sub", children: sub }) : null,
14554
+ tag ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-on-hint", children: tag.hint }) : null
14555
+ ] }),
14556
+ /* @__PURE__ */ jsxRuntime.jsx(Check, { size: 14, className: "cv-cp-check", "data-on": selected, "aria-hidden": true })
14557
+ ]
14558
+ }
14559
+ );
14560
+ }
14561
+ var optionsOf = (list) => list ? [...list.querySelectorAll('[role="option"]')] : [];
14562
+ var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
14563
+ var TYPEAHEAD_MS = 700;
14564
+ function useListKeyboard(open) {
14565
+ const listRef = React.useRef(null);
14566
+ const typed = React.useRef({ text: "", at: 0 });
14567
+ React.useEffect(() => {
14568
+ if (!open) return;
14569
+ const options = optionsOf(listRef.current);
14570
+ (options.find((o) => o.getAttribute("aria-selected") === "true") ?? options[0])?.focus();
14571
+ }, [open]);
14572
+ const onKeyDown = (e) => {
14573
+ const options = optionsOf(listRef.current);
14574
+ const at = options.indexOf(e.target);
14575
+ const move = {
14576
+ ArrowDown: Math.min(at + 1, options.length - 1),
14577
+ ArrowUp: Math.max(at - 1, 0),
14578
+ Home: 0,
14579
+ End: options.length - 1
14580
+ };
14581
+ let next = move[e.key];
14582
+ if (next === void 0 && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
14583
+ const now = Date.now();
14584
+ const text = now - typed.current.at < TYPEAHEAD_MS ? typed.current.text + e.key : e.key;
14585
+ typed.current = { text, at: now };
14586
+ const wanted = fold(text);
14587
+ const match = options.findIndex((o) => fold(o.querySelector(".cv-cp-name")?.textContent ?? "").startsWith(wanted));
14588
+ next = match >= 0 ? match : void 0;
14589
+ }
14590
+ if (next === void 0) return;
14591
+ e.preventDefault();
14592
+ options[next]?.focus();
14593
+ };
14594
+ return { listRef, onKeyDown };
14595
+ }
13979
14596
  var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
14597
+ var DISPLAY_SIZES = ["1.375rem", "1.25rem", "1.125rem", "1rem"];
13980
14598
  function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
13981
14599
  const softphone = useSoftphone();
13982
14600
  const state = useSoftphoneState();
13983
14601
  const [value, setValue] = React.useState("");
13984
14602
  const [country2, setCountry] = React.useState(DEFAULT_COUNTRY);
14603
+ const [changingNumber, setChangingNumber] = React.useState(false);
13985
14604
  const format = useNumberFormatter();
13986
14605
  const inputRef = React.useRef(null);
13987
14606
  const caretRef = React.useRef(null);
13988
14607
  const display = format(country2, value);
13989
14608
  const target = toE164(country2, value);
13990
- const canCall = state === "online" && target.length > 1;
14609
+ const canCall = state === "online" && target.length > 1 && !changingNumber;
14610
+ const typedInternational = value.trim().startsWith("+");
14611
+ const numberRegion = regionOfInternational(value);
13991
14612
  React.useLayoutEffect(() => {
13992
14613
  const caret = caretRef.current;
13993
14614
  if (caret === null || !inputRef.current) return;
@@ -13995,6 +14616,14 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
13995
14616
  const pos = caretAfterDigits(display, caret);
13996
14617
  inputRef.current.setSelectionRange(pos, pos);
13997
14618
  }, [display]);
14619
+ React.useLayoutEffect(() => {
14620
+ const input = inputRef.current;
14621
+ if (!input) return;
14622
+ for (const size of DISPLAY_SIZES) {
14623
+ input.style.fontSize = size;
14624
+ if (input.scrollWidth <= input.clientWidth) break;
14625
+ }
14626
+ }, [display]);
13998
14627
  const append = (key) => {
13999
14628
  onIntent?.();
14000
14629
  setValue((v) => v + key);
@@ -14010,9 +14639,16 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14010
14639
  void softphone.call(target);
14011
14640
  };
14012
14641
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
14013
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ jsxRuntime.jsx(CountryPicker, { value: country2, onChange: setCountry }) }),
14642
+ /* @__PURE__ */ jsxRuntime.jsx(OutboundNumberPicker, { onBusyChange: setChangingNumber }),
14014
14643
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-dial-head", children: [
14015
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-dial-side", "aria-hidden": true }),
14644
+ /* @__PURE__ */ jsxRuntime.jsx(
14645
+ CountryPicker,
14646
+ {
14647
+ value: numberRegion ?? country2,
14648
+ onChange: setCountry,
14649
+ fromNumber: typedInternational
14650
+ }
14651
+ ),
14016
14652
  /* @__PURE__ */ jsxRuntime.jsx(
14017
14653
  "input",
14018
14654
  {
@@ -14036,6 +14672,8 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
14036
14672
  type: "button",
14037
14673
  className: "cv-dial-side",
14038
14674
  "data-hidden": value.length === 0,
14675
+ tabIndex: value.length === 0 ? -1 : void 0,
14676
+ "aria-hidden": value.length === 0 || void 0,
14039
14677
  onClick: backspace,
14040
14678
  "aria-label": "Borrar",
14041
14679
  children: /* @__PURE__ */ jsxRuntime.jsx(Delete, { size: 19 })
@@ -14460,6 +15098,9 @@ function CallScreen({
14460
15098
  }) {
14461
15099
  const view = useCall(call);
14462
15100
  const softphone = useSoftphone();
15101
+ const outbound = useOutboundNumbers(call.direction === "outbound" ? outboundNumberStore(softphone) : null).settings;
15102
+ const formatIntl2 = useIntlFormatter();
15103
+ const fromName = outbound && outbound.numbers.length > 1 ? numberName(effectiveNumber(outbound), formatIntl2) : null;
14463
15104
  const [panel, setPanel] = React.useState(null);
14464
15105
  const showTransfer = panel === "transfer";
14465
15106
  const showKeypad = panel === "keypad";
@@ -14670,6 +15311,13 @@ function CallScreen({
14670
15311
  ] }),
14671
15312
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-name", children: name }),
14672
15313
  showNumber ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-number", children: number }) : null,
15314
+ fromName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-from", children: [
15315
+ /* @__PURE__ */ jsxRuntime.jsx(PhoneOutgoing, { size: 11, "aria-hidden": true }),
15316
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
15317
+ "Desde ",
15318
+ fromName
15319
+ ] })
15320
+ ] }) : null,
14673
15321
  s === "active" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
14674
15322
  /* @__PURE__ */ jsxRuntime.jsx("b", { children: mm }),
14675
15323
  /* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
@@ -15085,12 +15733,23 @@ function HandlerChain({ handlers }) {
15085
15733
  ] }, `${h.extension}-${i}`);
15086
15734
  }) });
15087
15735
  }
15736
+ function outboundFrom(entry, known, formatIntl2) {
15737
+ if (entry.direction !== "outbound" || !entry.from) return null;
15738
+ const number = formatIntl2(`+${entry.from}`);
15739
+ const label = known?.numbers.find((n) => n.number === entry.from)?.label;
15740
+ if (!label) return number;
15741
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15742
+ label,
15743
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-detail-row-sub", children: number })
15744
+ ] });
15745
+ }
15088
15746
  function HistoryDetail({
15089
15747
  entry,
15090
15748
  onBack
15091
15749
  }) {
15092
15750
  const softphone = useSoftphone();
15093
15751
  const formatIntl2 = useIntlFormatter();
15752
+ const knownNumbers = useKnownOutboundNumbers(outboundNumberStore(softphone));
15094
15753
  const [detail, setDetail] = React.useState(null);
15095
15754
  const [loadingDetail, setLoadingDetail] = React.useState(false);
15096
15755
  React.useEffect(() => {
@@ -15106,6 +15765,7 @@ function HistoryDetail({
15106
15765
  }, [entry]);
15107
15766
  const formatted = formatIntl2(entry.number);
15108
15767
  const name = entry.name ?? formatted;
15768
+ const from = outboundFrom(entry, knownNumbers, formatIntl2);
15109
15769
  const handledBy = detail?.handledBy ?? entry.handledBy;
15110
15770
  const recordingUrls = detail?.recordingUrls ?? [];
15111
15771
  const loadingRecording = entry.recorded && loadingDetail && recordingUrls.length === 0;
@@ -15142,7 +15802,8 @@ function HistoryDetail({
15142
15802
  entry.durationSec > 0 ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
15143
15803
  /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
15144
15804
  handledBy.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ jsxRuntime.jsx(HandlerChain, { handlers: handledBy }) }) : null,
15145
- entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null
15805
+ entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null,
15806
+ from ? /* @__PURE__ */ jsxRuntime.jsx(DetailRow, { icon: PhoneOutgoing, eyebrow: "Llamada desde", text: from }) : null
15146
15807
  ] })
15147
15808
  ] }),
15148
15809
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-detail-foot", children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -15335,6 +15996,110 @@ function HistoryTab({ source }) {
15335
15996
  selected ? /* @__PURE__ */ jsxRuntime.jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
15336
15997
  ] });
15337
15998
  }
15999
+
16000
+ // src/ui/audio/ringtone-store.ts
16001
+ var stores2 = /* @__PURE__ */ new WeakMap();
16002
+ function ringtoneStore(softphone) {
16003
+ const source = softphone.sources?.ringtones;
16004
+ if (!source) return null;
16005
+ let store = stores2.get(softphone);
16006
+ if (!store) {
16007
+ store = createRingtoneStore(source);
16008
+ stores2.set(softphone, store);
16009
+ }
16010
+ return store;
16011
+ }
16012
+ function createRingtoneStore(source, decodeImpl = decodeRingtone) {
16013
+ let settings = null;
16014
+ let audioWanted = false;
16015
+ let loaded = null;
16016
+ let loading2 = null;
16017
+ let inFlight = null;
16018
+ const listeners = /* @__PURE__ */ new Set();
16019
+ const notify = () => {
16020
+ for (const l of listeners) l();
16021
+ };
16022
+ const effectiveUrl = () => {
16023
+ if (!settings) return null;
16024
+ return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
16025
+ };
16026
+ const loadEffective = async () => {
16027
+ const url = effectiveUrl();
16028
+ if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
16029
+ loading2 = url;
16030
+ try {
16031
+ const buffer = await decodeImpl(url);
16032
+ if (audioWanted && effectiveUrl() === url) {
16033
+ loaded = { url, buffer };
16034
+ notify();
16035
+ }
16036
+ } catch (error) {
16037
+ console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
16038
+ } finally {
16039
+ if (loading2 === url) loading2 = null;
16040
+ }
16041
+ };
16042
+ const apply = (next) => {
16043
+ settings = next;
16044
+ if (loaded && loaded.url !== effectiveUrl()) loaded = null;
16045
+ notify();
16046
+ };
16047
+ return {
16048
+ getSettings: () => settings,
16049
+ readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
16050
+ readyLoop: () => {
16051
+ if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
16052
+ return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
16053
+ },
16054
+ refresh() {
16055
+ inFlight ??= (async () => {
16056
+ if (!settings && source.cached) {
16057
+ const early = await source.cached().catch(() => null);
16058
+ if (early && !settings) {
16059
+ apply(early);
16060
+ void loadEffective();
16061
+ }
16062
+ }
16063
+ try {
16064
+ apply(await source.get());
16065
+ } catch (error) {
16066
+ console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
16067
+ } finally {
16068
+ inFlight = null;
16069
+ }
16070
+ await loadEffective();
16071
+ })();
16072
+ return inFlight;
16073
+ },
16074
+ async setMine(id) {
16075
+ apply(await source.setMine(id));
16076
+ void loadEffective();
16077
+ },
16078
+ wantAudio(on) {
16079
+ if (audioWanted === on) return;
16080
+ audioWanted = on;
16081
+ if (!on) {
16082
+ loaded = null;
16083
+ notify();
16084
+ } else {
16085
+ void loadEffective();
16086
+ }
16087
+ },
16088
+ decode: (url) => decodeImpl(url),
16089
+ subscribe(listener) {
16090
+ listeners.add(listener);
16091
+ return () => listeners.delete(listener);
16092
+ }
16093
+ };
16094
+ }
16095
+ async function decodeRingtone(url) {
16096
+ const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
16097
+ if (!Offline) throw new Error("this browser cannot decode audio files");
16098
+ const res = await fetch(url, { credentials: "omit" });
16099
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
16100
+ const bytes = await res.arrayBuffer();
16101
+ return new Offline(1, 1, 48e3).decodeAudioData(bytes);
16102
+ }
15338
16103
  function SettingsSection({
15339
16104
  title,
15340
16105
  children
@@ -15344,11 +16109,641 @@ function SettingsSection({
15344
16109
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children })
15345
16110
  ] });
15346
16111
  }
16112
+ function SettingsNavRow({
16113
+ icon: Icon3,
16114
+ title,
16115
+ value,
16116
+ onOpen,
16117
+ rowRef
16118
+ }) {
16119
+ const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
16120
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 16 }) }),
16121
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-settings-nav-text", children: [
16122
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-title", children: title }),
16123
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-value", children: value })
16124
+ ] }),
16125
+ onOpen ? /* @__PURE__ */ jsxRuntime.jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
16126
+ ] });
16127
+ return onOpen ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
16128
+ }
16129
+ function ChoiceList({
16130
+ label,
16131
+ choices,
16132
+ value,
16133
+ onSelect,
16134
+ onReselect,
16135
+ disabled = false
16136
+ }) {
16137
+ const name = React.useId();
16138
+ const pointer = React.useRef(false);
16139
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, "aria-disabled": disabled || void 0, children: choices.map((c) => {
16140
+ const checked = c.value === value;
16141
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16142
+ "label",
16143
+ {
16144
+ className: "cv-choice",
16145
+ "data-checked": checked || void 0,
16146
+ "data-disabled": disabled || void 0,
16147
+ onPointerDown: () => {
16148
+ pointer.current = true;
16149
+ },
16150
+ children: [
16151
+ /* @__PURE__ */ jsxRuntime.jsx(
16152
+ "input",
16153
+ {
16154
+ type: "radio",
16155
+ className: "cv-choice-input",
16156
+ name,
16157
+ value: c.value,
16158
+ checked,
16159
+ onChange: () => {
16160
+ const via = pointer.current ? "pointer" : "keyboard";
16161
+ pointer.current = false;
16162
+ if (!disabled) onSelect(c.value, via);
16163
+ },
16164
+ onClick: () => {
16165
+ if (c.value === value) {
16166
+ pointer.current = false;
16167
+ if (!disabled) onReselect?.(c.value);
16168
+ }
16169
+ }
16170
+ }
16171
+ ),
16172
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-text", children: [
16173
+ c.tag ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-line", children: [
16174
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
16175
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-tag", children: c.tag })
16176
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
16177
+ c.sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-sub", children: c.sub }) : null,
16178
+ c.hint ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-hint", children: c.hint }) : null
16179
+ ] }),
16180
+ c.trailing,
16181
+ /* @__PURE__ */ jsxRuntime.jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
16182
+ ]
16183
+ },
16184
+ c.value
16185
+ );
16186
+ }) });
16187
+ }
16188
+ function SettingsPage({
16189
+ title,
16190
+ intro,
16191
+ onBack,
16192
+ children
16193
+ }) {
16194
+ const titleId = React.useId();
16195
+ const heading = React.useRef(null);
16196
+ React.useEffect(() => heading.current?.focus(), []);
16197
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16198
+ "div",
16199
+ {
16200
+ className: "cv-detail cv-settings-page",
16201
+ role: "region",
16202
+ "aria-labelledby": titleId,
16203
+ onKeyDown: (e) => {
16204
+ if (e.key === "Escape") {
16205
+ e.stopPropagation();
16206
+ onBack();
16207
+ }
16208
+ },
16209
+ children: [
16210
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
16211
+ /* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
16212
+ "Ajustes"
16213
+ ] }) }),
16214
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-page-body", children: [
16215
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
16216
+ intro ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
16217
+ children
16218
+ ] })
16219
+ ]
16220
+ }
16221
+ );
16222
+ }
16223
+ var SYSTEM_DEFAULT = "";
16224
+ var PAGES = {
16225
+ input: {
16226
+ title: "Micr\xF3fono",
16227
+ intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
16228
+ empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
16229
+ },
16230
+ output: {
16231
+ title: "Altavoces",
16232
+ intro: "D\xF3nde oyes a la otra persona durante una llamada.",
16233
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
16234
+ },
16235
+ ringtone: {
16236
+ title: "Salida del tono",
16237
+ intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
16238
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
16239
+ }
16240
+ };
16241
+ function deviceSummary(kind, list, id) {
16242
+ const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
16243
+ return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
16244
+ }
16245
+ function DevicePage({ kind, onBack }) {
16246
+ const devices = useDevices();
16247
+ const page = PAGES[kind];
16248
+ const list = kind === "input" ? devices.inputs : devices.outputs;
16249
+ const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
16250
+ const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
16251
+ const choices = [
16252
+ { value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
16253
+ ...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
16254
+ ];
16255
+ return /* @__PURE__ */ jsxRuntime.jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
16256
+ list.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
16257
+ ChoiceList,
16258
+ {
16259
+ label: page.title,
16260
+ choices,
16261
+ value: current ?? SYSTEM_DEFAULT,
16262
+ onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
16263
+ }
16264
+ ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
16265
+ kind === "output" ? /* @__PURE__ */ jsxRuntime.jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
16266
+ ] });
16267
+ }
16268
+ function SpeakerTest({ sinkId }) {
16269
+ const [testing, setTesting] = React.useState(false);
16270
+ const tone = React.useRef(null);
16271
+ const timer = React.useRef(null);
16272
+ React.useEffect(
16273
+ () => () => {
16274
+ tone.current?.stop();
16275
+ if (timer.current) clearTimeout(timer.current);
16276
+ },
16277
+ []
16278
+ );
16279
+ const test = () => {
16280
+ primeAudio();
16281
+ tone.current?.stop();
16282
+ tone.current = playTestTone(sinkId);
16283
+ setTesting(true);
16284
+ if (timer.current) clearTimeout(timer.current);
16285
+ timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
16286
+ };
16287
+ return /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
16288
+ /* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 16, "aria-hidden": true }),
16289
+ testing ? "Sonando\u2026" : "Probar altavoz"
16290
+ ] });
16291
+ }
16292
+
16293
+ // src/ui/audio/preview.ts
16294
+ var PREVIEW_MAX_MS = 1e4;
16295
+ var BUILT_IN_PREVIEW_MS = 6e3;
16296
+ function previewDurationMs(buffer, loop) {
16297
+ if (!buffer) return BUILT_IN_PREVIEW_MS;
16298
+ const seconds = loop ? loop.end - loop.start : buffer.duration;
16299
+ return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
16300
+ }
16301
+ function previewRingtone(ringtone, options = {}) {
16302
+ primeAudio();
16303
+ let tone = null;
16304
+ let timer = null;
16305
+ let over = false;
16306
+ let settle;
16307
+ let fail;
16308
+ const done = new Promise((resolve, reject) => {
16309
+ settle = resolve;
16310
+ fail = reject;
16311
+ });
16312
+ done.catch(() => {
16313
+ });
16314
+ const stop = () => {
16315
+ if (over) return;
16316
+ over = true;
16317
+ if (timer) clearTimeout(timer);
16318
+ tone?.stop();
16319
+ settle();
16320
+ };
16321
+ const play = (buffer) => {
16322
+ if (over) return;
16323
+ const loop = buffer ? ringtone.loop ?? null : null;
16324
+ tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
16325
+ timer = setTimeout(stop, previewDurationMs(buffer, loop));
16326
+ };
16327
+ if (!ringtone.url) {
16328
+ play(null);
16329
+ } else {
16330
+ decodeRingtone(ringtone.url).then(play, (error) => {
16331
+ if (over) return;
16332
+ over = true;
16333
+ fail(error);
16334
+ });
16335
+ }
16336
+ return { stop, done };
16337
+ }
16338
+
16339
+ // src/ui/views/settings/ringtone-state.ts
16340
+ var noSubscribe2 = () => () => {
16341
+ };
16342
+ var noSettings2 = () => null;
16343
+ function useRingtoneSettings(store) {
16344
+ const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe2, store?.getSettings ?? noSettings2);
16345
+ const [failed, setFailed] = React.useState(false);
16346
+ const load = React.useCallback(() => {
16347
+ if (!store) return () => {
16348
+ };
16349
+ let live = true;
16350
+ setFailed(false);
16351
+ void store.refresh().then(() => {
16352
+ if (live) setFailed(store.getSettings() === null);
16353
+ });
16354
+ return () => {
16355
+ live = false;
16356
+ };
16357
+ }, [store]);
16358
+ React.useEffect(load, [load]);
16359
+ return { settings, failed, retry: load };
16360
+ }
16361
+ function ringtoneSummary(settings, failed) {
16362
+ if (!settings) return failed ? "No disponible" : "Cargando\u2026";
16363
+ const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
16364
+ const mine = byId(settings.mine);
16365
+ if (mine) return mine.name;
16366
+ const org = byId(settings.orgDefault);
16367
+ if (org) return `${org.name} \xB7 organizaci\xF3n`;
16368
+ return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
16369
+ }
16370
+ function useRingtonePreview(store, sinkId) {
16371
+ const [playing, setPlaying] = React.useState(null);
16372
+ const [error, setError] = React.useState(null);
16373
+ const current = React.useRef(null);
16374
+ const generation = React.useRef(0);
16375
+ const stop = React.useCallback(() => {
16376
+ generation.current += 1;
16377
+ const c = current.current;
16378
+ current.current = null;
16379
+ if (c?.timer) clearTimeout(c.timer);
16380
+ c?.tone?.stop();
16381
+ setPlaying(null);
16382
+ }, []);
16383
+ React.useEffect(() => stop, [stop]);
16384
+ const start = (settings, choice) => {
16385
+ stop();
16386
+ primeAudio();
16387
+ setError(null);
16388
+ const gen = generation.current;
16389
+ const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
16390
+ const id = choice || orgDefault;
16391
+ const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
16392
+ setPlaying(choice);
16393
+ current.current = { tone: null, timer: null };
16394
+ const play = (buffer) => {
16395
+ if (gen !== generation.current) return;
16396
+ const loop = buffer ? option?.loop ?? null : null;
16397
+ const tone = playRingtone({ buffer, loop, sinkId });
16398
+ current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
16399
+ };
16400
+ if (!option?.url) {
16401
+ play(null);
16402
+ return;
16403
+ }
16404
+ store.decode(option.url).then(play, () => {
16405
+ if (gen !== generation.current) return;
16406
+ stop();
16407
+ setError("No se pudo reproducir el tono.");
16408
+ });
16409
+ };
16410
+ return { playing, error, start, stop };
16411
+ }
16412
+ var ORG_DEFAULT = "";
16413
+ var KEYBOARD_SETTLE_MS = 300;
16414
+ function RingtonePage({
16415
+ store,
16416
+ settings,
16417
+ failed,
16418
+ onRetry,
16419
+ onBack
16420
+ }) {
16421
+ const softphone = useSoftphone();
16422
+ const devices = useDevices();
16423
+ const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
16424
+ const [pending, setPending] = React.useState(void 0);
16425
+ const [error, setError] = React.useState(null);
16426
+ const save = useSerialSave(store, {
16427
+ onSettled: () => setPending(void 0),
16428
+ onFailed: () => {
16429
+ setPending(void 0);
16430
+ setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
16431
+ }
16432
+ });
16433
+ const latest = React.useRef(settings);
16434
+ latest.current = settings;
16435
+ const settle = React.useRef(null);
16436
+ const { stop: stopPreview } = preview;
16437
+ React.useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
16438
+ React.useEffect(
16439
+ () => () => {
16440
+ if (settle.current) {
16441
+ clearTimeout(settle.current.timer);
16442
+ save(settle.current.value || null);
16443
+ }
16444
+ },
16445
+ [save]
16446
+ );
16447
+ const commit = (value) => {
16448
+ save(value || null);
16449
+ if (latest.current) preview.start(latest.current, value);
16450
+ };
16451
+ const onSelect = (value, via) => {
16452
+ setError(null);
16453
+ setPending(value);
16454
+ if (settle.current) clearTimeout(settle.current.timer);
16455
+ settle.current = null;
16456
+ if (via === "pointer") {
16457
+ commit(value);
16458
+ return;
16459
+ }
16460
+ preview.stop();
16461
+ settle.current = {
16462
+ value,
16463
+ timer: setTimeout(() => {
16464
+ settle.current = null;
16465
+ commit(value);
16466
+ }, KEYBOARD_SETTLE_MS)
16467
+ };
16468
+ };
16469
+ const onReselect = (value) => {
16470
+ if (!settings) return;
16471
+ if (preview.playing === value) preview.stop();
16472
+ else preview.start(settings, value);
16473
+ };
16474
+ return /* @__PURE__ */ jsxRuntime.jsx(
16475
+ SettingsPage,
16476
+ {
16477
+ title: "Tono de llamada",
16478
+ intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
16479
+ onBack,
16480
+ children: settings ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
16481
+ /* @__PURE__ */ jsxRuntime.jsx(
16482
+ ChoiceList,
16483
+ {
16484
+ label: "Tono de llamada",
16485
+ choices: choicesFor(settings, preview.playing),
16486
+ value: pending !== void 0 ? pending : currentValue(settings),
16487
+ onSelect,
16488
+ onReselect
16489
+ }
16490
+ ),
16491
+ error ?? preview.error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
16492
+ ] }) : failed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", children: [
16493
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
16494
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
16495
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", role: "status", children: [
16496
+ /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
16497
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
16498
+ ] })
16499
+ }
16500
+ );
16501
+ }
16502
+ function currentValue(settings) {
16503
+ return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
16504
+ }
16505
+ function choicesFor(settings, playing) {
16506
+ const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
16507
+ const builtIn = settings.catalogue.find((r) => r.url === null);
16508
+ const mark = (value) => playing === value ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-playing", children: [
16509
+ /* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 15, "aria-hidden": true }),
16510
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: "Sonando" })
16511
+ ] }) : void 0;
16512
+ const orgChoice = {
16513
+ value: ORG_DEFAULT,
16514
+ label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
16515
+ sub: (org ?? builtIn)?.name ?? "",
16516
+ trailing: mark(ORG_DEFAULT)
16517
+ };
16518
+ return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
16519
+ }
16520
+ function useSerialSave(store, handlers) {
16521
+ const wanted = React.useRef(null);
16522
+ const busy = React.useRef(false);
16523
+ const on = React.useRef(handlers);
16524
+ on.current = handlers;
16525
+ const [save] = React.useState(() => (id) => {
16526
+ wanted.current = { id };
16527
+ if (busy.current) return;
16528
+ busy.current = true;
16529
+ void (async () => {
16530
+ while (wanted.current) {
16531
+ const { id: next } = wanted.current;
16532
+ wanted.current = null;
16533
+ try {
16534
+ await store.setMine(next);
16535
+ if (!wanted.current) on.current.onSettled();
16536
+ } catch {
16537
+ if (!wanted.current) on.current.onFailed();
16538
+ }
16539
+ }
16540
+ busy.current = false;
16541
+ })();
16542
+ });
16543
+ return save;
16544
+ }
16545
+ var ORG_DEFAULT2 = "";
16546
+ var KEYBOARD_SETTLE_MS2 = 800;
16547
+ function OutboundNumberPage({
16548
+ store,
16549
+ settings,
16550
+ onBack
16551
+ }) {
16552
+ const formatIntl2 = useIntlFormatter();
16553
+ const current = numberName(effectiveNumber(settings), formatIntl2);
16554
+ const change = useOutboundNumberChange(store, settings, current);
16555
+ const [pending, setPending] = React.useState(void 0);
16556
+ const settle = React.useRef(null);
16557
+ const choose = React.useRef(change.choose);
16558
+ choose.current = change.choose;
16559
+ React.useEffect(
16560
+ () => () => {
16561
+ if (settle.current) clearTimeout(settle.current);
16562
+ },
16563
+ []
16564
+ );
16565
+ const notice = React.useRef(null);
16566
+ const apply = async (value) => {
16567
+ setPending(value);
16568
+ const applied = await choose.current(value || null);
16569
+ setPending(void 0);
16570
+ if (!applied) requestAnimationFrame(() => notice.current?.scrollIntoView?.({ block: "nearest" }));
16571
+ };
16572
+ const onSelect = (value, via) => {
16573
+ if (settle.current) clearTimeout(settle.current);
16574
+ settle.current = null;
16575
+ if (via === "pointer") {
16576
+ void apply(value);
16577
+ return;
16578
+ }
16579
+ setPending(value);
16580
+ settle.current = setTimeout(() => {
16581
+ settle.current = null;
16582
+ void apply(value);
16583
+ }, KEYBOARD_SETTLE_MS2);
16584
+ };
16585
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16586
+ SettingsPage,
16587
+ {
16588
+ title: "N\xFAmero de salida",
16589
+ intro: "El n\xFAmero que ve la persona a la que llamas. Es el mismo en todos tus dispositivos y tambi\xE9n puedes cambiarlo desde el teclado.",
16590
+ onBack,
16591
+ children: [
16592
+ change.message ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-notice", role: "status", "data-kind": change.isError ? "error" : "info", ref: notice, children: change.message }) : null,
16593
+ /* @__PURE__ */ jsxRuntime.jsx(
16594
+ ChoiceList,
16595
+ {
16596
+ label: "N\xFAmero de salida",
16597
+ choices: choicesFor2(settings, formatIntl2, change.busy ? pending : void 0),
16598
+ value: pending ?? currentValue2(settings),
16599
+ onSelect,
16600
+ disabled: change.locked
16601
+ }
16602
+ )
16603
+ ]
16604
+ }
16605
+ );
16606
+ }
16607
+ function currentValue2(settings) {
16608
+ return settings.mine && settings.numbers.some((n) => n.id === settings.mine) ? settings.mine : ORG_DEFAULT2;
16609
+ }
16610
+ function choicesFor2(settings, formatIntl2, applying) {
16611
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
16612
+ const spinner = (value) => applying === value ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-applying", children: [
16613
+ /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { size: 15, className: "cv-choice-applying-icon", "aria-hidden": true }),
16614
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: "Aplicando" })
16615
+ ] }) : void 0;
16616
+ return [
16617
+ {
16618
+ value: ORG_DEFAULT2,
16619
+ label: "Por defecto de la organizaci\xF3n",
16620
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
16621
+ trailing: spinner(ORG_DEFAULT2)
16622
+ },
16623
+ ...settings.numbers.map((n) => {
16624
+ const kind = KIND_TAG[n.kind];
16625
+ return {
16626
+ value: n.id,
16627
+ label: numberName(n, formatIntl2),
16628
+ ...n.label ? { sub: formatIntl2(`+${n.number}`) } : {},
16629
+ ...kind ? { tag: kind.tag, hint: kind.hint } : {},
16630
+ trailing: spinner(n.id)
16631
+ };
16632
+ })
16633
+ ];
16634
+ }
16635
+ function outboundSummary(settings, formatIntl2) {
16636
+ const effective = effectiveNumber(settings);
16637
+ const name = numberName(effective, formatIntl2);
16638
+ return settings.mine || !effective ? name : `${name} \xB7 organizaci\xF3n`;
16639
+ }
15347
16640
  function SettingsTab() {
15348
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-view cv-settings", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-body", children: [
15349
- /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Dispositivos", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) }),
15350
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-more", children: "M\xE1s ajustes se ir\xE1n a\xF1adiendo pr\xF3ximamente." })
15351
- ] }) });
16641
+ const softphone = useSoftphone();
16642
+ const devices = useDevices();
16643
+ const store = ringtoneStore(softphone);
16644
+ const ringtone = useRingtoneSettings(store);
16645
+ const outStore = outboundNumberStore(softphone);
16646
+ const outbound = useOutboundNumbers(outStore).settings;
16647
+ const formatIntl2 = useIntlFormatter();
16648
+ const [page, setPage] = React.useState(null);
16649
+ const list = React.useRef(null);
16650
+ const rows = React.useRef({});
16651
+ const returnTo = React.useRef(null);
16652
+ React.useEffect(() => {
16653
+ void softphone.audio.refresh();
16654
+ }, [softphone]);
16655
+ React.useEffect(() => {
16656
+ list.current?.toggleAttribute("inert", page !== null);
16657
+ if (page === null && returnTo.current) {
16658
+ rows.current[returnTo.current]?.focus();
16659
+ returnTo.current = null;
16660
+ }
16661
+ }, [page]);
16662
+ const open = (p) => () => {
16663
+ returnTo.current = p;
16664
+ setPage(p);
16665
+ };
16666
+ const back = () => setPage(null);
16667
+ const rowRef = (p) => (el) => {
16668
+ rows.current[p] = el;
16669
+ };
16670
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view cv-settings", children: [
16671
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-body", ref: list, children: [
16672
+ store ? /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsxRuntime.jsx(
16673
+ SettingsNavRow,
16674
+ {
16675
+ icon: Music,
16676
+ title: "Tono de llamada",
16677
+ value: ringtoneSummary(ringtone.settings, ringtone.failed),
16678
+ onOpen: open("tone"),
16679
+ rowRef: rowRef("tone")
16680
+ }
16681
+ ) }) : null,
16682
+ outStore && outbound && outbound.numbers.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Llamadas salientes", children: /* @__PURE__ */ jsxRuntime.jsx(
16683
+ SettingsNavRow,
16684
+ {
16685
+ icon: PhoneOutgoing,
16686
+ title: "N\xFAmero de salida",
16687
+ value: outboundSummary(outbound, formatIntl2),
16688
+ onOpen: open("outbound"),
16689
+ rowRef: rowRef("outbound")
16690
+ }
16691
+ ) }) : null,
16692
+ /* @__PURE__ */ jsxRuntime.jsxs(SettingsSection, { title: "Dispositivos", children: [
16693
+ /* @__PURE__ */ jsxRuntime.jsx(
16694
+ SettingsNavRow,
16695
+ {
16696
+ icon: Mic,
16697
+ title: "Micr\xF3fono",
16698
+ value: deviceSummary("input", devices.inputs, devices.currentInputId),
16699
+ onOpen: open("input"),
16700
+ rowRef: rowRef("input")
16701
+ }
16702
+ ),
16703
+ devices.canSelectOutput ? /* @__PURE__ */ jsxRuntime.jsx(
16704
+ SettingsNavRow,
16705
+ {
16706
+ icon: Headphones,
16707
+ title: "Altavoces",
16708
+ value: deviceSummary("output", devices.outputs, devices.currentOutputId),
16709
+ onOpen: open("output"),
16710
+ rowRef: rowRef("output")
16711
+ }
16712
+ ) : (
16713
+ // Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
16714
+ // nothing is worse than saying where the choice really lives.
16715
+ /* @__PURE__ */ jsxRuntime.jsx(
16716
+ SettingsNavRow,
16717
+ {
16718
+ icon: Headphones,
16719
+ title: "Altavoces",
16720
+ value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
16721
+ }
16722
+ )
16723
+ ),
16724
+ devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsxRuntime.jsx(
16725
+ SettingsNavRow,
16726
+ {
16727
+ icon: BellRing,
16728
+ title: "Salida del tono",
16729
+ value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
16730
+ onOpen: open("ringtone"),
16731
+ rowRef: rowRef("ringtone")
16732
+ }
16733
+ ) : null
16734
+ ] })
16735
+ ] }),
16736
+ page === "tone" && store ? /* @__PURE__ */ jsxRuntime.jsx(
16737
+ RingtonePage,
16738
+ {
16739
+ store,
16740
+ settings: ringtone.settings,
16741
+ failed: ringtone.failed,
16742
+ onRetry: ringtone.retry,
16743
+ onBack: back
16744
+ }
16745
+ ) : page === "outbound" && outStore && outbound ? /* @__PURE__ */ jsxRuntime.jsx(OutboundNumberPage, { store: outStore, settings: outbound, onBack: back }) : page !== null && page !== "tone" && page !== "outbound" ? /* @__PURE__ */ jsxRuntime.jsx(DevicePage, { kind: page, onBack: back }) : null
16746
+ ] });
15352
16747
  }
15353
16748
 
15354
16749
  // src/core/env.ts
@@ -15587,14 +16982,29 @@ function useCallAudio(enabled) {
15587
16982
  const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
15588
16983
  void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
15589
16984
  });
16985
+ const ringtones = ringtoneStore(softphone);
16986
+ ringtones?.wantAudio(true);
16987
+ const refreshRingtone = () => {
16988
+ if (softphone.isLeader) void ringtones?.refresh();
16989
+ };
16990
+ refreshRingtone();
16991
+ const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
16992
+ if (isLeader) refreshRingtone();
16993
+ });
15590
16994
  const stopTone = () => {
15591
16995
  tone.current?.stop();
15592
16996
  tone.current = null;
15593
16997
  };
15594
16998
  const setTone = (next) => {
15595
16999
  stopTone();
15596
- if (next === "ringtone") tone.current = playRingtone();
15597
- else if (next === "ringback") tone.current = playRingback();
17000
+ if (next === "ringtone") {
17001
+ const audio = softphone.audio;
17002
+ tone.current = playRingtone({
17003
+ buffer: ringtones?.readyBuffer() ?? null,
17004
+ loop: ringtones?.readyLoop() ?? null,
17005
+ sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
17006
+ });
17007
+ } else if (next === "ringback") tone.current = playRingback();
15598
17008
  };
15599
17009
  const perCall = [];
15600
17010
  const followedCalls = /* @__PURE__ */ new Set();
@@ -15625,6 +17035,7 @@ function useCallAudio(enabled) {
15625
17035
  stopTone();
15626
17036
  remote.current?.detach();
15627
17037
  if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
17038
+ refreshRingtone();
15628
17039
  })
15629
17040
  );
15630
17041
  };
@@ -15642,6 +17053,8 @@ function useCallAudio(enabled) {
15642
17053
  offIncoming();
15643
17054
  offActive();
15644
17055
  offDeviceChange();
17056
+ offLeader();
17057
+ ringtones?.wantAudio(false);
15645
17058
  for (const off of perCall) off();
15646
17059
  stopTone();
15647
17060
  remote.current?.destroy();
@@ -15816,6 +17229,7 @@ lucide-react/dist/esm/createLucideIcon.js:
15816
17229
  lucide-react/dist/esm/icons/arrow-left-right.js:
15817
17230
  lucide-react/dist/esm/icons/arrow-left.js:
15818
17231
  lucide-react/dist/esm/icons/arrow-right.js:
17232
+ lucide-react/dist/esm/icons/bell-ring.js:
15819
17233
  lucide-react/dist/esm/icons/calendar-clock.js:
15820
17234
  lucide-react/dist/esm/icons/check.js:
15821
17235
  lucide-react/dist/esm/icons/chevron-down.js:
@@ -15831,6 +17245,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
15831
17245
  lucide-react/dist/esm/icons/mic-off.js:
15832
17246
  lucide-react/dist/esm/icons/mic.js:
15833
17247
  lucide-react/dist/esm/icons/monitor-smartphone.js:
17248
+ lucide-react/dist/esm/icons/music.js:
15834
17249
  lucide-react/dist/esm/icons/pause.js:
15835
17250
  lucide-react/dist/esm/icons/phone-incoming.js:
15836
17251
  lucide-react/dist/esm/icons/phone-missed.js:
@@ -15844,6 +17259,7 @@ lucide-react/dist/esm/icons/timer.js:
15844
17259
  lucide-react/dist/esm/icons/triangle-alert.js:
15845
17260
  lucide-react/dist/esm/icons/user.js:
15846
17261
  lucide-react/dist/esm/icons/users.js:
17262
+ lucide-react/dist/esm/icons/volume-2.js:
15847
17263
  lucide-react/dist/esm/icons/x.js:
15848
17264
  lucide-react/dist/esm/lucide-react.js:
15849
17265
  (**
@@ -15860,5 +17276,6 @@ exports.Dialpad = Dialpad;
15860
17276
  exports.HistoryTab = HistoryTab;
15861
17277
  exports.MicPermissionGate = MicPermissionGate;
15862
17278
  exports.SoftphoneWidget = SoftphoneWidget;
17279
+ exports.previewRingtone = previewRingtone;
15863
17280
  //# sourceMappingURL=index.cjs.map
15864
17281
  //# sourceMappingURL=index.cjs.map