@dataemergencia/emergency 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -734,6 +734,7 @@ function createEmergencyStore(config) {
734
734
  lastError: null,
735
735
  geolocation: null,
736
736
  geolocationError: null,
737
+ contactPhone: null,
737
738
  realtimeStatus: "idle",
738
739
  abortResult: null,
739
740
  simulationResult: null,
@@ -1176,7 +1177,8 @@ function createEmergencyController(deps) {
1176
1177
  patch({
1177
1178
  panelOpen: false,
1178
1179
  abortConfirmation: false,
1179
- simulationConfirmation: false
1180
+ simulationConfirmation: false,
1181
+ contactPhone: null
1180
1182
  });
1181
1183
  events.emit("panel:closed", { reason });
1182
1184
  }
@@ -1225,6 +1227,7 @@ function createEmergencyController(deps) {
1225
1227
  lastError: null,
1226
1228
  geolocation: null,
1227
1229
  geolocationError: null,
1230
+ contactPhone: null,
1228
1231
  realtimeStatus: "idle",
1229
1232
  abortResult: null,
1230
1233
  simulationResult: null,
@@ -1266,7 +1269,8 @@ function createEmergencyController(deps) {
1266
1269
  geolocationError: null,
1267
1270
  ...state.geolocationError !== null || state.abortResult?.ok === false || state.simulationResult?.ok === false || state.phase.kind === "ready" && state.lastError !== null ? { lastError: null } : {}
1268
1271
  });
1269
- }
1272
+ },
1273
+ setContactPhone: (phone) => patch({ contactPhone: phone })
1270
1274
  },
1271
1275
  store,
1272
1276
  events
@@ -1497,6 +1501,11 @@ const paths = {
1497
1501
  /* @__PURE__ */ jsx("path", { d: "M12 7h.01" })
1498
1502
  ] }),
1499
1503
  "arrow-left": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M19 12H5" }), /* @__PURE__ */ jsx("path", { d: "m12 19-7-7 7-7" })] }),
1504
+ "map-pin": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M12 22s7-6.2 7-12a7 7 0 1 0-14 0c0 5.8 7 12 7 12z" }), /* @__PURE__ */ jsx("circle", {
1505
+ cx: "12",
1506
+ cy: "10",
1507
+ r: "2.5"
1508
+ })] }),
1500
1509
  "credit-card-plus": /* @__PURE__ */ jsxs(Fragment, { children: [
1501
1510
  /* @__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
1511
  /* @__PURE__ */ jsx("path", { d: "M18 12h.01" }),
@@ -1538,6 +1547,12 @@ var panel_module_default = {
1538
1547
  "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-nA5Hwa",
1539
1548
  "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-nA5Hwa",
1540
1549
  "de-emergency-spin": "de-emergency-de-emergency-spin-nA5Hwa",
1550
+ "emergency-alert-row__badge": "de-emergency-emergency-alert-row__badge-nA5Hwa",
1551
+ "emergency-alert-row__button": "de-emergency-emergency-alert-row__button-nA5Hwa",
1552
+ "emergency-alert-row__stat": "de-emergency-emergency-alert-row__stat-nA5Hwa",
1553
+ "emergency-alert-row__stats": "de-emergency-emergency-alert-row__stats-nA5Hwa",
1554
+ "emergency-alert-row__time": "de-emergency-emergency-alert-row__time-nA5Hwa",
1555
+ "emergency-alert-row__top": "de-emergency-emergency-alert-row__top-nA5Hwa",
1541
1556
  "emergency-availability": "de-emergency-emergency-availability-nA5Hwa",
1542
1557
  "emergency-availability__card": "de-emergency-emergency-availability__card-nA5Hwa",
1543
1558
  "emergency-availability__card-body": "de-emergency-emergency-availability__card-body-nA5Hwa",
@@ -1561,6 +1576,19 @@ var panel_module_default = {
1561
1576
  "emergency-confirmation__icon--information": "de-emergency-emergency-confirmation__icon--information-nA5Hwa",
1562
1577
  "emergency-confirmation__text": "de-emergency-emergency-confirmation__text-nA5Hwa",
1563
1578
  "emergency-confirmation__title": "de-emergency-emergency-confirmation__title-nA5Hwa",
1579
+ "emergency-details": "de-emergency-emergency-details-nA5Hwa",
1580
+ "emergency-details__balance": "de-emergency-emergency-details__balance-nA5Hwa",
1581
+ "emergency-details__card": "de-emergency-emergency-details__card-nA5Hwa",
1582
+ "emergency-details__card-body": "de-emergency-emergency-details__card-body-nA5Hwa",
1583
+ "emergency-details__card-header": "de-emergency-emergency-details__card-header-nA5Hwa",
1584
+ "emergency-details__card-title": "de-emergency-emergency-details__card-title-nA5Hwa",
1585
+ "emergency-details__empty": "de-emergency-emergency-details__empty-nA5Hwa",
1586
+ "emergency-details__heading": "de-emergency-emergency-details__heading-nA5Hwa",
1587
+ "emergency-details__history-button": "de-emergency-emergency-details__history-button-nA5Hwa",
1588
+ "emergency-details__list": "de-emergency-emergency-details__list-nA5Hwa",
1589
+ "emergency-details__section": "de-emergency-emergency-details__section-nA5Hwa",
1590
+ "emergency-details__subtitle": "de-emergency-emergency-details__subtitle-nA5Hwa",
1591
+ "emergency-details__title": "de-emergency-emergency-details__title-nA5Hwa",
1564
1592
  "emergency-error-message": "de-emergency-emergency-error-message-nA5Hwa",
1565
1593
  "emergency-error-message__container": "de-emergency-emergency-error-message__container-nA5Hwa",
1566
1594
  "emergency-error-message__content": "de-emergency-emergency-error-message__content-nA5Hwa",
@@ -1571,10 +1599,33 @@ var panel_module_default = {
1571
1599
  "emergency-error-message__title": "de-emergency-emergency-error-message__title-nA5Hwa",
1572
1600
  "emergency-feedback": "de-emergency-emergency-feedback-nA5Hwa",
1573
1601
  "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-nA5Hwa",
1602
+ "emergency-history": "de-emergency-emergency-history-nA5Hwa",
1603
+ "emergency-history__empty": "de-emergency-emergency-history__empty-nA5Hwa",
1604
+ "emergency-history__heading": "de-emergency-emergency-history__heading-nA5Hwa",
1605
+ "emergency-history__list": "de-emergency-emergency-history__list-nA5Hwa",
1606
+ "emergency-history__more": "de-emergency-emergency-history__more-nA5Hwa",
1607
+ "emergency-history__subtitle": "de-emergency-emergency-history__subtitle-nA5Hwa",
1608
+ "emergency-history__title": "de-emergency-emergency-history__title-nA5Hwa",
1609
+ "emergency-message-detail": "de-emergency-emergency-message-detail-nA5Hwa",
1610
+ "emergency-message-detail__back": "de-emergency-emergency-message-detail__back-nA5Hwa",
1611
+ "emergency-message-detail__card": "de-emergency-emergency-message-detail__card-nA5Hwa",
1612
+ "emergency-message-detail__card-header": "de-emergency-emergency-message-detail__card-header-nA5Hwa",
1613
+ "emergency-message-detail__card-title": "de-emergency-emergency-message-detail__card-title-nA5Hwa",
1614
+ "emergency-message-detail__empty": "de-emergency-emergency-message-detail__empty-nA5Hwa",
1615
+ "emergency-message-detail__empty-text": "de-emergency-emergency-message-detail__empty-text-nA5Hwa",
1616
+ "emergency-message-detail__label": "de-emergency-emergency-message-detail__label-nA5Hwa",
1617
+ "emergency-message-detail__list": "de-emergency-emergency-message-detail__list-nA5Hwa",
1618
+ "emergency-message-detail__row": "de-emergency-emergency-message-detail__row-nA5Hwa",
1619
+ "emergency-message-detail__value": "de-emergency-emergency-message-detail__value-nA5Hwa",
1574
1620
  "emergency-panel": "de-emergency-emergency-panel-nA5Hwa",
1575
1621
  "emergency-panel__availability-region": "de-emergency-emergency-panel__availability-region-nA5Hwa",
1576
1622
  "emergency-panel__body": "de-emergency-emergency-panel__body-nA5Hwa",
1623
+ "emergency-panel__body--details": "de-emergency-emergency-panel__body--details-nA5Hwa",
1624
+ "emergency-panel__body--history": "de-emergency-emergency-panel__body--history-nA5Hwa",
1625
+ "emergency-panel__body--message-detail": "de-emergency-emergency-panel__body--message-detail-nA5Hwa",
1626
+ "emergency-panel__body--pre-emergency": "de-emergency-emergency-panel__body--pre-emergency-nA5Hwa",
1577
1627
  "emergency-panel__body--pricing": "de-emergency-emergency-panel__body--pricing-nA5Hwa",
1628
+ "emergency-panel__body--primary": "de-emergency-emergency-panel__body--primary-nA5Hwa",
1578
1629
  "emergency-panel__body-split": "de-emergency-emergency-panel__body-split-nA5Hwa",
1579
1630
  "emergency-panel__body-split--with-availability": "de-emergency-emergency-panel__body-split--with-availability-nA5Hwa",
1580
1631
  "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-nA5Hwa",
@@ -1583,19 +1634,37 @@ var panel_module_default = {
1583
1634
  "emergency-panel__button--emergency": "de-emergency-emergency-panel__button--emergency-nA5Hwa",
1584
1635
  "emergency-panel__button--simulation": "de-emergency-emergency-panel__button--simulation-nA5Hwa",
1585
1636
  "emergency-panel__close-button": "de-emergency-emergency-panel__close-button-nA5Hwa",
1637
+ "emergency-panel__close-button--prompt": "de-emergency-emergency-panel__close-button--prompt-nA5Hwa",
1586
1638
  "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-nA5Hwa",
1587
1639
  "emergency-panel__footer": "de-emergency-emergency-panel__footer-nA5Hwa",
1640
+ "emergency-panel__footer--single": "de-emergency-emergency-panel__footer--single-nA5Hwa",
1641
+ "emergency-panel__footer--square": "de-emergency-emergency-panel__footer--square-nA5Hwa",
1588
1642
  "emergency-panel__header": "de-emergency-emergency-panel__header-nA5Hwa",
1589
1643
  "emergency-panel__prompt-region": "de-emergency-emergency-panel__prompt-region-nA5Hwa",
1644
+ "emergency-panel__prompt-region--idle": "de-emergency-emergency-panel__prompt-region--idle-nA5Hwa",
1645
+ "emergency-panel__sr-only": "de-emergency-emergency-panel__sr-only-nA5Hwa",
1590
1646
  "emergency-panel__title": "de-emergency-emergency-panel__title-nA5Hwa",
1591
1647
  "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-nA5Hwa",
1648
+ "emergency-pre-emergency": "de-emergency-emergency-pre-emergency-nA5Hwa",
1649
+ "emergency-pre-emergency__error": "de-emergency-emergency-pre-emergency__error-nA5Hwa",
1650
+ "emergency-pre-emergency__form": "de-emergency-emergency-pre-emergency__form-nA5Hwa",
1651
+ "emergency-pre-emergency__input": "de-emergency-emergency-pre-emergency__input-nA5Hwa",
1652
+ "emergency-pre-emergency__input--area-code": "de-emergency-emergency-pre-emergency__input--area-code-nA5Hwa",
1653
+ "emergency-pre-emergency__input--invalid": "de-emergency-emergency-pre-emergency__input--invalid-nA5Hwa",
1654
+ "emergency-pre-emergency__input--number": "de-emergency-emergency-pre-emergency__input--number-nA5Hwa",
1655
+ "emergency-pre-emergency__input--valid": "de-emergency-emergency-pre-emergency__input--valid-nA5Hwa",
1656
+ "emergency-pre-emergency__label": "de-emergency-emergency-pre-emergency__label-nA5Hwa",
1657
+ "emergency-pre-emergency__notice": "de-emergency-emergency-pre-emergency__notice-nA5Hwa",
1658
+ "emergency-pre-emergency__notice-icon": "de-emergency-emergency-pre-emergency__notice-icon-nA5Hwa",
1659
+ "emergency-pre-emergency__notice-text": "de-emergency-emergency-pre-emergency__notice-text-nA5Hwa",
1660
+ "emergency-pre-emergency__phone-row": "de-emergency-emergency-pre-emergency__phone-row-nA5Hwa",
1592
1661
  "emergency-pricing": "de-emergency-emergency-pricing-nA5Hwa",
1593
1662
  "emergency-pricing__badge": "de-emergency-emergency-pricing__badge-nA5Hwa",
1594
- "emergency-pricing__footnote": "de-emergency-emergency-pricing__footnote-nA5Hwa",
1595
1663
  "emergency-pricing__heading": "de-emergency-emergency-pricing__heading-nA5Hwa",
1596
1664
  "emergency-pricing__package": "de-emergency-emergency-pricing__package-nA5Hwa",
1597
1665
  "emergency-pricing__package--highlighted": "de-emergency-emergency-pricing__package--highlighted-nA5Hwa",
1598
1666
  "emergency-pricing__package-caption": "de-emergency-emergency-pricing__package-caption-nA5Hwa",
1667
+ "emergency-pricing__package-footnote": "de-emergency-emergency-pricing__package-footnote-nA5Hwa",
1599
1668
  "emergency-pricing__package-header": "de-emergency-emergency-pricing__package-header-nA5Hwa",
1600
1669
  "emergency-pricing__package-label": "de-emergency-emergency-pricing__package-label-nA5Hwa",
1601
1670
  "emergency-pricing__package-name": "de-emergency-emergency-pricing__package-name-nA5Hwa",
@@ -1791,18 +1860,27 @@ function useFocusTrap(ref, active) {
1791
1860
  }
1792
1861
 
1793
1862
  // src/react/screens/context.tsx
1863
+ const EMPTY_PARAMS = {};
1794
1864
  const EmergencyScreenContext = createContext(null);
1865
+ function paramsEqual(first, second) {
1866
+ return first.messageId === second.messageId;
1867
+ }
1795
1868
  function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1796
1869
  const [state, setState] = useState({
1797
1870
  screen: initialScreen,
1871
+ params: EMPTY_PARAMS,
1798
1872
  history: []
1799
1873
  });
1800
- const navigate = useCallback((next) => {
1874
+ const navigate = useCallback((next, params = EMPTY_PARAMS) => {
1801
1875
  setState((current) => {
1802
- if (current.screen === next) return current;
1876
+ if (current.screen === next && paramsEqual(current.params, params)) return current;
1803
1877
  return {
1804
1878
  screen: next,
1805
- history: [...current.history, current.screen]
1879
+ params,
1880
+ history: [...current.history, {
1881
+ screen: current.screen,
1882
+ params: current.params
1883
+ }]
1806
1884
  };
1807
1885
  });
1808
1886
  }, []);
@@ -1811,31 +1889,47 @@ function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1811
1889
  const previous = current.history.at(-1);
1812
1890
  if (!previous) return current;
1813
1891
  return {
1814
- screen: previous,
1892
+ screen: previous.screen,
1893
+ params: previous.params,
1815
1894
  history: current.history.slice(0, -1)
1816
1895
  };
1817
1896
  });
1818
1897
  }, []);
1898
+ const replace = useCallback((next, params = EMPTY_PARAMS) => {
1899
+ setState((current) => {
1900
+ if (current.screen === next && paramsEqual(current.params, params)) return current;
1901
+ return {
1902
+ screen: next,
1903
+ params,
1904
+ history: current.history.at(-1)?.screen === next ? current.history.slice(0, -1) : current.history
1905
+ };
1906
+ });
1907
+ }, []);
1819
1908
  const reset = useCallback(() => {
1820
1909
  setState((current) => {
1821
- if (current.screen === "primary" && current.history.length === 0) return current;
1910
+ if (current.screen === "primary" && current.params.messageId === void 0 && current.history.length === 0) return current;
1822
1911
  return {
1823
1912
  screen: "primary",
1913
+ params: EMPTY_PARAMS,
1824
1914
  history: []
1825
1915
  };
1826
1916
  });
1827
1917
  }, []);
1828
1918
  const value = useMemo(() => ({
1829
1919
  screen: state.screen,
1920
+ params: state.params,
1830
1921
  history: state.history,
1831
1922
  navigate,
1923
+ replace,
1832
1924
  back,
1833
1925
  reset
1834
1926
  }), [
1835
1927
  back,
1836
1928
  navigate,
1929
+ replace,
1837
1930
  reset,
1838
1931
  state.history,
1932
+ state.params,
1839
1933
  state.screen
1840
1934
  ]);
1841
1935
  return /* @__PURE__ */ jsx(EmergencyScreenContext.Provider, {
@@ -1884,6 +1978,245 @@ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack })
1884
1978
  });
1885
1979
  }
1886
1980
 
1981
+ // src/react/screens/primary/data.ts
1982
+ /**
1983
+ * Derives the remaining alert rounds from available messages and designated
1984
+ * responders. Invalid or non-positive input collapses to the minimum of one
1985
+ * round instead of producing `Infinity`, `NaN` or a negative value.
1986
+ */
1987
+ function calculateRounds(messages, responders) {
1988
+ if (!Number.isFinite(messages) || !Number.isFinite(responders)) return 1;
1989
+ const availableMessages = Math.floor(messages);
1990
+ const designatedResponders = Math.floor(responders);
1991
+ if (availableMessages <= 0 || designatedResponders <= 0) return 1;
1992
+ return Math.max(1, Math.floor(availableMessages / designatedResponders));
1993
+ }
1994
+
1995
+ // src/react/screens/details/copy.ts
1996
+ const detailsCopy = {
1997
+ title: "Detalles",
1998
+ summaryTitle: "Alertas Disponibles",
1999
+ historyTitle: "Historial de alertas",
2000
+ historySubtitle: "Tus últimas alertas y recargas.",
2001
+ historyEmpty: "Todavía no tienes alertas en tu historial.",
2002
+ summaryEmpty: "Todavía no tienes un saldo de alertas disponible.",
2003
+ viewHistory: "Ver historial",
2004
+ messagesSingular: "mensaje de alerta",
2005
+ messagesPlural: "mensajes de alerta",
2006
+ contactsSingular: "contacto de emergencia",
2007
+ contactsPlural: "contactos de emergencia",
2008
+ roundsSingular: "ronda de avisos",
2009
+ roundsPlural: "rondas de avisos"
2010
+ };
2011
+ /** Returns the balance sentence split into text and emphasized number parts. */
2012
+ function alertBalanceParts(messages, responders) {
2013
+ if (!Number.isFinite(messages) || !Number.isFinite(responders) || messages < 0 || responders < 0) return [];
2014
+ const rounds = calculateRounds(messages, responders);
2015
+ const messagesUnit = messages === 1 ? detailsCopy.messagesSingular : detailsCopy.messagesPlural;
2016
+ const contactsUnit = responders === 1 ? detailsCopy.contactsSingular : detailsCopy.contactsPlural;
2017
+ const roundsUnit = rounds === 1 ? detailsCopy.roundsSingular : detailsCopy.roundsPlural;
2018
+ return [
2019
+ messages === 1 ? "Te queda " : "Te quedan ",
2020
+ messages,
2021
+ ` ${messagesUnit}, dado que tienes `,
2022
+ responders,
2023
+ ` ${contactsUnit}, `,
2024
+ rounds === 1 ? "te queda " : "te quedan ",
2025
+ rounds,
2026
+ ` ${roundsUnit}.`
2027
+ ];
2028
+ }
2029
+
2030
+ // src/react/screens/details/AlertBalanceSentence.tsx
2031
+ /** Renders the balance sentence with its numbers visually emphasized. */
2032
+ function AlertBalanceSentence({ messages, responders }) {
2033
+ const parts = alertBalanceParts(messages, responders);
2034
+ return /* @__PURE__ */ jsx(Fragment, { children: parts.map((part, index) => typeof part === "number" ? /* @__PURE__ */ jsx("strong", { children: part }, index) : part) });
2035
+ }
2036
+
2037
+ // src/core/date.ts
2038
+ /**
2039
+ * Formats a timestamp as `h:mmam` or `h:mmpm` in local time, without a space
2040
+ * or leading zero.
2041
+ */
2042
+ function formatShortTime(value, locale = "es") {
2043
+ if (value === null) return "";
2044
+ const date = value instanceof Date ? value : new Date(value);
2045
+ if (Number.isNaN(date.getTime())) return "";
2046
+ const parts = new Intl.DateTimeFormat(locale, {
2047
+ hour: "numeric",
2048
+ minute: "2-digit",
2049
+ hour12: true
2050
+ }).formatToParts(date);
2051
+ const hour = parts.find((part) => part.type === "hour")?.value;
2052
+ const minute = parts.find((part) => part.type === "minute")?.value;
2053
+ const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value;
2054
+ if (!hour || !minute) return "";
2055
+ return `${hour}:${minute}${normalizePeriod(dayPeriod)}`;
2056
+ }
2057
+ /**
2058
+ * Formats a timestamp as `dd/mm/yyyy hh:mm` in local time, using 24-hour time
2059
+ * and zero-padded parts. Missing or invalid input collapses to an empty string.
2060
+ */
2061
+ function formatDateTime(value, locale = "es") {
2062
+ if (value === null) return "";
2063
+ const date = value instanceof Date ? value : new Date(value);
2064
+ if (Number.isNaN(date.getTime())) return "";
2065
+ const parts = new Intl.DateTimeFormat(locale, {
2066
+ day: "2-digit",
2067
+ month: "2-digit",
2068
+ year: "numeric",
2069
+ hour: "2-digit",
2070
+ minute: "2-digit",
2071
+ hourCycle: "h23"
2072
+ }).formatToParts(date);
2073
+ const day = parts.find((part) => part.type === "day")?.value;
2074
+ const month = parts.find((part) => part.type === "month")?.value;
2075
+ const year = parts.find((part) => part.type === "year")?.value;
2076
+ const hour = parts.find((part) => part.type === "hour")?.value;
2077
+ const minute = parts.find((part) => part.type === "minute")?.value;
2078
+ if (!day || !month || !year || !hour || !minute) return "";
2079
+ return `${day}/${month}/${year} ${hour}:${minute}`;
2080
+ }
2081
+ function normalizePeriod(dayPeriod) {
2082
+ if (!dayPeriod) return "";
2083
+ if (/^p/i.test(dayPeriod)) return "pm";
2084
+ return /^a/i.test(dayPeriod) ? "am" : "";
2085
+ }
2086
+
2087
+ // src/react/screens/history/copy.ts
2088
+ const historyCopy = {
2089
+ title: "Historial de alertas",
2090
+ subtitle: "Consulta tus alertas, pruebas y recargas.",
2091
+ empty: "Todavía no tienes alertas en tu historial.",
2092
+ showMore: "Ver más",
2093
+ kindLabels: {
2094
+ emergency: "Emergencia",
2095
+ test: "Prueba",
2096
+ recharge: "Recarga"
2097
+ }
2098
+ };
2099
+ function formatContacts(count) {
2100
+ return count === 1 ? "1 contacto" : `${count} contactos`;
2101
+ }
2102
+ function formatMessages(count) {
2103
+ return count === 1 ? "1 mensaje" : `${count} mensajes`;
2104
+ }
2105
+ function formatHistoryRounds(count) {
2106
+ return count === 1 ? "1 ronda" : `${count} rondas`;
2107
+ }
2108
+
2109
+ // src/react/screens/history/data.ts
2110
+ /** Local demo alert history, newest first. */
2111
+ const DEMO_ALERT_HISTORY = [
2112
+ {
2113
+ id: "alert-2026-10-31",
2114
+ kind: "emergency",
2115
+ occurredAt: "2026-10-31T13:50:00",
2116
+ responders: 3,
2117
+ remainingMessages: 27
2118
+ },
2119
+ {
2120
+ id: "alert-2026-10-20",
2121
+ kind: "recharge",
2122
+ occurredAt: "2026-10-20T09:15:00",
2123
+ responders: 3,
2124
+ remainingMessages: 100
2125
+ },
2126
+ {
2127
+ id: "alert-2026-10-12",
2128
+ kind: "test",
2129
+ occurredAt: "2026-10-12T18:40:00",
2130
+ responders: 3,
2131
+ remainingMessages: 73
2132
+ },
2133
+ {
2134
+ id: "alert-2026-10-01",
2135
+ kind: "recharge",
2136
+ occurredAt: "2026-10-01T11:05:00",
2137
+ responders: 3,
2138
+ remainingMessages: 76
2139
+ }
2140
+ ];
2141
+ /** Returns a newest-first copy of the history. */
2142
+ function sortedHistoryEntries(entries = DEMO_ALERT_HISTORY) {
2143
+ return [...entries].sort((first, second) => new Date(second.occurredAt).getTime() - new Date(first.occurredAt).getTime());
2144
+ }
2145
+ /** Newest entry, used as the source of the current balance. */
2146
+ function latestHistoryEntry(entries = DEMO_ALERT_HISTORY) {
2147
+ return sortedHistoryEntries(entries)[0] ?? null;
2148
+ }
2149
+ /** Resolves a single history entry by id. */
2150
+ function historyEntryById(id, entries = DEMO_ALERT_HISTORY) {
2151
+ if (!id) return null;
2152
+ return entries.find((entry) => entry.id === id) ?? null;
2153
+ }
2154
+ /** Rounds represented by the entry's remaining messages and responders. */
2155
+ function historyEntryRounds(entry) {
2156
+ return calculateRounds(entry.remainingMessages, entry.responders);
2157
+ }
2158
+ /** Current balance derived from the newest history entry. */
2159
+ function currentAlertBalance(entries = DEMO_ALERT_HISTORY) {
2160
+ const latest = latestHistoryEntry(entries);
2161
+ if (!latest) return null;
2162
+ return {
2163
+ messages: latest.remainingMessages,
2164
+ responders: latest.responders,
2165
+ rounds: historyEntryRounds(latest)
2166
+ };
2167
+ }
2168
+
2169
+ // src/react/screens/history/AlertHistoryRow.tsx
2170
+ /** Reusable history row. The whole row is a button with a full text label. */
2171
+ function AlertHistoryRow({ entry, locale, onOpen }) {
2172
+ const occurredAt = formatDateTime(entry.occurredAt, locale);
2173
+ const rounds = historyEntryRounds(entry);
2174
+ const label = [
2175
+ historyCopy.kindLabels[entry.kind],
2176
+ occurredAt,
2177
+ formatContacts(entry.responders),
2178
+ `${formatMessages(entry.remainingMessages)} restantes`,
2179
+ `${formatHistoryRounds(rounds)} de avisos`
2180
+ ].join(", ");
2181
+ return /* @__PURE__ */ jsx("li", {
2182
+ className: cls("emergency-alert-row"),
2183
+ children: /* @__PURE__ */ jsxs("button", {
2184
+ type: "button",
2185
+ className: cls("emergency-alert-row__button"),
2186
+ "aria-label": label,
2187
+ onClick: () => onOpen(entry.id),
2188
+ children: [/* @__PURE__ */ jsxs("span", {
2189
+ className: cls("emergency-alert-row__top"),
2190
+ children: [/* @__PURE__ */ jsx("time", {
2191
+ className: cls("emergency-alert-row__time"),
2192
+ dateTime: entry.occurredAt,
2193
+ children: occurredAt
2194
+ }), /* @__PURE__ */ jsx("span", {
2195
+ className: cls("emergency-alert-row__badge"),
2196
+ "data-kind": entry.kind,
2197
+ children: historyCopy.kindLabels[entry.kind]
2198
+ })]
2199
+ }), /* @__PURE__ */ jsxs("span", {
2200
+ className: cls("emergency-alert-row__stats"),
2201
+ children: [
2202
+ /* @__PURE__ */ jsx("span", {
2203
+ className: cls("emergency-alert-row__stat"),
2204
+ children: formatContacts(entry.responders)
2205
+ }),
2206
+ /* @__PURE__ */ jsx("span", {
2207
+ className: cls("emergency-alert-row__stat"),
2208
+ children: formatMessages(entry.remainingMessages)
2209
+ }),
2210
+ /* @__PURE__ */ jsx("span", {
2211
+ className: cls("emergency-alert-row__stat"),
2212
+ children: formatHistoryRounds(rounds)
2213
+ })
2214
+ ]
2215
+ })]
2216
+ })
2217
+ });
2218
+ }
2219
+
1887
2220
  // src/react/screens/layout.tsx
1888
2221
  /**
1889
2222
  * Shared shell for every screen. The wrapper is `display: contents` so the
@@ -1905,45 +2238,429 @@ function EmergencyScreenLayout({ screen, titleId, header, footer, children, body
1905
2238
  });
1906
2239
  }
1907
2240
 
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 }) {
2241
+ // src/react/screens/details/index.tsx
2242
+ function DetailsScreen({ titleId, locale, onClose }) {
2243
+ const { back, navigate } = useScreenNavigation();
2244
+ const balance = currentAlertBalance();
2245
+ const preview = sortedHistoryEntries().slice(0, 2);
1911
2246
  return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
1912
- screen,
2247
+ screen: "details",
1913
2248
  titleId,
2249
+ bodyClassName: "emergency-panel__body--details",
1914
2250
  header: /* @__PURE__ */ jsx(EmergencyHeader, {
1915
2251
  titleId,
1916
- title,
1917
- onClose,
1918
- onBack
2252
+ title: detailsCopy.title,
2253
+ onClose: () => onClose("close"),
2254
+ onBack: back
1919
2255
  }),
1920
2256
  footer: null,
2257
+ children: /* @__PURE__ */ jsxs("div", {
2258
+ className: cls("emergency-details"),
2259
+ children: [/* @__PURE__ */ jsx("section", {
2260
+ className: cls("emergency-details__section"),
2261
+ "aria-label": detailsCopy.summaryTitle,
2262
+ children: /* @__PURE__ */ jsxs("div", {
2263
+ className: cls("emergency-details__card"),
2264
+ children: [/* @__PURE__ */ jsx("div", {
2265
+ className: cls("emergency-details__card-header"),
2266
+ children: /* @__PURE__ */ jsx("h3", {
2267
+ className: cls("emergency-details__card-title"),
2268
+ children: detailsCopy.summaryTitle
2269
+ })
2270
+ }), /* @__PURE__ */ jsx("div", {
2271
+ className: cls("emergency-details__card-body"),
2272
+ children: balance ? /* @__PURE__ */ jsx("p", {
2273
+ className: cls("emergency-details__balance"),
2274
+ children: /* @__PURE__ */ jsx(AlertBalanceSentence, {
2275
+ messages: balance.messages,
2276
+ responders: balance.responders
2277
+ })
2278
+ }) : /* @__PURE__ */ jsx("p", {
2279
+ className: cls("emergency-details__empty"),
2280
+ children: detailsCopy.summaryEmpty
2281
+ })
2282
+ })]
2283
+ })
2284
+ }), /* @__PURE__ */ jsxs("section", {
2285
+ className: cls("emergency-details__section"),
2286
+ "aria-label": detailsCopy.historyTitle,
2287
+ children: [
2288
+ /* @__PURE__ */ jsxs("div", {
2289
+ className: cls("emergency-details__heading"),
2290
+ children: [/* @__PURE__ */ jsx("h3", {
2291
+ className: cls("emergency-details__title"),
2292
+ children: detailsCopy.historyTitle
2293
+ }), /* @__PURE__ */ jsx("p", {
2294
+ className: cls("emergency-details__subtitle"),
2295
+ children: detailsCopy.historySubtitle
2296
+ })]
2297
+ }),
2298
+ preview.length > 0 ? /* @__PURE__ */ jsx("ul", {
2299
+ className: cls("emergency-details__list"),
2300
+ children: preview.map((entry) => /* @__PURE__ */ jsx(AlertHistoryRow, {
2301
+ entry,
2302
+ locale,
2303
+ onOpen: (messageId) => navigate("message-detail", { messageId })
2304
+ }, entry.id))
2305
+ }) : /* @__PURE__ */ jsx("p", {
2306
+ className: cls("emergency-details__empty"),
2307
+ children: detailsCopy.historyEmpty
2308
+ }),
2309
+ /* @__PURE__ */ jsx("button", {
2310
+ type: "button",
2311
+ className: cls("emergency-details__history-button"),
2312
+ onClick: () => navigate("history"),
2313
+ children: detailsCopy.viewHistory
2314
+ })
2315
+ ]
2316
+ })]
2317
+ })
2318
+ });
2319
+ }
2320
+
2321
+ // src/react/screens/history/index.tsx
2322
+ function HistoryScreen({ titleId, locale, onClose }) {
2323
+ const { back, navigate } = useScreenNavigation();
2324
+ const listId = useId();
2325
+ const [expanded, setExpanded] = useState(false);
2326
+ const entries = sortedHistoryEntries();
2327
+ const hiddenCount = Math.max(0, entries.length - 2);
2328
+ const visibleEntries = expanded ? entries : entries.slice(0, 2);
2329
+ const showMore = hiddenCount > 0 && !expanded;
2330
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2331
+ screen: "history",
2332
+ titleId,
2333
+ bodyClassName: "emergency-panel__body--history",
2334
+ header: /* @__PURE__ */ jsx(EmergencyHeader, {
2335
+ titleId,
2336
+ title: historyCopy.title,
2337
+ onClose: () => onClose("close"),
2338
+ onBack: back
2339
+ }),
2340
+ footer: null,
2341
+ children: /* @__PURE__ */ jsxs("div", {
2342
+ className: cls("emergency-history"),
2343
+ children: [/* @__PURE__ */ jsxs("div", {
2344
+ className: cls("emergency-history__heading"),
2345
+ children: [/* @__PURE__ */ jsx("h3", {
2346
+ className: cls("emergency-history__title"),
2347
+ children: historyCopy.title
2348
+ }), /* @__PURE__ */ jsx("p", {
2349
+ className: cls("emergency-history__subtitle"),
2350
+ children: historyCopy.subtitle
2351
+ })]
2352
+ }), entries.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
2353
+ className: cls("emergency-history__list"),
2354
+ id: listId,
2355
+ children: visibleEntries.map((entry) => /* @__PURE__ */ jsx(AlertHistoryRow, {
2356
+ entry,
2357
+ locale,
2358
+ onOpen: (messageId) => navigate("message-detail", { messageId })
2359
+ }, entry.id))
2360
+ }), showMore ? /* @__PURE__ */ jsx("button", {
2361
+ type: "button",
2362
+ className: cls("emergency-history__more"),
2363
+ "aria-expanded": expanded,
2364
+ "aria-controls": listId,
2365
+ onClick: () => setExpanded(true),
2366
+ children: historyCopy.showMore
2367
+ }) : null] }) : /* @__PURE__ */ jsx("p", {
2368
+ className: cls("emergency-history__empty"),
2369
+ children: historyCopy.empty
2370
+ })]
2371
+ })
2372
+ });
2373
+ }
2374
+
2375
+ // src/react/screens/message-detail/index.tsx
2376
+ const messageDetailCopy = {
2377
+ title: "Detalle del mensaje",
2378
+ dateTime: "Fecha y hora",
2379
+ kind: "Tipo",
2380
+ responders: "Contactos de emergencia",
2381
+ messages: "Mensajes restantes",
2382
+ rounds: "Rondas de avisos",
2383
+ notFound: "No encontramos este mensaje. Puede que ya no esté disponible en tu historial.",
2384
+ backToHistory: "Volver al historial"
2385
+ };
2386
+ function MessageDetailScreen({ titleId, locale, onClose }) {
2387
+ const { back, params, replace } = useScreenNavigation();
2388
+ const entry = historyEntryById(params.messageId);
2389
+ const header = /* @__PURE__ */ jsx(EmergencyHeader, {
2390
+ titleId,
2391
+ title: messageDetailCopy.title,
2392
+ onClose: () => onClose("close"),
2393
+ onBack: back
2394
+ });
2395
+ if (!entry) return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2396
+ screen: "message-detail",
2397
+ titleId,
2398
+ bodyClassName: "emergency-panel__body--message-detail",
2399
+ header,
2400
+ footer: null,
2401
+ children: /* @__PURE__ */ jsxs("div", {
2402
+ className: cls("emergency-message-detail__empty"),
2403
+ children: [/* @__PURE__ */ jsx("p", {
2404
+ className: cls("emergency-message-detail__empty-text"),
2405
+ children: messageDetailCopy.notFound
2406
+ }), /* @__PURE__ */ jsx("button", {
2407
+ type: "button",
2408
+ className: cls("emergency-message-detail__back"),
2409
+ onClick: () => replace("history"),
2410
+ children: messageDetailCopy.backToHistory
2411
+ })]
2412
+ })
2413
+ });
2414
+ const rounds = historyEntryRounds(entry);
2415
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2416
+ screen: "message-detail",
2417
+ titleId,
2418
+ bodyClassName: "emergency-panel__body--message-detail",
2419
+ header,
2420
+ footer: null,
1921
2421
  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
2422
+ className: cls("emergency-message-detail"),
2423
+ children: /* @__PURE__ */ jsxs("div", {
2424
+ className: cls("emergency-message-detail__card"),
2425
+ children: [/* @__PURE__ */ jsx("div", {
2426
+ className: cls("emergency-message-detail__card-header"),
2427
+ children: /* @__PURE__ */ jsx("h3", {
2428
+ className: cls("emergency-message-detail__card-title"),
2429
+ children: messageDetailCopy.title
2430
+ })
2431
+ }), /* @__PURE__ */ jsxs("dl", {
2432
+ className: cls("emergency-message-detail__list"),
2433
+ children: [
2434
+ /* @__PURE__ */ jsxs("div", {
2435
+ className: cls("emergency-message-detail__row"),
2436
+ children: [/* @__PURE__ */ jsx("dt", {
2437
+ className: cls("emergency-message-detail__label"),
2438
+ children: messageDetailCopy.dateTime
2439
+ }), /* @__PURE__ */ jsx("dd", {
2440
+ className: cls("emergency-message-detail__value"),
2441
+ children: formatDateTime(entry.occurredAt, locale)
2442
+ })]
2443
+ }),
2444
+ /* @__PURE__ */ jsxs("div", {
2445
+ className: cls("emergency-message-detail__row"),
2446
+ children: [/* @__PURE__ */ jsx("dt", {
2447
+ className: cls("emergency-message-detail__label"),
2448
+ children: messageDetailCopy.kind
2449
+ }), /* @__PURE__ */ jsx("dd", {
2450
+ className: cls("emergency-message-detail__value"),
2451
+ children: historyCopy.kindLabels[entry.kind]
2452
+ })]
2453
+ }),
2454
+ /* @__PURE__ */ jsxs("div", {
2455
+ className: cls("emergency-message-detail__row"),
2456
+ children: [/* @__PURE__ */ jsx("dt", {
2457
+ className: cls("emergency-message-detail__label"),
2458
+ children: messageDetailCopy.responders
2459
+ }), /* @__PURE__ */ jsx("dd", {
2460
+ className: cls("emergency-message-detail__value"),
2461
+ children: formatContacts(entry.responders)
2462
+ })]
2463
+ }),
2464
+ /* @__PURE__ */ jsxs("div", {
2465
+ className: cls("emergency-message-detail__row"),
2466
+ children: [/* @__PURE__ */ jsx("dt", {
2467
+ className: cls("emergency-message-detail__label"),
2468
+ children: messageDetailCopy.messages
2469
+ }), /* @__PURE__ */ jsx("dd", {
2470
+ className: cls("emergency-message-detail__value"),
2471
+ children: formatMessages(entry.remainingMessages)
2472
+ })]
2473
+ }),
2474
+ /* @__PURE__ */ jsxs("div", {
2475
+ className: cls("emergency-message-detail__row"),
2476
+ children: [/* @__PURE__ */ jsx("dt", {
2477
+ className: cls("emergency-message-detail__label"),
2478
+ children: messageDetailCopy.rounds
2479
+ }), /* @__PURE__ */ jsx("dd", {
2480
+ className: cls("emergency-message-detail__value"),
2481
+ children: formatHistoryRounds(rounds)
2482
+ })]
2483
+ })
2484
+ ]
2485
+ })]
1926
2486
  })
1927
2487
  })
1928
2488
  });
1929
2489
  }
1930
2490
 
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."
2491
+ // src/core/phone.ts
2492
+ const VENEZUELA_DIALLING_CODE = "+58";
2493
+ const VENEZUELAN_MOBILE_PATTERN = /^4\d{9}$/;
2494
+ function normalizeVenezuelanPhone(input) {
2495
+ return input.replace(/\D/g, "").replace(/^0+/, "");
2496
+ }
2497
+ function formatVenezuelanPhone(input) {
2498
+ const digits = normalizeVenezuelanPhone(input).slice(0, 10);
2499
+ if (digits.length <= 3) return digits;
2500
+ return `${digits.slice(0, 3)} ${digits.slice(3)}`;
2501
+ }
2502
+ function isValidVenezuelanPhone(input) {
2503
+ return VENEZUELAN_MOBILE_PATTERN.test(normalizeVenezuelanPhone(input));
2504
+ }
2505
+ function toVenezuelanE164(input) {
2506
+ const digits = normalizeVenezuelanPhone(input);
2507
+ if (!VENEZUELAN_MOBILE_PATTERN.test(digits)) return null;
2508
+ return `${"+58"}${digits}`;
2509
+ }
2510
+
2511
+ // src/react/components/EmergencyFooter/index.tsx
2512
+ function EmergencyFooter(props) {
2513
+ const { active, busy, aborting, feedbackVisible, showSimulation = true, emergencyDisabled = false, square = false, onActivate, onRequestAbort, onRequestSimulation } = props;
2514
+ if (active) return /* @__PURE__ */ jsx("footer", {
2515
+ className: cls("emergency-panel__footer", square && "emergency-panel__footer--square"),
2516
+ children: /* @__PURE__ */ jsxs("button", {
2517
+ type: "button",
2518
+ className: cls("emergency-status__abort-btn"),
2519
+ "aria-label": copy.abort,
2520
+ disabled: aborting || feedbackVisible,
2521
+ onClick: onRequestAbort,
2522
+ children: [copy.abort, /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })]
2523
+ })
2524
+ });
2525
+ return /* @__PURE__ */ jsxs("footer", {
2526
+ className: cls("emergency-panel__footer", !showSimulation && "emergency-panel__footer--single", square && "emergency-panel__footer--square"),
2527
+ children: [/* @__PURE__ */ jsxs("button", {
2528
+ type: "button",
2529
+ className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
2530
+ "aria-label": copy.emergencyAria,
2531
+ disabled: busy || feedbackVisible || emergencyDisabled,
2532
+ onClick: onActivate,
2533
+ children: [copy.emergency, /* @__PURE__ */ jsx(Icon, { name: "emergency" })]
2534
+ }), showSimulation ? /* @__PURE__ */ jsxs("button", {
2535
+ type: "button",
2536
+ className: cls("emergency-panel__button", "emergency-panel__button--simulation"),
2537
+ "aria-label": copy.testAria,
2538
+ disabled: busy || feedbackVisible,
2539
+ onClick: onRequestSimulation,
2540
+ children: [copy.test, /* @__PURE__ */ jsx(Icon, { name: "list-check" })]
2541
+ }) : null]
2542
+ });
2543
+ }
2544
+
2545
+ // src/react/screens/pre-emergency/copy.ts
2546
+ const preEmergencyCopy = {
2547
+ title: "Pre-emergencia",
2548
+ permissionNotice: "Debes autorizar todos los permisos de ubicación",
2549
+ phoneLabel: "Indique su número de teléfono",
2550
+ phonePlaceholder: "Ej. 0412 000 0000",
2551
+ areaCodeLabel: "Código de país",
2552
+ areaCode: "+58",
2553
+ invalidPhone: "Ingresa un número móvil venezolano válido. Ej. 412 1301234"
1935
2554
  };
1936
2555
 
1937
- // src/react/screens/details/index.tsx
1938
- function DetailsScreen({ titleId, onClose }) {
2556
+ // src/react/screens/pre-emergency/index.tsx
2557
+ function PreEmergencyScreen(props) {
1939
2558
  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
2559
+ const phoneId = useId();
2560
+ const errorId = `${phoneId}-error`;
2561
+ const permissionId = `${phoneId}-permission`;
2562
+ const [digits, setDigits] = useState("");
2563
+ const [touched, setTouched] = useState(false);
2564
+ const valid = isValidVenezuelanPhone(digits);
2565
+ const invalid = touched && digits.length > 0 && !valid;
2566
+ const handleChange = (event) => {
2567
+ const next = normalizeVenezuelanPhone(event.target.value).slice(0, 10);
2568
+ setDigits(next);
2569
+ };
2570
+ const handleSubmit = () => {
2571
+ setTouched(true);
2572
+ if (!valid) return;
2573
+ const phone = toVenezuelanE164(digits);
2574
+ if (phone === null) return;
2575
+ props.onSetContactPhone(phone);
2576
+ props.onActivate();
2577
+ };
2578
+ return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
2579
+ screen: "pre-emergency",
2580
+ titleId: props.titleId,
2581
+ bodyClassName: "emergency-panel__body--pre-emergency",
2582
+ header: /* @__PURE__ */ jsx(EmergencyHeader, {
2583
+ titleId: props.titleId,
2584
+ title: preEmergencyCopy.title,
2585
+ onClose: () => props.onClose("close"),
2586
+ onBack: back
2587
+ }),
2588
+ footer: /* @__PURE__ */ jsx(EmergencyFooter, {
2589
+ active: props.active,
2590
+ busy: props.busy,
2591
+ aborting: props.aborting,
2592
+ feedbackVisible: props.feedbackVisible,
2593
+ emergencyDisabled: !valid,
2594
+ showSimulation: false,
2595
+ onActivate: handleSubmit,
2596
+ onRequestAbort: props.onRequestAbort,
2597
+ onRequestSimulation: props.onRequestSimulation
2598
+ }),
2599
+ children: /* @__PURE__ */ jsxs("div", {
2600
+ className: cls("emergency-pre-emergency"),
2601
+ children: [/* @__PURE__ */ jsxs("section", {
2602
+ role: "note",
2603
+ "aria-labelledby": permissionId,
2604
+ className: cls("emergency-pre-emergency__notice"),
2605
+ children: [/* @__PURE__ */ jsx(Icon, {
2606
+ name: "map-pin",
2607
+ className: cls("emergency-pre-emergency__notice-icon")
2608
+ }), /* @__PURE__ */ jsx("p", {
2609
+ id: permissionId,
2610
+ className: cls("emergency-pre-emergency__notice-text"),
2611
+ children: preEmergencyCopy.permissionNotice
2612
+ })]
2613
+ }), /* @__PURE__ */ jsxs("form", {
2614
+ className: cls("emergency-pre-emergency__form"),
2615
+ noValidate: true,
2616
+ onSubmit: (event) => {
2617
+ event.preventDefault();
2618
+ handleSubmit();
2619
+ },
2620
+ onKeyDown: (event) => {
2621
+ if (event.key === "Enter") {
2622
+ event.preventDefault();
2623
+ handleSubmit();
2624
+ }
2625
+ },
2626
+ children: [
2627
+ /* @__PURE__ */ jsx("label", {
2628
+ htmlFor: phoneId,
2629
+ className: cls("emergency-pre-emergency__label"),
2630
+ children: preEmergencyCopy.phoneLabel
2631
+ }),
2632
+ /* @__PURE__ */ jsxs("div", {
2633
+ className: cls("emergency-pre-emergency__phone-row"),
2634
+ children: [/* @__PURE__ */ jsx("input", {
2635
+ type: "tel",
2636
+ "aria-label": preEmergencyCopy.areaCodeLabel,
2637
+ className: cls("emergency-pre-emergency__input", "emergency-pre-emergency__input--area-code", valid && "emergency-pre-emergency__input--valid"),
2638
+ value: preEmergencyCopy.areaCode,
2639
+ disabled: true,
2640
+ readOnly: true
2641
+ }), /* @__PURE__ */ jsx("input", {
2642
+ id: phoneId,
2643
+ type: "tel",
2644
+ inputMode: "tel",
2645
+ autoComplete: "tel-national",
2646
+ placeholder: preEmergencyCopy.phonePlaceholder,
2647
+ className: cls("emergency-pre-emergency__input", "emergency-pre-emergency__input--number", valid && "emergency-pre-emergency__input--valid", invalid && "emergency-pre-emergency__input--invalid"),
2648
+ value: formatVenezuelanPhone(digits),
2649
+ "aria-invalid": invalid,
2650
+ "aria-describedby": invalid ? errorId : void 0,
2651
+ onChange: handleChange,
2652
+ onBlur: () => setTouched(true)
2653
+ })]
2654
+ }),
2655
+ invalid ? /* @__PURE__ */ jsx("p", {
2656
+ id: errorId,
2657
+ role: "alert",
2658
+ className: cls("emergency-pre-emergency__error"),
2659
+ children: preEmergencyCopy.invalidPhone
2660
+ }) : null
2661
+ ]
2662
+ })]
2663
+ })
1947
2664
  });
1948
2665
  }
1949
2666
 
@@ -2169,32 +2886,6 @@ function EmergencyLoading() {
2169
2886
  });
2170
2887
  }
2171
2888
 
2172
- // src/core/date.ts
2173
- /**
2174
- * Formats a timestamp as `h:mmam` or `h:mmpm` in local time, without a space
2175
- * or leading zero.
2176
- */
2177
- function formatShortTime(value, locale = "es") {
2178
- if (value === null) return "";
2179
- const date = value instanceof Date ? value : new Date(value);
2180
- if (Number.isNaN(date.getTime())) return "";
2181
- const parts = new Intl.DateTimeFormat(locale, {
2182
- hour: "numeric",
2183
- minute: "2-digit",
2184
- hour12: true
2185
- }).formatToParts(date);
2186
- const hour = parts.find((part) => part.type === "hour")?.value;
2187
- const minute = parts.find((part) => part.type === "minute")?.value;
2188
- const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value;
2189
- if (!hour || !minute) return "";
2190
- return `${hour}:${minute}${normalizePeriod(dayPeriod)}`;
2191
- }
2192
- function normalizePeriod(dayPeriod) {
2193
- if (!dayPeriod) return "";
2194
- if (/^p/i.test(dayPeriod)) return "pm";
2195
- return /^a/i.test(dayPeriod) ? "am" : "";
2196
- }
2197
-
2198
2889
  // src/react/components/StatusIcon/index.tsx
2199
2890
  /**
2200
2891
  * Explicit responder state model:
@@ -2355,23 +3046,9 @@ function formatRounds(rounds) {
2355
3046
  return `${rounds} ${roundsUnit(rounds)}`;
2356
3047
  }
2357
3048
 
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
3049
  // src/react/screens/primary/Availability.tsx
2373
3050
  function Availability({ onRecharge, onDetails }) {
2374
- const rounds = calculateRounds(100, 3);
3051
+ const rounds = calculateRounds(27, 3);
2375
3052
  return /* @__PURE__ */ jsxs("section", {
2376
3053
  className: cls("emergency-availability"),
2377
3054
  "aria-label": primaryCopy.availabilityTitle,
@@ -2413,7 +3090,7 @@ function Availability({ onRecharge, onDetails }) {
2413
3090
 
2414
3091
  // src/react/components/EmergencyBody/index.tsx
2415
3092
  function EmergencyBody(props) {
2416
- const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback, onRecharge, onDetails } = props;
3093
+ const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback, onRecharge, onDetails, onClose } = props;
2417
3094
  const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2418
3095
  const idle = !active && !loading && !showingFeedback && error === null;
2419
3096
  return /* @__PURE__ */ jsxs("div", {
@@ -2425,8 +3102,18 @@ function EmergencyBody(props) {
2425
3102
  onDetails
2426
3103
  })
2427
3104
  }) : null, /* @__PURE__ */ jsxs("div", {
2428
- className: cls("emergency-panel__prompt-region"),
3105
+ className: cls("emergency-panel__prompt-region", idle && "emergency-panel__prompt-region--idle"),
2429
3106
  children: [
3107
+ idle ? /* @__PURE__ */ jsx("button", {
3108
+ type: "button",
3109
+ className: cls("emergency-panel__close-button", "emergency-panel__close-button--prompt"),
3110
+ "aria-label": copy.close,
3111
+ onClick: onClose,
3112
+ children: /* @__PURE__ */ jsx(Icon, {
3113
+ name: "close",
3114
+ className: cls("emergency-panel__close-icon")
3115
+ })
3116
+ }) : null,
2430
3117
  !loading && !showingFeedback ? /* @__PURE__ */ jsx("div", {
2431
3118
  className: cls("sos-img__container"),
2432
3119
  id: "sos-img__container",
@@ -2465,47 +3152,16 @@ function EmergencyBody(props) {
2465
3152
  });
2466
3153
  }
2467
3154
 
2468
- // src/react/components/EmergencyFooter/index.tsx
2469
- function EmergencyFooter(props) {
2470
- const { active, busy, aborting, feedbackVisible, onActivate, onRequestAbort, onRequestSimulation } = props;
2471
- if (active) return /* @__PURE__ */ jsx("footer", {
2472
- className: cls("emergency-panel__footer"),
2473
- children: /* @__PURE__ */ jsxs("button", {
2474
- type: "button",
2475
- className: cls("emergency-status__abort-btn"),
2476
- "aria-label": copy.abort,
2477
- disabled: aborting || feedbackVisible,
2478
- onClick: onRequestAbort,
2479
- children: [copy.abort, /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })]
2480
- })
2481
- });
2482
- return /* @__PURE__ */ jsxs("footer", {
2483
- className: cls("emergency-panel__footer"),
2484
- children: [/* @__PURE__ */ jsxs("button", {
2485
- type: "button",
2486
- className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
2487
- "aria-label": copy.emergencyAria,
2488
- disabled: busy || feedbackVisible,
2489
- onClick: onActivate,
2490
- children: [copy.emergency, /* @__PURE__ */ jsx(Icon, { name: "emergency" })]
2491
- }), /* @__PURE__ */ jsxs("button", {
2492
- type: "button",
2493
- className: cls("emergency-panel__button", "emergency-panel__button--simulation"),
2494
- "aria-label": copy.testAria,
2495
- disabled: busy || feedbackVisible,
2496
- onClick: onRequestSimulation,
2497
- children: [copy.test, /* @__PURE__ */ jsx(Icon, { name: "list-check" })]
2498
- })]
2499
- });
2500
- }
2501
-
2502
3155
  // src/react/screens/primary/index.tsx
2503
3156
  function PrimaryScreen(props) {
2504
3157
  const { navigate } = useScreenNavigation();
2505
- return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
3158
+ const idle = !props.active && !props.loading && !props.feedbackVisible && props.error === null;
3159
+ const needsContactPhone = props.contactPhone === null;
3160
+ return /* @__PURE__ */ jsxs(EmergencyScreenLayout, {
2506
3161
  screen: "primary",
2507
3162
  titleId: props.titleId,
2508
- header: /* @__PURE__ */ jsx(EmergencyHeader, {
3163
+ bodyClassName: idle ? "emergency-panel__body--primary" : void 0,
3164
+ header: idle ? null : /* @__PURE__ */ jsx(EmergencyHeader, {
2509
3165
  titleId: props.titleId,
2510
3166
  onClose: () => props.onClose("close")
2511
3167
  }),
@@ -2514,11 +3170,17 @@ function PrimaryScreen(props) {
2514
3170
  busy: props.busy,
2515
3171
  aborting: props.aborting,
2516
3172
  feedbackVisible: props.feedbackVisible,
2517
- onActivate: props.onActivate,
3173
+ square: idle,
3174
+ onActivate: needsContactPhone ? () => navigate("pre-emergency") : props.onActivate,
2518
3175
  onRequestAbort: props.onRequestAbort,
2519
3176
  onRequestSimulation: props.onRequestSimulation
2520
3177
  }),
2521
- children: /* @__PURE__ */ jsx(EmergencyBody, {
3178
+ children: [idle ? /* @__PURE__ */ jsx("h2", {
3179
+ className: cls("emergency-panel__sr-only"),
3180
+ id: props.titleId,
3181
+ tabIndex: -1,
3182
+ children: copy.panelLabel
3183
+ }) : null, /* @__PURE__ */ jsx(EmergencyBody, {
2522
3184
  active: props.active,
2523
3185
  responders: props.responders,
2524
3186
  refreshing: props.refreshing,
@@ -2533,8 +3195,9 @@ function PrimaryScreen(props) {
2533
3195
  onRetry: props.onRetry,
2534
3196
  onDismissFeedback: props.onDismissFeedback,
2535
3197
  onRecharge: () => navigate("pricing"),
2536
- onDetails: () => navigate("details")
2537
- })
3198
+ onDetails: () => navigate("details"),
3199
+ onClose: () => props.onClose("close")
3200
+ })]
2538
3201
  });
2539
3202
  }
2540
3203
 
@@ -2548,7 +3211,7 @@ const pricingCopy = {
2548
3211
  chosen: "Seleccionado",
2549
3212
  whatsapp: "de WhatsApp",
2550
3213
  rounds: "rondas de alerta",
2551
- perMessage: "por mensaje + IVA",
3214
+ perMessage: "IVA Incluido",
2552
3215
  footnote: "Rondas calculadas con 3 contactos de emergencia. Válidas para pruebas y emergencias."
2553
3216
  };
2554
3217
 
@@ -2562,17 +3225,16 @@ const PRICING_PACKAGES = [{
2562
3225
  label: "PAQUETE 1",
2563
3226
  messages: 100,
2564
3227
  priceLabel: "$7",
2565
- priceCaption: "IVA incluido",
2566
- perMessageLabel: "$0.06",
3228
+ priceCaption: "IVA incluído",
3229
+ perMessageLabel: "$0.07",
2567
3230
  highlighted: false
2568
3231
  }, {
2569
3232
  id: "package-2",
2570
3233
  label: "PAQUETE 2",
2571
3234
  messages: 200,
2572
3235
  priceLabel: "12$",
2573
- priceNote: "($10 + IVA)",
2574
- priceCaption: "IVA incluido",
2575
- perMessageLabel: "$0.05",
3236
+ priceCaption: "IVA incluído",
3237
+ perMessageLabel: "$0.06",
2576
3238
  highlighted: true,
2577
3239
  badge: "MEJOR VALOR"
2578
3240
  }];
@@ -2649,6 +3311,10 @@ function PackageCard({ pkg, selected, onSelect }) {
2649
3311
  })
2650
3312
  ]
2651
3313
  }),
3314
+ /* @__PURE__ */ jsx("p", {
3315
+ className: cls("emergency-pricing__package-footnote"),
3316
+ children: pricingCopy.footnote
3317
+ }),
2652
3318
  /* @__PURE__ */ jsx("button", {
2653
3319
  type: "button",
2654
3320
  className: cls("emergency-pricing__select"),
@@ -2677,30 +3343,23 @@ function PricingScreen(props) {
2677
3343
  footer: null,
2678
3344
  children: /* @__PURE__ */ jsxs("div", {
2679
3345
  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
3346
+ children: [/* @__PURE__ */ jsx("div", {
3347
+ className: cls("emergency-pricing__heading"),
3348
+ children: /* @__PURE__ */ jsxs("div", {
3349
+ className: cls("emergency-pricing__titles"),
3350
+ children: [/* @__PURE__ */ jsx("h3", {
3351
+ className: cls("emergency-pricing__title"),
3352
+ children: pricingCopy.heading
3353
+ }), /* @__PURE__ */ jsx("p", {
3354
+ className: cls("emergency-pricing__subtitle"),
3355
+ children: pricingCopy.subtitle
3356
+ })]
2702
3357
  })
2703
- ]
3358
+ }), PRICING_PACKAGES.map((pkg) => /* @__PURE__ */ jsx(PackageCard, {
3359
+ pkg,
3360
+ selected: selected === pkg.id,
3361
+ onSelect: setSelected
3362
+ }, pkg.id))]
2704
3363
  })
2705
3364
  });
2706
3365
  }
@@ -2709,8 +3368,11 @@ function PricingScreen(props) {
2709
3368
  /** Every screen the panel can render. Add one entry to add a screen. */
2710
3369
  const screenRegistry = {
2711
3370
  primary: PrimaryScreen,
3371
+ "pre-emergency": PreEmergencyScreen,
2712
3372
  pricing: PricingScreen,
2713
- details: DetailsScreen
3373
+ details: DetailsScreen,
3374
+ history: HistoryScreen,
3375
+ "message-detail": MessageDetailScreen
2714
3376
  };
2715
3377
 
2716
3378
  // src/react/components/EmergencyPanel/index.tsx
@@ -2739,7 +3401,7 @@ function ActiveScreen({ screenProps, open, forcePrimary }) {
2739
3401
  return /* @__PURE__ */ jsx(Screen, { ...screenProps });
2740
3402
  }
2741
3403
  function EmergencyPanel(props) {
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;
3404
+ const { open, theme, active, busy, aborting, loading, locale, responders, refreshing, geolocationError, contactPhone, activationError, error, abortResult, simulationResult, abortConfirmation, simulationConfirmation, onClose, onActivate, onRefresh, onRetry, onDismissFeedback, onRequestAbort, onRequestSimulation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation, onSetContactPhone } = props;
2743
3405
  const titleId = useId();
2744
3406
  const dialogRef = useRef(null);
2745
3407
  const feedbackVisible = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
@@ -2764,13 +3426,15 @@ function EmergencyPanel(props) {
2764
3426
  abortResult,
2765
3427
  simulationResult,
2766
3428
  feedbackVisible,
3429
+ contactPhone,
2767
3430
  onClose,
2768
3431
  onActivate,
2769
3432
  onRefresh,
2770
3433
  onRetry,
2771
3434
  onDismissFeedback,
2772
3435
  onRequestAbort,
2773
- onRequestSimulation
3436
+ onRequestSimulation,
3437
+ onSetContactPhone
2774
3438
  };
2775
3439
  return /* @__PURE__ */ jsxs("div", {
2776
3440
  ref: dialogRef,
@@ -2877,6 +3541,7 @@ function EmergencyRoot() {
2877
3541
  const responders = useEmergencyStore((state) => state.responders);
2878
3542
  const resumeAvailable = useEmergencyStore((state) => state.resumeAvailable);
2879
3543
  const geolocationError = useEmergencyStore((state) => state.geolocationError);
3544
+ const contactPhone = useEmergencyStore((state) => state.contactPhone);
2880
3545
  const lastError = useEmergencyStore((state) => state.lastError);
2881
3546
  const abortResult = useEmergencyStore((state) => state.abortResult);
2882
3547
  const simulationResult = useEmergencyStore((state) => state.simulationResult);
@@ -2902,6 +3567,7 @@ function EmergencyRoot() {
2902
3567
  responders,
2903
3568
  refreshing: phase.kind === "refreshing",
2904
3569
  geolocationError,
3570
+ contactPhone,
2905
3571
  activationError,
2906
3572
  error: showError ? error : null,
2907
3573
  abortResult,
@@ -2928,7 +3594,8 @@ function EmergencyRoot() {
2928
3594
  },
2929
3595
  onConfirmSimulation: () => {
2930
3596
  client.simulate();
2931
- }
3597
+ },
3598
+ onSetContactPhone: (phone) => client.setContactPhone(phone)
2932
3599
  })] });
2933
3600
  }
2934
3601
 
@@ -2960,6 +3627,7 @@ function createFailedClient(error) {
2960
3627
  lastError: error,
2961
3628
  geolocation: null,
2962
3629
  geolocationError: null,
3630
+ contactPhone: null,
2963
3631
  realtimeStatus: "idle",
2964
3632
  abortResult: null,
2965
3633
  simulationResult: null,
@@ -2988,7 +3656,8 @@ function createFailedClient(error) {
2988
3656
  cancelAbortConfirmation: noop,
2989
3657
  requestSimulationConfirmation: noop,
2990
3658
  cancelSimulationConfirmation: noop,
2991
- dismissFeedback: noop
3659
+ dismissFeedback: noop,
3660
+ setContactPhone: noop
2992
3661
  };
2993
3662
  }
2994
3663
  function attachTrigger(trigger, client) {
@@ -3077,6 +3746,7 @@ function mount(options) {
3077
3746
  requestSimulationConfirmation: () => client.requestSimulationConfirmation(),
3078
3747
  cancelSimulationConfirmation: () => client.cancelSimulationConfirmation(),
3079
3748
  dismissFeedback: () => client.dismissFeedback(),
3749
+ setContactPhone: (phone) => client.setContactPhone(phone),
3080
3750
  destroy
3081
3751
  };
3082
3752
  }