@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/README.md +16 -9
- package/dist/iife/emergency.iife.global.js +24 -24
- package/dist/index.cjs +926 -398
- package/dist/index.d.cts +22 -14
- package/dist/index.d.ts +22 -14
- package/dist/index.js +927 -399
- package/dist/styles.css +418 -3
- package/package.json +1 -1
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"])
|
|
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"])
|
|
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.
|
|
621
|
+
if (typeof window === "undefined" || !config.key || !config.cluster) return null;
|
|
625
622
|
let pusher;
|
|
626
623
|
try {
|
|
627
|
-
pusher = new Pusher(config.
|
|
628
|
-
cluster: config.
|
|
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.
|
|
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(
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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/
|
|
1663
|
-
function
|
|
1664
|
-
const {
|
|
1665
|
-
const
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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("
|
|
1683
|
-
className: cls(
|
|
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(
|
|
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/
|
|
1727
|
-
|
|
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
|
-
|
|
1756
|
-
|
|
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
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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/
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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/
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
"
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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-
|
|
2068
|
-
children: [/* @__PURE__ */
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
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-
|
|
2076
|
-
onClick:
|
|
2077
|
-
children:
|
|
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/
|
|
2119
|
-
function
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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/
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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/
|
|
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
|
-
*
|
|
2167
|
-
*
|
|
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
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
|