@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.js CHANGED
@@ -2,7 +2,7 @@ import { createRoot } from "react-dom/client";
2
2
  import axios, { isAxiosError } from "axios";
3
3
  import Pusher from "pusher-js";
4
4
  import { createStore } from "zustand/vanilla";
5
- import { createContext, useCallback, useContext, useEffect, useId, useRef } from "react";
5
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { useStore } from "zustand";
8
8
 
@@ -55,13 +55,8 @@ function isFiniteNumber(value) {
55
55
  function isArrayOf(value, guard) {
56
56
  return Array.isArray(value) && value.every(guard);
57
57
  }
58
- function isRealtimeConfig(value) {
59
- if (!isRecord(value) || value["mode"] !== "public" || !isRecord(value["pusher"])) return false;
60
- const pusher = value["pusher"];
61
- return isString(pusher["key"]) && isString(pusher["cluster"]) && isString(pusher["channel_pattern"]);
62
- }
63
58
  function isConfigResponse(value) {
64
- return isRecord(value) && isString(value["route_prefix"]) && isString(value["csrf_token"]) && isRealtimeConfig(value["realtime"]);
59
+ return isRecord(value) && isString(value["route_prefix"]) && isString(value["csrf_token"]);
65
60
  }
66
61
  function isSessionResponse(value) {
67
62
  return isRecord(value) && isString(value["code"]) && isString(value["csrf_token"]) && isFiniteNumber(value["expires_in"]);
@@ -79,7 +74,7 @@ function isEmergencyResultPayload(value) {
79
74
  return (value["emergency_id"] === null || isString(value["emergency_id"])) && isBoolean(value["simulated"]) && isArrayOf(value["responders"], isResponderPayload) && isArrayOf(value["failures"], isFailurePayload);
80
75
  }
81
76
  function isCurrentResponse(value) {
82
- if (!isRecord(value) || !isBoolean(value["active"]) || !isRealtimeConfig(value["realtime"])) return false;
77
+ if (!isRecord(value) || !isBoolean(value["active"])) return false;
83
78
  return value["emergency"] === null || isRecord(value["emergency"]) && !Array.isArray(value["emergency"]);
84
79
  }
85
80
  function isRefreshResponse(value) {
@@ -367,6 +362,7 @@ function createBffClient(options) {
367
362
  const copy = {
368
363
  panelLabel: "Panel de emergencia",
369
364
  close: "Cerrar panel de emergencia",
365
+ back: "Volver",
370
366
  question: "¿Tiene usted una emergencia?",
371
367
  illustrationAlt: "Ícono paramédico",
372
368
  emergency: "Emergencia",
@@ -605,8 +601,9 @@ function themeToCssVariables(theme) {
605
601
  }
606
602
 
607
603
  // src/core/realtime.ts
604
+ const DEFAULT_CHANNEL_PATTERN = "whatsapp.{phone}";
608
605
  function channelForPhone(pattern, phone) {
609
- return pattern.replace("{phone}", phone.replace(/^\+/, ""));
606
+ return pattern.replace("{phone}", phone.replace(/\D/g, ""));
610
607
  }
611
608
  function toStatus(state) {
612
609
  switch (state) {
@@ -621,11 +618,11 @@ function toStatus(state) {
621
618
  }
622
619
  function createPusherRealtimeProvider() {
623
620
  return { subscribe(config, phones, handlers) {
624
- if (typeof window === "undefined" || !config.pusher.key || !config.pusher.cluster) return null;
621
+ if (typeof window === "undefined" || !config.key || !config.cluster) return null;
625
622
  let pusher;
626
623
  try {
627
- pusher = new Pusher(config.pusher.key, {
628
- cluster: config.pusher.cluster,
624
+ pusher = new Pusher(config.key, {
625
+ cluster: config.cluster,
629
626
  forceTLS: true
630
627
  });
631
628
  } catch {
@@ -637,7 +634,7 @@ function createPusherRealtimeProvider() {
637
634
  handlers.onStatus(toStatus(change.current));
638
635
  });
639
636
  const channels = [...new Set(phones.filter((phone) => phone !== ""))].map((phone) => {
640
- const channelName = channelForPhone(config.pusher.channel_pattern, phone);
637
+ const channelName = channelForPhone(config.channelPattern ?? DEFAULT_CHANNEL_PATTERN, phone);
641
638
  pusher.subscribe(channelName).bind_global((eventName, data) => {
642
639
  handlers.onEvent(channelName, eventName, data);
643
640
  });
@@ -766,7 +763,6 @@ function createEmergencyController(deps) {
766
763
  let sessionPromise = null;
767
764
  let refreshing = false;
768
765
  let realtimeSubscription = null;
769
- let realtimeConfig = null;
770
766
  let subscribedPhones = [];
771
767
  let messageSequence = 0;
772
768
  let lastReconcileAt = 0;
@@ -824,9 +820,12 @@ function createEmergencyController(deps) {
824
820
  realtimeSubscription = null;
825
821
  subscribedPhones = [];
826
822
  }
827
- function startRealtime(realtime, responders) {
828
- if (!realtime) return;
829
- realtimeConfig = realtime;
823
+ function startRealtime(responders) {
824
+ const realtime = config.realtime ?? null;
825
+ if (!realtime) {
826
+ if (responders.length > 0) patch({ realtimeStatus: "unavailable" });
827
+ return;
828
+ }
830
829
  const phones = [...new Set(responders.map((responder) => responder.phone))];
831
830
  if (realtimeSubscription && phones.length === subscribedPhones.length && phones.every((phone, index) => phone === subscribedPhones[index])) return;
832
831
  realtimeSubscription?.disconnect();
@@ -860,7 +859,7 @@ function createEmergencyController(deps) {
860
859
  const phase = get().phase.kind;
861
860
  if (phase === "idle" || phase === "error") transition({ kind: "initializing" });
862
861
  try {
863
- realtimeConfig = (await bff.config()).realtime;
862
+ await bff.config();
864
863
  const session = await bff.session(config.code);
865
864
  patch({
866
865
  sessionReady: true,
@@ -890,7 +889,6 @@ function createEmergencyController(deps) {
890
889
  async function hydrateCurrent() {
891
890
  try {
892
891
  const current = await bff.current();
893
- realtimeConfig = current.realtime;
894
892
  if (current.active && current.emergency) {
895
893
  const snapshot = payloadToSnapshot(current.emergency, { code: config.code });
896
894
  patch({
@@ -906,7 +904,7 @@ function createEmergencyController(deps) {
906
904
  emergencyId: snapshot.emergencyId,
907
905
  simulated: snapshot.simulated
908
906
  });
909
- startRealtime(current.realtime, snapshot.responders);
907
+ startRealtime(snapshot.responders);
910
908
  events.emit("emergency:resumed", { snapshot });
911
909
  return;
912
910
  }
@@ -988,7 +986,7 @@ function createEmergencyController(deps) {
988
986
  });
989
987
  pushMessage(failures.length > 0 ? "warning" : "success", failures.length > 0 ? copy.partialFailure : copy.activeBanner);
990
988
  events.emit("emergency:activated", { snapshot: hydrated });
991
- startRealtime(realtimeConfig, responders);
989
+ startRealtime(responders);
992
990
  } catch (error) {
993
991
  const normalized = toEmergencyError(error, "network");
994
992
  if (normalized.code === "emergency_already_active") {
@@ -1080,7 +1078,7 @@ function createEmergencyController(deps) {
1080
1078
  emergencyId: updated.emergencyId,
1081
1079
  simulated: updated.simulated
1082
1080
  });
1083
- startRealtime(realtimeConfig, responders);
1081
+ startRealtime(responders);
1084
1082
  events.emit("emergency:updated", { snapshot: updated });
1085
1083
  } catch (error) {
1086
1084
  const normalized = toEmergencyError(error, "network");
@@ -1213,7 +1211,6 @@ function createEmergencyController(deps) {
1213
1211
  bff.destroy();
1214
1212
  realtimeSubscription?.disconnect();
1215
1213
  realtimeSubscription = null;
1216
- realtimeConfig = null;
1217
1214
  subscribedPhones = [];
1218
1215
  patch({
1219
1216
  destroyed: true,
@@ -1344,6 +1341,24 @@ function installEmergencyFonts(input) {
1344
1341
 
1345
1342
  // src/core/config.ts
1346
1343
  const EMERGENCY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9]{5}$/;
1344
+ const DEFAULT_REALTIME_CHANNEL_PATTERN = "whatsapp.{phone}";
1345
+ /**
1346
+ * Validates the host-supplied realtime block. Returns `null` when the value is
1347
+ * present but invalid; callers treat `undefined` as "realtime not requested".
1348
+ */
1349
+ function normalizeRealtimeConfig(value) {
1350
+ if (!isRecord(value)) return null;
1351
+ const key = value["key"];
1352
+ const cluster = value["cluster"];
1353
+ const channelPattern = value["channelPattern"];
1354
+ if (typeof key !== "string" || key.trim() === "" || typeof cluster !== "string" || cluster.trim() === "") return null;
1355
+ if (channelPattern !== void 0 && (typeof channelPattern !== "string" || channelPattern.trim() === "" || !channelPattern.includes("{phone}"))) return null;
1356
+ return {
1357
+ key,
1358
+ cluster,
1359
+ channelPattern: channelPattern ?? "whatsapp.{phone}"
1360
+ };
1361
+ }
1347
1362
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
1348
1363
  "localhost",
1349
1364
  "127.0.0.1",
@@ -1404,6 +1419,12 @@ function validateConfig(config) {
1404
1419
  config: null,
1405
1420
  error: createEmergencyError("configuration", "The fallback requires a non-empty phone and label.")
1406
1421
  };
1422
+ const realtime = config.realtime === void 0 ? void 0 : normalizeRealtimeConfig(config.realtime);
1423
+ if (realtime === null) return {
1424
+ ok: false,
1425
+ config: null,
1426
+ error: createEmergencyError("configuration", "The realtime configuration requires a non-empty key and cluster, and a channel pattern containing {phone}.")
1427
+ };
1407
1428
  return {
1408
1429
  ok: true,
1409
1430
  error: null,
@@ -1414,79 +1435,12 @@ function validateConfig(config) {
1414
1435
  locale: config.locale ?? "es",
1415
1436
  geolocationTimeoutMs: config.geolocationTimeoutMs ?? 8e3,
1416
1437
  requestTimeoutMs: config.requestTimeoutMs ?? 15e3,
1417
- fontLoading: normalizeFontLoading(config.fontLoading)
1438
+ fontLoading: normalizeFontLoading(config.fontLoading),
1439
+ realtime
1418
1440
  }
1419
1441
  };
1420
1442
  }
1421
1443
 
1422
- // src/react/components/icons/paramedical-mark.tsx
1423
- /**
1424
- * Neutral inline illustration used by the idle panel.
1425
- */
1426
- function ParamedicalMark({ className, title }) {
1427
- return /* @__PURE__ */ jsxs("svg", {
1428
- xmlns: "http://www.w3.org/2000/svg",
1429
- version: "1.0",
1430
- viewBox: "0 0 192 192",
1431
- width: "192",
1432
- height: "192",
1433
- id: "Star of life",
1434
- className,
1435
- "aria-label": title,
1436
- children: [
1437
- /* @__PURE__ */ jsx("defs", { id: "defs2886" }),
1438
- /* @__PURE__ */ jsxs("g", {
1439
- id: "cross",
1440
- children: [/* @__PURE__ */ jsx("path", {
1441
- 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",
1442
- id: "outline",
1443
- style: {
1444
- fill: "none",
1445
- stroke: "#3333bb",
1446
- strokeWidth: 6
1447
- }
1448
- }), /* @__PURE__ */ jsx("path", {
1449
- 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",
1450
- id: "inner",
1451
- style: {
1452
- fill: "#0000bb",
1453
- fillRule: "evenodd",
1454
- stroke: "#ffffff",
1455
- strokeWidth: 2
1456
- }
1457
- })]
1458
- }),
1459
- /* @__PURE__ */ jsxs("g", {
1460
- id: "symbols",
1461
- style: {
1462
- fill: "#ffffff",
1463
- fillRule: "evenodd",
1464
- stroke: "#3333bb",
1465
- strokeWidth: 2
1466
- },
1467
- children: [/* @__PURE__ */ jsx("path", {
1468
- 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",
1469
- id: "rod"
1470
- }), /* @__PURE__ */ jsxs("g", {
1471
- id: "snake",
1472
- children: [/* @__PURE__ */ jsx("path", {
1473
- 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",
1474
- id: "body"
1475
- }), /* @__PURE__ */ jsx("path", {
1476
- d: "m 95,43 c 7,-3 -7,-3 0,0 z",
1477
- id: "eye",
1478
- style: {
1479
- fill: "#3333bb",
1480
- fillRule: "evenodd",
1481
- stroke: "none"
1482
- }
1483
- })]
1484
- })]
1485
- })
1486
- ]
1487
- });
1488
- }
1489
-
1490
1444
  // src/react/components/Icon/index.tsx
1491
1445
  /** Icons whose artwork does not fit the default 24x24 grid need their own canvas. */
1492
1446
  const viewBoxes = { emergency: "0 0 640 640" };
@@ -1543,6 +1497,18 @@ const paths = {
1543
1497
  /* @__PURE__ */ jsx("path", { d: "M12 7h.01" })
1544
1498
  ] }),
1545
1499
  "arrow-left": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M19 12H5" }), /* @__PURE__ */ jsx("path", { d: "m12 19-7-7 7-7" })] }),
1500
+ "credit-card-plus": /* @__PURE__ */ jsxs(Fragment, { children: [
1501
+ /* @__PURE__ */ jsx("path", { d: "M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5" }),
1502
+ /* @__PURE__ */ jsx("path", { d: "M18 12h.01" }),
1503
+ /* @__PURE__ */ jsx("path", { d: "M19 22v-6" }),
1504
+ /* @__PURE__ */ jsx("path", { d: "m22 19-3-3-3 3" }),
1505
+ /* @__PURE__ */ jsx("path", { d: "M6 12h.01" }),
1506
+ /* @__PURE__ */ jsx("circle", {
1507
+ cx: "12",
1508
+ cy: "12",
1509
+ r: "2"
1510
+ })
1511
+ ] }),
1546
1512
  emergency: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ 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" }) })
1547
1513
  };
1548
1514
  function Icon({ name, className, title }) {
@@ -1572,6 +1538,16 @@ var panel_module_default = {
1572
1538
  "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-nA5Hwa",
1573
1539
  "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-nA5Hwa",
1574
1540
  "de-emergency-spin": "de-emergency-de-emergency-spin-nA5Hwa",
1541
+ "emergency-availability": "de-emergency-emergency-availability-nA5Hwa",
1542
+ "emergency-availability__card": "de-emergency-emergency-availability__card-nA5Hwa",
1543
+ "emergency-availability__card-body": "de-emergency-emergency-availability__card-body-nA5Hwa",
1544
+ "emergency-availability__card-header": "de-emergency-emergency-availability__card-header-nA5Hwa",
1545
+ "emergency-availability__card-title": "de-emergency-emergency-availability__card-title-nA5Hwa",
1546
+ "emergency-availability__details": "de-emergency-emergency-availability__details-nA5Hwa",
1547
+ "emergency-availability__label": "de-emergency-emergency-availability__label-nA5Hwa",
1548
+ "emergency-availability__recharge": "de-emergency-emergency-availability__recharge-nA5Hwa",
1549
+ "emergency-availability__rounds": "de-emergency-emergency-availability__rounds-nA5Hwa",
1550
+ "emergency-availability__rounds-group": "de-emergency-emergency-availability__rounds-group-nA5Hwa",
1575
1551
  "emergency-confirmation": "de-emergency-emergency-confirmation-nA5Hwa",
1576
1552
  "emergency-confirmation__body": "de-emergency-emergency-confirmation__body-nA5Hwa",
1577
1553
  "emergency-confirmation__button": "de-emergency-emergency-confirmation__button-nA5Hwa",
@@ -1596,7 +1572,11 @@ var panel_module_default = {
1596
1572
  "emergency-feedback": "de-emergency-emergency-feedback-nA5Hwa",
1597
1573
  "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-nA5Hwa",
1598
1574
  "emergency-panel": "de-emergency-emergency-panel-nA5Hwa",
1575
+ "emergency-panel__availability-region": "de-emergency-emergency-panel__availability-region-nA5Hwa",
1599
1576
  "emergency-panel__body": "de-emergency-emergency-panel__body-nA5Hwa",
1577
+ "emergency-panel__body--pricing": "de-emergency-emergency-panel__body--pricing-nA5Hwa",
1578
+ "emergency-panel__body-split": "de-emergency-emergency-panel__body-split-nA5Hwa",
1579
+ "emergency-panel__body-split--with-availability": "de-emergency-emergency-panel__body-split--with-availability-nA5Hwa",
1600
1580
  "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-nA5Hwa",
1601
1581
  "emergency-panel__body-title-container": "de-emergency-emergency-panel__body-title-container-nA5Hwa",
1602
1582
  "emergency-panel__button": "de-emergency-emergency-panel__button-nA5Hwa",
@@ -1606,14 +1586,41 @@ var panel_module_default = {
1606
1586
  "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-nA5Hwa",
1607
1587
  "emergency-panel__footer": "de-emergency-emergency-panel__footer-nA5Hwa",
1608
1588
  "emergency-panel__header": "de-emergency-emergency-panel__header-nA5Hwa",
1589
+ "emergency-panel__prompt-region": "de-emergency-emergency-panel__prompt-region-nA5Hwa",
1609
1590
  "emergency-panel__title": "de-emergency-emergency-panel__title-nA5Hwa",
1610
1591
  "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-nA5Hwa",
1592
+ "emergency-pricing": "de-emergency-emergency-pricing-nA5Hwa",
1593
+ "emergency-pricing__badge": "de-emergency-emergency-pricing__badge-nA5Hwa",
1594
+ "emergency-pricing__footnote": "de-emergency-emergency-pricing__footnote-nA5Hwa",
1595
+ "emergency-pricing__heading": "de-emergency-emergency-pricing__heading-nA5Hwa",
1596
+ "emergency-pricing__package": "de-emergency-emergency-pricing__package-nA5Hwa",
1597
+ "emergency-pricing__package--highlighted": "de-emergency-emergency-pricing__package--highlighted-nA5Hwa",
1598
+ "emergency-pricing__package-caption": "de-emergency-emergency-pricing__package-caption-nA5Hwa",
1599
+ "emergency-pricing__package-header": "de-emergency-emergency-pricing__package-header-nA5Hwa",
1600
+ "emergency-pricing__package-label": "de-emergency-emergency-pricing__package-label-nA5Hwa",
1601
+ "emergency-pricing__package-name": "de-emergency-emergency-pricing__package-name-nA5Hwa",
1602
+ "emergency-pricing__price": "de-emergency-emergency-pricing__price-nA5Hwa",
1603
+ "emergency-pricing__price-meta": "de-emergency-emergency-pricing__price-meta-nA5Hwa",
1604
+ "emergency-pricing__price-note": "de-emergency-emergency-pricing__price-note-nA5Hwa",
1605
+ "emergency-pricing__price-row": "de-emergency-emergency-pricing__price-row-nA5Hwa",
1606
+ "emergency-pricing__select": "de-emergency-emergency-pricing__select-nA5Hwa",
1607
+ "emergency-pricing__stat": "de-emergency-emergency-pricing__stat-nA5Hwa",
1608
+ "emergency-pricing__stat-divider": "de-emergency-emergency-pricing__stat-divider-nA5Hwa",
1609
+ "emergency-pricing__stat-label": "de-emergency-emergency-pricing__stat-label-nA5Hwa",
1610
+ "emergency-pricing__stat-value": "de-emergency-emergency-pricing__stat-value-nA5Hwa",
1611
+ "emergency-pricing__stats": "de-emergency-emergency-pricing__stats-nA5Hwa",
1612
+ "emergency-pricing__subtitle": "de-emergency-emergency-pricing__subtitle-nA5Hwa",
1613
+ "emergency-pricing__title": "de-emergency-emergency-pricing__title-nA5Hwa",
1614
+ "emergency-pricing__titles": "de-emergency-emergency-pricing__titles-nA5Hwa",
1611
1615
  "emergency-resume-notification-button": "de-emergency-emergency-resume-notification-button-nA5Hwa",
1612
1616
  "emergency-resume-notification-card": "de-emergency-emergency-resume-notification-card-nA5Hwa",
1613
1617
  "emergency-resume-notification-icon": "de-emergency-emergency-resume-notification-icon-nA5Hwa",
1614
1618
  "emergency-resume-notification-message": "de-emergency-emergency-resume-notification-message-nA5Hwa",
1615
1619
  "emergency-resume-notification-overlay": "de-emergency-emergency-resume-notification-overlay-nA5Hwa",
1616
1620
  "emergency-resume-notification-title": "de-emergency-emergency-resume-notification-title-nA5Hwa",
1621
+ "emergency-screen": "de-emergency-emergency-screen-nA5Hwa",
1622
+ "emergency-screen__placeholder": "de-emergency-emergency-screen__placeholder-nA5Hwa",
1623
+ "emergency-screen__placeholder-text": "de-emergency-emergency-screen__placeholder-text-nA5Hwa",
1617
1624
  "emergency-status__abort-btn": "de-emergency-emergency-status__abort-btn-nA5Hwa",
1618
1625
  "emergency-status__body": "de-emergency-emergency-status__body-nA5Hwa",
1619
1626
  "emergency-status__cell": "de-emergency-emergency-status__cell-nA5Hwa",
@@ -1659,117 +1666,466 @@ function cls(...names) {
1659
1666
  return names.filter((name) => Boolean(name)).map((name) => panel_module_default[name] ?? "").filter(Boolean).join(" ");
1660
1667
  }
1661
1668
 
1662
- // src/react/components/MessageSurface/index.tsx
1663
- function MessageSurface(props) {
1664
- const { variant, icon, title, text, details, className, children } = props;
1665
- const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
1666
- const success = variant === "success";
1667
- return /* @__PURE__ */ jsxs("div", {
1668
- className: [cls(prefix), className].filter(Boolean).join(" "),
1669
- role: success ? "status" : "alert",
1670
- "aria-live": success ? "polite" : "assertive",
1671
- children: [/* @__PURE__ */ jsxs("div", {
1672
- className: cls(`${prefix}__container`),
1673
- children: [/* @__PURE__ */ jsx("div", {
1674
- className: cls(`${prefix}__icon`),
1675
- children: /* @__PURE__ */ jsx(Icon, {
1676
- name: icon,
1677
- className: "status-icon"
1678
- })
1679
- }), /* @__PURE__ */ jsxs("div", {
1680
- className: cls(`${prefix}__content`),
1669
+ // src/react/components/EmergencyConfirmation/index.tsx
1670
+ function EmergencyConfirmation(props) {
1671
+ const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
1672
+ const cardRef = useRef(null);
1673
+ const titleId = useId();
1674
+ const danger = abortConfirmation;
1675
+ const open = abortConfirmation || simulationConfirmation;
1676
+ useEffect(() => {
1677
+ if (open) cardRef.current?.querySelector("button")?.focus();
1678
+ }, [open]);
1679
+ if (!open) return null;
1680
+ const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
1681
+ const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
1682
+ const tone = danger ? "danger" : "information";
1683
+ return /* @__PURE__ */ jsx("div", {
1684
+ className: cls("emergency-confirmation"),
1685
+ role: "dialog",
1686
+ "aria-modal": "true",
1687
+ "aria-labelledby": titleId,
1688
+ children: /* @__PURE__ */ jsxs("div", {
1689
+ ref: cardRef,
1690
+ className: cls("emergency-confirmation__card"),
1691
+ children: [/* @__PURE__ */ jsxs("div", {
1692
+ className: cls("emergency-confirmation__body"),
1681
1693
  children: [
1682
- /* @__PURE__ */ jsx("h3", {
1683
- className: cls(`${prefix}__title`),
1694
+ /* @__PURE__ */ jsx("div", {
1695
+ className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
1696
+ children: /* @__PURE__ */ jsx(Icon, { name: danger ? "alert-triangle" : "info" })
1697
+ }),
1698
+ /* @__PURE__ */ jsx("h5", {
1699
+ className: cls("emergency-confirmation__title"),
1700
+ id: titleId,
1684
1701
  children: title
1685
1702
  }),
1686
1703
  /* @__PURE__ */ jsx("p", {
1687
- className: cls(`${prefix}__text`),
1704
+ className: cls("emergency-confirmation__text"),
1688
1705
  children: text
1689
- }),
1690
- /* @__PURE__ */ jsx("div", {
1691
- className: cls(`${prefix}__details`),
1692
- children: details.map((detail) => /* @__PURE__ */ jsxs("span", {
1693
- className: cls(`${prefix}__detail`),
1694
- children: [/* @__PURE__ */ jsx(Icon, { name: detail.icon }), detail.text]
1695
- }, detail.text))
1696
1706
  })
1697
1707
  ]
1708
+ }), /* @__PURE__ */ jsxs("div", {
1709
+ className: cls("emergency-confirmation__footer"),
1710
+ children: [/* @__PURE__ */ jsx("button", {
1711
+ type: "button",
1712
+ className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
1713
+ onClick: danger ? onCancelAbort : onCancelSimulation,
1714
+ children: danger ? copy.abortCancel : copy.simulateCancel
1715
+ }), /* @__PURE__ */ jsx("button", {
1716
+ type: "button",
1717
+ className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
1718
+ onClick: danger ? onConfirmAbort : onConfirmSimulation,
1719
+ children: danger ? copy.abortConfirm : copy.simulateConfirm
1720
+ })]
1698
1721
  })]
1699
- }), children]
1700
- });
1701
- }
1702
-
1703
- // src/react/components/EmergencyError/index.tsx
1704
- function EmergencyError({ error, onRetry }) {
1705
- return /* @__PURE__ */ jsx(MessageSurface, {
1706
- variant: "error",
1707
- icon: "exclamation-circle",
1708
- title: copy.errorTitle,
1709
- text: error.message || copy.errorText,
1710
- details: [{
1711
- icon: "info",
1712
- text: copy.errorDetailInfo
1713
- }, {
1714
- icon: "arrow-left",
1715
- text: copy.errorDetailRetry
1716
- }],
1717
- children: /* @__PURE__ */ jsx("button", {
1718
- type: "button",
1719
- className: cls("emergency-feedback__dismiss"),
1720
- onClick: onRetry,
1721
- children: copy.retry
1722
1722
  })
1723
1723
  });
1724
1724
  }
1725
1725
 
1726
- // src/react/components/EmergencyFeedback/index.tsx
1727
- /** Time the feedback stays on screen before dismissing itself. */
1728
- const FEEDBACK_AUTO_DISMISS_MS = 3e3;
1729
- /** Time the geolocation feedback stays on screen before dismissing itself. */
1730
- const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
1731
- function geolocationText(error) {
1732
- switch (error.code) {
1733
- case "geolocation_denied": return copy.geolocationDenied;
1734
- case "geolocation_timeout": return copy.geolocationTimeout;
1735
- case "geolocation_unsupported": return copy.geolocationUnsupported;
1736
- default: return copy.geolocationUnavailable;
1737
- }
1738
- }
1739
- function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
1740
- const result = abortResult ?? simulationResult;
1741
- const feedbackActivationError = result ? null : activationError ?? null;
1742
- const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
1743
- if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
1744
- return /* @__PURE__ */ jsx(FeedbackSurface, {
1745
- result,
1746
- aborting: abortResult !== null,
1747
- geolocationError: feedbackGeolocationError,
1748
- activationError: feedbackActivationError,
1749
- onDismiss
1750
- });
1751
- }
1752
- function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
1753
- const dismissRef = useRef(onDismiss);
1726
+ // src/react/hooks/useEscapeKey.ts
1727
+ function useEscapeKey(active, onEscape) {
1754
1728
  useEffect(() => {
1755
- dismissRef.current = onDismiss;
1756
- }, [onDismiss]);
1729
+ if (!active || typeof document === "undefined") return;
1730
+ const handleKeyDown = (event) => {
1731
+ if (event.key === "Escape") {
1732
+ event.stopPropagation();
1733
+ onEscape();
1734
+ }
1735
+ };
1736
+ document.addEventListener("keydown", handleKeyDown);
1737
+ return () => {
1738
+ document.removeEventListener("keydown", handleKeyDown);
1739
+ };
1740
+ }, [active, onEscape]);
1741
+ }
1742
+
1743
+ // src/react/hooks/useFocusTrap.ts
1744
+ const FOCUSABLE_SELECTOR = [
1745
+ "a[href]",
1746
+ "button:not([disabled])",
1747
+ "input:not([disabled])",
1748
+ "select:not([disabled])",
1749
+ "textarea:not([disabled])",
1750
+ "[tabindex]:not([tabindex=\"-1\"])"
1751
+ ].join(",");
1752
+ /**
1753
+ * Keeps keyboard focus inside the dialog while it is open and restores focus to
1754
+ * the element that opened it when it closes.
1755
+ */
1756
+ function useFocusTrap(ref, active) {
1757
1757
  useEffect(() => {
1758
- const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
1759
- return () => clearTimeout(timeout);
1760
- }, [
1761
- result?.at,
1762
- geolocationError,
1763
- activationError
1764
- ]);
1765
- const success = result?.ok === true;
1766
- const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
1767
- const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
1768
- const details = success ? aborting ? [{
1769
- icon: "message",
1770
- text: copy.abortSuccessDetails
1771
- }] : [{
1772
- icon: "message",
1758
+ if (!active || typeof document === "undefined") return;
1759
+ const container = ref.current;
1760
+ if (!container) return;
1761
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1762
+ const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
1763
+ (focusable()[0] ?? container).focus({ preventScroll: true });
1764
+ const handleKeyDown = (event) => {
1765
+ if (event.key !== "Tab") return;
1766
+ const items = focusable();
1767
+ if (items.length === 0) {
1768
+ event.preventDefault();
1769
+ container.focus({ preventScroll: true });
1770
+ return;
1771
+ }
1772
+ const first = items[0];
1773
+ const last = items[items.length - 1];
1774
+ const current = document.activeElement;
1775
+ if (event.shiftKey && (current === first || !container.contains(current))) {
1776
+ event.preventDefault();
1777
+ if (last) last.focus({ preventScroll: true });
1778
+ return;
1779
+ }
1780
+ if (!event.shiftKey && (current === last || !container.contains(current)) && first) {
1781
+ event.preventDefault();
1782
+ first.focus({ preventScroll: true });
1783
+ }
1784
+ };
1785
+ document.addEventListener("keydown", handleKeyDown, true);
1786
+ return () => {
1787
+ document.removeEventListener("keydown", handleKeyDown, true);
1788
+ if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
1789
+ };
1790
+ }, [active, ref]);
1791
+ }
1792
+
1793
+ // src/react/screens/context.tsx
1794
+ const EmergencyScreenContext = createContext(null);
1795
+ function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1796
+ const [state, setState] = useState({
1797
+ screen: initialScreen,
1798
+ history: []
1799
+ });
1800
+ const navigate = useCallback((next) => {
1801
+ setState((current) => {
1802
+ if (current.screen === next) return current;
1803
+ return {
1804
+ screen: next,
1805
+ history: [...current.history, current.screen]
1806
+ };
1807
+ });
1808
+ }, []);
1809
+ const back = useCallback(() => {
1810
+ setState((current) => {
1811
+ const previous = current.history.at(-1);
1812
+ if (!previous) return current;
1813
+ return {
1814
+ screen: previous,
1815
+ history: current.history.slice(0, -1)
1816
+ };
1817
+ });
1818
+ }, []);
1819
+ const reset = useCallback(() => {
1820
+ setState((current) => {
1821
+ if (current.screen === "primary" && current.history.length === 0) return current;
1822
+ return {
1823
+ screen: "primary",
1824
+ history: []
1825
+ };
1826
+ });
1827
+ }, []);
1828
+ const value = useMemo(() => ({
1829
+ screen: state.screen,
1830
+ history: state.history,
1831
+ navigate,
1832
+ back,
1833
+ reset
1834
+ }), [
1835
+ back,
1836
+ navigate,
1837
+ reset,
1838
+ state.history,
1839
+ state.screen
1840
+ ]);
1841
+ return /* @__PURE__ */ jsx(EmergencyScreenContext.Provider, {
1842
+ value,
1843
+ children
1844
+ });
1845
+ }
1846
+ function useScreenNavigation() {
1847
+ const value = useContext(EmergencyScreenContext);
1848
+ if (!value) throw new Error("useScreenNavigation must be used inside EmergencyScreenProvider.");
1849
+ return value;
1850
+ }
1851
+
1852
+ // src/react/components/EmergencyHeader/index.tsx
1853
+ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack }) {
1854
+ return /* @__PURE__ */ jsxs("header", {
1855
+ className: cls("emergency-panel__header"),
1856
+ children: [
1857
+ onBack ? /* @__PURE__ */ jsx("button", {
1858
+ type: "button",
1859
+ className: cls("emergency-panel__close-button"),
1860
+ "aria-label": copy.back,
1861
+ onClick: onBack,
1862
+ children: /* @__PURE__ */ jsx(Icon, {
1863
+ name: "arrow-left",
1864
+ className: cls("emergency-panel__close-icon")
1865
+ })
1866
+ }) : null,
1867
+ /* @__PURE__ */ jsx("h2", {
1868
+ className: cls("emergency-panel__title"),
1869
+ id: titleId,
1870
+ tabIndex: -1,
1871
+ children: title
1872
+ }),
1873
+ /* @__PURE__ */ jsx("button", {
1874
+ type: "button",
1875
+ className: cls("emergency-panel__close-button"),
1876
+ "aria-label": copy.close,
1877
+ onClick: onClose,
1878
+ children: /* @__PURE__ */ jsx(Icon, {
1879
+ name: "close",
1880
+ className: cls("emergency-panel__close-icon")
1881
+ })
1882
+ })
1883
+ ]
1884
+ });
1885
+ }
1886
+
1887
+ // src/react/screens/layout.tsx
1888
+ /**
1889
+ * Shared shell for every screen. The wrapper is `display: contents` so the
1890
+ * header, body and footer stay direct grid rows of the emergency panel.
1891
+ */
1892
+ function EmergencyScreenLayout({ screen, titleId, header, footer, children, bodyClassName }) {
1893
+ return /* @__PURE__ */ jsxs("div", {
1894
+ className: cls("emergency-screen"),
1895
+ "data-screen": screen,
1896
+ children: [
1897
+ header,
1898
+ /* @__PURE__ */ jsx("section", {
1899
+ className: cls("emergency-panel__body", bodyClassName ?? null),
1900
+ "aria-labelledby": titleId,
1901
+ children
1902
+ }),
1903
+ footer
1904
+ ]
1905
+ });
1906
+ }
1907
+
1908
+ // src/react/screens/PlaceholderScreen.tsx
1909
+ /** Minimal in-design screen used to prove navigation before real designs land. */
1910
+ function PlaceholderScreen({ screen, titleId, title, description, onClose, onBack }) {
1911
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
1912
+ screen,
1913
+ titleId,
1914
+ header: /* @__PURE__ */ jsx(EmergencyHeader, {
1915
+ titleId,
1916
+ title,
1917
+ onClose,
1918
+ onBack
1919
+ }),
1920
+ footer: null,
1921
+ children: /* @__PURE__ */ jsx("div", {
1922
+ className: cls("emergency-screen__placeholder"),
1923
+ children: /* @__PURE__ */ jsx("p", {
1924
+ className: cls("emergency-screen__placeholder-text"),
1925
+ children: description
1926
+ })
1927
+ })
1928
+ });
1929
+ }
1930
+
1931
+ // src/react/screens/details/copy.ts
1932
+ const detailsCopy = {
1933
+ title: "Detalles",
1934
+ description: "Aquí podrás consultar el detalle de tus alertas disponibles. Esta pantalla estará disponible próximamente."
1935
+ };
1936
+
1937
+ // src/react/screens/details/index.tsx
1938
+ function DetailsScreen({ titleId, onClose }) {
1939
+ const { back } = useScreenNavigation();
1940
+ return /* @__PURE__ */ jsx(PlaceholderScreen, {
1941
+ screen: "details",
1942
+ titleId,
1943
+ title: detailsCopy.title,
1944
+ description: detailsCopy.description,
1945
+ onClose: () => onClose("close"),
1946
+ onBack: back
1947
+ });
1948
+ }
1949
+
1950
+ // src/react/components/icons/paramedical-mark.tsx
1951
+ /**
1952
+ * Neutral inline illustration used by the idle panel.
1953
+ */
1954
+ function ParamedicalMark({ className, title }) {
1955
+ return /* @__PURE__ */ jsxs("svg", {
1956
+ xmlns: "http://www.w3.org/2000/svg",
1957
+ version: "1.0",
1958
+ viewBox: "0 0 192 192",
1959
+ width: "192",
1960
+ height: "192",
1961
+ id: "Star of life",
1962
+ className,
1963
+ "aria-label": title,
1964
+ children: [
1965
+ /* @__PURE__ */ jsx("defs", { id: "defs2886" }),
1966
+ /* @__PURE__ */ jsxs("g", {
1967
+ id: "cross",
1968
+ children: [/* @__PURE__ */ jsx("path", {
1969
+ 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",
1970
+ id: "outline",
1971
+ style: {
1972
+ fill: "none",
1973
+ stroke: "#3333bb",
1974
+ strokeWidth: 6
1975
+ }
1976
+ }), /* @__PURE__ */ jsx("path", {
1977
+ 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",
1978
+ id: "inner",
1979
+ style: {
1980
+ fill: "#0000bb",
1981
+ fillRule: "evenodd",
1982
+ stroke: "#ffffff",
1983
+ strokeWidth: 2
1984
+ }
1985
+ })]
1986
+ }),
1987
+ /* @__PURE__ */ jsxs("g", {
1988
+ id: "symbols",
1989
+ style: {
1990
+ fill: "#ffffff",
1991
+ fillRule: "evenodd",
1992
+ stroke: "#3333bb",
1993
+ strokeWidth: 2
1994
+ },
1995
+ children: [/* @__PURE__ */ jsx("path", {
1996
+ 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",
1997
+ id: "rod"
1998
+ }), /* @__PURE__ */ jsxs("g", {
1999
+ id: "snake",
2000
+ children: [/* @__PURE__ */ jsx("path", {
2001
+ 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",
2002
+ id: "body"
2003
+ }), /* @__PURE__ */ jsx("path", {
2004
+ d: "m 95,43 c 7,-3 -7,-3 0,0 z",
2005
+ id: "eye",
2006
+ style: {
2007
+ fill: "#3333bb",
2008
+ fillRule: "evenodd",
2009
+ stroke: "none"
2010
+ }
2011
+ })]
2012
+ })]
2013
+ })
2014
+ ]
2015
+ });
2016
+ }
2017
+
2018
+ // src/react/components/MessageSurface/index.tsx
2019
+ function MessageSurface(props) {
2020
+ const { variant, icon, title, text, details, className, children } = props;
2021
+ const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
2022
+ const success = variant === "success";
2023
+ return /* @__PURE__ */ jsxs("div", {
2024
+ className: [cls(prefix), className].filter(Boolean).join(" "),
2025
+ role: success ? "status" : "alert",
2026
+ "aria-live": success ? "polite" : "assertive",
2027
+ children: [/* @__PURE__ */ jsxs("div", {
2028
+ className: cls(`${prefix}__container`),
2029
+ children: [/* @__PURE__ */ jsx("div", {
2030
+ className: cls(`${prefix}__icon`),
2031
+ children: /* @__PURE__ */ jsx(Icon, {
2032
+ name: icon,
2033
+ className: "status-icon"
2034
+ })
2035
+ }), /* @__PURE__ */ jsxs("div", {
2036
+ className: cls(`${prefix}__content`),
2037
+ children: [
2038
+ /* @__PURE__ */ jsx("h3", {
2039
+ className: cls(`${prefix}__title`),
2040
+ children: title
2041
+ }),
2042
+ /* @__PURE__ */ jsx("p", {
2043
+ className: cls(`${prefix}__text`),
2044
+ children: text
2045
+ }),
2046
+ /* @__PURE__ */ jsx("div", {
2047
+ className: cls(`${prefix}__details`),
2048
+ children: details.map((detail) => /* @__PURE__ */ jsxs("span", {
2049
+ className: cls(`${prefix}__detail`),
2050
+ children: [/* @__PURE__ */ jsx(Icon, { name: detail.icon }), detail.text]
2051
+ }, detail.text))
2052
+ })
2053
+ ]
2054
+ })]
2055
+ }), children]
2056
+ });
2057
+ }
2058
+
2059
+ // src/react/components/EmergencyError/index.tsx
2060
+ function EmergencyError({ error, onRetry }) {
2061
+ return /* @__PURE__ */ jsx(MessageSurface, {
2062
+ variant: "error",
2063
+ icon: "exclamation-circle",
2064
+ title: copy.errorTitle,
2065
+ text: error.message || copy.errorText,
2066
+ details: [{
2067
+ icon: "info",
2068
+ text: copy.errorDetailInfo
2069
+ }, {
2070
+ icon: "arrow-left",
2071
+ text: copy.errorDetailRetry
2072
+ }],
2073
+ children: /* @__PURE__ */ jsx("button", {
2074
+ type: "button",
2075
+ className: cls("emergency-feedback__dismiss"),
2076
+ onClick: onRetry,
2077
+ children: copy.retry
2078
+ })
2079
+ });
2080
+ }
2081
+
2082
+ // src/react/components/EmergencyFeedback/index.tsx
2083
+ /** Time the feedback stays on screen before dismissing itself. */
2084
+ const FEEDBACK_AUTO_DISMISS_MS = 3e3;
2085
+ /** Time the geolocation feedback stays on screen before dismissing itself. */
2086
+ const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
2087
+ function geolocationText(error) {
2088
+ switch (error.code) {
2089
+ case "geolocation_denied": return copy.geolocationDenied;
2090
+ case "geolocation_timeout": return copy.geolocationTimeout;
2091
+ case "geolocation_unsupported": return copy.geolocationUnsupported;
2092
+ default: return copy.geolocationUnavailable;
2093
+ }
2094
+ }
2095
+ function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
2096
+ const result = abortResult ?? simulationResult;
2097
+ const feedbackActivationError = result ? null : activationError ?? null;
2098
+ const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
2099
+ if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
2100
+ return /* @__PURE__ */ jsx(FeedbackSurface, {
2101
+ result,
2102
+ aborting: abortResult !== null,
2103
+ geolocationError: feedbackGeolocationError,
2104
+ activationError: feedbackActivationError,
2105
+ onDismiss
2106
+ });
2107
+ }
2108
+ function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
2109
+ const dismissRef = useRef(onDismiss);
2110
+ useEffect(() => {
2111
+ dismissRef.current = onDismiss;
2112
+ }, [onDismiss]);
2113
+ useEffect(() => {
2114
+ const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
2115
+ return () => clearTimeout(timeout);
2116
+ }, [
2117
+ result?.at,
2118
+ geolocationError,
2119
+ activationError
2120
+ ]);
2121
+ const success = result?.ok === true;
2122
+ const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
2123
+ const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
2124
+ const details = success ? aborting ? [{
2125
+ icon: "message",
2126
+ text: copy.abortSuccessDetails
2127
+ }] : [{
2128
+ icon: "message",
1773
2129
  text: copy.simulateSuccessDetails
1774
2130
  }, {
1775
2131
  icon: "clock",
@@ -1983,101 +2339,129 @@ function EmergencyStatusPanel({ responders, refreshing, locale, onRefresh }) {
1983
2339
  });
1984
2340
  }
1985
2341
 
1986
- // src/react/components/EmergencyBody/index.tsx
1987
- function EmergencyBody(props) {
1988
- const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
1989
- const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
1990
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1991
- !loading && !showingFeedback ? /* @__PURE__ */ jsx("div", {
1992
- className: cls("sos-img__container"),
1993
- id: "sos-img__container",
1994
- children: /* @__PURE__ */ jsx(ParamedicalMark, {
1995
- className: cls("sos-img"),
1996
- title: copy.illustrationAlt
1997
- })
1998
- }) : null,
1999
- active || loading || showingFeedback ? null : /* @__PURE__ */ jsx("div", {
2000
- className: cls("emergency-panel__body-title-container"),
2001
- children: /* @__PURE__ */ jsx("h4", {
2002
- className: cls("emergency-panel__body-title"),
2003
- children: copy.question
2004
- })
2005
- }),
2006
- active && !showingFeedback ? /* @__PURE__ */ jsx(EmergencyStatusPanel, {
2007
- responders,
2008
- refreshing,
2009
- locale,
2010
- onRefresh
2011
- }) : null,
2012
- loading ? /* @__PURE__ */ jsx(EmergencyLoading, {}) : null,
2013
- error ? /* @__PURE__ */ jsx(EmergencyError, {
2014
- error,
2015
- onRetry
2016
- }) : null,
2017
- /* @__PURE__ */ jsx(EmergencyFeedback, {
2018
- abortResult,
2019
- simulationResult,
2020
- geolocationError,
2021
- activationError,
2022
- onDismiss: onDismissFeedback
2023
- })
2024
- ] });
2342
+ // src/react/screens/primary/copy.ts
2343
+ const primaryCopy = {
2344
+ availabilityTitle: "Alertas Disponibles",
2345
+ recharge: "Recargar",
2346
+ details: "Detalles",
2347
+ roundSingular: "ronda disponible",
2348
+ roundPlural: "rondas disponibles"
2349
+ };
2350
+ /** Singular/plural label for the remaining rounds. */
2351
+ function roundsUnit(rounds) {
2352
+ return rounds === 1 ? primaryCopy.roundSingular : primaryCopy.roundPlural;
2353
+ }
2354
+ function formatRounds(rounds) {
2355
+ return `${rounds} ${roundsUnit(rounds)}`;
2025
2356
  }
2026
2357
 
2027
- // src/react/components/EmergencyConfirmation/index.tsx
2028
- function EmergencyConfirmation(props) {
2029
- const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2030
- const cardRef = useRef(null);
2031
- const titleId = useId();
2032
- const danger = abortConfirmation;
2033
- const open = abortConfirmation || simulationConfirmation;
2034
- useEffect(() => {
2035
- if (open) cardRef.current?.querySelector("button")?.focus();
2036
- }, [open]);
2037
- if (!open) return null;
2038
- const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
2039
- const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
2040
- const tone = danger ? "danger" : "information";
2041
- return /* @__PURE__ */ jsx("div", {
2042
- className: cls("emergency-confirmation"),
2043
- role: "dialog",
2044
- "aria-modal": "true",
2045
- "aria-labelledby": titleId,
2046
- children: /* @__PURE__ */ jsxs("div", {
2047
- ref: cardRef,
2048
- className: cls("emergency-confirmation__card"),
2049
- children: [/* @__PURE__ */ jsxs("div", {
2050
- className: cls("emergency-confirmation__body"),
2051
- children: [
2052
- /* @__PURE__ */ jsx("div", {
2053
- className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
2054
- children: /* @__PURE__ */ jsx(Icon, { name: danger ? "alert-triangle" : "info" })
2055
- }),
2056
- /* @__PURE__ */ jsx("h5", {
2057
- className: cls("emergency-confirmation__title"),
2058
- id: titleId,
2059
- children: title
2060
- }),
2061
- /* @__PURE__ */ jsx("p", {
2062
- className: cls("emergency-confirmation__text"),
2063
- children: text
2064
- })
2065
- ]
2358
+ // src/react/screens/primary/data.ts
2359
+ /**
2360
+ * Derives the remaining alert rounds from available messages and designated
2361
+ * responders. Invalid or non-positive input collapses to the minimum of one
2362
+ * round instead of producing `Infinity`, `NaN` or a negative value.
2363
+ */
2364
+ function calculateRounds(messages, responders) {
2365
+ if (!Number.isFinite(messages) || !Number.isFinite(responders)) return 1;
2366
+ const availableMessages = Math.floor(messages);
2367
+ const designatedResponders = Math.floor(responders);
2368
+ if (availableMessages <= 0 || designatedResponders <= 0) return 1;
2369
+ return Math.max(1, Math.floor(availableMessages / designatedResponders));
2370
+ }
2371
+
2372
+ // src/react/screens/primary/Availability.tsx
2373
+ function Availability({ onRecharge, onDetails }) {
2374
+ const rounds = calculateRounds(100, 3);
2375
+ return /* @__PURE__ */ jsxs("section", {
2376
+ className: cls("emergency-availability"),
2377
+ "aria-label": primaryCopy.availabilityTitle,
2378
+ children: [/* @__PURE__ */ jsxs("button", {
2379
+ type: "button",
2380
+ className: cls("emergency-availability__recharge"),
2381
+ onClick: onRecharge,
2382
+ children: [/* @__PURE__ */ jsx(Icon, { name: "credit-card-plus" }), primaryCopy.recharge]
2383
+ }), /* @__PURE__ */ jsxs("div", {
2384
+ className: cls("emergency-availability__card"),
2385
+ children: [/* @__PURE__ */ jsx("div", {
2386
+ className: cls("emergency-availability__card-header"),
2387
+ children: /* @__PURE__ */ jsx("h3", {
2388
+ className: cls("emergency-availability__card-title"),
2389
+ children: primaryCopy.availabilityTitle
2390
+ })
2066
2391
  }), /* @__PURE__ */ jsxs("div", {
2067
- className: cls("emergency-confirmation__footer"),
2068
- children: [/* @__PURE__ */ jsx("button", {
2069
- type: "button",
2070
- className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2071
- onClick: danger ? onCancelAbort : onCancelSimulation,
2072
- children: danger ? copy.abortCancel : copy.simulateCancel
2392
+ className: cls("emergency-availability__card-body"),
2393
+ children: [/* @__PURE__ */ jsxs("div", {
2394
+ className: cls("emergency-availability__rounds-group"),
2395
+ "aria-label": formatRounds(rounds),
2396
+ children: [/* @__PURE__ */ jsx("p", {
2397
+ className: cls("emergency-availability__rounds"),
2398
+ children: rounds
2399
+ }), /* @__PURE__ */ jsx("p", {
2400
+ className: cls("emergency-availability__label"),
2401
+ children: roundsUnit(rounds)
2402
+ })]
2073
2403
  }), /* @__PURE__ */ jsx("button", {
2074
2404
  type: "button",
2075
- className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2076
- onClick: danger ? onConfirmAbort : onConfirmSimulation,
2077
- children: danger ? copy.abortConfirm : copy.simulateConfirm
2405
+ className: cls("emergency-availability__details"),
2406
+ onClick: onDetails,
2407
+ children: primaryCopy.details
2078
2408
  })]
2079
2409
  })]
2080
- })
2410
+ })]
2411
+ });
2412
+ }
2413
+
2414
+ // src/react/components/EmergencyBody/index.tsx
2415
+ function EmergencyBody(props) {
2416
+ const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback, onRecharge, onDetails } = props;
2417
+ const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2418
+ const idle = !active && !loading && !showingFeedback && error === null;
2419
+ return /* @__PURE__ */ jsxs("div", {
2420
+ className: cls("emergency-panel__body-split", idle && "emergency-panel__body-split--with-availability"),
2421
+ children: [idle ? /* @__PURE__ */ jsx("div", {
2422
+ className: cls("emergency-panel__availability-region"),
2423
+ children: /* @__PURE__ */ jsx(Availability, {
2424
+ onRecharge,
2425
+ onDetails
2426
+ })
2427
+ }) : null, /* @__PURE__ */ jsxs("div", {
2428
+ className: cls("emergency-panel__prompt-region"),
2429
+ children: [
2430
+ !loading && !showingFeedback ? /* @__PURE__ */ jsx("div", {
2431
+ className: cls("sos-img__container"),
2432
+ id: "sos-img__container",
2433
+ children: /* @__PURE__ */ jsx(ParamedicalMark, {
2434
+ className: cls("sos-img"),
2435
+ title: copy.illustrationAlt
2436
+ })
2437
+ }) : null,
2438
+ active || loading || showingFeedback ? null : /* @__PURE__ */ jsx("div", {
2439
+ className: cls("emergency-panel__body-title-container"),
2440
+ children: /* @__PURE__ */ jsx("h4", {
2441
+ className: cls("emergency-panel__body-title"),
2442
+ children: copy.question
2443
+ })
2444
+ }),
2445
+ active && !showingFeedback ? /* @__PURE__ */ jsx(EmergencyStatusPanel, {
2446
+ responders,
2447
+ refreshing,
2448
+ locale,
2449
+ onRefresh
2450
+ }) : null,
2451
+ loading ? /* @__PURE__ */ jsx(EmergencyLoading, {}) : null,
2452
+ error ? /* @__PURE__ */ jsx(EmergencyError, {
2453
+ error,
2454
+ onRetry
2455
+ }) : null,
2456
+ /* @__PURE__ */ jsx(EmergencyFeedback, {
2457
+ abortResult,
2458
+ simulationResult,
2459
+ geolocationError,
2460
+ activationError,
2461
+ onDismiss: onDismissFeedback
2462
+ })
2463
+ ]
2464
+ })]
2081
2465
  });
2082
2466
  }
2083
2467
 
@@ -2115,105 +2499,279 @@ function EmergencyFooter(props) {
2115
2499
  });
2116
2500
  }
2117
2501
 
2118
- // src/react/components/EmergencyHeader/index.tsx
2119
- function EmergencyHeader({ titleId, onClose }) {
2120
- return /* @__PURE__ */ jsxs("header", {
2121
- className: cls("emergency-panel__header"),
2122
- children: [/* @__PURE__ */ jsx("h2", {
2123
- className: cls("emergency-panel__title"),
2124
- id: titleId,
2125
- children: copy.panelLabel
2126
- }), /* @__PURE__ */ jsx("button", {
2127
- type: "button",
2128
- className: cls("emergency-panel__close-button"),
2129
- "aria-label": copy.close,
2130
- onClick: onClose,
2131
- children: /* @__PURE__ */ jsx(Icon, {
2132
- name: "close",
2133
- className: cls("emergency-panel__close-icon")
2134
- })
2135
- })]
2502
+ // src/react/screens/primary/index.tsx
2503
+ function PrimaryScreen(props) {
2504
+ const { navigate } = useScreenNavigation();
2505
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2506
+ screen: "primary",
2507
+ titleId: props.titleId,
2508
+ header: /* @__PURE__ */ jsx(EmergencyHeader, {
2509
+ titleId: props.titleId,
2510
+ onClose: () => props.onClose("close")
2511
+ }),
2512
+ footer: /* @__PURE__ */ jsx(EmergencyFooter, {
2513
+ active: props.active,
2514
+ busy: props.busy,
2515
+ aborting: props.aborting,
2516
+ feedbackVisible: props.feedbackVisible,
2517
+ onActivate: props.onActivate,
2518
+ onRequestAbort: props.onRequestAbort,
2519
+ onRequestSimulation: props.onRequestSimulation
2520
+ }),
2521
+ children: /* @__PURE__ */ jsx(EmergencyBody, {
2522
+ active: props.active,
2523
+ responders: props.responders,
2524
+ refreshing: props.refreshing,
2525
+ loading: props.loading,
2526
+ locale: props.locale,
2527
+ geolocationError: props.geolocationError,
2528
+ activationError: props.activationError,
2529
+ error: props.error,
2530
+ abortResult: props.abortResult,
2531
+ simulationResult: props.simulationResult,
2532
+ onRefresh: props.onRefresh,
2533
+ onRetry: props.onRetry,
2534
+ onDismissFeedback: props.onDismissFeedback,
2535
+ onRecharge: () => navigate("pricing"),
2536
+ onDetails: () => navigate("details")
2537
+ })
2136
2538
  });
2137
2539
  }
2138
2540
 
2139
- // src/react/hooks/useEscapeKey.ts
2140
- function useEscapeKey(active, onEscape) {
2141
- useEffect(() => {
2142
- if (!active || typeof document === "undefined") return;
2143
- const handleKeyDown = (event) => {
2144
- if (event.key === "Escape") {
2145
- event.stopPropagation();
2146
- onEscape();
2147
- }
2148
- };
2149
- document.addEventListener("keydown", handleKeyDown);
2150
- return () => {
2151
- document.removeEventListener("keydown", handleKeyDown);
2152
- };
2153
- }, [active, onEscape]);
2154
- }
2541
+ // src/react/screens/pricing/copy.ts
2542
+ const pricingCopy = {
2543
+ title: "Recargar",
2544
+ heading: "Elige tu paquete",
2545
+ subtitle: "Alertas de WhatsApp para tus contactos",
2546
+ bestValue: "MEJOR VALOR",
2547
+ choose: "Seleccionar",
2548
+ chosen: "Seleccionado",
2549
+ whatsapp: "de WhatsApp",
2550
+ rounds: "rondas de alerta",
2551
+ perMessage: "por mensaje + IVA",
2552
+ footnote: "Rondas calculadas con 3 contactos de emergencia. Válidas para pruebas y emergencias."
2553
+ };
2155
2554
 
2156
- // src/react/hooks/useFocusTrap.ts
2157
- const FOCUSABLE_SELECTOR = [
2158
- "a[href]",
2159
- "button:not([disabled])",
2160
- "input:not([disabled])",
2161
- "select:not([disabled])",
2162
- "textarea:not([disabled])",
2163
- "[tabindex]:not([tabindex=\"-1\"])"
2164
- ].join(",");
2555
+ // src/react/screens/pricing/data.ts
2165
2556
  /**
2166
- * Keeps keyboard focus inside the dialog while it is open and restores focus to
2167
- * the element that opened it when it closes.
2557
+ * Local demo catalog. Prices and per-message labels are literal values from the
2558
+ * mock; they are not derived from the package totals.
2168
2559
  */
2169
- function useFocusTrap(ref, active) {
2170
- useEffect(() => {
2171
- if (!active || typeof document === "undefined") return;
2172
- const container = ref.current;
2173
- if (!container) return;
2174
- const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2175
- const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
2176
- (focusable()[0] ?? container).focus({ preventScroll: true });
2177
- const handleKeyDown = (event) => {
2178
- if (event.key !== "Tab") return;
2179
- const items = focusable();
2180
- if (items.length === 0) {
2181
- event.preventDefault();
2182
- container.focus({ preventScroll: true });
2183
- return;
2184
- }
2185
- const first = items[0];
2186
- const last = items[items.length - 1];
2187
- const current = document.activeElement;
2188
- if (event.shiftKey && (current === first || !container.contains(current))) {
2189
- event.preventDefault();
2190
- if (last) last.focus({ preventScroll: true });
2191
- return;
2192
- }
2193
- if (!event.shiftKey && current === last && first) {
2194
- event.preventDefault();
2195
- first.focus({ preventScroll: true });
2196
- }
2197
- };
2198
- document.addEventListener("keydown", handleKeyDown, true);
2199
- return () => {
2200
- document.removeEventListener("keydown", handleKeyDown, true);
2201
- if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
2202
- };
2203
- }, [active, ref]);
2560
+ const PRICING_PACKAGES = [{
2561
+ id: "package-1",
2562
+ label: "PAQUETE 1",
2563
+ messages: 100,
2564
+ priceLabel: "$7",
2565
+ priceCaption: "IVA incluido",
2566
+ perMessageLabel: "$0.06",
2567
+ highlighted: false
2568
+ }, {
2569
+ id: "package-2",
2570
+ label: "PAQUETE 2",
2571
+ messages: 200,
2572
+ priceLabel: "12$",
2573
+ priceNote: "($10 + IVA)",
2574
+ priceCaption: "IVA incluido",
2575
+ perMessageLabel: "$0.05",
2576
+ highlighted: true,
2577
+ badge: "MEJOR VALOR"
2578
+ }];
2579
+ /** Derives the alert rounds a package provides for the demo responder count. */
2580
+ function packageRounds(pkg) {
2581
+ return calculateRounds(pkg.messages, 3);
2582
+ }
2583
+
2584
+ // src/react/screens/pricing/PackageCard.tsx
2585
+ function PackageCard({ pkg, selected, onSelect }) {
2586
+ const labelId = useId();
2587
+ const rounds = packageRounds(pkg);
2588
+ return /* @__PURE__ */ jsxs("section", {
2589
+ className: cls("emergency-pricing__package", pkg.highlighted && "emergency-pricing__package--highlighted"),
2590
+ "aria-labelledby": labelId,
2591
+ "data-selected": selected ? "true" : void 0,
2592
+ children: [
2593
+ /* @__PURE__ */ jsxs("div", {
2594
+ className: cls("emergency-pricing__package-header"),
2595
+ children: [/* @__PURE__ */ jsx("span", {
2596
+ className: cls("emergency-pricing__package-label"),
2597
+ id: labelId,
2598
+ children: pkg.label
2599
+ }), pkg.badge ? /* @__PURE__ */ jsx("span", {
2600
+ className: cls("emergency-pricing__badge"),
2601
+ children: pkg.badge
2602
+ }) : null]
2603
+ }),
2604
+ /* @__PURE__ */ jsxs("div", {
2605
+ className: cls("emergency-pricing__price-row"),
2606
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h4", {
2607
+ className: cls("emergency-pricing__package-name"),
2608
+ children: [pkg.messages, " alertas"]
2609
+ }), /* @__PURE__ */ jsx("p", {
2610
+ className: cls("emergency-pricing__package-caption"),
2611
+ children: pricingCopy.whatsapp
2612
+ })] }), /* @__PURE__ */ jsxs("div", {
2613
+ className: cls("emergency-pricing__price-meta"),
2614
+ children: [/* @__PURE__ */ jsx("span", {
2615
+ className: cls("emergency-pricing__price"),
2616
+ children: pkg.priceLabel
2617
+ }), /* @__PURE__ */ jsxs("span", {
2618
+ className: cls("emergency-pricing__price-note"),
2619
+ children: [pkg.priceCaption, pkg.priceNote ? ` ${pkg.priceNote}` : null]
2620
+ })]
2621
+ })]
2622
+ }),
2623
+ /* @__PURE__ */ jsxs("div", {
2624
+ className: cls("emergency-pricing__stats"),
2625
+ children: [
2626
+ /* @__PURE__ */ jsxs("div", {
2627
+ className: cls("emergency-pricing__stat"),
2628
+ children: [/* @__PURE__ */ jsx("p", {
2629
+ className: cls("emergency-pricing__stat-value"),
2630
+ children: rounds
2631
+ }), /* @__PURE__ */ jsx("p", {
2632
+ className: cls("emergency-pricing__stat-label"),
2633
+ children: pricingCopy.rounds
2634
+ })]
2635
+ }),
2636
+ /* @__PURE__ */ jsx("div", {
2637
+ className: cls("emergency-pricing__stat-divider"),
2638
+ "aria-hidden": "true"
2639
+ }),
2640
+ /* @__PURE__ */ jsxs("div", {
2641
+ className: cls("emergency-pricing__stat"),
2642
+ children: [/* @__PURE__ */ jsx("p", {
2643
+ className: cls("emergency-pricing__stat-value"),
2644
+ children: pkg.perMessageLabel
2645
+ }), /* @__PURE__ */ jsx("p", {
2646
+ className: cls("emergency-pricing__stat-label"),
2647
+ children: pricingCopy.perMessage
2648
+ })]
2649
+ })
2650
+ ]
2651
+ }),
2652
+ /* @__PURE__ */ jsx("button", {
2653
+ type: "button",
2654
+ className: cls("emergency-pricing__select"),
2655
+ "aria-pressed": selected,
2656
+ onClick: () => onSelect(pkg.id),
2657
+ children: selected ? pricingCopy.chosen : pricingCopy.choose
2658
+ })
2659
+ ]
2660
+ });
2204
2661
  }
2205
2662
 
2663
+ // src/react/screens/pricing/index.tsx
2664
+ function PricingScreen(props) {
2665
+ const { back } = useScreenNavigation();
2666
+ const [selected, setSelected] = useState(null);
2667
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2668
+ screen: "pricing",
2669
+ titleId: props.titleId,
2670
+ bodyClassName: "emergency-panel__body--pricing",
2671
+ header: /* @__PURE__ */ jsx(EmergencyHeader, {
2672
+ titleId: props.titleId,
2673
+ title: pricingCopy.title,
2674
+ onClose: () => props.onClose("close"),
2675
+ onBack: back
2676
+ }),
2677
+ footer: null,
2678
+ children: /* @__PURE__ */ jsxs("div", {
2679
+ className: cls("emergency-pricing"),
2680
+ children: [
2681
+ /* @__PURE__ */ jsx("div", {
2682
+ className: cls("emergency-pricing__heading"),
2683
+ children: /* @__PURE__ */ jsxs("div", {
2684
+ className: cls("emergency-pricing__titles"),
2685
+ children: [/* @__PURE__ */ jsx("h3", {
2686
+ className: cls("emergency-pricing__title"),
2687
+ children: pricingCopy.heading
2688
+ }), /* @__PURE__ */ jsx("p", {
2689
+ className: cls("emergency-pricing__subtitle"),
2690
+ children: pricingCopy.subtitle
2691
+ })]
2692
+ })
2693
+ }),
2694
+ PRICING_PACKAGES.map((pkg) => /* @__PURE__ */ jsx(PackageCard, {
2695
+ pkg,
2696
+ selected: selected === pkg.id,
2697
+ onSelect: setSelected
2698
+ }, pkg.id)),
2699
+ /* @__PURE__ */ jsx("p", {
2700
+ className: cls("emergency-pricing__footnote"),
2701
+ children: pricingCopy.footnote
2702
+ })
2703
+ ]
2704
+ })
2705
+ });
2706
+ }
2707
+
2708
+ // src/react/screens/registry.ts
2709
+ /** Every screen the panel can render. Add one entry to add a screen. */
2710
+ const screenRegistry = {
2711
+ primary: PrimaryScreen,
2712
+ pricing: PricingScreen,
2713
+ details: DetailsScreen
2714
+ };
2715
+
2206
2716
  // src/react/components/EmergencyPanel/index.tsx
2717
+ function ActiveScreen({ screenProps, open, forcePrimary }) {
2718
+ const { screen, reset } = useScreenNavigation();
2719
+ const shouldReset = !open || forcePrimary;
2720
+ const effectiveScreen = shouldReset ? "primary" : screen;
2721
+ const previousOpen = useRef(open);
2722
+ const previousScreen = useRef(effectiveScreen);
2723
+ useEffect(() => {
2724
+ if (shouldReset) reset();
2725
+ }, [reset, shouldReset]);
2726
+ useEffect(() => {
2727
+ const wasOpen = previousOpen.current;
2728
+ const previousEffectiveScreen = previousScreen.current;
2729
+ previousOpen.current = open;
2730
+ previousScreen.current = effectiveScreen;
2731
+ if (!open || !wasOpen || previousEffectiveScreen === effectiveScreen) return;
2732
+ document.getElementById(screenProps.titleId)?.focus({ preventScroll: true });
2733
+ }, [
2734
+ effectiveScreen,
2735
+ open,
2736
+ screenProps.titleId
2737
+ ]);
2738
+ const Screen = screenRegistry[effectiveScreen];
2739
+ return /* @__PURE__ */ jsx(Screen, { ...screenProps });
2740
+ }
2207
2741
  function EmergencyPanel(props) {
2208
2742
  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;
2209
2743
  const titleId = useId();
2210
2744
  const dialogRef = useRef(null);
2211
2745
  const feedbackVisible = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2746
+ const forcePrimary = active || loading || error !== null || feedbackVisible;
2212
2747
  const closeOnEscape = useCallback(() => {
2213
2748
  onClose("escape");
2214
2749
  }, [onClose]);
2215
2750
  useFocusTrap(dialogRef, open);
2216
2751
  useEscapeKey(open, closeOnEscape);
2752
+ const screenProps = {
2753
+ titleId,
2754
+ active,
2755
+ busy,
2756
+ aborting,
2757
+ loading,
2758
+ locale,
2759
+ responders,
2760
+ refreshing,
2761
+ geolocationError,
2762
+ activationError,
2763
+ error,
2764
+ abortResult,
2765
+ simulationResult,
2766
+ feedbackVisible,
2767
+ onClose,
2768
+ onActivate,
2769
+ onRefresh,
2770
+ onRetry,
2771
+ onDismissFeedback,
2772
+ onRequestAbort,
2773
+ onRequestSimulation
2774
+ };
2217
2775
  return /* @__PURE__ */ jsxs("div", {
2218
2776
  ref: dialogRef,
2219
2777
  className: cls("emergency-panel", !open && "emergency-panel--hidden"),
@@ -2224,48 +2782,18 @@ function EmergencyPanel(props) {
2224
2782
  "aria-hidden": !open,
2225
2783
  inert: !open,
2226
2784
  tabIndex: -1,
2227
- children: [
2228
- /* @__PURE__ */ jsx(EmergencyHeader, {
2229
- titleId,
2230
- onClose: () => onClose("close")
2231
- }),
2232
- /* @__PURE__ */ jsx("section", {
2233
- className: cls("emergency-panel__body"),
2234
- "aria-labelledby": titleId,
2235
- children: /* @__PURE__ */ jsx(EmergencyBody, {
2236
- active,
2237
- responders,
2238
- refreshing,
2239
- loading,
2240
- locale,
2241
- geolocationError,
2242
- activationError,
2243
- error,
2244
- abortResult,
2245
- simulationResult,
2246
- onRefresh,
2247
- onRetry,
2248
- onDismissFeedback
2249
- })
2250
- }),
2251
- /* @__PURE__ */ jsx(EmergencyFooter, {
2252
- active,
2253
- busy,
2254
- aborting,
2255
- feedbackVisible,
2256
- onActivate,
2257
- onRequestAbort,
2258
- onRequestSimulation
2259
- }),
2260
- /* @__PURE__ */ jsx(EmergencyConfirmation, {
2261
- abortConfirmation,
2262
- simulationConfirmation,
2263
- onCancelAbort,
2264
- onCancelSimulation,
2265
- onConfirmAbort,
2266
- onConfirmSimulation
2267
- })
2268
- ]
2785
+ children: [/* @__PURE__ */ jsx(EmergencyScreenProvider, { children: /* @__PURE__ */ jsx(ActiveScreen, {
2786
+ screenProps,
2787
+ open,
2788
+ forcePrimary
2789
+ }) }), /* @__PURE__ */ jsx(EmergencyConfirmation, {
2790
+ abortConfirmation,
2791
+ simulationConfirmation,
2792
+ onCancelAbort,
2793
+ onCancelSimulation,
2794
+ onConfirmAbort,
2795
+ onConfirmSimulation
2796
+ })]
2269
2797
  });
2270
2798
  }
2271
2799