@dataemergencia/emergency 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.
package/dist/index.cjs CHANGED
@@ -84,13 +84,8 @@ function isFiniteNumber(value) {
84
84
  function isArrayOf(value, guard) {
85
85
  return Array.isArray(value) && value.every(guard);
86
86
  }
87
- function isRealtimeConfig(value) {
88
- if (!isRecord(value) || value["mode"] !== "public" || !isRecord(value["pusher"])) return false;
89
- const pusher = value["pusher"];
90
- return isString(pusher["key"]) && isString(pusher["cluster"]) && isString(pusher["channel_pattern"]);
91
- }
92
87
  function isConfigResponse(value) {
93
- return isRecord(value) && isString(value["route_prefix"]) && isString(value["csrf_token"]) && isRealtimeConfig(value["realtime"]);
88
+ return isRecord(value) && isString(value["route_prefix"]) && isString(value["csrf_token"]);
94
89
  }
95
90
  function isSessionResponse(value) {
96
91
  return isRecord(value) && isString(value["code"]) && isString(value["csrf_token"]) && isFiniteNumber(value["expires_in"]);
@@ -108,7 +103,7 @@ function isEmergencyResultPayload(value) {
108
103
  return (value["emergency_id"] === null || isString(value["emergency_id"])) && isBoolean(value["simulated"]) && isArrayOf(value["responders"], isResponderPayload) && isArrayOf(value["failures"], isFailurePayload);
109
104
  }
110
105
  function isCurrentResponse(value) {
111
- if (!isRecord(value) || !isBoolean(value["active"]) || !isRealtimeConfig(value["realtime"])) return false;
106
+ if (!isRecord(value) || !isBoolean(value["active"])) return false;
112
107
  return value["emergency"] === null || isRecord(value["emergency"]) && !Array.isArray(value["emergency"]);
113
108
  }
114
109
  function isRefreshResponse(value) {
@@ -396,6 +391,7 @@ function createBffClient(options) {
396
391
  const copy = {
397
392
  panelLabel: "Panel de emergencia",
398
393
  close: "Cerrar panel de emergencia",
394
+ back: "Volver",
399
395
  question: "¿Tiene usted una emergencia?",
400
396
  illustrationAlt: "Ícono paramédico",
401
397
  emergency: "Emergencia",
@@ -634,8 +630,9 @@ function themeToCssVariables(theme) {
634
630
  }
635
631
 
636
632
  // src/core/realtime.ts
633
+ const DEFAULT_CHANNEL_PATTERN = "whatsapp.{phone}";
637
634
  function channelForPhone(pattern, phone) {
638
- return pattern.replace("{phone}", phone.replace(/^\+/, ""));
635
+ return pattern.replace("{phone}", phone.replace(/\D/g, ""));
639
636
  }
640
637
  function toStatus(state) {
641
638
  switch (state) {
@@ -650,11 +647,11 @@ function toStatus(state) {
650
647
  }
651
648
  function createPusherRealtimeProvider() {
652
649
  return { subscribe(config, phones, handlers) {
653
- if (typeof window === "undefined" || !config.pusher.key || !config.pusher.cluster) return null;
650
+ if (typeof window === "undefined" || !config.key || !config.cluster) return null;
654
651
  let pusher;
655
652
  try {
656
- pusher = new pusher_js.default(config.pusher.key, {
657
- cluster: config.pusher.cluster,
653
+ pusher = new pusher_js.default(config.key, {
654
+ cluster: config.cluster,
658
655
  forceTLS: true
659
656
  });
660
657
  } catch {
@@ -666,7 +663,7 @@ function createPusherRealtimeProvider() {
666
663
  handlers.onStatus(toStatus(change.current));
667
664
  });
668
665
  const channels = [...new Set(phones.filter((phone) => phone !== ""))].map((phone) => {
669
- const channelName = channelForPhone(config.pusher.channel_pattern, phone);
666
+ const channelName = channelForPhone(config.channelPattern ?? DEFAULT_CHANNEL_PATTERN, phone);
670
667
  pusher.subscribe(channelName).bind_global((eventName, data) => {
671
668
  handlers.onEvent(channelName, eventName, data);
672
669
  });
@@ -795,7 +792,6 @@ function createEmergencyController(deps) {
795
792
  let sessionPromise = null;
796
793
  let refreshing = false;
797
794
  let realtimeSubscription = null;
798
- let realtimeConfig = null;
799
795
  let subscribedPhones = [];
800
796
  let messageSequence = 0;
801
797
  let lastReconcileAt = 0;
@@ -853,9 +849,12 @@ function createEmergencyController(deps) {
853
849
  realtimeSubscription = null;
854
850
  subscribedPhones = [];
855
851
  }
856
- function startRealtime(realtime, responders) {
857
- if (!realtime) return;
858
- realtimeConfig = realtime;
852
+ function startRealtime(responders) {
853
+ const realtime = config.realtime ?? null;
854
+ if (!realtime) {
855
+ if (responders.length > 0) patch({ realtimeStatus: "unavailable" });
856
+ return;
857
+ }
859
858
  const phones = [...new Set(responders.map((responder) => responder.phone))];
860
859
  if (realtimeSubscription && phones.length === subscribedPhones.length && phones.every((phone, index) => phone === subscribedPhones[index])) return;
861
860
  realtimeSubscription?.disconnect();
@@ -889,7 +888,7 @@ function createEmergencyController(deps) {
889
888
  const phase = get().phase.kind;
890
889
  if (phase === "idle" || phase === "error") transition({ kind: "initializing" });
891
890
  try {
892
- realtimeConfig = (await bff.config()).realtime;
891
+ await bff.config();
893
892
  const session = await bff.session(config.code);
894
893
  patch({
895
894
  sessionReady: true,
@@ -919,7 +918,6 @@ function createEmergencyController(deps) {
919
918
  async function hydrateCurrent() {
920
919
  try {
921
920
  const current = await bff.current();
922
- realtimeConfig = current.realtime;
923
921
  if (current.active && current.emergency) {
924
922
  const snapshot = payloadToSnapshot(current.emergency, { code: config.code });
925
923
  patch({
@@ -935,7 +933,7 @@ function createEmergencyController(deps) {
935
933
  emergencyId: snapshot.emergencyId,
936
934
  simulated: snapshot.simulated
937
935
  });
938
- startRealtime(current.realtime, snapshot.responders);
936
+ startRealtime(snapshot.responders);
939
937
  events.emit("emergency:resumed", { snapshot });
940
938
  return;
941
939
  }
@@ -1017,7 +1015,7 @@ function createEmergencyController(deps) {
1017
1015
  });
1018
1016
  pushMessage(failures.length > 0 ? "warning" : "success", failures.length > 0 ? copy.partialFailure : copy.activeBanner);
1019
1017
  events.emit("emergency:activated", { snapshot: hydrated });
1020
- startRealtime(realtimeConfig, responders);
1018
+ startRealtime(responders);
1021
1019
  } catch (error) {
1022
1020
  const normalized = toEmergencyError(error, "network");
1023
1021
  if (normalized.code === "emergency_already_active") {
@@ -1109,7 +1107,7 @@ function createEmergencyController(deps) {
1109
1107
  emergencyId: updated.emergencyId,
1110
1108
  simulated: updated.simulated
1111
1109
  });
1112
- startRealtime(realtimeConfig, responders);
1110
+ startRealtime(responders);
1113
1111
  events.emit("emergency:updated", { snapshot: updated });
1114
1112
  } catch (error) {
1115
1113
  const normalized = toEmergencyError(error, "network");
@@ -1242,7 +1240,6 @@ function createEmergencyController(deps) {
1242
1240
  bff.destroy();
1243
1241
  realtimeSubscription?.disconnect();
1244
1242
  realtimeSubscription = null;
1245
- realtimeConfig = null;
1246
1243
  subscribedPhones = [];
1247
1244
  patch({
1248
1245
  destroyed: true,
@@ -1373,6 +1370,24 @@ function installEmergencyFonts(input) {
1373
1370
 
1374
1371
  // src/core/config.ts
1375
1372
  const EMERGENCY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9]{5}$/;
1373
+ const DEFAULT_REALTIME_CHANNEL_PATTERN = "whatsapp.{phone}";
1374
+ /**
1375
+ * Validates the host-supplied realtime block. Returns `null` when the value is
1376
+ * present but invalid; callers treat `undefined` as "realtime not requested".
1377
+ */
1378
+ function normalizeRealtimeConfig(value) {
1379
+ if (!isRecord(value)) return null;
1380
+ const key = value["key"];
1381
+ const cluster = value["cluster"];
1382
+ const channelPattern = value["channelPattern"];
1383
+ if (typeof key !== "string" || key.trim() === "" || typeof cluster !== "string" || cluster.trim() === "") return null;
1384
+ if (channelPattern !== void 0 && (typeof channelPattern !== "string" || channelPattern.trim() === "" || !channelPattern.includes("{phone}"))) return null;
1385
+ return {
1386
+ key,
1387
+ cluster,
1388
+ channelPattern: channelPattern ?? "whatsapp.{phone}"
1389
+ };
1390
+ }
1376
1391
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
1377
1392
  "localhost",
1378
1393
  "127.0.0.1",
@@ -1433,6 +1448,12 @@ function validateConfig(config) {
1433
1448
  config: null,
1434
1449
  error: createEmergencyError("configuration", "The fallback requires a non-empty phone and label.")
1435
1450
  };
1451
+ const realtime = config.realtime === void 0 ? void 0 : normalizeRealtimeConfig(config.realtime);
1452
+ if (realtime === null) return {
1453
+ ok: false,
1454
+ config: null,
1455
+ error: createEmergencyError("configuration", "The realtime configuration requires a non-empty key and cluster, and a channel pattern containing {phone}.")
1456
+ };
1436
1457
  return {
1437
1458
  ok: true,
1438
1459
  error: null,
@@ -1443,79 +1464,12 @@ function validateConfig(config) {
1443
1464
  locale: config.locale ?? "es",
1444
1465
  geolocationTimeoutMs: config.geolocationTimeoutMs ?? 8e3,
1445
1466
  requestTimeoutMs: config.requestTimeoutMs ?? 15e3,
1446
- fontLoading: normalizeFontLoading(config.fontLoading)
1467
+ fontLoading: normalizeFontLoading(config.fontLoading),
1468
+ realtime
1447
1469
  }
1448
1470
  };
1449
1471
  }
1450
1472
 
1451
- // src/react/components/icons/paramedical-mark.tsx
1452
- /**
1453
- * Neutral inline illustration used by the idle panel.
1454
- */
1455
- function ParamedicalMark({ className, title }) {
1456
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1457
- xmlns: "http://www.w3.org/2000/svg",
1458
- version: "1.0",
1459
- viewBox: "0 0 192 192",
1460
- width: "192",
1461
- height: "192",
1462
- id: "Star of life",
1463
- className,
1464
- "aria-label": title,
1465
- children: [
1466
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { id: "defs2886" }),
1467
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1468
- id: "cross",
1469
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1470
- d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1471
- id: "outline",
1472
- style: {
1473
- fill: "none",
1474
- stroke: "#3333bb",
1475
- strokeWidth: 6
1476
- }
1477
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1478
- d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1479
- id: "inner",
1480
- style: {
1481
- fill: "#0000bb",
1482
- fillRule: "evenodd",
1483
- stroke: "#ffffff",
1484
- strokeWidth: 2
1485
- }
1486
- })]
1487
- }),
1488
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1489
- id: "symbols",
1490
- style: {
1491
- fill: "#ffffff",
1492
- fillRule: "evenodd",
1493
- stroke: "#3333bb",
1494
- strokeWidth: 2
1495
- },
1496
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1497
- d: "m 93,166 c 0,7 6,7 6,0 l 3,-138 c 0,-8 -12,-8 -12,0 l 3,135 0,3 z",
1498
- id: "rod"
1499
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1500
- id: "snake",
1501
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1502
- d: "m 100,153 c 7,4 3,15 -3,15 2,-5 3,-9 3,-15 z m 1,-40 c 10,9 4,22 -3,26 -6,3 -6,7 -6,15 -9,-10 -4,-19 4,-24 5,-4 5,-10 5,-17 z m 1,-30 c 0,11 -17,14 -17,22 0,3 1,9 6,12 0,-11 9,-12 15,-18 7,-7 7,-19 -4,-28 0,4 0,8 0,12 z M 90,77 C 80,69 76,55 80,46 c 3,-10 20,-11 20,-3 0,8 -20,3 -13,16 l 3,5 0,13 z",
1503
- id: "body"
1504
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1505
- d: "m 95,43 c 7,-3 -7,-3 0,0 z",
1506
- id: "eye",
1507
- style: {
1508
- fill: "#3333bb",
1509
- fillRule: "evenodd",
1510
- stroke: "none"
1511
- }
1512
- })]
1513
- })]
1514
- })
1515
- ]
1516
- });
1517
- }
1518
-
1519
1473
  // src/react/components/Icon/index.tsx
1520
1474
  /** Icons whose artwork does not fit the default 24x24 grid need their own canvas. */
1521
1475
  const viewBoxes = { emergency: "0 0 640 640" };
@@ -1572,6 +1526,18 @@ const paths = {
1572
1526
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 7h.01" })
1573
1527
  ] }),
1574
1528
  "arrow-left": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 12H5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 19-7-7 7-7" })] }),
1529
+ "credit-card-plus": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5" }),
1531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 12h.01" }),
1532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 22v-6" }),
1533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m22 19-3-3-3 3" }),
1534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 12h.01" }),
1535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1536
+ cx: "12",
1537
+ cy: "12",
1538
+ r: "2"
1539
+ })
1540
+ ] }),
1575
1541
  emergency: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M272.5 96C272.5 78.3 286.8 64 304.5 64L336.5 64C354.2 64 368.5 78.3 368.5 96L368.5 236.9L490.5 166.5C505.8 157.7 525.4 162.9 534.2 178.2L550.2 205.9C559 221.2 553.8 240.8 538.5 249.6L416.5 320L538.5 390.4C553.8 399.2 559.1 418.8 550.2 434.1L534.2 461.8C525.4 477.1 505.8 482.4 490.5 473.5L368.5 403.1L368.5 544C368.5 561.7 354.2 576 336.5 576L304.5 576C286.8 576 272.5 561.7 272.5 544L272.5 403.1L150.5 473.5C135.2 482.3 115.6 477.1 106.8 461.8L90.8 434.1C82 418.8 87.2 399.2 102.5 390.4L224.5 320L102.5 249.6C87.2 240.8 82 221.2 90.8 205.9L106.8 178.2C115.6 162.9 135.2 157.7 150.5 166.5L272.5 236.9L272.5 96z" }) })
1576
1542
  };
1577
1543
  function Icon({ name, className, title }) {
@@ -1601,6 +1567,16 @@ var panel_module_default = {
1601
1567
  "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-nA5Hwa",
1602
1568
  "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-nA5Hwa",
1603
1569
  "de-emergency-spin": "de-emergency-de-emergency-spin-nA5Hwa",
1570
+ "emergency-availability": "de-emergency-emergency-availability-nA5Hwa",
1571
+ "emergency-availability__card": "de-emergency-emergency-availability__card-nA5Hwa",
1572
+ "emergency-availability__card-body": "de-emergency-emergency-availability__card-body-nA5Hwa",
1573
+ "emergency-availability__card-header": "de-emergency-emergency-availability__card-header-nA5Hwa",
1574
+ "emergency-availability__card-title": "de-emergency-emergency-availability__card-title-nA5Hwa",
1575
+ "emergency-availability__details": "de-emergency-emergency-availability__details-nA5Hwa",
1576
+ "emergency-availability__label": "de-emergency-emergency-availability__label-nA5Hwa",
1577
+ "emergency-availability__recharge": "de-emergency-emergency-availability__recharge-nA5Hwa",
1578
+ "emergency-availability__rounds": "de-emergency-emergency-availability__rounds-nA5Hwa",
1579
+ "emergency-availability__rounds-group": "de-emergency-emergency-availability__rounds-group-nA5Hwa",
1604
1580
  "emergency-confirmation": "de-emergency-emergency-confirmation-nA5Hwa",
1605
1581
  "emergency-confirmation__body": "de-emergency-emergency-confirmation__body-nA5Hwa",
1606
1582
  "emergency-confirmation__button": "de-emergency-emergency-confirmation__button-nA5Hwa",
@@ -1625,7 +1601,11 @@ var panel_module_default = {
1625
1601
  "emergency-feedback": "de-emergency-emergency-feedback-nA5Hwa",
1626
1602
  "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-nA5Hwa",
1627
1603
  "emergency-panel": "de-emergency-emergency-panel-nA5Hwa",
1604
+ "emergency-panel__availability-region": "de-emergency-emergency-panel__availability-region-nA5Hwa",
1628
1605
  "emergency-panel__body": "de-emergency-emergency-panel__body-nA5Hwa",
1606
+ "emergency-panel__body--pricing": "de-emergency-emergency-panel__body--pricing-nA5Hwa",
1607
+ "emergency-panel__body-split": "de-emergency-emergency-panel__body-split-nA5Hwa",
1608
+ "emergency-panel__body-split--with-availability": "de-emergency-emergency-panel__body-split--with-availability-nA5Hwa",
1629
1609
  "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-nA5Hwa",
1630
1610
  "emergency-panel__body-title-container": "de-emergency-emergency-panel__body-title-container-nA5Hwa",
1631
1611
  "emergency-panel__button": "de-emergency-emergency-panel__button-nA5Hwa",
@@ -1635,14 +1615,41 @@ var panel_module_default = {
1635
1615
  "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-nA5Hwa",
1636
1616
  "emergency-panel__footer": "de-emergency-emergency-panel__footer-nA5Hwa",
1637
1617
  "emergency-panel__header": "de-emergency-emergency-panel__header-nA5Hwa",
1618
+ "emergency-panel__prompt-region": "de-emergency-emergency-panel__prompt-region-nA5Hwa",
1638
1619
  "emergency-panel__title": "de-emergency-emergency-panel__title-nA5Hwa",
1639
1620
  "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-nA5Hwa",
1621
+ "emergency-pricing": "de-emergency-emergency-pricing-nA5Hwa",
1622
+ "emergency-pricing__badge": "de-emergency-emergency-pricing__badge-nA5Hwa",
1623
+ "emergency-pricing__footnote": "de-emergency-emergency-pricing__footnote-nA5Hwa",
1624
+ "emergency-pricing__heading": "de-emergency-emergency-pricing__heading-nA5Hwa",
1625
+ "emergency-pricing__package": "de-emergency-emergency-pricing__package-nA5Hwa",
1626
+ "emergency-pricing__package--highlighted": "de-emergency-emergency-pricing__package--highlighted-nA5Hwa",
1627
+ "emergency-pricing__package-caption": "de-emergency-emergency-pricing__package-caption-nA5Hwa",
1628
+ "emergency-pricing__package-header": "de-emergency-emergency-pricing__package-header-nA5Hwa",
1629
+ "emergency-pricing__package-label": "de-emergency-emergency-pricing__package-label-nA5Hwa",
1630
+ "emergency-pricing__package-name": "de-emergency-emergency-pricing__package-name-nA5Hwa",
1631
+ "emergency-pricing__price": "de-emergency-emergency-pricing__price-nA5Hwa",
1632
+ "emergency-pricing__price-meta": "de-emergency-emergency-pricing__price-meta-nA5Hwa",
1633
+ "emergency-pricing__price-note": "de-emergency-emergency-pricing__price-note-nA5Hwa",
1634
+ "emergency-pricing__price-row": "de-emergency-emergency-pricing__price-row-nA5Hwa",
1635
+ "emergency-pricing__select": "de-emergency-emergency-pricing__select-nA5Hwa",
1636
+ "emergency-pricing__stat": "de-emergency-emergency-pricing__stat-nA5Hwa",
1637
+ "emergency-pricing__stat-divider": "de-emergency-emergency-pricing__stat-divider-nA5Hwa",
1638
+ "emergency-pricing__stat-label": "de-emergency-emergency-pricing__stat-label-nA5Hwa",
1639
+ "emergency-pricing__stat-value": "de-emergency-emergency-pricing__stat-value-nA5Hwa",
1640
+ "emergency-pricing__stats": "de-emergency-emergency-pricing__stats-nA5Hwa",
1641
+ "emergency-pricing__subtitle": "de-emergency-emergency-pricing__subtitle-nA5Hwa",
1642
+ "emergency-pricing__title": "de-emergency-emergency-pricing__title-nA5Hwa",
1643
+ "emergency-pricing__titles": "de-emergency-emergency-pricing__titles-nA5Hwa",
1640
1644
  "emergency-resume-notification-button": "de-emergency-emergency-resume-notification-button-nA5Hwa",
1641
1645
  "emergency-resume-notification-card": "de-emergency-emergency-resume-notification-card-nA5Hwa",
1642
1646
  "emergency-resume-notification-icon": "de-emergency-emergency-resume-notification-icon-nA5Hwa",
1643
1647
  "emergency-resume-notification-message": "de-emergency-emergency-resume-notification-message-nA5Hwa",
1644
1648
  "emergency-resume-notification-overlay": "de-emergency-emergency-resume-notification-overlay-nA5Hwa",
1645
1649
  "emergency-resume-notification-title": "de-emergency-emergency-resume-notification-title-nA5Hwa",
1650
+ "emergency-screen": "de-emergency-emergency-screen-nA5Hwa",
1651
+ "emergency-screen__placeholder": "de-emergency-emergency-screen__placeholder-nA5Hwa",
1652
+ "emergency-screen__placeholder-text": "de-emergency-emergency-screen__placeholder-text-nA5Hwa",
1646
1653
  "emergency-status__abort-btn": "de-emergency-emergency-status__abort-btn-nA5Hwa",
1647
1654
  "emergency-status__body": "de-emergency-emergency-status__body-nA5Hwa",
1648
1655
  "emergency-status__cell": "de-emergency-emergency-status__cell-nA5Hwa",
@@ -1688,117 +1695,466 @@ function cls(...names) {
1688
1695
  return names.filter((name) => Boolean(name)).map((name) => panel_module_default[name] ?? "").filter(Boolean).join(" ");
1689
1696
  }
1690
1697
 
1691
- // src/react/components/MessageSurface/index.tsx
1692
- function MessageSurface(props) {
1693
- const { variant, icon, title, text, details, className, children } = props;
1694
- const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
1695
- const success = variant === "success";
1696
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1697
- className: [cls(prefix), className].filter(Boolean).join(" "),
1698
- role: success ? "status" : "alert",
1699
- "aria-live": success ? "polite" : "assertive",
1700
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1701
- className: cls(`${prefix}__container`),
1702
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1703
- className: cls(`${prefix}__icon`),
1704
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1705
- name: icon,
1706
- className: "status-icon"
1707
- })
1708
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1709
- className: cls(`${prefix}__content`),
1698
+ // src/react/components/EmergencyConfirmation/index.tsx
1699
+ function EmergencyConfirmation(props) {
1700
+ const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
1701
+ const cardRef = (0, react.useRef)(null);
1702
+ const titleId = (0, react.useId)();
1703
+ const danger = abortConfirmation;
1704
+ const open = abortConfirmation || simulationConfirmation;
1705
+ (0, react.useEffect)(() => {
1706
+ if (open) cardRef.current?.querySelector("button")?.focus();
1707
+ }, [open]);
1708
+ if (!open) return null;
1709
+ const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
1710
+ const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
1711
+ const tone = danger ? "danger" : "information";
1712
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1713
+ className: cls("emergency-confirmation"),
1714
+ role: "dialog",
1715
+ "aria-modal": "true",
1716
+ "aria-labelledby": titleId,
1717
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1718
+ ref: cardRef,
1719
+ className: cls("emergency-confirmation__card"),
1720
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1721
+ className: cls("emergency-confirmation__body"),
1710
1722
  children: [
1711
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1712
- className: cls(`${prefix}__title`),
1723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1724
+ className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
1725
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: danger ? "alert-triangle" : "info" })
1726
+ }),
1727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h5", {
1728
+ className: cls("emergency-confirmation__title"),
1729
+ id: titleId,
1713
1730
  children: title
1714
1731
  }),
1715
1732
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1716
- className: cls(`${prefix}__text`),
1733
+ className: cls("emergency-confirmation__text"),
1717
1734
  children: text
1718
- }),
1719
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1720
- className: cls(`${prefix}__details`),
1721
- children: details.map((detail) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1722
- className: cls(`${prefix}__detail`),
1723
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: detail.icon }), detail.text]
1724
- }, detail.text))
1725
1735
  })
1726
1736
  ]
1737
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1738
+ className: cls("emergency-confirmation__footer"),
1739
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1740
+ type: "button",
1741
+ className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
1742
+ onClick: danger ? onCancelAbort : onCancelSimulation,
1743
+ children: danger ? copy.abortCancel : copy.simulateCancel
1744
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1745
+ type: "button",
1746
+ className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
1747
+ onClick: danger ? onConfirmAbort : onConfirmSimulation,
1748
+ children: danger ? copy.abortConfirm : copy.simulateConfirm
1749
+ })]
1727
1750
  })]
1728
- }), children]
1729
- });
1730
- }
1731
-
1732
- // src/react/components/EmergencyError/index.tsx
1733
- function EmergencyError({ error, onRetry }) {
1734
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageSurface, {
1735
- variant: "error",
1736
- icon: "exclamation-circle",
1737
- title: copy.errorTitle,
1738
- text: error.message || copy.errorText,
1739
- details: [{
1740
- icon: "info",
1741
- text: copy.errorDetailInfo
1742
- }, {
1743
- icon: "arrow-left",
1744
- text: copy.errorDetailRetry
1745
- }],
1746
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1747
- type: "button",
1748
- className: cls("emergency-feedback__dismiss"),
1749
- onClick: onRetry,
1750
- children: copy.retry
1751
1751
  })
1752
1752
  });
1753
1753
  }
1754
1754
 
1755
- // src/react/components/EmergencyFeedback/index.tsx
1756
- /** Time the feedback stays on screen before dismissing itself. */
1757
- const FEEDBACK_AUTO_DISMISS_MS = 3e3;
1758
- /** Time the geolocation feedback stays on screen before dismissing itself. */
1759
- const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
1760
- function geolocationText(error) {
1761
- switch (error.code) {
1762
- case "geolocation_denied": return copy.geolocationDenied;
1763
- case "geolocation_timeout": return copy.geolocationTimeout;
1764
- case "geolocation_unsupported": return copy.geolocationUnsupported;
1765
- default: return copy.geolocationUnavailable;
1766
- }
1767
- }
1768
- function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
1769
- const result = abortResult ?? simulationResult;
1770
- const feedbackActivationError = result ? null : activationError ?? null;
1771
- const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
1772
- if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
1773
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeedbackSurface, {
1774
- result,
1775
- aborting: abortResult !== null,
1776
- geolocationError: feedbackGeolocationError,
1777
- activationError: feedbackActivationError,
1778
- onDismiss
1779
- });
1780
- }
1781
- function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
1782
- const dismissRef = (0, react.useRef)(onDismiss);
1755
+ // src/react/hooks/useEscapeKey.ts
1756
+ function useEscapeKey(active, onEscape) {
1783
1757
  (0, react.useEffect)(() => {
1784
- dismissRef.current = onDismiss;
1785
- }, [onDismiss]);
1758
+ if (!active || typeof document === "undefined") return;
1759
+ const handleKeyDown = (event) => {
1760
+ if (event.key === "Escape") {
1761
+ event.stopPropagation();
1762
+ onEscape();
1763
+ }
1764
+ };
1765
+ document.addEventListener("keydown", handleKeyDown);
1766
+ return () => {
1767
+ document.removeEventListener("keydown", handleKeyDown);
1768
+ };
1769
+ }, [active, onEscape]);
1770
+ }
1771
+
1772
+ // src/react/hooks/useFocusTrap.ts
1773
+ const FOCUSABLE_SELECTOR = [
1774
+ "a[href]",
1775
+ "button:not([disabled])",
1776
+ "input:not([disabled])",
1777
+ "select:not([disabled])",
1778
+ "textarea:not([disabled])",
1779
+ "[tabindex]:not([tabindex=\"-1\"])"
1780
+ ].join(",");
1781
+ /**
1782
+ * Keeps keyboard focus inside the dialog while it is open and restores focus to
1783
+ * the element that opened it when it closes.
1784
+ */
1785
+ function useFocusTrap(ref, active) {
1786
1786
  (0, react.useEffect)(() => {
1787
- const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
1788
- return () => clearTimeout(timeout);
1789
- }, [
1790
- result?.at,
1791
- geolocationError,
1792
- activationError
1793
- ]);
1794
- const success = result?.ok === true;
1795
- const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
1796
- const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
1797
- const details = success ? aborting ? [{
1798
- icon: "message",
1799
- text: copy.abortSuccessDetails
1800
- }] : [{
1801
- icon: "message",
1787
+ if (!active || typeof document === "undefined") return;
1788
+ const container = ref.current;
1789
+ if (!container) return;
1790
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1791
+ const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
1792
+ (focusable()[0] ?? container).focus({ preventScroll: true });
1793
+ const handleKeyDown = (event) => {
1794
+ if (event.key !== "Tab") return;
1795
+ const items = focusable();
1796
+ if (items.length === 0) {
1797
+ event.preventDefault();
1798
+ container.focus({ preventScroll: true });
1799
+ return;
1800
+ }
1801
+ const first = items[0];
1802
+ const last = items[items.length - 1];
1803
+ const current = document.activeElement;
1804
+ if (event.shiftKey && (current === first || !container.contains(current))) {
1805
+ event.preventDefault();
1806
+ if (last) last.focus({ preventScroll: true });
1807
+ return;
1808
+ }
1809
+ if (!event.shiftKey && (current === last || !container.contains(current)) && first) {
1810
+ event.preventDefault();
1811
+ first.focus({ preventScroll: true });
1812
+ }
1813
+ };
1814
+ document.addEventListener("keydown", handleKeyDown, true);
1815
+ return () => {
1816
+ document.removeEventListener("keydown", handleKeyDown, true);
1817
+ if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
1818
+ };
1819
+ }, [active, ref]);
1820
+ }
1821
+
1822
+ // src/react/screens/context.tsx
1823
+ const EmergencyScreenContext = (0, react.createContext)(null);
1824
+ function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1825
+ const [state, setState] = (0, react.useState)({
1826
+ screen: initialScreen,
1827
+ history: []
1828
+ });
1829
+ const navigate = (0, react.useCallback)((next) => {
1830
+ setState((current) => {
1831
+ if (current.screen === next) return current;
1832
+ return {
1833
+ screen: next,
1834
+ history: [...current.history, current.screen]
1835
+ };
1836
+ });
1837
+ }, []);
1838
+ const back = (0, react.useCallback)(() => {
1839
+ setState((current) => {
1840
+ const previous = current.history.at(-1);
1841
+ if (!previous) return current;
1842
+ return {
1843
+ screen: previous,
1844
+ history: current.history.slice(0, -1)
1845
+ };
1846
+ });
1847
+ }, []);
1848
+ const reset = (0, react.useCallback)(() => {
1849
+ setState((current) => {
1850
+ if (current.screen === "primary" && current.history.length === 0) return current;
1851
+ return {
1852
+ screen: "primary",
1853
+ history: []
1854
+ };
1855
+ });
1856
+ }, []);
1857
+ const value = (0, react.useMemo)(() => ({
1858
+ screen: state.screen,
1859
+ history: state.history,
1860
+ navigate,
1861
+ back,
1862
+ reset
1863
+ }), [
1864
+ back,
1865
+ navigate,
1866
+ reset,
1867
+ state.history,
1868
+ state.screen
1869
+ ]);
1870
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyScreenContext.Provider, {
1871
+ value,
1872
+ children
1873
+ });
1874
+ }
1875
+ function useScreenNavigation() {
1876
+ const value = (0, react.useContext)(EmergencyScreenContext);
1877
+ if (!value) throw new Error("useScreenNavigation must be used inside EmergencyScreenProvider.");
1878
+ return value;
1879
+ }
1880
+
1881
+ // src/react/components/EmergencyHeader/index.tsx
1882
+ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack }) {
1883
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1884
+ className: cls("emergency-panel__header"),
1885
+ children: [
1886
+ onBack ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1887
+ type: "button",
1888
+ className: cls("emergency-panel__close-button"),
1889
+ "aria-label": copy.back,
1890
+ onClick: onBack,
1891
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1892
+ name: "arrow-left",
1893
+ className: cls("emergency-panel__close-icon")
1894
+ })
1895
+ }) : null,
1896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1897
+ className: cls("emergency-panel__title"),
1898
+ id: titleId,
1899
+ tabIndex: -1,
1900
+ children: title
1901
+ }),
1902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1903
+ type: "button",
1904
+ className: cls("emergency-panel__close-button"),
1905
+ "aria-label": copy.close,
1906
+ onClick: onClose,
1907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1908
+ name: "close",
1909
+ className: cls("emergency-panel__close-icon")
1910
+ })
1911
+ })
1912
+ ]
1913
+ });
1914
+ }
1915
+
1916
+ // src/react/screens/layout.tsx
1917
+ /**
1918
+ * Shared shell for every screen. The wrapper is `display: contents` so the
1919
+ * header, body and footer stay direct grid rows of the emergency panel.
1920
+ */
1921
+ function EmergencyScreenLayout({ screen, titleId, header, footer, children, bodyClassName }) {
1922
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1923
+ className: cls("emergency-screen"),
1924
+ "data-screen": screen,
1925
+ children: [
1926
+ header,
1927
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1928
+ className: cls("emergency-panel__body", bodyClassName ?? null),
1929
+ "aria-labelledby": titleId,
1930
+ children
1931
+ }),
1932
+ footer
1933
+ ]
1934
+ });
1935
+ }
1936
+
1937
+ // src/react/screens/PlaceholderScreen.tsx
1938
+ /** Minimal in-design screen used to prove navigation before real designs land. */
1939
+ function PlaceholderScreen({ screen, titleId, title, description, onClose, onBack }) {
1940
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyScreenLayout, {
1941
+ screen,
1942
+ titleId,
1943
+ header: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyHeader, {
1944
+ titleId,
1945
+ title,
1946
+ onClose,
1947
+ onBack
1948
+ }),
1949
+ footer: null,
1950
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1951
+ className: cls("emergency-screen__placeholder"),
1952
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1953
+ className: cls("emergency-screen__placeholder-text"),
1954
+ children: description
1955
+ })
1956
+ })
1957
+ });
1958
+ }
1959
+
1960
+ // src/react/screens/details/copy.ts
1961
+ const detailsCopy = {
1962
+ title: "Detalles",
1963
+ description: "Aquí podrás consultar el detalle de tus alertas disponibles. Esta pantalla estará disponible próximamente."
1964
+ };
1965
+
1966
+ // src/react/screens/details/index.tsx
1967
+ function DetailsScreen({ titleId, onClose }) {
1968
+ const { back } = useScreenNavigation();
1969
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderScreen, {
1970
+ screen: "details",
1971
+ titleId,
1972
+ title: detailsCopy.title,
1973
+ description: detailsCopy.description,
1974
+ onClose: () => onClose("close"),
1975
+ onBack: back
1976
+ });
1977
+ }
1978
+
1979
+ // src/react/components/icons/paramedical-mark.tsx
1980
+ /**
1981
+ * Neutral inline illustration used by the idle panel.
1982
+ */
1983
+ function ParamedicalMark({ className, title }) {
1984
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1985
+ xmlns: "http://www.w3.org/2000/svg",
1986
+ version: "1.0",
1987
+ viewBox: "0 0 192 192",
1988
+ width: "192",
1989
+ height: "192",
1990
+ id: "Star of life",
1991
+ className,
1992
+ "aria-label": title,
1993
+ children: [
1994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { id: "defs2886" }),
1995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1996
+ id: "cross",
1997
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1998
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1999
+ id: "outline",
2000
+ style: {
2001
+ fill: "none",
2002
+ stroke: "#3333bb",
2003
+ strokeWidth: 6
2004
+ }
2005
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2006
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
2007
+ id: "inner",
2008
+ style: {
2009
+ fill: "#0000bb",
2010
+ fillRule: "evenodd",
2011
+ stroke: "#ffffff",
2012
+ strokeWidth: 2
2013
+ }
2014
+ })]
2015
+ }),
2016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
2017
+ id: "symbols",
2018
+ style: {
2019
+ fill: "#ffffff",
2020
+ fillRule: "evenodd",
2021
+ stroke: "#3333bb",
2022
+ strokeWidth: 2
2023
+ },
2024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2025
+ d: "m 93,166 c 0,7 6,7 6,0 l 3,-138 c 0,-8 -12,-8 -12,0 l 3,135 0,3 z",
2026
+ id: "rod"
2027
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
2028
+ id: "snake",
2029
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2030
+ d: "m 100,153 c 7,4 3,15 -3,15 2,-5 3,-9 3,-15 z m 1,-40 c 10,9 4,22 -3,26 -6,3 -6,7 -6,15 -9,-10 -4,-19 4,-24 5,-4 5,-10 5,-17 z m 1,-30 c 0,11 -17,14 -17,22 0,3 1,9 6,12 0,-11 9,-12 15,-18 7,-7 7,-19 -4,-28 0,4 0,8 0,12 z M 90,77 C 80,69 76,55 80,46 c 3,-10 20,-11 20,-3 0,8 -20,3 -13,16 l 3,5 0,13 z",
2031
+ id: "body"
2032
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2033
+ d: "m 95,43 c 7,-3 -7,-3 0,0 z",
2034
+ id: "eye",
2035
+ style: {
2036
+ fill: "#3333bb",
2037
+ fillRule: "evenodd",
2038
+ stroke: "none"
2039
+ }
2040
+ })]
2041
+ })]
2042
+ })
2043
+ ]
2044
+ });
2045
+ }
2046
+
2047
+ // src/react/components/MessageSurface/index.tsx
2048
+ function MessageSurface(props) {
2049
+ const { variant, icon, title, text, details, className, children } = props;
2050
+ const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
2051
+ const success = variant === "success";
2052
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2053
+ className: [cls(prefix), className].filter(Boolean).join(" "),
2054
+ role: success ? "status" : "alert",
2055
+ "aria-live": success ? "polite" : "assertive",
2056
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2057
+ className: cls(`${prefix}__container`),
2058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2059
+ className: cls(`${prefix}__icon`),
2060
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2061
+ name: icon,
2062
+ className: "status-icon"
2063
+ })
2064
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2065
+ className: cls(`${prefix}__content`),
2066
+ children: [
2067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2068
+ className: cls(`${prefix}__title`),
2069
+ children: title
2070
+ }),
2071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2072
+ className: cls(`${prefix}__text`),
2073
+ children: text
2074
+ }),
2075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2076
+ className: cls(`${prefix}__details`),
2077
+ children: details.map((detail) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2078
+ className: cls(`${prefix}__detail`),
2079
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: detail.icon }), detail.text]
2080
+ }, detail.text))
2081
+ })
2082
+ ]
2083
+ })]
2084
+ }), children]
2085
+ });
2086
+ }
2087
+
2088
+ // src/react/components/EmergencyError/index.tsx
2089
+ function EmergencyError({ error, onRetry }) {
2090
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageSurface, {
2091
+ variant: "error",
2092
+ icon: "exclamation-circle",
2093
+ title: copy.errorTitle,
2094
+ text: error.message || copy.errorText,
2095
+ details: [{
2096
+ icon: "info",
2097
+ text: copy.errorDetailInfo
2098
+ }, {
2099
+ icon: "arrow-left",
2100
+ text: copy.errorDetailRetry
2101
+ }],
2102
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2103
+ type: "button",
2104
+ className: cls("emergency-feedback__dismiss"),
2105
+ onClick: onRetry,
2106
+ children: copy.retry
2107
+ })
2108
+ });
2109
+ }
2110
+
2111
+ // src/react/components/EmergencyFeedback/index.tsx
2112
+ /** Time the feedback stays on screen before dismissing itself. */
2113
+ const FEEDBACK_AUTO_DISMISS_MS = 3e3;
2114
+ /** Time the geolocation feedback stays on screen before dismissing itself. */
2115
+ const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
2116
+ function geolocationText(error) {
2117
+ switch (error.code) {
2118
+ case "geolocation_denied": return copy.geolocationDenied;
2119
+ case "geolocation_timeout": return copy.geolocationTimeout;
2120
+ case "geolocation_unsupported": return copy.geolocationUnsupported;
2121
+ default: return copy.geolocationUnavailable;
2122
+ }
2123
+ }
2124
+ function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
2125
+ const result = abortResult ?? simulationResult;
2126
+ const feedbackActivationError = result ? null : activationError ?? null;
2127
+ const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
2128
+ if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
2129
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeedbackSurface, {
2130
+ result,
2131
+ aborting: abortResult !== null,
2132
+ geolocationError: feedbackGeolocationError,
2133
+ activationError: feedbackActivationError,
2134
+ onDismiss
2135
+ });
2136
+ }
2137
+ function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
2138
+ const dismissRef = (0, react.useRef)(onDismiss);
2139
+ (0, react.useEffect)(() => {
2140
+ dismissRef.current = onDismiss;
2141
+ }, [onDismiss]);
2142
+ (0, react.useEffect)(() => {
2143
+ const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
2144
+ return () => clearTimeout(timeout);
2145
+ }, [
2146
+ result?.at,
2147
+ geolocationError,
2148
+ activationError
2149
+ ]);
2150
+ const success = result?.ok === true;
2151
+ const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
2152
+ const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
2153
+ const details = success ? aborting ? [{
2154
+ icon: "message",
2155
+ text: copy.abortSuccessDetails
2156
+ }] : [{
2157
+ icon: "message",
1802
2158
  text: copy.simulateSuccessDetails
1803
2159
  }, {
1804
2160
  icon: "clock",
@@ -2012,101 +2368,129 @@ function EmergencyStatusPanel({ responders, refreshing, locale, onRefresh }) {
2012
2368
  });
2013
2369
  }
2014
2370
 
2015
- // src/react/components/EmergencyBody/index.tsx
2016
- function EmergencyBody(props) {
2017
- const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
2018
- const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2019
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2020
- !loading && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2021
- className: cls("sos-img__container"),
2022
- id: "sos-img__container",
2023
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamedicalMark, {
2024
- className: cls("sos-img"),
2025
- title: copy.illustrationAlt
2026
- })
2027
- }) : null,
2028
- active || loading || showingFeedback ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2029
- className: cls("emergency-panel__body-title-container"),
2030
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
2031
- className: cls("emergency-panel__body-title"),
2032
- children: copy.question
2033
- })
2034
- }),
2035
- active && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyStatusPanel, {
2036
- responders,
2037
- refreshing,
2038
- locale,
2039
- onRefresh
2040
- }) : null,
2041
- loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyLoading, {}) : null,
2042
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyError, {
2043
- error,
2044
- onRetry
2045
- }) : null,
2046
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFeedback, {
2047
- abortResult,
2048
- simulationResult,
2049
- geolocationError,
2050
- activationError,
2051
- onDismiss: onDismissFeedback
2052
- })
2053
- ] });
2371
+ // src/react/screens/primary/copy.ts
2372
+ const primaryCopy = {
2373
+ availabilityTitle: "Alertas Disponibles",
2374
+ recharge: "Recargar",
2375
+ details: "Detalles",
2376
+ roundSingular: "ronda disponible",
2377
+ roundPlural: "rondas disponibles"
2378
+ };
2379
+ /** Singular/plural label for the remaining rounds. */
2380
+ function roundsUnit(rounds) {
2381
+ return rounds === 1 ? primaryCopy.roundSingular : primaryCopy.roundPlural;
2382
+ }
2383
+ function formatRounds(rounds) {
2384
+ return `${rounds} ${roundsUnit(rounds)}`;
2054
2385
  }
2055
2386
 
2056
- // src/react/components/EmergencyConfirmation/index.tsx
2057
- function EmergencyConfirmation(props) {
2058
- const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2059
- const cardRef = (0, react.useRef)(null);
2060
- const titleId = (0, react.useId)();
2061
- const danger = abortConfirmation;
2062
- const open = abortConfirmation || simulationConfirmation;
2063
- (0, react.useEffect)(() => {
2064
- if (open) cardRef.current?.querySelector("button")?.focus();
2065
- }, [open]);
2066
- if (!open) return null;
2067
- const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
2068
- const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
2069
- const tone = danger ? "danger" : "information";
2070
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2071
- className: cls("emergency-confirmation"),
2072
- role: "dialog",
2073
- "aria-modal": "true",
2074
- "aria-labelledby": titleId,
2075
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2076
- ref: cardRef,
2077
- className: cls("emergency-confirmation__card"),
2078
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2079
- className: cls("emergency-confirmation__body"),
2080
- children: [
2081
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2082
- className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
2083
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: danger ? "alert-triangle" : "info" })
2084
- }),
2085
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h5", {
2086
- className: cls("emergency-confirmation__title"),
2087
- id: titleId,
2088
- children: title
2089
- }),
2090
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2091
- className: cls("emergency-confirmation__text"),
2092
- children: text
2093
- })
2094
- ]
2387
+ // src/react/screens/primary/data.ts
2388
+ /**
2389
+ * Derives the remaining alert rounds from available messages and designated
2390
+ * responders. Invalid or non-positive input collapses to the minimum of one
2391
+ * round instead of producing `Infinity`, `NaN` or a negative value.
2392
+ */
2393
+ function calculateRounds(messages, responders) {
2394
+ if (!Number.isFinite(messages) || !Number.isFinite(responders)) return 1;
2395
+ const availableMessages = Math.floor(messages);
2396
+ const designatedResponders = Math.floor(responders);
2397
+ if (availableMessages <= 0 || designatedResponders <= 0) return 1;
2398
+ return Math.max(1, Math.floor(availableMessages / designatedResponders));
2399
+ }
2400
+
2401
+ // src/react/screens/primary/Availability.tsx
2402
+ function Availability({ onRecharge, onDetails }) {
2403
+ const rounds = calculateRounds(100, 3);
2404
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2405
+ className: cls("emergency-availability"),
2406
+ "aria-label": primaryCopy.availabilityTitle,
2407
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2408
+ type: "button",
2409
+ className: cls("emergency-availability__recharge"),
2410
+ onClick: onRecharge,
2411
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "credit-card-plus" }), primaryCopy.recharge]
2412
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2413
+ className: cls("emergency-availability__card"),
2414
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2415
+ className: cls("emergency-availability__card-header"),
2416
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2417
+ className: cls("emergency-availability__card-title"),
2418
+ children: primaryCopy.availabilityTitle
2419
+ })
2095
2420
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2096
- className: cls("emergency-confirmation__footer"),
2097
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2098
- type: "button",
2099
- className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2100
- onClick: danger ? onCancelAbort : onCancelSimulation,
2101
- children: danger ? copy.abortCancel : copy.simulateCancel
2421
+ className: cls("emergency-availability__card-body"),
2422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2423
+ className: cls("emergency-availability__rounds-group"),
2424
+ "aria-label": formatRounds(rounds),
2425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2426
+ className: cls("emergency-availability__rounds"),
2427
+ children: rounds
2428
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2429
+ className: cls("emergency-availability__label"),
2430
+ children: roundsUnit(rounds)
2431
+ })]
2102
2432
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2103
2433
  type: "button",
2104
- className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2105
- onClick: danger ? onConfirmAbort : onConfirmSimulation,
2106
- children: danger ? copy.abortConfirm : copy.simulateConfirm
2434
+ className: cls("emergency-availability__details"),
2435
+ onClick: onDetails,
2436
+ children: primaryCopy.details
2107
2437
  })]
2108
2438
  })]
2109
- })
2439
+ })]
2440
+ });
2441
+ }
2442
+
2443
+ // src/react/components/EmergencyBody/index.tsx
2444
+ function EmergencyBody(props) {
2445
+ const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback, onRecharge, onDetails } = props;
2446
+ const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2447
+ const idle = !active && !loading && !showingFeedback && error === null;
2448
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2449
+ className: cls("emergency-panel__body-split", idle && "emergency-panel__body-split--with-availability"),
2450
+ children: [idle ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2451
+ className: cls("emergency-panel__availability-region"),
2452
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Availability, {
2453
+ onRecharge,
2454
+ onDetails
2455
+ })
2456
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2457
+ className: cls("emergency-panel__prompt-region"),
2458
+ children: [
2459
+ !loading && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2460
+ className: cls("sos-img__container"),
2461
+ id: "sos-img__container",
2462
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamedicalMark, {
2463
+ className: cls("sos-img"),
2464
+ title: copy.illustrationAlt
2465
+ })
2466
+ }) : null,
2467
+ active || loading || showingFeedback ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2468
+ className: cls("emergency-panel__body-title-container"),
2469
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
2470
+ className: cls("emergency-panel__body-title"),
2471
+ children: copy.question
2472
+ })
2473
+ }),
2474
+ active && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyStatusPanel, {
2475
+ responders,
2476
+ refreshing,
2477
+ locale,
2478
+ onRefresh
2479
+ }) : null,
2480
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyLoading, {}) : null,
2481
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyError, {
2482
+ error,
2483
+ onRetry
2484
+ }) : null,
2485
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFeedback, {
2486
+ abortResult,
2487
+ simulationResult,
2488
+ geolocationError,
2489
+ activationError,
2490
+ onDismiss: onDismissFeedback
2491
+ })
2492
+ ]
2493
+ })]
2110
2494
  });
2111
2495
  }
2112
2496
 
@@ -2144,105 +2528,279 @@ function EmergencyFooter(props) {
2144
2528
  });
2145
2529
  }
2146
2530
 
2147
- // src/react/components/EmergencyHeader/index.tsx
2148
- function EmergencyHeader({ titleId, onClose }) {
2149
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2150
- className: cls("emergency-panel__header"),
2151
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2152
- className: cls("emergency-panel__title"),
2153
- id: titleId,
2154
- children: copy.panelLabel
2155
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2156
- type: "button",
2157
- className: cls("emergency-panel__close-button"),
2158
- "aria-label": copy.close,
2159
- onClick: onClose,
2160
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2161
- name: "close",
2162
- className: cls("emergency-panel__close-icon")
2163
- })
2164
- })]
2531
+ // src/react/screens/primary/index.tsx
2532
+ function PrimaryScreen(props) {
2533
+ const { navigate } = useScreenNavigation();
2534
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyScreenLayout, {
2535
+ screen: "primary",
2536
+ titleId: props.titleId,
2537
+ header: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyHeader, {
2538
+ titleId: props.titleId,
2539
+ onClose: () => props.onClose("close")
2540
+ }),
2541
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFooter, {
2542
+ active: props.active,
2543
+ busy: props.busy,
2544
+ aborting: props.aborting,
2545
+ feedbackVisible: props.feedbackVisible,
2546
+ onActivate: props.onActivate,
2547
+ onRequestAbort: props.onRequestAbort,
2548
+ onRequestSimulation: props.onRequestSimulation
2549
+ }),
2550
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyBody, {
2551
+ active: props.active,
2552
+ responders: props.responders,
2553
+ refreshing: props.refreshing,
2554
+ loading: props.loading,
2555
+ locale: props.locale,
2556
+ geolocationError: props.geolocationError,
2557
+ activationError: props.activationError,
2558
+ error: props.error,
2559
+ abortResult: props.abortResult,
2560
+ simulationResult: props.simulationResult,
2561
+ onRefresh: props.onRefresh,
2562
+ onRetry: props.onRetry,
2563
+ onDismissFeedback: props.onDismissFeedback,
2564
+ onRecharge: () => navigate("pricing"),
2565
+ onDetails: () => navigate("details")
2566
+ })
2165
2567
  });
2166
2568
  }
2167
2569
 
2168
- // src/react/hooks/useEscapeKey.ts
2169
- function useEscapeKey(active, onEscape) {
2170
- (0, react.useEffect)(() => {
2171
- if (!active || typeof document === "undefined") return;
2172
- const handleKeyDown = (event) => {
2173
- if (event.key === "Escape") {
2174
- event.stopPropagation();
2175
- onEscape();
2176
- }
2177
- };
2178
- document.addEventListener("keydown", handleKeyDown);
2179
- return () => {
2180
- document.removeEventListener("keydown", handleKeyDown);
2181
- };
2182
- }, [active, onEscape]);
2183
- }
2570
+ // src/react/screens/pricing/copy.ts
2571
+ const pricingCopy = {
2572
+ title: "Recargar",
2573
+ heading: "Elige tu paquete",
2574
+ subtitle: "Alertas de WhatsApp para tus contactos",
2575
+ bestValue: "MEJOR VALOR",
2576
+ choose: "Seleccionar",
2577
+ chosen: "Seleccionado",
2578
+ whatsapp: "de WhatsApp",
2579
+ rounds: "rondas de alerta",
2580
+ perMessage: "por mensaje + IVA",
2581
+ footnote: "Rondas calculadas con 3 contactos de emergencia. Válidas para pruebas y emergencias."
2582
+ };
2184
2583
 
2185
- // src/react/hooks/useFocusTrap.ts
2186
- const FOCUSABLE_SELECTOR = [
2187
- "a[href]",
2188
- "button:not([disabled])",
2189
- "input:not([disabled])",
2190
- "select:not([disabled])",
2191
- "textarea:not([disabled])",
2192
- "[tabindex]:not([tabindex=\"-1\"])"
2193
- ].join(",");
2584
+ // src/react/screens/pricing/data.ts
2194
2585
  /**
2195
- * Keeps keyboard focus inside the dialog while it is open and restores focus to
2196
- * the element that opened it when it closes.
2586
+ * Local demo catalog. Prices and per-message labels are literal values from the
2587
+ * mock; they are not derived from the package totals.
2197
2588
  */
2198
- function useFocusTrap(ref, active) {
2199
- (0, react.useEffect)(() => {
2200
- if (!active || typeof document === "undefined") return;
2201
- const container = ref.current;
2202
- if (!container) return;
2203
- const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2204
- const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
2205
- (focusable()[0] ?? container).focus({ preventScroll: true });
2206
- const handleKeyDown = (event) => {
2207
- if (event.key !== "Tab") return;
2208
- const items = focusable();
2209
- if (items.length === 0) {
2210
- event.preventDefault();
2211
- container.focus({ preventScroll: true });
2212
- return;
2213
- }
2214
- const first = items[0];
2215
- const last = items[items.length - 1];
2216
- const current = document.activeElement;
2217
- if (event.shiftKey && (current === first || !container.contains(current))) {
2218
- event.preventDefault();
2219
- if (last) last.focus({ preventScroll: true });
2220
- return;
2221
- }
2222
- if (!event.shiftKey && current === last && first) {
2223
- event.preventDefault();
2224
- first.focus({ preventScroll: true });
2225
- }
2226
- };
2227
- document.addEventListener("keydown", handleKeyDown, true);
2228
- return () => {
2229
- document.removeEventListener("keydown", handleKeyDown, true);
2230
- if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
2231
- };
2232
- }, [active, ref]);
2589
+ const PRICING_PACKAGES = [{
2590
+ id: "package-1",
2591
+ label: "PAQUETE 1",
2592
+ messages: 100,
2593
+ priceLabel: "$7",
2594
+ priceCaption: "IVA incluido",
2595
+ perMessageLabel: "$0.06",
2596
+ highlighted: false
2597
+ }, {
2598
+ id: "package-2",
2599
+ label: "PAQUETE 2",
2600
+ messages: 200,
2601
+ priceLabel: "12$",
2602
+ priceNote: "($10 + IVA)",
2603
+ priceCaption: "IVA incluido",
2604
+ perMessageLabel: "$0.05",
2605
+ highlighted: true,
2606
+ badge: "MEJOR VALOR"
2607
+ }];
2608
+ /** Derives the alert rounds a package provides for the demo responder count. */
2609
+ function packageRounds(pkg) {
2610
+ return calculateRounds(pkg.messages, 3);
2611
+ }
2612
+
2613
+ // src/react/screens/pricing/PackageCard.tsx
2614
+ function PackageCard({ pkg, selected, onSelect }) {
2615
+ const labelId = (0, react.useId)();
2616
+ const rounds = packageRounds(pkg);
2617
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2618
+ className: cls("emergency-pricing__package", pkg.highlighted && "emergency-pricing__package--highlighted"),
2619
+ "aria-labelledby": labelId,
2620
+ "data-selected": selected ? "true" : void 0,
2621
+ children: [
2622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2623
+ className: cls("emergency-pricing__package-header"),
2624
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2625
+ className: cls("emergency-pricing__package-label"),
2626
+ id: labelId,
2627
+ children: pkg.label
2628
+ }), pkg.badge ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2629
+ className: cls("emergency-pricing__badge"),
2630
+ children: pkg.badge
2631
+ }) : null]
2632
+ }),
2633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2634
+ className: cls("emergency-pricing__price-row"),
2635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", {
2636
+ className: cls("emergency-pricing__package-name"),
2637
+ children: [pkg.messages, " alertas"]
2638
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2639
+ className: cls("emergency-pricing__package-caption"),
2640
+ children: pricingCopy.whatsapp
2641
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2642
+ className: cls("emergency-pricing__price-meta"),
2643
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2644
+ className: cls("emergency-pricing__price"),
2645
+ children: pkg.priceLabel
2646
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2647
+ className: cls("emergency-pricing__price-note"),
2648
+ children: [pkg.priceCaption, pkg.priceNote ? ` ${pkg.priceNote}` : null]
2649
+ })]
2650
+ })]
2651
+ }),
2652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2653
+ className: cls("emergency-pricing__stats"),
2654
+ children: [
2655
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2656
+ className: cls("emergency-pricing__stat"),
2657
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2658
+ className: cls("emergency-pricing__stat-value"),
2659
+ children: rounds
2660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2661
+ className: cls("emergency-pricing__stat-label"),
2662
+ children: pricingCopy.rounds
2663
+ })]
2664
+ }),
2665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2666
+ className: cls("emergency-pricing__stat-divider"),
2667
+ "aria-hidden": "true"
2668
+ }),
2669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2670
+ className: cls("emergency-pricing__stat"),
2671
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2672
+ className: cls("emergency-pricing__stat-value"),
2673
+ children: pkg.perMessageLabel
2674
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2675
+ className: cls("emergency-pricing__stat-label"),
2676
+ children: pricingCopy.perMessage
2677
+ })]
2678
+ })
2679
+ ]
2680
+ }),
2681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2682
+ type: "button",
2683
+ className: cls("emergency-pricing__select"),
2684
+ "aria-pressed": selected,
2685
+ onClick: () => onSelect(pkg.id),
2686
+ children: selected ? pricingCopy.chosen : pricingCopy.choose
2687
+ })
2688
+ ]
2689
+ });
2233
2690
  }
2234
2691
 
2692
+ // src/react/screens/pricing/index.tsx
2693
+ function PricingScreen(props) {
2694
+ const { back } = useScreenNavigation();
2695
+ const [selected, setSelected] = (0, react.useState)(null);
2696
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyScreenLayout, {
2697
+ screen: "pricing",
2698
+ titleId: props.titleId,
2699
+ bodyClassName: "emergency-panel__body--pricing",
2700
+ header: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyHeader, {
2701
+ titleId: props.titleId,
2702
+ title: pricingCopy.title,
2703
+ onClose: () => props.onClose("close"),
2704
+ onBack: back
2705
+ }),
2706
+ footer: null,
2707
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2708
+ className: cls("emergency-pricing"),
2709
+ children: [
2710
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2711
+ className: cls("emergency-pricing__heading"),
2712
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2713
+ className: cls("emergency-pricing__titles"),
2714
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2715
+ className: cls("emergency-pricing__title"),
2716
+ children: pricingCopy.heading
2717
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2718
+ className: cls("emergency-pricing__subtitle"),
2719
+ children: pricingCopy.subtitle
2720
+ })]
2721
+ })
2722
+ }),
2723
+ PRICING_PACKAGES.map((pkg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackageCard, {
2724
+ pkg,
2725
+ selected: selected === pkg.id,
2726
+ onSelect: setSelected
2727
+ }, pkg.id)),
2728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2729
+ className: cls("emergency-pricing__footnote"),
2730
+ children: pricingCopy.footnote
2731
+ })
2732
+ ]
2733
+ })
2734
+ });
2735
+ }
2736
+
2737
+ // src/react/screens/registry.ts
2738
+ /** Every screen the panel can render. Add one entry to add a screen. */
2739
+ const screenRegistry = {
2740
+ primary: PrimaryScreen,
2741
+ pricing: PricingScreen,
2742
+ details: DetailsScreen
2743
+ };
2744
+
2235
2745
  // src/react/components/EmergencyPanel/index.tsx
2746
+ function ActiveScreen({ screenProps, open, forcePrimary }) {
2747
+ const { screen, reset } = useScreenNavigation();
2748
+ const shouldReset = !open || forcePrimary;
2749
+ const effectiveScreen = shouldReset ? "primary" : screen;
2750
+ const previousOpen = (0, react.useRef)(open);
2751
+ const previousScreen = (0, react.useRef)(effectiveScreen);
2752
+ (0, react.useEffect)(() => {
2753
+ if (shouldReset) reset();
2754
+ }, [reset, shouldReset]);
2755
+ (0, react.useEffect)(() => {
2756
+ const wasOpen = previousOpen.current;
2757
+ const previousEffectiveScreen = previousScreen.current;
2758
+ previousOpen.current = open;
2759
+ previousScreen.current = effectiveScreen;
2760
+ if (!open || !wasOpen || previousEffectiveScreen === effectiveScreen) return;
2761
+ document.getElementById(screenProps.titleId)?.focus({ preventScroll: true });
2762
+ }, [
2763
+ effectiveScreen,
2764
+ open,
2765
+ screenProps.titleId
2766
+ ]);
2767
+ const Screen = screenRegistry[effectiveScreen];
2768
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Screen, { ...screenProps });
2769
+ }
2236
2770
  function EmergencyPanel(props) {
2237
2771
  const { open, theme, active, busy, aborting, loading, locale, responders, refreshing, geolocationError, activationError, error, abortResult, simulationResult, abortConfirmation, simulationConfirmation, onClose, onActivate, onRefresh, onRetry, onDismissFeedback, onRequestAbort, onRequestSimulation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2238
2772
  const titleId = (0, react.useId)();
2239
2773
  const dialogRef = (0, react.useRef)(null);
2240
2774
  const feedbackVisible = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2775
+ const forcePrimary = active || loading || error !== null || feedbackVisible;
2241
2776
  const closeOnEscape = (0, react.useCallback)(() => {
2242
2777
  onClose("escape");
2243
2778
  }, [onClose]);
2244
2779
  useFocusTrap(dialogRef, open);
2245
2780
  useEscapeKey(open, closeOnEscape);
2781
+ const screenProps = {
2782
+ titleId,
2783
+ active,
2784
+ busy,
2785
+ aborting,
2786
+ loading,
2787
+ locale,
2788
+ responders,
2789
+ refreshing,
2790
+ geolocationError,
2791
+ activationError,
2792
+ error,
2793
+ abortResult,
2794
+ simulationResult,
2795
+ feedbackVisible,
2796
+ onClose,
2797
+ onActivate,
2798
+ onRefresh,
2799
+ onRetry,
2800
+ onDismissFeedback,
2801
+ onRequestAbort,
2802
+ onRequestSimulation
2803
+ };
2246
2804
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2247
2805
  ref: dialogRef,
2248
2806
  className: cls("emergency-panel", !open && "emergency-panel--hidden"),
@@ -2253,48 +2811,18 @@ function EmergencyPanel(props) {
2253
2811
  "aria-hidden": !open,
2254
2812
  inert: !open,
2255
2813
  tabIndex: -1,
2256
- children: [
2257
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyHeader, {
2258
- titleId,
2259
- onClose: () => onClose("close")
2260
- }),
2261
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2262
- className: cls("emergency-panel__body"),
2263
- "aria-labelledby": titleId,
2264
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyBody, {
2265
- active,
2266
- responders,
2267
- refreshing,
2268
- loading,
2269
- locale,
2270
- geolocationError,
2271
- activationError,
2272
- error,
2273
- abortResult,
2274
- simulationResult,
2275
- onRefresh,
2276
- onRetry,
2277
- onDismissFeedback
2278
- })
2279
- }),
2280
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFooter, {
2281
- active,
2282
- busy,
2283
- aborting,
2284
- feedbackVisible,
2285
- onActivate,
2286
- onRequestAbort,
2287
- onRequestSimulation
2288
- }),
2289
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyConfirmation, {
2290
- abortConfirmation,
2291
- simulationConfirmation,
2292
- onCancelAbort,
2293
- onCancelSimulation,
2294
- onConfirmAbort,
2295
- onConfirmSimulation
2296
- })
2297
- ]
2814
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyScreenProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveScreen, {
2815
+ screenProps,
2816
+ open,
2817
+ forcePrimary
2818
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyConfirmation, {
2819
+ abortConfirmation,
2820
+ simulationConfirmation,
2821
+ onCancelAbort,
2822
+ onCancelSimulation,
2823
+ onConfirmAbort,
2824
+ onConfirmSimulation
2825
+ })]
2298
2826
  });
2299
2827
  }
2300
2828