@dataemergencia/emergency 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -436,19 +436,16 @@ const copy = {
436
436
  simulateSuccessDetails: "Mensajes enviados",
437
437
  simulateSuccessCompleted: "Proceso completado",
438
438
  feedbackDismiss: "Entendido",
439
- errorTitle: "Error",
439
+ errorTitle: "Ha ocurrido un error",
440
440
  errorText: "Ha ocurrido un error inesperado.",
441
441
  errorDetailInfo: "Revisa la información requerida",
442
442
  errorDetailRetry: "Intenta nuevamente",
443
443
  retry: "Reintentar",
444
444
  geolocationTitle: "No pudimos obtener tu ubicación",
445
- geolocationDenied: "Permiso de ubicación denegado. Activa el permiso o llama directamente.",
446
- geolocationUnavailable: "Tu ubicación no está disponible en este momento. Llama directamente.",
447
- geolocationTimeout: "La ubicación tardó demasiado. Inténtalo de nuevo o llama directamente.",
448
- geolocationUnsupported: "Este dispositivo no permite compartir la ubicación. Llama directamente.",
449
- fallbackTitle: "Llama directamente",
450
- fallbackDescription: "Si no puedes completar la emergencia desde aquí, llama a este número.",
451
- fallbackCall: "Llamar",
445
+ geolocationDenied: "Permiso de ubicación denegado. Habilita el acceso a la ubicación en tu navegador e inténtalo de nuevo.",
446
+ geolocationUnavailable: "Tu ubicación no está disponible en este momento. Verifica que el GPS esté activado e inténtalo de nuevo.",
447
+ geolocationTimeout: "La ubicación tardó demasiado. Muévete a un lugar con mejor señal e inténtalo de nuevo.",
448
+ geolocationUnsupported: "Este dispositivo no permite compartir la ubicación. Inténtalo desde otro navegador o dispositivo.",
452
449
  resumeTitle: "Emergencia Activa Detectada",
453
450
  resumeDescription: "Se ha detectado una emergencia activa en el sistema. Por favor, revisa el estado de la emergencia inmediatamente.",
454
451
  resumeAction: "Abrir Panel de Emergencia",
@@ -990,7 +987,6 @@ function createEmergencyController(deps) {
990
987
  geolocationError: normalized
991
988
  });
992
989
  transition({ kind: "ready" });
993
- pushMessage("error", normalized.message);
994
990
  events.emit("emergency:failed", {
995
991
  error: normalized,
996
992
  operation: "geolocation"
@@ -1294,16 +1290,87 @@ function createEmergencyController(deps) {
1294
1290
  if (!get().snapshot?.active) patch({ simulationConfirmation: true });
1295
1291
  },
1296
1292
  cancelSimulationConfirmation: () => patch({ simulationConfirmation: false }),
1297
- dismissFeedback: () => patch({
1298
- abortResult: null,
1299
- simulationResult: null
1300
- })
1293
+ dismissFeedback: () => {
1294
+ const state = get();
1295
+ patch({
1296
+ abortResult: null,
1297
+ simulationResult: null,
1298
+ geolocationError: null,
1299
+ ...state.geolocationError !== null || state.abortResult?.ok === false || state.simulationResult?.ok === false || state.phase.kind === "ready" && state.lastError !== null ? { lastError: null } : {}
1300
+ });
1301
+ }
1301
1302
  },
1302
1303
  store,
1303
1304
  events
1304
1305
  };
1305
1306
  }
1306
1307
 
1308
+ // src/core/fonts.ts
1309
+ /**
1310
+ * The exact Google Fonts query segment for each family. Family order matters:
1311
+ * with every family enabled the generated href is the documented stylesheet
1312
+ * URL, byte for byte.
1313
+ */
1314
+ const FONT_FAMILY_PARAMS = {
1315
+ googleSans: "family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700",
1316
+ montserrat: "family=Montserrat:ital,wght@0,100..900;1,100..900",
1317
+ poppins: "family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900"
1318
+ };
1319
+ const FONT_FAMILY_ORDER = [
1320
+ "googleSans",
1321
+ "montserrat",
1322
+ "poppins"
1323
+ ];
1324
+ const FONT_STYLESHEET_BASE = "https://fonts.googleapis.com/css2";
1325
+ const FONT_ORIGIN = "https://fonts.googleapis.com";
1326
+ const FONT_STATIC_ORIGIN = "https://fonts.gstatic.com";
1327
+ /** Marks every link the client injects so repeated mounts stay idempotent. */
1328
+ const FONT_LINK_ATTRIBUTE = "data-de-emergency-fonts";
1329
+ function normalizeFontLoading(input) {
1330
+ const flags = input?.fonts ?? {};
1331
+ return {
1332
+ load: input?.load ?? true,
1333
+ fonts: {
1334
+ googleSans: flags.googleSans ?? true,
1335
+ montserrat: flags.montserrat ?? true,
1336
+ poppins: flags.poppins ?? true
1337
+ }
1338
+ };
1339
+ }
1340
+ /**
1341
+ * Builds the stylesheet URL for the enabled families, or `null` when the
1342
+ * master switch is off or no family is enabled. A disabled family is left out
1343
+ * of the request entirely.
1344
+ */
1345
+ function buildFontStylesheetHref(input) {
1346
+ const { load, fonts } = normalizeFontLoading(input);
1347
+ if (!load) return null;
1348
+ const params = FONT_FAMILY_ORDER.filter((family) => fonts[family]).map((family) => FONT_FAMILY_PARAMS[family]);
1349
+ if (params.length === 0) return null;
1350
+ return `${FONT_STYLESHEET_BASE}?${params.join("&")}&display=swap`;
1351
+ }
1352
+ function createFontLink(rel, href, crossorigin = false) {
1353
+ const link = document.createElement("link");
1354
+ link.rel = rel;
1355
+ link.href = href;
1356
+ if (crossorigin) link.crossOrigin = "anonymous";
1357
+ link.setAttribute(FONT_LINK_ATTRIBUTE, "");
1358
+ return link;
1359
+ }
1360
+ /**
1361
+ * Injects the preconnect and stylesheet links the panel needs, once per page.
1362
+ * The first successful mount wins: later mounts, including mounts from a
1363
+ * duplicated copy of the library, reuse the links already present. The links
1364
+ * are page-level assets and are intentionally not removed on `destroy()`.
1365
+ */
1366
+ function installEmergencyFonts(input) {
1367
+ if (typeof document === "undefined") return;
1368
+ const href = buildFontStylesheetHref(input);
1369
+ if (href === null) return;
1370
+ if (document.querySelector(`link[${"data-de-emergency-fonts"}]`)) return;
1371
+ (document.head ?? document.documentElement).append(createFontLink("preconnect", FONT_ORIGIN), createFontLink("preconnect", FONT_STATIC_ORIGIN, true), createFontLink("stylesheet", href));
1372
+ }
1373
+
1307
1374
  // src/core/config.ts
1308
1375
  const EMERGENCY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9]{5}$/;
1309
1376
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
@@ -1375,44 +1442,85 @@ function validateConfig(config) {
1375
1442
  code,
1376
1443
  locale: config.locale ?? "es",
1377
1444
  geolocationTimeoutMs: config.geolocationTimeoutMs ?? 8e3,
1378
- requestTimeoutMs: config.requestTimeoutMs ?? 15e3
1445
+ requestTimeoutMs: config.requestTimeoutMs ?? 15e3,
1446
+ fontLoading: normalizeFontLoading(config.fontLoading)
1379
1447
  }
1380
1448
  };
1381
1449
  }
1382
1450
 
1383
- // src/styles/assets/paramedical-mark.tsx
1451
+ // src/react/components/icons/paramedical-mark.tsx
1384
1452
  /**
1385
1453
  * Neutral inline illustration used by the idle panel.
1386
1454
  */
1387
1455
  function ParamedicalMark({ className, title }) {
1388
1456
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1457
+ xmlns: "http://www.w3.org/2000/svg",
1458
+ version: "1.0",
1459
+ viewBox: "0 0 192 192",
1460
+ width: "192",
1461
+ height: "192",
1462
+ id: "Star of life",
1389
1463
  className,
1390
- viewBox: "0 0 96 96",
1391
- role: "img",
1392
1464
  "aria-label": title,
1393
- xmlns: "http://www.w3.org/2000/svg",
1394
1465
  children: [
1395
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1396
- cx: "48",
1397
- cy: "48",
1398
- r: "46",
1399
- fill: "#ff0000"
1400
- }),
1401
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1402
- cx: "48",
1403
- cy: "48",
1404
- r: "37",
1405
- fill: "#fff"
1466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { id: "defs2886" }),
1467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1468
+ id: "cross",
1469
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1470
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1471
+ id: "outline",
1472
+ style: {
1473
+ fill: "none",
1474
+ stroke: "#3333bb",
1475
+ strokeWidth: 6
1476
+ }
1477
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1478
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1479
+ id: "inner",
1480
+ style: {
1481
+ fill: "#0000bb",
1482
+ fillRule: "evenodd",
1483
+ stroke: "#ffffff",
1484
+ strokeWidth: 2
1485
+ }
1486
+ })]
1406
1487
  }),
1407
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1408
- d: "M42 26h12v16h16v12H54v16H42V54H26V42h16z",
1409
- fill: "#ff0000"
1488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1489
+ id: "symbols",
1490
+ style: {
1491
+ fill: "#ffffff",
1492
+ fillRule: "evenodd",
1493
+ stroke: "#3333bb",
1494
+ strokeWidth: 2
1495
+ },
1496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1497
+ d: "m 93,166 c 0,7 6,7 6,0 l 3,-138 c 0,-8 -12,-8 -12,0 l 3,135 0,3 z",
1498
+ id: "rod"
1499
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
1500
+ id: "snake",
1501
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1502
+ d: "m 100,153 c 7,4 3,15 -3,15 2,-5 3,-9 3,-15 z m 1,-40 c 10,9 4,22 -3,26 -6,3 -6,7 -6,15 -9,-10 -4,-19 4,-24 5,-4 5,-10 5,-17 z m 1,-30 c 0,11 -17,14 -17,22 0,3 1,9 6,12 0,-11 9,-12 15,-18 7,-7 7,-19 -4,-28 0,4 0,8 0,12 z M 90,77 C 80,69 76,55 80,46 c 3,-10 20,-11 20,-3 0,8 -20,3 -13,16 l 3,5 0,13 z",
1503
+ id: "body"
1504
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1505
+ d: "m 95,43 c 7,-3 -7,-3 0,0 z",
1506
+ id: "eye",
1507
+ style: {
1508
+ fill: "#3333bb",
1509
+ fillRule: "evenodd",
1510
+ stroke: "none"
1511
+ }
1512
+ })]
1513
+ })]
1410
1514
  })
1411
1515
  ]
1412
1516
  });
1413
1517
  }
1414
1518
 
1415
1519
  // src/react/components/Icon/index.tsx
1520
+ /** Icons whose artwork does not fit the default 24x24 grid need their own canvas. */
1521
+ const viewBoxes = { emergency: "0 0 640 640" };
1522
+ /** Icons rendered as a solid shape instead of a stroked outline. */
1523
+ const filledIcons = /* @__PURE__ */ new Set(["emergency"]);
1416
1524
  const paths = {
1417
1525
  close: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 6l12 12M18 6L6 18" }),
1418
1526
  "alert-triangle": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -1464,21 +1572,18 @@ const paths = {
1464
1572
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 7h.01" })
1465
1573
  ] }),
1466
1574
  "arrow-left": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 12H5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 19-7-7 7-7" })] }),
1467
- emergency: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 2v6M12 16v6M2 12h6M16 12h6" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1468
- cx: "12",
1469
- cy: "12",
1470
- r: "3"
1471
- })] })
1575
+ emergency: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M272.5 96C272.5 78.3 286.8 64 304.5 64L336.5 64C354.2 64 368.5 78.3 368.5 96L368.5 236.9L490.5 166.5C505.8 157.7 525.4 162.9 534.2 178.2L550.2 205.9C559 221.2 553.8 240.8 538.5 249.6L416.5 320L538.5 390.4C553.8 399.2 559.1 418.8 550.2 434.1L534.2 461.8C525.4 477.1 505.8 482.4 490.5 473.5L368.5 403.1L368.5 544C368.5 561.7 354.2 576 336.5 576L304.5 576C286.8 576 272.5 561.7 272.5 544L272.5 403.1L150.5 473.5C135.2 482.3 115.6 477.1 106.8 461.8L90.8 434.1C82 418.8 87.2 399.2 102.5 390.4L224.5 320L102.5 249.6C87.2 240.8 82 221.2 90.8 205.9L106.8 178.2C115.6 162.9 135.2 157.7 150.5 166.5L272.5 236.9L272.5 96z" }) })
1472
1576
  };
1473
1577
  function Icon({ name, className, title }) {
1578
+ const stroked = !filledIcons.has(name);
1474
1579
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1475
1580
  className,
1476
- viewBox: "0 0 24 24",
1477
- width: "1em",
1478
- height: "1em",
1479
- fill: "none",
1480
- stroke: "currentColor",
1481
- strokeWidth: "2",
1581
+ viewBox: viewBoxes[name] ?? "0 0 24 24",
1582
+ width: "1rem",
1583
+ height: "1rem",
1584
+ fill: stroked ? "none" : "currentColor",
1585
+ stroke: stroked ? "currentColor" : "none",
1586
+ strokeWidth: stroked ? 2 : void 0,
1482
1587
  strokeLinecap: "round",
1483
1588
  strokeLinejoin: "round",
1484
1589
  "aria-hidden": title ? void 0 : "true",
@@ -1497,14 +1602,16 @@ var panel_module_default = {
1497
1602
  "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-kMlO0G",
1498
1603
  "de-emergency-spin": "de-emergency-de-emergency-spin-kMlO0G",
1499
1604
  "emergency-confirmation": "de-emergency-emergency-confirmation-kMlO0G",
1500
- "emergency-confirmation__actions": "de-emergency-emergency-confirmation__actions-kMlO0G",
1605
+ "emergency-confirmation__body": "de-emergency-emergency-confirmation__body-kMlO0G",
1501
1606
  "emergency-confirmation__button": "de-emergency-emergency-confirmation__button-kMlO0G",
1502
1607
  "emergency-confirmation__button--cancel": "de-emergency-emergency-confirmation__button--cancel-kMlO0G",
1503
1608
  "emergency-confirmation__button--confirm": "de-emergency-emergency-confirmation__button--confirm-kMlO0G",
1504
1609
  "emergency-confirmation__button--danger": "de-emergency-emergency-confirmation__button--danger-kMlO0G",
1505
1610
  "emergency-confirmation__card": "de-emergency-emergency-confirmation__card-kMlO0G",
1506
- "emergency-confirmation__card--danger": "de-emergency-emergency-confirmation__card--danger-kMlO0G",
1611
+ "emergency-confirmation__footer": "de-emergency-emergency-confirmation__footer-kMlO0G",
1507
1612
  "emergency-confirmation__icon": "de-emergency-emergency-confirmation__icon-kMlO0G",
1613
+ "emergency-confirmation__icon--danger": "de-emergency-emergency-confirmation__icon--danger-kMlO0G",
1614
+ "emergency-confirmation__icon--information": "de-emergency-emergency-confirmation__icon--information-kMlO0G",
1508
1615
  "emergency-confirmation__text": "de-emergency-emergency-confirmation__text-kMlO0G",
1509
1616
  "emergency-confirmation__title": "de-emergency-emergency-confirmation__title-kMlO0G",
1510
1617
  "emergency-error-message": "de-emergency-emergency-error-message-kMlO0G",
@@ -1515,10 +1622,6 @@ var panel_module_default = {
1515
1622
  "emergency-error-message__icon": "de-emergency-emergency-error-message__icon-kMlO0G",
1516
1623
  "emergency-error-message__text": "de-emergency-emergency-error-message__text-kMlO0G",
1517
1624
  "emergency-error-message__title": "de-emergency-emergency-error-message__title-kMlO0G",
1518
- "emergency-fallback": "de-emergency-emergency-fallback-kMlO0G",
1519
- "emergency-fallback__phone": "de-emergency-emergency-fallback__phone-kMlO0G",
1520
- "emergency-fallback__text": "de-emergency-emergency-fallback__text-kMlO0G",
1521
- "emergency-fallback__title": "de-emergency-emergency-fallback__title-kMlO0G",
1522
1625
  "emergency-feedback": "de-emergency-emergency-feedback-kMlO0G",
1523
1626
  "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-kMlO0G",
1524
1627
  "emergency-panel": "de-emergency-emergency-panel-kMlO0G",
@@ -1550,15 +1653,6 @@ var panel_module_default = {
1550
1653
  "emergency-status__header": "de-emergency-emergency-status__header-kMlO0G",
1551
1654
  "emergency-status__loading": "de-emergency-emergency-status__loading-kMlO0G",
1552
1655
  "emergency-status__loading-text": "de-emergency-emergency-status__loading-text-kMlO0G",
1553
- "emergency-status__message-icon": "de-emergency-emergency-status__message-icon-kMlO0G",
1554
- "emergency-status__message-icon--failed": "de-emergency-emergency-status__message-icon--failed-kMlO0G",
1555
- "emergency-status__message-icon--success": "de-emergency-emergency-status__message-icon--success-kMlO0G",
1556
- "emergency-status__message-row": "de-emergency-emergency-status__message-row-kMlO0G",
1557
- "emergency-status__message-row--failed": "de-emergency-emergency-status__message-row--failed-kMlO0G",
1558
- "emergency-status__message-row--success": "de-emergency-emergency-status__message-row--success-kMlO0G",
1559
- "emergency-status__message-sent-at": "de-emergency-emergency-status__message-sent-at-kMlO0G",
1560
- "emergency-status__message-text": "de-emergency-emergency-status__message-text-kMlO0G",
1561
- "emergency-status__message-text-container": "de-emergency-emergency-status__message-text-container-kMlO0G",
1562
1656
  "emergency-status__panel": "de-emergency-emergency-status__panel-kMlO0G",
1563
1657
  "emergency-status__row": "de-emergency-emergency-status__row-kMlO0G",
1564
1658
  "emergency-status__row-container": "de-emergency-emergency-status__row-container-kMlO0G",
@@ -1569,7 +1663,6 @@ var panel_module_default = {
1569
1663
  "emergency-status-panel__refresh-icon": "de-emergency-emergency-status-panel__refresh-icon-kMlO0G",
1570
1664
  "emergency-status-panel__title": "de-emergency-emergency-status-panel__title-kMlO0G",
1571
1665
  "responder-name": "de-emergency-responder-name-kMlO0G",
1572
- "sent-at--verified": "de-emergency-sent-at--verified-kMlO0G",
1573
1666
  "simulacrum-success-message": "de-emergency-simulacrum-success-message-kMlO0G",
1574
1667
  "simulacrum-success-message__container": "de-emergency-simulacrum-success-message__container-kMlO0G",
1575
1668
  "simulacrum-success-message__content": "de-emergency-simulacrum-success-message__content-kMlO0G",
@@ -1608,7 +1701,10 @@ function MessageSurface(props) {
1608
1701
  className: cls(`${prefix}__container`),
1609
1702
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1610
1703
  className: cls(`${prefix}__icon`),
1611
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: icon })
1704
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1705
+ name: icon,
1706
+ className: "status-icon"
1707
+ })
1612
1708
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1613
1709
  className: cls(`${prefix}__content`),
1614
1710
  children: [
@@ -1656,8 +1752,12 @@ function EmergencyError({ error, onRetry }) {
1656
1752
  });
1657
1753
  }
1658
1754
 
1659
- // src/react/components/EmergencyFallback/index.tsx
1660
- function errorCopy(error) {
1755
+ // src/react/components/EmergencyFeedback/index.tsx
1756
+ /** Time the feedback stays on screen before dismissing itself. */
1757
+ const FEEDBACK_AUTO_DISMISS_MS = 3e3;
1758
+ /** Time the geolocation feedback stays on screen before dismissing itself. */
1759
+ const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
1760
+ function geolocationText(error) {
1661
1761
  switch (error.code) {
1662
1762
  case "geolocation_denied": return copy.geolocationDenied;
1663
1763
  case "geolocation_timeout": return copy.geolocationTimeout;
@@ -1665,34 +1765,35 @@ function errorCopy(error) {
1665
1765
  default: return copy.geolocationUnavailable;
1666
1766
  }
1667
1767
  }
1668
- function EmergencyFallback({ fallback, error }) {
1669
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1670
- className: cls("emergency-fallback"),
1671
- children: [
1672
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1673
- className: cls("emergency-fallback__title"),
1674
- children: copy.fallbackTitle
1675
- }),
1676
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1677
- className: cls("emergency-fallback__text"),
1678
- children: errorCopy(error)
1679
- }),
1680
- fallback ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
1681
- className: cls("emergency-fallback__phone"),
1682
- href: `tel:${fallback.phone.replace(/[^\d+]/g, "")}`,
1683
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "message" }), fallback.label]
1684
- }) : null
1685
- ]
1768
+ function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
1769
+ const result = abortResult ?? simulationResult;
1770
+ const feedbackActivationError = result ? null : activationError ?? null;
1771
+ const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
1772
+ if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
1773
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeedbackSurface, {
1774
+ result,
1775
+ aborting: abortResult !== null,
1776
+ geolocationError: feedbackGeolocationError,
1777
+ activationError: feedbackActivationError,
1778
+ onDismiss
1686
1779
  });
1687
1780
  }
1688
-
1689
- // src/react/components/EmergencyFeedback/index.tsx
1690
- function EmergencyFeedback({ abortResult, simulationResult, onDismiss }) {
1691
- const result = abortResult ?? simulationResult;
1692
- if (!result) return null;
1693
- const aborting = abortResult !== null;
1694
- const success = result.ok;
1781
+ function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
1782
+ const dismissRef = (0, react.useRef)(onDismiss);
1783
+ (0, react.useEffect)(() => {
1784
+ dismissRef.current = onDismiss;
1785
+ }, [onDismiss]);
1786
+ (0, react.useEffect)(() => {
1787
+ const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
1788
+ return () => clearTimeout(timeout);
1789
+ }, [
1790
+ result?.at,
1791
+ geolocationError,
1792
+ activationError
1793
+ ]);
1794
+ const success = result?.ok === true;
1695
1795
  const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
1796
+ const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
1696
1797
  const details = success ? aborting ? [{
1697
1798
  icon: "message",
1698
1799
  text: copy.abortSuccessDetails
@@ -1713,7 +1814,7 @@ function EmergencyFeedback({ abortResult, simulationResult, onDismiss }) {
1713
1814
  variant: success ? "success" : "error",
1714
1815
  icon: success ? "check-circle" : "alert-triangle",
1715
1816
  title,
1716
- text: result.message,
1817
+ text,
1717
1818
  details,
1718
1819
  className: cls("emergency-feedback"),
1719
1820
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1767,43 +1868,6 @@ function normalizePeriod(dayPeriod) {
1767
1868
  return /^a/i.test(dayPeriod) ? "am" : "";
1768
1869
  }
1769
1870
 
1770
- // src/react/components/EmergencyMessageRow/index.tsx
1771
- /**
1772
- * Renders one message row per responder.
1773
- */
1774
- function EmergencyMessageRow({ responder, locale }) {
1775
- const message = responder.lastMessage;
1776
- const hasContent = Boolean(message?.content);
1777
- const modifier = hasContent ? "emergency-status__message-row--success" : "emergency-status__message-row--failed";
1778
- const iconModifier = hasContent ? "emergency-status__message-icon--success" : "emergency-status__message-icon--failed";
1779
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1780
- className: cls("emergency-status__message-row", modifier),
1781
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1782
- className: cls("emergency-status__message-text-container"),
1783
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1784
- name: "flag",
1785
- className: cls("emergency-status__message-icon", iconModifier)
1786
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1787
- className: cls("emergency-status__message-text"),
1788
- children: [
1789
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: copy.statusPrefix }),
1790
- " ",
1791
- message?.content ?? ""
1792
- ]
1793
- })]
1794
- }), message ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1795
- className: cls("emergency-status__message-text-container", "sent-at--verified"),
1796
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1797
- name: "double-check",
1798
- className: cls("emergency-status__message-icon")
1799
- }), message.sentAt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1800
- className: cls("emergency-status__message-sent-at"),
1801
- children: formatShortTime(message.sentAt, locale)
1802
- }) : null]
1803
- }) : null]
1804
- });
1805
- }
1806
-
1807
1871
  // src/react/components/StatusIcon/index.tsx
1808
1872
  /**
1809
1873
  * Explicit responder state model:
@@ -1839,10 +1903,10 @@ function StatusCell({ confirmed, timestamp, failed = false, isFailedIcon = false
1839
1903
 
1840
1904
  // src/react/components/ResponderRow/index.tsx
1841
1905
  function ResponderRow({ responder, locale }) {
1842
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1906
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1843
1907
  className: cls("emergency-status__row-container"),
1844
1908
  "data-responder-id": responder.phone,
1845
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1909
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1846
1910
  className: cls("emergency-status__row"),
1847
1911
  children: [
1848
1912
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1872,10 +1936,7 @@ function ResponderRow({ responder, locale }) {
1872
1936
  locale
1873
1937
  })
1874
1938
  ]
1875
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyMessageRow, {
1876
- responder,
1877
- locale
1878
- })]
1939
+ })
1879
1940
  });
1880
1941
  }
1881
1942
 
@@ -1953,34 +2014,31 @@ function EmergencyStatusPanel({ responders, refreshing, locale, onRefresh }) {
1953
2014
 
1954
2015
  // src/react/components/EmergencyBody/index.tsx
1955
2016
  function EmergencyBody(props) {
1956
- const { active, responders, refreshing, loading, locale, geolocationError, error, fallback, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
2017
+ const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
2018
+ const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
1957
2019
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1958
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2020
+ !loading && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1959
2021
  className: cls("sos-img__container"),
1960
2022
  id: "sos-img__container",
1961
2023
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamedicalMark, {
1962
2024
  className: cls("sos-img"),
1963
2025
  title: copy.illustrationAlt
1964
2026
  })
1965
- }),
1966
- active ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2027
+ }) : null,
2028
+ active || loading || showingFeedback ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1967
2029
  className: cls("emergency-panel__body-title-container"),
1968
2030
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1969
2031
  className: cls("emergency-panel__body-title"),
1970
2032
  children: copy.question
1971
2033
  })
1972
2034
  }),
1973
- active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyStatusPanel, {
2035
+ active && !showingFeedback ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyStatusPanel, {
1974
2036
  responders,
1975
2037
  refreshing,
1976
2038
  locale,
1977
2039
  onRefresh
1978
2040
  }) : null,
1979
2041
  loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyLoading, {}) : null,
1980
- geolocationError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFallback, {
1981
- fallback,
1982
- error: geolocationError
1983
- }) : null,
1984
2042
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyError, {
1985
2043
  error,
1986
2044
  onRetry
@@ -1988,6 +2046,8 @@ function EmergencyBody(props) {
1988
2046
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyFeedback, {
1989
2047
  abortResult,
1990
2048
  simulationResult,
2049
+ geolocationError,
2050
+ activationError,
1991
2051
  onDismiss: onDismissFeedback
1992
2052
  })
1993
2053
  ] });
@@ -2006,6 +2066,7 @@ function EmergencyConfirmation(props) {
2006
2066
  if (!open) return null;
2007
2067
  const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
2008
2068
  const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
2069
+ const tone = danger ? "danger" : "information";
2009
2070
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2010
2071
  className: cls("emergency-confirmation"),
2011
2072
  role: "dialog",
@@ -2013,50 +2074,52 @@ function EmergencyConfirmation(props) {
2013
2074
  "aria-labelledby": titleId,
2014
2075
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2015
2076
  ref: cardRef,
2016
- className: cls("emergency-confirmation__card", danger && "emergency-confirmation__card--danger"),
2017
- children: [
2018
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2019
- className: cls("emergency-confirmation__icon"),
2020
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: danger ? "alert-triangle" : "info" })
2021
- }),
2022
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2023
- className: cls("emergency-confirmation__title"),
2024
- id: titleId,
2025
- children: title
2026
- }),
2027
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2028
- className: cls("emergency-confirmation__text"),
2029
- children: text
2030
- }),
2031
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2032
- className: cls("emergency-confirmation__actions"),
2033
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2034
- type: "button",
2035
- className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2036
- onClick: danger ? onCancelAbort : onCancelSimulation,
2037
- children: danger ? copy.abortCancel : copy.simulateCancel
2038
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2039
- type: "button",
2040
- className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2041
- onClick: danger ? onConfirmAbort : onConfirmSimulation,
2042
- children: danger ? copy.abortConfirm : copy.simulateConfirm
2043
- })]
2044
- })
2045
- ]
2077
+ className: cls("emergency-confirmation__card"),
2078
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2079
+ className: cls("emergency-confirmation__body"),
2080
+ children: [
2081
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2082
+ className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
2083
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: danger ? "alert-triangle" : "info" })
2084
+ }),
2085
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h5", {
2086
+ className: cls("emergency-confirmation__title"),
2087
+ id: titleId,
2088
+ children: title
2089
+ }),
2090
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2091
+ className: cls("emergency-confirmation__text"),
2092
+ children: text
2093
+ })
2094
+ ]
2095
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2096
+ className: cls("emergency-confirmation__footer"),
2097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2098
+ type: "button",
2099
+ className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2100
+ onClick: danger ? onCancelAbort : onCancelSimulation,
2101
+ children: danger ? copy.abortCancel : copy.simulateCancel
2102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2103
+ type: "button",
2104
+ className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2105
+ onClick: danger ? onConfirmAbort : onConfirmSimulation,
2106
+ children: danger ? copy.abortConfirm : copy.simulateConfirm
2107
+ })]
2108
+ })]
2046
2109
  })
2047
2110
  });
2048
2111
  }
2049
2112
 
2050
2113
  // src/react/components/EmergencyFooter/index.tsx
2051
2114
  function EmergencyFooter(props) {
2052
- const { active, busy, aborting, onActivate, onRequestAbort, onRequestSimulation } = props;
2115
+ const { active, busy, aborting, feedbackVisible, onActivate, onRequestAbort, onRequestSimulation } = props;
2053
2116
  if (active) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
2054
2117
  className: cls("emergency-panel__footer"),
2055
2118
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2056
2119
  type: "button",
2057
2120
  className: cls("emergency-status__abort-btn"),
2058
2121
  "aria-label": copy.abort,
2059
- disabled: aborting,
2122
+ disabled: aborting || feedbackVisible,
2060
2123
  onClick: onRequestAbort,
2061
2124
  children: [copy.abort, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "alert-triangle" })]
2062
2125
  })
@@ -2067,14 +2130,14 @@ function EmergencyFooter(props) {
2067
2130
  type: "button",
2068
2131
  className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
2069
2132
  "aria-label": copy.emergencyAria,
2070
- disabled: busy,
2133
+ disabled: busy || feedbackVisible,
2071
2134
  onClick: onActivate,
2072
2135
  children: [copy.emergency, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "emergency" })]
2073
2136
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2074
2137
  type: "button",
2075
2138
  className: cls("emergency-panel__button", "emergency-panel__button--simulation"),
2076
2139
  "aria-label": copy.testAria,
2077
- disabled: busy,
2140
+ disabled: busy || feedbackVisible,
2078
2141
  onClick: onRequestSimulation,
2079
2142
  children: [copy.test, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "list-check" })]
2080
2143
  })]
@@ -2171,9 +2234,10 @@ function useFocusTrap(ref, active) {
2171
2234
 
2172
2235
  // src/react/components/EmergencyPanel/index.tsx
2173
2236
  function EmergencyPanel(props) {
2174
- const { open, theme, active, busy, aborting, loading, locale, responders, refreshing, geolocationError, error, fallback, abortResult, simulationResult, abortConfirmation, simulationConfirmation, onClose, onActivate, onRefresh, onRetry, onDismissFeedback, onRequestAbort, onRequestSimulation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2237
+ 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;
2175
2238
  const titleId = (0, react.useId)();
2176
2239
  const dialogRef = (0, react.useRef)(null);
2240
+ const feedbackVisible = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2177
2241
  const closeOnEscape = (0, react.useCallback)(() => {
2178
2242
  onClose("escape");
2179
2243
  }, [onClose]);
@@ -2204,8 +2268,8 @@ function EmergencyPanel(props) {
2204
2268
  loading,
2205
2269
  locale,
2206
2270
  geolocationError,
2271
+ activationError,
2207
2272
  error,
2208
- fallback,
2209
2273
  abortResult,
2210
2274
  simulationResult,
2211
2275
  onRefresh,
@@ -2217,6 +2281,7 @@ function EmergencyPanel(props) {
2217
2281
  active,
2218
2282
  busy,
2219
2283
  aborting,
2284
+ feedbackVisible,
2220
2285
  onActivate,
2221
2286
  onRequestAbort,
2222
2287
  onRequestSimulation
@@ -2320,7 +2385,9 @@ function EmergencyRoot() {
2320
2385
  const simulationConfirmation = useEmergencyStore((state) => state.simulationConfirmation);
2321
2386
  const active = snapshot?.active === true;
2322
2387
  const busy = BUSY_PHASES.includes(phase.kind);
2323
- const showError = phase.kind === "error" || lastError !== null && geolocationError === null && abortResult === null && simulationResult === null;
2388
+ const feedbackActive = geolocationError !== null || abortResult !== null || simulationResult !== null;
2389
+ const activationError = phase.kind === "ready" && !feedbackActive ? lastError : null;
2390
+ const showError = phase.kind === "error" || lastError !== null && !feedbackActive && activationError === null;
2324
2391
  const error = phase.kind === "error" ? phase.error : lastError;
2325
2392
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmergencyResumeNotification, {
2326
2393
  resumeAvailable,
@@ -2336,8 +2403,8 @@ function EmergencyRoot() {
2336
2403
  responders,
2337
2404
  refreshing: phase.kind === "refreshing",
2338
2405
  geolocationError,
2406
+ activationError,
2339
2407
  error: showError ? error : null,
2340
- fallback: config.fallback ?? null,
2341
2408
  abortResult,
2342
2409
  simulationResult,
2343
2410
  abortConfirmation,
@@ -2462,6 +2529,7 @@ function mount(options) {
2462
2529
  const validation = validateConfig(options);
2463
2530
  if (!validation.ok || !validation.config) return reportConfigurationError(validation.error ?? createEmergencyError("configuration", "Invalid emergency configuration."), options);
2464
2531
  const config = validation.config;
2532
+ installEmergencyFonts(config.fontLoading);
2465
2533
  const controller = createEmergencyController({ config });
2466
2534
  const container = document.createElement("div");
2467
2535
  container.setAttribute("data-de-emergency-container", "");