@dataemergencia/emergency 0.5.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
@@ -1547,6 +1547,12 @@ var panel_module_default = {
1547
1547
  "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-nA5Hwa",
1548
1548
  "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-nA5Hwa",
1549
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",
1550
1556
  "emergency-availability": "de-emergency-emergency-availability-nA5Hwa",
1551
1557
  "emergency-availability__card": "de-emergency-emergency-availability__card-nA5Hwa",
1552
1558
  "emergency-availability__card-body": "de-emergency-emergency-availability__card-body-nA5Hwa",
@@ -1570,6 +1576,19 @@ var panel_module_default = {
1570
1576
  "emergency-confirmation__icon--information": "de-emergency-emergency-confirmation__icon--information-nA5Hwa",
1571
1577
  "emergency-confirmation__text": "de-emergency-emergency-confirmation__text-nA5Hwa",
1572
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",
1573
1592
  "emergency-error-message": "de-emergency-emergency-error-message-nA5Hwa",
1574
1593
  "emergency-error-message__container": "de-emergency-emergency-error-message__container-nA5Hwa",
1575
1594
  "emergency-error-message__content": "de-emergency-emergency-error-message__content-nA5Hwa",
@@ -1580,11 +1599,33 @@ var panel_module_default = {
1580
1599
  "emergency-error-message__title": "de-emergency-emergency-error-message__title-nA5Hwa",
1581
1600
  "emergency-feedback": "de-emergency-emergency-feedback-nA5Hwa",
1582
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",
1583
1620
  "emergency-panel": "de-emergency-emergency-panel-nA5Hwa",
1584
1621
  "emergency-panel__availability-region": "de-emergency-emergency-panel__availability-region-nA5Hwa",
1585
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",
1586
1626
  "emergency-panel__body--pre-emergency": "de-emergency-emergency-panel__body--pre-emergency-nA5Hwa",
1587
1627
  "emergency-panel__body--pricing": "de-emergency-emergency-panel__body--pricing-nA5Hwa",
1628
+ "emergency-panel__body--primary": "de-emergency-emergency-panel__body--primary-nA5Hwa",
1588
1629
  "emergency-panel__body-split": "de-emergency-emergency-panel__body-split-nA5Hwa",
1589
1630
  "emergency-panel__body-split--with-availability": "de-emergency-emergency-panel__body-split--with-availability-nA5Hwa",
1590
1631
  "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-nA5Hwa",
@@ -1593,13 +1634,14 @@ var panel_module_default = {
1593
1634
  "emergency-panel__button--emergency": "de-emergency-emergency-panel__button--emergency-nA5Hwa",
1594
1635
  "emergency-panel__button--simulation": "de-emergency-emergency-panel__button--simulation-nA5Hwa",
1595
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",
1596
1638
  "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-nA5Hwa",
1597
- "emergency-panel__divider": "de-emergency-emergency-panel__divider-nA5Hwa",
1598
1639
  "emergency-panel__footer": "de-emergency-emergency-panel__footer-nA5Hwa",
1599
1640
  "emergency-panel__footer--single": "de-emergency-emergency-panel__footer--single-nA5Hwa",
1641
+ "emergency-panel__footer--square": "de-emergency-emergency-panel__footer--square-nA5Hwa",
1600
1642
  "emergency-panel__header": "de-emergency-emergency-panel__header-nA5Hwa",
1601
- "emergency-panel__header--close-only": "de-emergency-emergency-panel__header--close-only-nA5Hwa",
1602
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",
1603
1645
  "emergency-panel__sr-only": "de-emergency-emergency-panel__sr-only-nA5Hwa",
1604
1646
  "emergency-panel__title": "de-emergency-emergency-panel__title-nA5Hwa",
1605
1647
  "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-nA5Hwa",
@@ -1818,18 +1860,27 @@ function useFocusTrap(ref, active) {
1818
1860
  }
1819
1861
 
1820
1862
  // src/react/screens/context.tsx
1863
+ const EMPTY_PARAMS = {};
1821
1864
  const EmergencyScreenContext = createContext(null);
1865
+ function paramsEqual(first, second) {
1866
+ return first.messageId === second.messageId;
1867
+ }
1822
1868
  function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1823
1869
  const [state, setState] = useState({
1824
1870
  screen: initialScreen,
1871
+ params: EMPTY_PARAMS,
1825
1872
  history: []
1826
1873
  });
1827
- const navigate = useCallback((next) => {
1874
+ const navigate = useCallback((next, params = EMPTY_PARAMS) => {
1828
1875
  setState((current) => {
1829
- if (current.screen === next) return current;
1876
+ if (current.screen === next && paramsEqual(current.params, params)) return current;
1830
1877
  return {
1831
1878
  screen: next,
1832
- history: [...current.history, current.screen]
1879
+ params,
1880
+ history: [...current.history, {
1881
+ screen: current.screen,
1882
+ params: current.params
1883
+ }]
1833
1884
  };
1834
1885
  });
1835
1886
  }, []);
@@ -1838,31 +1889,47 @@ function EmergencyScreenProvider({ children, initialScreen = "primary" }) {
1838
1889
  const previous = current.history.at(-1);
1839
1890
  if (!previous) return current;
1840
1891
  return {
1841
- screen: previous,
1892
+ screen: previous.screen,
1893
+ params: previous.params,
1842
1894
  history: current.history.slice(0, -1)
1843
1895
  };
1844
1896
  });
1845
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
+ }, []);
1846
1908
  const reset = useCallback(() => {
1847
1909
  setState((current) => {
1848
- 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;
1849
1911
  return {
1850
1912
  screen: "primary",
1913
+ params: EMPTY_PARAMS,
1851
1914
  history: []
1852
1915
  };
1853
1916
  });
1854
1917
  }, []);
1855
1918
  const value = useMemo(() => ({
1856
1919
  screen: state.screen,
1920
+ params: state.params,
1857
1921
  history: state.history,
1858
1922
  navigate,
1923
+ replace,
1859
1924
  back,
1860
1925
  reset
1861
1926
  }), [
1862
1927
  back,
1863
1928
  navigate,
1929
+ replace,
1864
1930
  reset,
1865
1931
  state.history,
1932
+ state.params,
1866
1933
  state.screen
1867
1934
  ]);
1868
1935
  return /* @__PURE__ */ jsx(EmergencyScreenContext.Provider, {
@@ -1877,12 +1944,11 @@ function useScreenNavigation() {
1877
1944
  }
1878
1945
 
1879
1946
  // src/react/components/EmergencyHeader/index.tsx
1880
- function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack, variant = "default" }) {
1881
- const closeOnly = variant === "close-only";
1947
+ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack }) {
1882
1948
  return /* @__PURE__ */ jsxs("header", {
1883
- className: cls("emergency-panel__header", closeOnly && "emergency-panel__header--close-only"),
1949
+ className: cls("emergency-panel__header"),
1884
1950
  children: [
1885
- onBack && !closeOnly ? /* @__PURE__ */ jsx("button", {
1951
+ onBack ? /* @__PURE__ */ jsx("button", {
1886
1952
  type: "button",
1887
1953
  className: cls("emergency-panel__close-button"),
1888
1954
  "aria-label": copy.back,
@@ -1893,7 +1959,7 @@ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack, va
1893
1959
  })
1894
1960
  }) : null,
1895
1961
  /* @__PURE__ */ jsx("h2", {
1896
- className: cls("emergency-panel__title", closeOnly && "emergency-panel__sr-only"),
1962
+ className: cls("emergency-panel__title"),
1897
1963
  id: titleId,
1898
1964
  tabIndex: -1,
1899
1965
  children: title
@@ -1912,6 +1978,245 @@ function EmergencyHeader({ titleId, onClose, title = copy.panelLabel, onBack, va
1912
1978
  });
1913
1979
  }
1914
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
+
1915
2220
  // src/react/screens/layout.tsx
1916
2221
  /**
1917
2222
  * Shared shell for every screen. The wrapper is `display: contents` so the
@@ -1933,46 +2238,254 @@ function EmergencyScreenLayout({ screen, titleId, header, footer, children, body
1933
2238
  });
1934
2239
  }
1935
2240
 
1936
- // src/react/screens/PlaceholderScreen.tsx
1937
- /** Minimal in-design screen used to prove navigation before real designs land. */
1938
- 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);
1939
2246
  return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
1940
- screen,
2247
+ screen: "details",
1941
2248
  titleId,
2249
+ bodyClassName: "emergency-panel__body--details",
1942
2250
  header: /* @__PURE__ */ jsx(EmergencyHeader, {
1943
2251
  titleId,
1944
- title,
1945
- onClose,
1946
- onBack
2252
+ title: detailsCopy.title,
2253
+ onClose: () => onClose("close"),
2254
+ onBack: back
1947
2255
  }),
1948
2256
  footer: null,
1949
- children: /* @__PURE__ */ jsx("div", {
1950
- className: cls("emergency-screen__placeholder"),
1951
- children: /* @__PURE__ */ jsx("p", {
1952
- className: cls("emergency-screen__placeholder-text"),
1953
- children: description
1954
- })
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
+ })]
1955
2317
  })
1956
2318
  });
1957
2319
  }
1958
2320
 
1959
- // src/react/screens/details/copy.ts
1960
- const detailsCopy = {
1961
- title: "Detalles",
1962
- description: "Aquí podrás consultar el detalle de tus alertas disponibles. Esta pantalla estará disponible próximamente."
1963
- };
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
+ }
1964
2374
 
1965
- // src/react/screens/details/index.tsx
1966
- function DetailsScreen({ titleId, onClose }) {
1967
- const { back } = useScreenNavigation();
1968
- return /* @__PURE__ */ jsx(PlaceholderScreen, {
1969
- screen: "details",
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, {
1970
2390
  titleId,
1971
- title: detailsCopy.title,
1972
- description: detailsCopy.description,
2391
+ title: messageDetailCopy.title,
1973
2392
  onClose: () => onClose("close"),
1974
2393
  onBack: back
1975
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,
2421
+ children: /* @__PURE__ */ jsx("div", {
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
+ })]
2486
+ })
2487
+ })
2488
+ });
1976
2489
  }
1977
2490
 
1978
2491
  // src/core/phone.ts
@@ -1997,9 +2510,9 @@ function toVenezuelanE164(input) {
1997
2510
 
1998
2511
  // src/react/components/EmergencyFooter/index.tsx
1999
2512
  function EmergencyFooter(props) {
2000
- const { active, busy, aborting, feedbackVisible, showSimulation = true, emergencyDisabled = false, onActivate, onRequestAbort, onRequestSimulation } = props;
2513
+ const { active, busy, aborting, feedbackVisible, showSimulation = true, emergencyDisabled = false, square = false, onActivate, onRequestAbort, onRequestSimulation } = props;
2001
2514
  if (active) return /* @__PURE__ */ jsx("footer", {
2002
- className: cls("emergency-panel__footer"),
2515
+ className: cls("emergency-panel__footer", square && "emergency-panel__footer--square"),
2003
2516
  children: /* @__PURE__ */ jsxs("button", {
2004
2517
  type: "button",
2005
2518
  className: cls("emergency-status__abort-btn"),
@@ -2010,7 +2523,7 @@ function EmergencyFooter(props) {
2010
2523
  })
2011
2524
  });
2012
2525
  return /* @__PURE__ */ jsxs("footer", {
2013
- className: cls("emergency-panel__footer", !showSimulation && "emergency-panel__footer--single"),
2526
+ className: cls("emergency-panel__footer", !showSimulation && "emergency-panel__footer--single", square && "emergency-panel__footer--square"),
2014
2527
  children: [/* @__PURE__ */ jsxs("button", {
2015
2528
  type: "button",
2016
2529
  className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
@@ -2373,32 +2886,6 @@ function EmergencyLoading() {
2373
2886
  });
2374
2887
  }
2375
2888
 
2376
- // src/core/date.ts
2377
- /**
2378
- * Formats a timestamp as `h:mmam` or `h:mmpm` in local time, without a space
2379
- * or leading zero.
2380
- */
2381
- function formatShortTime(value, locale = "es") {
2382
- if (value === null) return "";
2383
- const date = value instanceof Date ? value : new Date(value);
2384
- if (Number.isNaN(date.getTime())) return "";
2385
- const parts = new Intl.DateTimeFormat(locale, {
2386
- hour: "numeric",
2387
- minute: "2-digit",
2388
- hour12: true
2389
- }).formatToParts(date);
2390
- const hour = parts.find((part) => part.type === "hour")?.value;
2391
- const minute = parts.find((part) => part.type === "minute")?.value;
2392
- const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value;
2393
- if (!hour || !minute) return "";
2394
- return `${hour}:${minute}${normalizePeriod(dayPeriod)}`;
2395
- }
2396
- function normalizePeriod(dayPeriod) {
2397
- if (!dayPeriod) return "";
2398
- if (/^p/i.test(dayPeriod)) return "pm";
2399
- return /^a/i.test(dayPeriod) ? "am" : "";
2400
- }
2401
-
2402
2889
  // src/react/components/StatusIcon/index.tsx
2403
2890
  /**
2404
2891
  * Explicit responder state model:
@@ -2559,23 +3046,9 @@ function formatRounds(rounds) {
2559
3046
  return `${rounds} ${roundsUnit(rounds)}`;
2560
3047
  }
2561
3048
 
2562
- // src/react/screens/primary/data.ts
2563
- /**
2564
- * Derives the remaining alert rounds from available messages and designated
2565
- * responders. Invalid or non-positive input collapses to the minimum of one
2566
- * round instead of producing `Infinity`, `NaN` or a negative value.
2567
- */
2568
- function calculateRounds(messages, responders) {
2569
- if (!Number.isFinite(messages) || !Number.isFinite(responders)) return 1;
2570
- const availableMessages = Math.floor(messages);
2571
- const designatedResponders = Math.floor(responders);
2572
- if (availableMessages <= 0 || designatedResponders <= 0) return 1;
2573
- return Math.max(1, Math.floor(availableMessages / designatedResponders));
2574
- }
2575
-
2576
3049
  // src/react/screens/primary/Availability.tsx
2577
3050
  function Availability({ onRecharge, onDetails }) {
2578
- const rounds = calculateRounds(100, 3);
3051
+ const rounds = calculateRounds(27, 3);
2579
3052
  return /* @__PURE__ */ jsxs("section", {
2580
3053
  className: cls("emergency-availability"),
2581
3054
  "aria-label": primaryCopy.availabilityTitle,
@@ -2617,23 +3090,30 @@ function Availability({ onRecharge, onDetails }) {
2617
3090
 
2618
3091
  // src/react/components/EmergencyBody/index.tsx
2619
3092
  function EmergencyBody(props) {
2620
- 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;
2621
3094
  const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2622
3095
  const idle = !active && !loading && !showingFeedback && error === null;
2623
3096
  return /* @__PURE__ */ jsxs("div", {
2624
3097
  className: cls("emergency-panel__body-split", idle && "emergency-panel__body-split--with-availability"),
2625
- children: [idle ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
3098
+ children: [idle ? /* @__PURE__ */ jsx("div", {
2626
3099
  className: cls("emergency-panel__availability-region"),
2627
3100
  children: /* @__PURE__ */ jsx(Availability, {
2628
3101
  onRecharge,
2629
3102
  onDetails
2630
3103
  })
2631
- }), /* @__PURE__ */ jsx("hr", {
2632
- className: cls("emergency-panel__divider"),
2633
- "aria-hidden": "true"
2634
- })] }) : null, /* @__PURE__ */ jsxs("div", {
2635
- className: cls("emergency-panel__prompt-region"),
3104
+ }) : null, /* @__PURE__ */ jsxs("div", {
3105
+ className: cls("emergency-panel__prompt-region", idle && "emergency-panel__prompt-region--idle"),
2636
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,
2637
3117
  !loading && !showingFeedback ? /* @__PURE__ */ jsx("div", {
2638
3118
  className: cls("sos-img__container"),
2639
3119
  id: "sos-img__container",
@@ -2677,12 +3157,12 @@ function PrimaryScreen(props) {
2677
3157
  const { navigate } = useScreenNavigation();
2678
3158
  const idle = !props.active && !props.loading && !props.feedbackVisible && props.error === null;
2679
3159
  const needsContactPhone = props.contactPhone === null;
2680
- return /* @__PURE__ */ jsx(EmergencyScreenLayout, {
3160
+ return /* @__PURE__ */ jsxs(EmergencyScreenLayout, {
2681
3161
  screen: "primary",
2682
3162
  titleId: props.titleId,
2683
- header: /* @__PURE__ */ jsx(EmergencyHeader, {
3163
+ bodyClassName: idle ? "emergency-panel__body--primary" : void 0,
3164
+ header: idle ? null : /* @__PURE__ */ jsx(EmergencyHeader, {
2684
3165
  titleId: props.titleId,
2685
- variant: idle ? "close-only" : "default",
2686
3166
  onClose: () => props.onClose("close")
2687
3167
  }),
2688
3168
  footer: /* @__PURE__ */ jsx(EmergencyFooter, {
@@ -2690,11 +3170,17 @@ function PrimaryScreen(props) {
2690
3170
  busy: props.busy,
2691
3171
  aborting: props.aborting,
2692
3172
  feedbackVisible: props.feedbackVisible,
3173
+ square: idle,
2693
3174
  onActivate: needsContactPhone ? () => navigate("pre-emergency") : props.onActivate,
2694
3175
  onRequestAbort: props.onRequestAbort,
2695
3176
  onRequestSimulation: props.onRequestSimulation
2696
3177
  }),
2697
- 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, {
2698
3184
  active: props.active,
2699
3185
  responders: props.responders,
2700
3186
  refreshing: props.refreshing,
@@ -2709,8 +3195,9 @@ function PrimaryScreen(props) {
2709
3195
  onRetry: props.onRetry,
2710
3196
  onDismissFeedback: props.onDismissFeedback,
2711
3197
  onRecharge: () => navigate("pricing"),
2712
- onDetails: () => navigate("details")
2713
- })
3198
+ onDetails: () => navigate("details"),
3199
+ onClose: () => props.onClose("close")
3200
+ })]
2714
3201
  });
2715
3202
  }
2716
3203
 
@@ -2883,7 +3370,9 @@ const screenRegistry = {
2883
3370
  primary: PrimaryScreen,
2884
3371
  "pre-emergency": PreEmergencyScreen,
2885
3372
  pricing: PricingScreen,
2886
- details: DetailsScreen
3373
+ details: DetailsScreen,
3374
+ history: HistoryScreen,
3375
+ "message-detail": MessageDetailScreen
2887
3376
  };
2888
3377
 
2889
3378
  // src/react/components/EmergencyPanel/index.tsx