@dataemergencia/emergency 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,2489 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import axios, { isAxiosError } from "axios";
3
+ import Pusher from "pusher-js";
4
+ import { createStore } from "zustand/vanilla";
5
+ import { createContext, useCallback, useContext, useEffect, useId, useRef } from "react";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { useStore } from "zustand";
8
+
9
+ // src/core/types.ts
10
+ const EMERGENCY_ERROR_CODES = [
11
+ "configuration",
12
+ "network",
13
+ "cancelled",
14
+ "invalid_response",
15
+ "invalid_transition",
16
+ "destroyed",
17
+ "geolocation_denied",
18
+ "geolocation_unavailable",
19
+ "geolocation_timeout",
20
+ "geolocation_unsupported",
21
+ "validation_failed",
22
+ "invalid_code",
23
+ "inactive_code",
24
+ "unauthenticated",
25
+ "csrf_failed",
26
+ "cross_origin_denied",
27
+ "rate_limited",
28
+ "not_found",
29
+ "method_not_allowed",
30
+ "emergency_already_active",
31
+ "emergency_forbidden",
32
+ "emergency_not_found",
33
+ "missing_emergency_contacts",
34
+ "missing_medical_profile",
35
+ "whatsapp_delivery_failed",
36
+ "upstream_error",
37
+ "internal_error",
38
+ "emergency_error",
39
+ "unknown"
40
+ ];
41
+
42
+ // src/core/guards.ts
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function isString(value) {
47
+ return typeof value === "string";
48
+ }
49
+ function isBoolean(value) {
50
+ return typeof value === "boolean";
51
+ }
52
+ function isFiniteNumber(value) {
53
+ return typeof value === "number" && Number.isFinite(value);
54
+ }
55
+ function isArrayOf(value, guard) {
56
+ return Array.isArray(value) && value.every(guard);
57
+ }
58
+ function isRealtimeConfig(value) {
59
+ if (!isRecord(value) || value["mode"] !== "public" || !isRecord(value["pusher"])) return false;
60
+ const pusher = value["pusher"];
61
+ return isString(pusher["key"]) && isString(pusher["cluster"]) && isString(pusher["channel_pattern"]);
62
+ }
63
+ function isConfigResponse(value) {
64
+ return isRecord(value) && isString(value["route_prefix"]) && isString(value["csrf_token"]) && isRealtimeConfig(value["realtime"]);
65
+ }
66
+ function isSessionResponse(value) {
67
+ return isRecord(value) && isString(value["code"]) && isString(value["csrf_token"]) && isFiniteNumber(value["expires_in"]);
68
+ }
69
+ function isResponderPayload(value) {
70
+ if (!isRecord(value)) return false;
71
+ return isString(value["phone"]) && isString(value["name"]) && isBoolean(value["sent"]) && isBoolean(value["delivered"]) && isBoolean(value["read"]) && isBoolean(value["failed"]) && (value["message_id"] === null || isString(value["message_id"]));
72
+ }
73
+ function isFailurePayload(value) {
74
+ if (!isRecord(value)) return false;
75
+ return isString(value["phone"]) && isString(value["message"]) && (value["status"] === void 0 || value["status"] === null || isFiniteNumber(value["status"]));
76
+ }
77
+ function isEmergencyResultPayload(value) {
78
+ if (!isRecord(value)) return false;
79
+ return (value["emergency_id"] === null || isString(value["emergency_id"])) && isBoolean(value["simulated"]) && isArrayOf(value["responders"], isResponderPayload) && isArrayOf(value["failures"], isFailurePayload);
80
+ }
81
+ function isCurrentResponse(value) {
82
+ if (!isRecord(value) || !isBoolean(value["active"]) || !isRealtimeConfig(value["realtime"])) return false;
83
+ return value["emergency"] === null || isRecord(value["emergency"]) && !Array.isArray(value["emergency"]);
84
+ }
85
+ function isRefreshResponse(value) {
86
+ return isRecord(value) && isRecord(value["emergency"]) && isArrayOf(value["responders"], isResponderPayload);
87
+ }
88
+ function isAbortResponse(value) {
89
+ return isEmergencyResultPayload(value) && isRecord(value) && isBoolean(value["deleted"]);
90
+ }
91
+ function isErrorEnvelope(value) {
92
+ if (!isRecord(value) || !isRecord(value["error"])) return false;
93
+ const error = value["error"];
94
+ return isString(error["code"]) && isString(error["message"]) && (error["details"] === void 0 || error["details"] === null || isRecord(error["details"]) || Array.isArray(error["details"])) && (error["request_id"] === void 0 || isString(error["request_id"]));
95
+ }
96
+
97
+ // src/core/errors.ts
98
+ function isEmergencyErrorCode(value) {
99
+ return typeof value === "string" && EMERGENCY_ERROR_CODES.some((code) => code === value);
100
+ }
101
+ function createEmergencyError(code, message, options = {}) {
102
+ return {
103
+ code,
104
+ message,
105
+ status: options.status ?? null,
106
+ requestId: options.requestId ?? null,
107
+ details: options.details ?? null,
108
+ cause: options.cause ?? null
109
+ };
110
+ }
111
+ function isEmergencyError(value) {
112
+ if (!isRecord(value)) return false;
113
+ return typeof value["code"] === "string" && typeof value["message"] === "string" && "status" in value && "requestId" in value;
114
+ }
115
+ function toEmergencyError(value, fallbackCode = "unknown") {
116
+ if (isEmergencyError(value)) return value;
117
+ if (value instanceof Error) return createEmergencyError(fallbackCode, value.message, { cause: value });
118
+ return createEmergencyError(fallbackCode, "An unexpected error occurred.", { cause: value });
119
+ }
120
+ /**
121
+ * Error object accepted by host integrations that prefer `throw`/`catch` while
122
+ * the library still exposes the typed error through `.error`.
123
+ */
124
+ var EmergencyClientError = class extends Error {
125
+ error;
126
+ constructor(error) {
127
+ super(error.message);
128
+ this.name = "EmergencyClientError";
129
+ this.error = error;
130
+ }
131
+ };
132
+
133
+ // src/core/bff-client.ts
134
+ function createMemoryCsrfStrategy() {
135
+ let token = null;
136
+ return {
137
+ headerName: "X-CSRF-Token",
138
+ read: () => token,
139
+ setToken: (next) => {
140
+ token = next;
141
+ }
142
+ };
143
+ }
144
+ /**
145
+ * Laravel-compatible adapter. The core never assumes Laravel; a host that uses
146
+ * the `XSRF-TOKEN` cookie opts in by passing this strategy.
147
+ */
148
+ function createCookieCsrfStrategy(cookieName = "XSRF-TOKEN") {
149
+ return {
150
+ headerName: "X-XSRF-TOKEN",
151
+ read: () => {
152
+ if (typeof document === "undefined" || document.cookie === "") return null;
153
+ const prefix = `${cookieName}=`;
154
+ for (const entry of document.cookie.split(";")) {
155
+ const trimmed = entry.trim();
156
+ if (trimmed.startsWith(prefix)) return decodeURIComponent(trimmed.slice(prefix.length));
157
+ }
158
+ return null;
159
+ }
160
+ };
161
+ }
162
+ function defaultIdempotencyKey() {
163
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
164
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
165
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
166
+ crypto.getRandomValues(bytes);
167
+ return `de-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
168
+ }
169
+ return `de-${Date.now()}-${Math.random().toString(16).slice(2)}`;
170
+ }
171
+ const MAX_DETAILS_DEPTH = 2;
172
+ const MAX_DETAILS_ITEMS = 20;
173
+ const MAX_DETAILS_NODES = 500;
174
+ const MAX_DETAILS_STRING = 200;
175
+ /**
176
+ * Error `details` may carry the whole upstream payload, including responder
177
+ * phone numbers. Hosts forward errors to logging and telemetry, so keep only a
178
+ * shallow, bounded copy.
179
+ *
180
+ * Arrays are collections, not structure: their entries are processed at the
181
+ * array's depth so a list of records stays readable. A node budget keeps
182
+ * pathological nesting bounded even without a depth increment per array.
183
+ */
184
+ function redactDetails(value) {
185
+ return redactValue(value, 0, { remaining: MAX_DETAILS_NODES });
186
+ }
187
+ function redactValue(value, depth, budget) {
188
+ if (budget.remaining <= 0) return "…";
189
+ budget.remaining -= 1;
190
+ if (value === null || typeof value === "boolean") return value;
191
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
192
+ if (typeof value === "string") return value.length > MAX_DETAILS_STRING ? `${value.slice(0, MAX_DETAILS_STRING)}…` : value;
193
+ if (Array.isArray(value)) return value.slice(0, MAX_DETAILS_ITEMS).map((entry) => redactValue(entry, depth, budget));
194
+ if (isRecord(value)) {
195
+ if (depth >= MAX_DETAILS_DEPTH) return "{…}";
196
+ const result = {};
197
+ for (const [key, entry] of Object.entries(value).slice(0, MAX_DETAILS_ITEMS)) result[key] = redactValue(entry, depth + 1, budget);
198
+ return result;
199
+ }
200
+ return null;
201
+ }
202
+ function normalizeAxiosError(error) {
203
+ if (isAxiosError(error)) {
204
+ if (error.code === "ERR_CANCELED" || error.name === "CanceledError") return createEmergencyError("cancelled", "The request was cancelled.", { cause: error });
205
+ const status = error.response?.status ?? null;
206
+ const data = error.response?.data;
207
+ const headerRequestId = readHeader(error.response?.headers, "x-request-id");
208
+ if (isErrorEnvelope(data)) {
209
+ const code = isEmergencyErrorCode(data.error.code) ? data.error.code : "upstream_error";
210
+ return createEmergencyError(code, data.error.message, {
211
+ status,
212
+ requestId: data.error.request_id ?? headerRequestId,
213
+ details: redactDetails(data.error.details),
214
+ cause: error
215
+ });
216
+ }
217
+ if (status === null) return createEmergencyError("network", "The emergency service could not be reached.", { cause: error });
218
+ return createEmergencyError(status >= 500 ? "upstream_error" : "unknown", error.message || "The emergency service returned an error.", {
219
+ status,
220
+ requestId: headerRequestId,
221
+ cause: error
222
+ });
223
+ }
224
+ if (error instanceof Error) {
225
+ if (error.name === "AbortError") return createEmergencyError("cancelled", "The request was cancelled.", { cause: error });
226
+ return createEmergencyError("network", error.message, { cause: error });
227
+ }
228
+ return createEmergencyError("unknown", "The emergency service returned an error.", { cause: error });
229
+ }
230
+ function readHeader(headers, name) {
231
+ if (!isRecord(headers)) return null;
232
+ const direct = headers[name] ?? headers[name.toLowerCase()];
233
+ if (typeof direct === "string" && direct !== "") return direct;
234
+ const getter = headers["get"];
235
+ if (typeof getter === "function") {
236
+ const value = getter.call(headers, name);
237
+ return typeof value === "string" && value !== "" ? value : null;
238
+ }
239
+ return typeof direct === "string" && direct !== "" ? direct : null;
240
+ }
241
+ function createBffAxiosInstance(options) {
242
+ return axios.create({
243
+ baseURL: options.bffBaseUrl.replace(/\/+$/, ""),
244
+ timeout: options.timeoutMs ?? 15e3,
245
+ withCredentials: options.withCredentials ?? true,
246
+ maxRedirects: options.maxRedirects ?? 0,
247
+ headers: {
248
+ Accept: "application/json",
249
+ "X-Requested-With": "XMLHttpRequest"
250
+ }
251
+ });
252
+ }
253
+ function createBffClient(options) {
254
+ const csrf = options.csrf ?? createMemoryCsrfStrategy();
255
+ const controllers = /* @__PURE__ */ new Set();
256
+ let destroyed = false;
257
+ let csrfToken = csrf.read();
258
+ const instance = options.axiosInstance ?? createBffAxiosInstance(options);
259
+ function captureCsrf(payload) {
260
+ if (typeof payload.csrf_token === "string" && payload.csrf_token !== "") {
261
+ csrfToken = payload.csrf_token;
262
+ if ("setToken" in csrf && typeof csrf.setToken === "function") csrf.setToken(csrfToken);
263
+ }
264
+ }
265
+ async function execute(config, guard, externalSignal) {
266
+ if (destroyed) throw createEmergencyError("destroyed", "The emergency client has been destroyed.");
267
+ const controller = new AbortController();
268
+ controllers.add(controller);
269
+ const abortFromExternal = () => {
270
+ controller.abort();
271
+ };
272
+ if (externalSignal) {
273
+ if (externalSignal.aborted) controller.abort();
274
+ else externalSignal.addEventListener("abort", abortFromExternal, { once: true });
275
+ }
276
+ const token = csrf.read() ?? csrfToken;
277
+ const headers = {};
278
+ if (isRecord(config.headers)) {
279
+ for (const [key, entry] of Object.entries(config.headers)) if (typeof entry === "string") headers[key] = entry;
280
+ }
281
+ if (token) headers[csrf.headerName] = token;
282
+ try {
283
+ const response = await instance.request({
284
+ ...config,
285
+ headers,
286
+ signal: controller.signal
287
+ });
288
+ if (!guard(response.data)) throw createEmergencyError("invalid_response", "The emergency service returned an unexpected response.", {
289
+ status: response.status,
290
+ details: redactDetails(response.data)
291
+ });
292
+ return response.data;
293
+ } catch (error) {
294
+ if (isAxiosError(error) || error instanceof Error) throw normalizeAxiosError(error);
295
+ throw error;
296
+ } finally {
297
+ controllers.delete(controller);
298
+ externalSignal?.removeEventListener("abort", abortFromExternal);
299
+ }
300
+ }
301
+ function idempotencyKey(provided) {
302
+ return provided ?? (options.generateIdempotencyKey ?? defaultIdempotencyKey)();
303
+ }
304
+ return {
305
+ async config(signal) {
306
+ const payload = await execute({
307
+ method: "GET",
308
+ url: "/config"
309
+ }, isConfigResponse, signal);
310
+ captureCsrf(payload);
311
+ return payload;
312
+ },
313
+ async session(code, signal) {
314
+ const payload = await execute({
315
+ method: "POST",
316
+ url: "/session",
317
+ data: { code }
318
+ }, isSessionResponse, signal);
319
+ captureCsrf(payload);
320
+ return payload;
321
+ },
322
+ async current(signal) {
323
+ return execute({
324
+ method: "GET",
325
+ url: "/current"
326
+ }, isCurrentResponse, signal);
327
+ },
328
+ async activate(request, signal, key) {
329
+ return execute({
330
+ method: "POST",
331
+ url: "/activate",
332
+ data: request,
333
+ headers: { "Idempotency-Key": idempotencyKey(key) }
334
+ }, isEmergencyResultPayload, signal);
335
+ },
336
+ async simulacrum(signal, key) {
337
+ return execute({
338
+ method: "POST",
339
+ url: "/simulacrum",
340
+ headers: { "Idempotency-Key": idempotencyKey(key) }
341
+ }, isEmergencyResultPayload, signal);
342
+ },
343
+ async refresh(emergencyId, signal) {
344
+ return execute({
345
+ method: "POST",
346
+ url: `/${encodeURIComponent(emergencyId)}/refresh`
347
+ }, isRefreshResponse, signal);
348
+ },
349
+ async abort(emergencyId, signal) {
350
+ return execute({
351
+ method: "DELETE",
352
+ url: `/${encodeURIComponent(emergencyId)}`
353
+ }, isAbortResponse, signal);
354
+ },
355
+ destroy() {
356
+ destroyed = true;
357
+ for (const controller of controllers) controller.abort();
358
+ controllers.clear();
359
+ }
360
+ };
361
+ }
362
+
363
+ // src/core/copy.ts
364
+ /**
365
+ * Visible UI copy in Spanish. `locale` controls date and number formatting.
366
+ */
367
+ const copy = {
368
+ panelLabel: "Panel de emergencia",
369
+ close: "Cerrar panel de emergencia",
370
+ question: "¿Tiene usted una emergencia?",
371
+ illustrationAlt: "Ícono paramédico",
372
+ emergency: "Emergencia",
373
+ emergencyAria: "Botón de emergencia",
374
+ test: "Prueba",
375
+ testAria: "Botón de simulación de emergencia",
376
+ statusTitle: "Estado de Respuestas de Emergencia",
377
+ responderColumn: "Respondedor",
378
+ sentColumn: "Enviado",
379
+ deliveredColumn: "Recibido",
380
+ readColumn: "Leído",
381
+ refresh: "Actualizar",
382
+ refreshing: "Actualizando...",
383
+ loading: "Cargando…",
384
+ statusPrefix: "Estado:",
385
+ statusPending: "Pendiente",
386
+ statusSent: "Enviado",
387
+ statusDelivered: "Recibido",
388
+ statusRead: "Leído",
389
+ statusFailed: "Fallido",
390
+ abort: "Abortar Aviso de Emergencia",
391
+ abortConfirmTitle: "Confirmar aborto de emergencia",
392
+ abortConfirmText: "Estas apunto de abortar la emergencia, esta acción es irreversible ¿Desea continuar?",
393
+ abortConfirm: "Si, abortar",
394
+ abortCancel: "No, continuar",
395
+ abortResultOk: "Emergencia abortada con éxito, tus contactos han sido notificados",
396
+ abortResultFail: "No se pudo abortar la emergencia. Inténtalo de nuevo.",
397
+ abortSuccessTitle: "¡Emergencia Abortada Con Éxito!",
398
+ abortSuccessDetails: "Tus contactos han sido notificados",
399
+ simulate: "Prueba",
400
+ simulateConfirmTitle: "Enviar mensaje de prueba",
401
+ simulateConfirmText: "Se enviará un mensaje de prueba a tus contactos. No se crea una emergencia real.",
402
+ simulateConfirm: "Sí, enviar prueba",
403
+ simulateCancel: "Cancelar",
404
+ simulateResultOk: "¡Prueba de simulación iniciada con éxito!",
405
+ simulateResultFail: "No se pudo enviar el mensaje de prueba.",
406
+ simulateSuccessTitle: "¡Éxito!",
407
+ simulateSuccessDetails: "Mensajes enviados",
408
+ simulateSuccessCompleted: "Proceso completado",
409
+ feedbackDismiss: "Entendido",
410
+ errorTitle: "Error",
411
+ errorText: "Ha ocurrido un error inesperado.",
412
+ errorDetailInfo: "Revisa la información requerida",
413
+ errorDetailRetry: "Intenta nuevamente",
414
+ retry: "Reintentar",
415
+ geolocationTitle: "No pudimos obtener tu ubicación",
416
+ geolocationDenied: "Permiso de ubicación denegado. Activa el permiso o llama directamente.",
417
+ geolocationUnavailable: "Tu ubicación no está disponible en este momento. Llama directamente.",
418
+ geolocationTimeout: "La ubicación tardó demasiado. Inténtalo de nuevo o llama directamente.",
419
+ geolocationUnsupported: "Este dispositivo no permite compartir la ubicación. Llama directamente.",
420
+ fallbackTitle: "Llama directamente",
421
+ fallbackDescription: "Si no puedes completar la emergencia desde aquí, llama a este número.",
422
+ fallbackCall: "Llamar",
423
+ resumeTitle: "Emergencia Activa Detectada",
424
+ resumeDescription: "Se ha detectado una emergencia activa en el sistema. Por favor, revisa el estado de la emergencia inmediatamente.",
425
+ resumeAction: "Abrir Panel de Emergencia",
426
+ partialFailure: "Algunos contactos no pudieron ser notificados. Revisa la lista.",
427
+ simulatedBanner: "Mensaje de prueba: no hay una emergencia real activa.",
428
+ activeBanner: "Emergencia activa. Tus contactos fueron notificados.",
429
+ emergencyActiveError: "Ya tienes una emergencia activa. Revisa el estado antes de continuar.",
430
+ respondersEmpty: "Aún no hay contactos notificados."
431
+ };
432
+
433
+ // src/core/events.ts
434
+ /**
435
+ * Typed, dependency-free event bus used by the controller and by `mount()`.
436
+ * DOM custom events are not used so the core stays framework agnostic.
437
+ */
438
+ const EMERGENCY_EVENT_NAMES = [
439
+ "session:ready",
440
+ "emergency:activated",
441
+ "emergency:simulated",
442
+ "emergency:resumed",
443
+ "emergency:updated",
444
+ "emergency:aborted",
445
+ "emergency:failed",
446
+ "emergency:fallback",
447
+ "panel:opened",
448
+ "panel:closed"
449
+ ];
450
+ var EmergencyEventEmitter = class {
451
+ handlers = /* @__PURE__ */ new Map();
452
+ on(event, handler) {
453
+ const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
454
+ set.add(handler);
455
+ this.handlers.set(event, set);
456
+ return () => {
457
+ this.off(event, handler);
458
+ };
459
+ }
460
+ off(event, handler) {
461
+ const set = this.handlers.get(event);
462
+ if (!set) return;
463
+ set.delete(handler);
464
+ if (set.size === 0) this.handlers.delete(event);
465
+ }
466
+ emit(event, payload) {
467
+ const set = this.handlers.get(event);
468
+ if (!set) return;
469
+ for (const handler of set) try {
470
+ handler(payload);
471
+ } catch {}
472
+ }
473
+ clear() {
474
+ this.handlers.clear();
475
+ }
476
+ };
477
+ function createEventEmitter() {
478
+ return new EmergencyEventEmitter();
479
+ }
480
+
481
+ // src/core/geolocation.ts
482
+ function mapFailure(error) {
483
+ const mapped = createEmergencyError(error.code === 1 ? "geolocation_denied" : error.code === 3 ? "geolocation_timeout" : "geolocation_unavailable", error.message ?? "The current position could not be determined.");
484
+ return Object.assign(new Error(mapped.message), { emergencyError: mapped });
485
+ }
486
+ function createBrowserGeolocationProvider() {
487
+ return { getCurrentPosition(timeoutMs) {
488
+ return new Promise((resolve, reject) => {
489
+ if (typeof navigator === "undefined" || typeof navigator.geolocation === "undefined" || typeof navigator.geolocation.getCurrentPosition !== "function") {
490
+ reject(Object.assign(/* @__PURE__ */ new Error("Geolocation is not supported in this environment."), { emergencyError: createEmergencyError("geolocation_unsupported", "Geolocation is not supported in this environment.") }));
491
+ return;
492
+ }
493
+ navigator.geolocation.getCurrentPosition((position) => {
494
+ resolve({
495
+ latitude: position.coords.latitude,
496
+ longitude: position.coords.longitude,
497
+ accuracy: position.coords.accuracy
498
+ });
499
+ }, (error) => {
500
+ reject(mapFailure(error));
501
+ }, {
502
+ enableHighAccuracy: true,
503
+ timeout: timeoutMs
504
+ });
505
+ });
506
+ } };
507
+ }
508
+
509
+ // src/core/mappers.ts
510
+ function responderStatus(payload) {
511
+ if (payload.failed) return "failed";
512
+ if (payload.read) return "read";
513
+ if (payload.delivered) return "delivered";
514
+ if (payload.sent) return "sent";
515
+ return "pending";
516
+ }
517
+ function toResponder(payload) {
518
+ const last = payload.last_messages?.at(-1) ?? null;
519
+ return {
520
+ phone: payload.phone,
521
+ name: payload.name,
522
+ status: responderStatus(payload),
523
+ sent: payload.sent,
524
+ delivered: payload.delivered,
525
+ read: payload.read,
526
+ failed: payload.failed,
527
+ messageId: payload.message_id,
528
+ sentAt: payload.sent_at ?? null,
529
+ deliveredAt: payload.delivered_at ?? null,
530
+ readAt: payload.read_at ?? null,
531
+ lastMessage: last ? {
532
+ content: last.content ?? "",
533
+ sentAt: last.sent_at ?? null
534
+ } : null
535
+ };
536
+ }
537
+ function toFailure(payload) {
538
+ return {
539
+ phone: payload.phone,
540
+ message: payload.message,
541
+ status: payload.status ?? null
542
+ };
543
+ }
544
+ function resultToSnapshot(result, code) {
545
+ return {
546
+ emergencyId: result.emergency_id ?? "",
547
+ code,
548
+ active: true,
549
+ simulated: result.simulated,
550
+ responders: result.responders.map(toResponder),
551
+ createdAt: null,
552
+ updatedAt: null,
553
+ raw: { ...result }
554
+ };
555
+ }
556
+ function readString(raw, keys) {
557
+ for (const key of keys) {
558
+ const value = raw[key];
559
+ if (typeof value === "string" && value !== "") return value;
560
+ }
561
+ return null;
562
+ }
563
+ function readBoolean(raw, keys) {
564
+ for (const key of keys) {
565
+ const value = raw[key];
566
+ if (typeof value === "boolean") return value;
567
+ }
568
+ return null;
569
+ }
570
+ /**
571
+ * Maps the upstream emergency payload (proxied by the BFF) into the snapshot the
572
+ * UI renders. Unknown fields survive in `raw`, so the client never loses data
573
+ * the BFF adds later.
574
+ */
575
+ function payloadToSnapshot(raw, options = {}) {
576
+ const respondersValue = raw["responders"];
577
+ const responders = Array.isArray(respondersValue) ? respondersValue.filter(isResponderPayloadLike).map(toResponder) : [];
578
+ return {
579
+ emergencyId: readString(raw, ["emergency_id", "id"]) ?? "",
580
+ code: options.code ?? readString(raw, ["initiator_id", "code"]),
581
+ active: readBoolean(raw, ["is_active", "active"]) ?? true,
582
+ simulated: options.simulated ?? readBoolean(raw, ["is_simulated", "simulated"]) ?? false,
583
+ responders,
584
+ createdAt: readString(raw, ["created_at", "createdAt"]),
585
+ updatedAt: readString(raw, ["updated_at", "updatedAt"]),
586
+ raw
587
+ };
588
+ }
589
+ function isResponderPayloadLike(value) {
590
+ if (!isRecord(value)) return false;
591
+ return typeof value["phone"] === "string" && typeof value["name"] === "string";
592
+ }
593
+ function themeToCssVariables(theme) {
594
+ if (!theme) return {};
595
+ const variables = {};
596
+ for (const [key, variable] of [
597
+ ["accent", "--de-emergency-red"],
598
+ ["accentContrast", "--de-emergency-on-red"],
599
+ ["surface", "--de-emergency-white"],
600
+ ["text", "--de-emergency-text"],
601
+ ["muted", "--de-emergency-text-secondary"],
602
+ ["radius", "--de-emergency-radius"]
603
+ ]) {
604
+ const value = theme[key];
605
+ if (value) variables[variable] = value;
606
+ }
607
+ return variables;
608
+ }
609
+
610
+ // src/core/realtime.ts
611
+ function channelForPhone(pattern, phone) {
612
+ return pattern.replace("{phone}", phone.replace(/^\+/, ""));
613
+ }
614
+ function toStatus(state) {
615
+ switch (state) {
616
+ case "connected": return "connected";
617
+ case "connecting":
618
+ case "initialized": return "connecting";
619
+ case "unavailable":
620
+ case "failed": return "unavailable";
621
+ case "disconnected": return "disconnected";
622
+ default: return "idle";
623
+ }
624
+ }
625
+ function createPusherRealtimeProvider() {
626
+ return { subscribe(config, phones, handlers) {
627
+ if (typeof window === "undefined" || !config.pusher.key || !config.pusher.cluster) return null;
628
+ let pusher;
629
+ try {
630
+ pusher = new Pusher(config.pusher.key, {
631
+ cluster: config.pusher.cluster,
632
+ forceTLS: true
633
+ });
634
+ } catch {
635
+ handlers.onStatus("unavailable");
636
+ return null;
637
+ }
638
+ handlers.onStatus("connecting");
639
+ pusher.connection.bind("state_change", (change) => {
640
+ handlers.onStatus(toStatus(change.current));
641
+ });
642
+ const channels = [...new Set(phones.filter((phone) => phone !== ""))].map((phone) => {
643
+ const channelName = channelForPhone(config.pusher.channel_pattern, phone);
644
+ pusher.subscribe(channelName).bind_global((eventName, data) => {
645
+ handlers.onEvent(channelName, eventName, data);
646
+ });
647
+ return channelName;
648
+ });
649
+ return { disconnect() {
650
+ for (const channelName of channels) pusher.unsubscribe(channelName);
651
+ pusher.disconnect();
652
+ } };
653
+ } };
654
+ }
655
+
656
+ // src/core/state-machine.ts
657
+ const EMERGENCY_PHASE_NAMES = [
658
+ "idle",
659
+ "initializing",
660
+ "ready",
661
+ "activating",
662
+ "simulating",
663
+ "active",
664
+ "refreshing",
665
+ "aborting",
666
+ "error"
667
+ ];
668
+ /**
669
+ * Every allowed transition. `idle` is reachable from every phase so `destroy()`
670
+ * and a hard reset always leave the store in a known state.
671
+ */
672
+ const EMERGENCY_TRANSITIONS = {
673
+ idle: ["initializing"],
674
+ initializing: [
675
+ "idle",
676
+ "ready",
677
+ "active",
678
+ "error"
679
+ ],
680
+ ready: [
681
+ "idle",
682
+ "initializing",
683
+ "activating",
684
+ "simulating",
685
+ "active",
686
+ "refreshing",
687
+ "error"
688
+ ],
689
+ activating: [
690
+ "idle",
691
+ "ready",
692
+ "active",
693
+ "error"
694
+ ],
695
+ simulating: [
696
+ "idle",
697
+ "ready",
698
+ "error"
699
+ ],
700
+ active: [
701
+ "idle",
702
+ "ready",
703
+ "refreshing",
704
+ "aborting",
705
+ "error"
706
+ ],
707
+ refreshing: [
708
+ "idle",
709
+ "active",
710
+ "error"
711
+ ],
712
+ aborting: [
713
+ "idle",
714
+ "ready",
715
+ "active",
716
+ "error"
717
+ ],
718
+ error: [
719
+ "idle",
720
+ "initializing",
721
+ "ready"
722
+ ]
723
+ };
724
+ function canTransition(from, to) {
725
+ return EMERGENCY_TRANSITIONS[from].includes(to);
726
+ }
727
+
728
+ // src/core/store.ts
729
+ function createEmergencyStore(config) {
730
+ return createStore(() => ({
731
+ config,
732
+ phase: { kind: "idle" },
733
+ panelOpen: false,
734
+ sessionReady: false,
735
+ resumeAvailable: false,
736
+ snapshot: null,
737
+ responders: [],
738
+ failures: [],
739
+ messages: [],
740
+ lastError: null,
741
+ geolocation: null,
742
+ geolocationError: null,
743
+ realtimeStatus: "idle",
744
+ abortResult: null,
745
+ simulationResult: null,
746
+ abortConfirmation: false,
747
+ simulationConfirmation: false,
748
+ destroyed: false
749
+ }));
750
+ }
751
+
752
+ // src/core/controller.ts
753
+ const RECONCILE_THROTTLE_MS = 1500;
754
+ function geolocationFailure(error) {
755
+ const attached = isRecord(error) ? error["emergencyError"] : void 0;
756
+ if (attached) return toEmergencyError(attached, "geolocation_unavailable");
757
+ return toEmergencyError(error, "geolocation_unavailable");
758
+ }
759
+ function createEmergencyController(deps) {
760
+ const config = deps.config;
761
+ const bff = deps.bff ?? createBffClient({
762
+ bffBaseUrl: config.bffBaseUrl,
763
+ timeoutMs: config.requestTimeoutMs
764
+ });
765
+ const geolocation = deps.geolocation ?? createBrowserGeolocationProvider();
766
+ const realtimeProvider = deps.realtime ?? createPusherRealtimeProvider();
767
+ const events = deps.events ?? createEventEmitter();
768
+ const store = createEmergencyStore(config);
769
+ let sessionPromise = null;
770
+ let refreshing = false;
771
+ let realtimeSubscription = null;
772
+ let realtimeConfig = null;
773
+ let subscribedPhones = [];
774
+ let messageSequence = 0;
775
+ let lastReconcileAt = 0;
776
+ let reconcileTimer = null;
777
+ const get = () => store.getState();
778
+ function patch(partial) {
779
+ store.setState(partial);
780
+ }
781
+ function transition(next) {
782
+ const current = get().phase;
783
+ if (current.kind === next.kind) {
784
+ patch({ phase: next });
785
+ return true;
786
+ }
787
+ if (!canTransition(current.kind, next.kind)) {
788
+ events.emit("emergency:failed", {
789
+ error: createEmergencyError("invalid_transition", `Cannot move from ${current.kind} to ${next.kind}.`),
790
+ operation: "mount"
791
+ });
792
+ return false;
793
+ }
794
+ patch({ phase: next });
795
+ return true;
796
+ }
797
+ function pushMessage(kind, text) {
798
+ messageSequence += 1;
799
+ const message = {
800
+ id: `${Date.now()}-${messageSequence}`,
801
+ kind,
802
+ text,
803
+ createdAt: Date.now()
804
+ };
805
+ patch({ messages: [...get().messages, message].slice(-20) });
806
+ }
807
+ function fail(error, operation, options = {}) {
808
+ patch({ lastError: error });
809
+ if (options.message !== null) pushMessage("error", options.message ?? error.message);
810
+ if (options.phase !== null) transition(options.phase ?? {
811
+ kind: "error",
812
+ error
813
+ });
814
+ events.emit("emergency:failed", {
815
+ error,
816
+ operation
817
+ });
818
+ }
819
+ function emitFallback(error) {
820
+ events.emit("emergency:fallback", {
821
+ error,
822
+ fallback: config.fallback ?? null
823
+ });
824
+ }
825
+ function stopRealtime() {
826
+ realtimeSubscription?.disconnect();
827
+ realtimeSubscription = null;
828
+ subscribedPhones = [];
829
+ }
830
+ function startRealtime(realtime, responders) {
831
+ if (!realtime) return;
832
+ realtimeConfig = realtime;
833
+ const phones = [...new Set(responders.map((responder) => responder.phone))];
834
+ if (realtimeSubscription && phones.length === subscribedPhones.length && phones.every((phone, index) => phone === subscribedPhones[index])) return;
835
+ realtimeSubscription?.disconnect();
836
+ subscribedPhones = phones;
837
+ if (phones.length === 0) {
838
+ patch({ realtimeStatus: "unavailable" });
839
+ return;
840
+ }
841
+ realtimeSubscription = realtimeProvider.subscribe(realtime, phones, {
842
+ onStatus(status) {
843
+ const previous = get().realtimeStatus;
844
+ patch({ realtimeStatus: status });
845
+ if (status === "connected" && previous !== "connected") scheduleReconcile();
846
+ },
847
+ onEvent() {
848
+ scheduleReconcile();
849
+ }
850
+ });
851
+ if (!realtimeSubscription) patch({ realtimeStatus: "unavailable" });
852
+ }
853
+ function scheduleReconcile() {
854
+ if (reconcileTimer !== null) return;
855
+ const wait = Math.max(0, RECONCILE_THROTTLE_MS - (Date.now() - lastReconcileAt));
856
+ reconcileTimer = setTimeout(() => {
857
+ reconcileTimer = null;
858
+ lastReconcileAt = Date.now();
859
+ refresh(true);
860
+ }, wait);
861
+ }
862
+ async function performSession() {
863
+ const phase = get().phase.kind;
864
+ if (phase === "idle" || phase === "error") transition({ kind: "initializing" });
865
+ try {
866
+ realtimeConfig = (await bff.config()).realtime;
867
+ const session = await bff.session(config.code);
868
+ patch({
869
+ sessionReady: true,
870
+ lastError: null
871
+ });
872
+ if (get().phase.kind === "initializing") transition({ kind: "ready" });
873
+ events.emit("session:ready", {
874
+ code: session.code,
875
+ expiresIn: session.expires_in
876
+ });
877
+ } catch (error) {
878
+ const normalized = toEmergencyError(error, "network");
879
+ patch({ sessionReady: false });
880
+ fail(normalized, "session", { phase: {
881
+ kind: "error",
882
+ error: normalized
883
+ } });
884
+ }
885
+ }
886
+ async function ensureSession() {
887
+ if (get().destroyed || get().sessionReady) return;
888
+ if (!sessionPromise) sessionPromise = performSession().finally(() => {
889
+ sessionPromise = null;
890
+ });
891
+ return sessionPromise;
892
+ }
893
+ async function hydrateCurrent() {
894
+ try {
895
+ const current = await bff.current();
896
+ realtimeConfig = current.realtime;
897
+ if (current.active && current.emergency) {
898
+ const snapshot = payloadToSnapshot(current.emergency, { code: config.code });
899
+ patch({
900
+ snapshot,
901
+ responders: snapshot.responders,
902
+ resumeAvailable: !get().panelOpen,
903
+ failures: [],
904
+ lastError: null,
905
+ geolocationError: null
906
+ });
907
+ transition({
908
+ kind: "active",
909
+ emergencyId: snapshot.emergencyId,
910
+ simulated: snapshot.simulated
911
+ });
912
+ startRealtime(current.realtime, snapshot.responders);
913
+ events.emit("emergency:resumed", { snapshot });
914
+ return;
915
+ }
916
+ patch({
917
+ resumeAvailable: false,
918
+ snapshot: null,
919
+ responders: []
920
+ });
921
+ if (get().phase.kind !== "ready") transition({ kind: "ready" });
922
+ } catch (error) {
923
+ const normalized = toEmergencyError(error, "network");
924
+ patch({ resumeAvailable: false });
925
+ if (normalized.code === "unauthenticated") return;
926
+ fail(normalized, "resume", { phase: get().phase.kind === "active" ? null : {
927
+ kind: "error",
928
+ error: normalized
929
+ } });
930
+ }
931
+ }
932
+ async function resume() {
933
+ if (get().destroyed) return;
934
+ await ensureSession();
935
+ if (!get().sessionReady) return;
936
+ await hydrateCurrent();
937
+ }
938
+ async function activate() {
939
+ if (get().destroyed) return;
940
+ const phase = get().phase.kind;
941
+ if (phase === "activating" || phase === "active") {
942
+ open("activate");
943
+ return;
944
+ }
945
+ open("activate");
946
+ await ensureSession();
947
+ if (!get().sessionReady || !transition({ kind: "activating" })) return;
948
+ patch({
949
+ lastError: null,
950
+ geolocationError: null,
951
+ abortResult: null
952
+ });
953
+ let position;
954
+ try {
955
+ position = await geolocation.getCurrentPosition(config.geolocationTimeoutMs ?? 8e3);
956
+ patch({ geolocation: position });
957
+ } catch (error) {
958
+ const normalized = geolocationFailure(error);
959
+ patch({
960
+ geolocation: null,
961
+ geolocationError: normalized
962
+ });
963
+ transition({ kind: "ready" });
964
+ pushMessage("error", normalized.message);
965
+ events.emit("emergency:failed", {
966
+ error: normalized,
967
+ operation: "geolocation"
968
+ });
969
+ emitFallback(normalized);
970
+ return;
971
+ }
972
+ try {
973
+ const result = await bff.activate({ geolocation: position });
974
+ const snapshot = resultToSnapshot(result, config.code);
975
+ const responders = result.responders.map(toResponder);
976
+ const failures = result.failures.map(toFailure);
977
+ const hydrated = {
978
+ ...snapshot,
979
+ responders
980
+ };
981
+ patch({
982
+ snapshot: hydrated,
983
+ responders,
984
+ failures,
985
+ resumeAvailable: false,
986
+ lastError: null
987
+ });
988
+ transition({
989
+ kind: "active",
990
+ emergencyId: hydrated.emergencyId,
991
+ simulated: result.simulated
992
+ });
993
+ pushMessage(failures.length > 0 ? "warning" : "success", failures.length > 0 ? copy.partialFailure : copy.activeBanner);
994
+ events.emit("emergency:activated", { snapshot: hydrated });
995
+ startRealtime(realtimeConfig, responders);
996
+ } catch (error) {
997
+ const normalized = toEmergencyError(error, "network");
998
+ if (normalized.code === "emergency_already_active") {
999
+ pushMessage("info", copy.emergencyActiveError);
1000
+ await hydrateCurrent();
1001
+ return;
1002
+ }
1003
+ fail(normalized, "activate", { phase: { kind: "ready" } });
1004
+ emitFallback(normalized);
1005
+ }
1006
+ }
1007
+ async function simulate() {
1008
+ if (get().destroyed) return;
1009
+ const snapshot = get().snapshot;
1010
+ if (snapshot?.active && !snapshot.simulated) {
1011
+ pushMessage("warning", copy.emergencyActiveError);
1012
+ return;
1013
+ }
1014
+ if ([
1015
+ "simulating",
1016
+ "activating",
1017
+ "aborting"
1018
+ ].includes(get().phase.kind)) return;
1019
+ await ensureSession();
1020
+ if (!get().sessionReady || !transition({ kind: "simulating" })) return;
1021
+ patch({
1022
+ lastError: null,
1023
+ simulationResult: null
1024
+ });
1025
+ try {
1026
+ const result = await bff.simulacrum();
1027
+ const responders = result.responders.map(toResponder);
1028
+ const failures = result.failures.map(toFailure);
1029
+ const simulationResult = {
1030
+ ok: true,
1031
+ message: failures.length > 0 ? copy.partialFailure : copy.simulateResultOk,
1032
+ at: Date.now()
1033
+ };
1034
+ patch({
1035
+ responders,
1036
+ failures,
1037
+ simulationResult,
1038
+ simulationConfirmation: false
1039
+ });
1040
+ transition({ kind: "ready" });
1041
+ pushMessage(failures.length > 0 ? "warning" : "success", simulationResult.message);
1042
+ events.emit("emergency:simulated", {
1043
+ responders,
1044
+ failures
1045
+ });
1046
+ } catch (error) {
1047
+ const normalized = toEmergencyError(error, "network");
1048
+ patch({ simulationResult: {
1049
+ ok: false,
1050
+ message: copy.simulateResultFail,
1051
+ at: Date.now()
1052
+ } });
1053
+ transition({ kind: "ready" });
1054
+ fail(normalized, "simulate", { phase: null });
1055
+ }
1056
+ }
1057
+ async function refresh(silent = false) {
1058
+ const snapshot = get().snapshot;
1059
+ if (get().destroyed || !snapshot || snapshot.simulated || snapshot.emergencyId === "") return;
1060
+ if (refreshing) return;
1061
+ refreshing = true;
1062
+ if (!silent) transition({
1063
+ kind: "refreshing",
1064
+ emergencyId: snapshot.emergencyId
1065
+ });
1066
+ try {
1067
+ const result = await bff.refresh(snapshot.emergencyId);
1068
+ const next = payloadToSnapshot(result.emergency, {
1069
+ code: config.code,
1070
+ simulated: snapshot.simulated
1071
+ });
1072
+ const responders = result.responders.length > 0 ? result.responders.map(toResponder) : next.responders.length > 0 ? next.responders : get().responders;
1073
+ const updated = {
1074
+ ...next,
1075
+ responders
1076
+ };
1077
+ patch({
1078
+ snapshot: updated,
1079
+ responders,
1080
+ lastError: null
1081
+ });
1082
+ if (get().phase.kind === "refreshing") transition({
1083
+ kind: "active",
1084
+ emergencyId: updated.emergencyId,
1085
+ simulated: updated.simulated
1086
+ });
1087
+ startRealtime(realtimeConfig, responders);
1088
+ events.emit("emergency:updated", { snapshot: updated });
1089
+ } catch (error) {
1090
+ const normalized = toEmergencyError(error, "network");
1091
+ if (get().phase.kind === "refreshing") transition({
1092
+ kind: "active",
1093
+ emergencyId: snapshot.emergencyId,
1094
+ simulated: snapshot.simulated
1095
+ });
1096
+ fail(normalized, "refresh", {
1097
+ phase: null,
1098
+ message: silent ? null : void 0
1099
+ });
1100
+ } finally {
1101
+ refreshing = false;
1102
+ }
1103
+ }
1104
+ async function abort() {
1105
+ if (get().destroyed) return;
1106
+ const snapshot = get().snapshot;
1107
+ if (get().phase.kind !== "active" || !snapshot) return;
1108
+ if (!get().abortConfirmation) {
1109
+ patch({ abortConfirmation: true });
1110
+ return;
1111
+ }
1112
+ if (snapshot.simulated || snapshot.emergencyId === "") {
1113
+ patch({
1114
+ snapshot: null,
1115
+ responders: [],
1116
+ resumeAvailable: false,
1117
+ abortConfirmation: false
1118
+ });
1119
+ transition({ kind: "ready" });
1120
+ pushMessage("info", "No hay una emergencia real activa.");
1121
+ return;
1122
+ }
1123
+ if (!transition({
1124
+ kind: "aborting",
1125
+ emergencyId: snapshot.emergencyId
1126
+ })) return;
1127
+ patch({
1128
+ lastError: null,
1129
+ abortResult: null
1130
+ });
1131
+ try {
1132
+ const result = await bff.abort(snapshot.emergencyId);
1133
+ const responders = result.responders.map(toResponder);
1134
+ const abortResult = {
1135
+ ok: true,
1136
+ message: copy.abortResultOk,
1137
+ at: Date.now()
1138
+ };
1139
+ patch({
1140
+ abortResult,
1141
+ responders,
1142
+ failures: result.failures.map(toFailure),
1143
+ abortConfirmation: false,
1144
+ snapshot: null,
1145
+ resumeAvailable: false
1146
+ });
1147
+ stopRealtime();
1148
+ transition({ kind: "ready" });
1149
+ pushMessage("success", abortResult.message);
1150
+ events.emit("emergency:aborted", {
1151
+ emergencyId: snapshot.emergencyId,
1152
+ responders
1153
+ });
1154
+ } catch (error) {
1155
+ const normalized = toEmergencyError(error, "network");
1156
+ patch({
1157
+ abortResult: {
1158
+ ok: false,
1159
+ message: copy.abortResultFail,
1160
+ at: Date.now()
1161
+ },
1162
+ abortConfirmation: false
1163
+ });
1164
+ transition({
1165
+ kind: "active",
1166
+ emergencyId: snapshot.emergencyId,
1167
+ simulated: snapshot.simulated
1168
+ });
1169
+ fail(normalized, "abort", { phase: null });
1170
+ }
1171
+ }
1172
+ function open(reason = "manual") {
1173
+ if (get().destroyed) return;
1174
+ patch({
1175
+ panelOpen: true,
1176
+ resumeAvailable: false
1177
+ });
1178
+ events.emit("panel:opened", { reason });
1179
+ }
1180
+ function close(reason = "close") {
1181
+ if (!get().panelOpen) return;
1182
+ patch({
1183
+ panelOpen: false,
1184
+ abortConfirmation: false,
1185
+ simulationConfirmation: false
1186
+ });
1187
+ events.emit("panel:closed", { reason });
1188
+ }
1189
+ function reconcileNow() {
1190
+ const snapshot = get().snapshot;
1191
+ if (!snapshot || snapshot.simulated || snapshot.emergencyId === "") return;
1192
+ lastReconcileAt = Date.now();
1193
+ refresh(true);
1194
+ }
1195
+ function onVisibilityChange() {
1196
+ if (typeof document !== "undefined" && document.visibilityState === "visible") scheduleReconcile();
1197
+ }
1198
+ function registerReconcileListeners() {
1199
+ if (typeof window === "undefined") return () => void 0;
1200
+ window.addEventListener("focus", scheduleReconcile);
1201
+ window.addEventListener("online", reconcileNow);
1202
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
1203
+ return () => {
1204
+ window.removeEventListener("focus", scheduleReconcile);
1205
+ window.removeEventListener("online", reconcileNow);
1206
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
1207
+ };
1208
+ }
1209
+ const removeReconcileListeners = registerReconcileListeners();
1210
+ function destroy() {
1211
+ if (get().destroyed) return;
1212
+ removeReconcileListeners();
1213
+ if (reconcileTimer !== null) {
1214
+ clearTimeout(reconcileTimer);
1215
+ reconcileTimer = null;
1216
+ }
1217
+ bff.destroy();
1218
+ realtimeSubscription?.disconnect();
1219
+ realtimeSubscription = null;
1220
+ realtimeConfig = null;
1221
+ subscribedPhones = [];
1222
+ patch({
1223
+ destroyed: true,
1224
+ phase: { kind: "idle" },
1225
+ panelOpen: false,
1226
+ sessionReady: false,
1227
+ resumeAvailable: false,
1228
+ snapshot: null,
1229
+ responders: [],
1230
+ failures: [],
1231
+ messages: [],
1232
+ lastError: null,
1233
+ geolocation: null,
1234
+ geolocationError: null,
1235
+ realtimeStatus: "idle",
1236
+ abortResult: null,
1237
+ simulationResult: null,
1238
+ abortConfirmation: false,
1239
+ simulationConfirmation: false
1240
+ });
1241
+ events.emit("panel:closed", { reason: "destroy" });
1242
+ events.clear();
1243
+ }
1244
+ return {
1245
+ client: {
1246
+ open,
1247
+ close,
1248
+ destroy,
1249
+ getState: get,
1250
+ subscribe: (listener) => store.subscribe(listener),
1251
+ on: (event, handler) => events.on(event, handler),
1252
+ off: (event, handler) => events.off(event, handler),
1253
+ ensureSession,
1254
+ resume,
1255
+ dismissResume: () => patch({ resumeAvailable: false }),
1256
+ activate,
1257
+ simulate,
1258
+ refresh: () => refresh(false),
1259
+ abort,
1260
+ requestAbortConfirmation: () => {
1261
+ if (get().phase.kind === "active") patch({ abortConfirmation: true });
1262
+ },
1263
+ cancelAbortConfirmation: () => patch({ abortConfirmation: false }),
1264
+ requestSimulationConfirmation: () => {
1265
+ if (!get().snapshot?.active) patch({ simulationConfirmation: true });
1266
+ },
1267
+ cancelSimulationConfirmation: () => patch({ simulationConfirmation: false }),
1268
+ dismissFeedback: () => patch({
1269
+ abortResult: null,
1270
+ simulationResult: null
1271
+ })
1272
+ },
1273
+ store,
1274
+ events
1275
+ };
1276
+ }
1277
+
1278
+ // src/core/config.ts
1279
+ const EMERGENCY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9]{5}$/;
1280
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
1281
+ "localhost",
1282
+ "127.0.0.1",
1283
+ "[::1]"
1284
+ ]);
1285
+ function isValidEmergencyCode(code) {
1286
+ return EMERGENCY_CODE_PATTERN.test(code);
1287
+ }
1288
+ function isValidFallback(value) {
1289
+ if (!isRecord(value)) return false;
1290
+ return typeof value["phone"] === "string" && value["phone"].trim() !== "" && typeof value["label"] === "string" && value["label"].trim() !== "";
1291
+ }
1292
+ function normalizeBaseUrl(baseUrl) {
1293
+ return baseUrl.trim().replace(/\/+$/, "");
1294
+ }
1295
+ /**
1296
+ * Requests are sent with credentials and the CSRF token, so a plaintext or
1297
+ * attacker-controlled base URL would leak both. Same-origin paths (`/prefix`)
1298
+ * and HTTPS are accepted; plain HTTP is only accepted for loopback hosts.
1299
+ */
1300
+ function validateBaseUrl(baseUrl) {
1301
+ if (baseUrl.startsWith("/")) {
1302
+ if (baseUrl.startsWith("//")) return "The BFF base URL must not be protocol-relative.";
1303
+ return null;
1304
+ }
1305
+ let parsed;
1306
+ try {
1307
+ parsed = new URL(baseUrl);
1308
+ } catch {
1309
+ return "The BFF base URL must be an absolute URL or a same-origin path.";
1310
+ }
1311
+ if (parsed.username !== "" || parsed.password !== "") return "The BFF base URL must not embed credentials.";
1312
+ if (parsed.protocol === "https:") return null;
1313
+ if (parsed.protocol === "http:" && (LOOPBACK_HOSTS.has(parsed.hostname) || parsed.hostname.endsWith(".localhost"))) return null;
1314
+ return "The BFF base URL must use https: (http: is only allowed for localhost).";
1315
+ }
1316
+ function validateConfig(config) {
1317
+ const bffBaseUrl = typeof config.bffBaseUrl === "string" ? normalizeBaseUrl(config.bffBaseUrl) : "";
1318
+ const code = typeof config.code === "string" ? config.code.trim() : "";
1319
+ if (bffBaseUrl === "") return {
1320
+ ok: false,
1321
+ config: null,
1322
+ error: createEmergencyError("configuration", "A BFF base URL is required to mount the emergency client.")
1323
+ };
1324
+ const baseUrlError = validateBaseUrl(bffBaseUrl);
1325
+ if (baseUrlError !== null) return {
1326
+ ok: false,
1327
+ config: null,
1328
+ error: createEmergencyError("configuration", baseUrlError)
1329
+ };
1330
+ if (!isValidEmergencyCode(code)) return {
1331
+ ok: false,
1332
+ config: null,
1333
+ error: createEmergencyError("configuration", "The identity code must be 6 characters and start with a letter.", { details: { code } })
1334
+ };
1335
+ if (config.fallback !== void 0 && !isValidFallback(config.fallback)) return {
1336
+ ok: false,
1337
+ config: null,
1338
+ error: createEmergencyError("configuration", "The fallback requires a non-empty phone and label.")
1339
+ };
1340
+ return {
1341
+ ok: true,
1342
+ error: null,
1343
+ config: {
1344
+ ...config,
1345
+ bffBaseUrl,
1346
+ code,
1347
+ locale: config.locale ?? "es",
1348
+ geolocationTimeoutMs: config.geolocationTimeoutMs ?? 8e3,
1349
+ requestTimeoutMs: config.requestTimeoutMs ?? 15e3
1350
+ }
1351
+ };
1352
+ }
1353
+
1354
+ // src/styles/assets/paramedical-mark.tsx
1355
+ /**
1356
+ * Neutral inline illustration used by the idle panel.
1357
+ */
1358
+ function ParamedicalMark({ className, title }) {
1359
+ return /* @__PURE__ */ jsxs("svg", {
1360
+ className,
1361
+ viewBox: "0 0 96 96",
1362
+ role: "img",
1363
+ "aria-label": title,
1364
+ xmlns: "http://www.w3.org/2000/svg",
1365
+ children: [
1366
+ /* @__PURE__ */ jsx("circle", {
1367
+ cx: "48",
1368
+ cy: "48",
1369
+ r: "46",
1370
+ fill: "#ff0000"
1371
+ }),
1372
+ /* @__PURE__ */ jsx("circle", {
1373
+ cx: "48",
1374
+ cy: "48",
1375
+ r: "37",
1376
+ fill: "#fff"
1377
+ }),
1378
+ /* @__PURE__ */ jsx("path", {
1379
+ d: "M42 26h12v16h16v12H54v16H42V54H26V42h16z",
1380
+ fill: "#ff0000"
1381
+ })
1382
+ ]
1383
+ });
1384
+ }
1385
+
1386
+ // src/react/components/Icon/index.tsx
1387
+ const paths = {
1388
+ close: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }),
1389
+ "alert-triangle": /* @__PURE__ */ jsxs(Fragment, { children: [
1390
+ /* @__PURE__ */ jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" }),
1391
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1392
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1393
+ ] }),
1394
+ "check-circle": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1395
+ cx: "12",
1396
+ cy: "12",
1397
+ r: "9"
1398
+ }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }),
1399
+ "x-circle": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1400
+ cx: "12",
1401
+ cy: "12",
1402
+ r: "9"
1403
+ }), /* @__PURE__ */ jsx("path", { d: "m9 9 6 6M15 9l-6 6" })] }),
1404
+ "exclamation-circle": /* @__PURE__ */ jsxs(Fragment, { children: [
1405
+ /* @__PURE__ */ jsx("circle", {
1406
+ cx: "12",
1407
+ cy: "12",
1408
+ r: "9"
1409
+ }),
1410
+ /* @__PURE__ */ jsx("path", { d: "M12 7v6" }),
1411
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1412
+ ] }),
1413
+ flag: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M5 3v18" }), /* @__PURE__ */ jsx("path", { d: "M5 4h12l-2.5 4L17 12H5" })] }),
1414
+ "double-check": /* @__PURE__ */ jsx("path", { d: "m2 13 4 4 8-9M11 16l1 1 9-10" }),
1415
+ rotate: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }), /* @__PURE__ */ jsx("path", { d: "M21 3v6h-6" })] }),
1416
+ "list-check": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M10 6h10M10 12h10M10 18h10" }), /* @__PURE__ */ jsx("path", { d: "m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17" })] }),
1417
+ eye: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z" }), /* @__PURE__ */ jsx("circle", {
1418
+ cx: "12",
1419
+ cy: "12",
1420
+ r: "2.5"
1421
+ })] }),
1422
+ message: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M21 12a8 8 0 0 1-8 8H7l-4 3V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8Z" }), /* @__PURE__ */ jsx("path", { d: "M8 10h8M8 14h5" })] }),
1423
+ clock: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1424
+ cx: "12",
1425
+ cy: "12",
1426
+ r: "9"
1427
+ }), /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })] }),
1428
+ info: /* @__PURE__ */ jsxs(Fragment, { children: [
1429
+ /* @__PURE__ */ jsx("circle", {
1430
+ cx: "12",
1431
+ cy: "12",
1432
+ r: "9"
1433
+ }),
1434
+ /* @__PURE__ */ jsx("path", { d: "M12 11v6" }),
1435
+ /* @__PURE__ */ jsx("path", { d: "M12 7h.01" })
1436
+ ] }),
1437
+ "arrow-left": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M19 12H5" }), /* @__PURE__ */ jsx("path", { d: "m12 19-7-7 7-7" })] }),
1438
+ emergency: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M12 2v6M12 16v6M2 12h6M16 12h6" }), /* @__PURE__ */ jsx("circle", {
1439
+ cx: "12",
1440
+ cy: "12",
1441
+ r: "3"
1442
+ })] })
1443
+ };
1444
+ function Icon({ name, className, title }) {
1445
+ return /* @__PURE__ */ jsxs("svg", {
1446
+ className,
1447
+ viewBox: "0 0 24 24",
1448
+ width: "1em",
1449
+ height: "1em",
1450
+ fill: "none",
1451
+ stroke: "currentColor",
1452
+ strokeWidth: "2",
1453
+ strokeLinecap: "round",
1454
+ strokeLinejoin: "round",
1455
+ "aria-hidden": title ? void 0 : "true",
1456
+ role: title ? "img" : void 0,
1457
+ focusable: "false",
1458
+ children: [title ? /* @__PURE__ */ jsx("title", { children: title }) : null, paths[name]]
1459
+ });
1460
+ }
1461
+
1462
+ // src/styles/panel.module.css
1463
+ var panel_module_default = {
1464
+ "de-emergency-fade-in": "de-emergency-de-emergency-fade-in-kMlO0G",
1465
+ "de-emergency-pulse": "de-emergency-de-emergency-pulse-kMlO0G",
1466
+ "de-emergency-pulse-error": "de-emergency-de-emergency-pulse-error-kMlO0G",
1467
+ "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-kMlO0G",
1468
+ "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-kMlO0G",
1469
+ "de-emergency-spin": "de-emergency-de-emergency-spin-kMlO0G",
1470
+ "emergency-confirmation": "de-emergency-emergency-confirmation-kMlO0G",
1471
+ "emergency-confirmation__actions": "de-emergency-emergency-confirmation__actions-kMlO0G",
1472
+ "emergency-confirmation__button": "de-emergency-emergency-confirmation__button-kMlO0G",
1473
+ "emergency-confirmation__button--cancel": "de-emergency-emergency-confirmation__button--cancel-kMlO0G",
1474
+ "emergency-confirmation__button--confirm": "de-emergency-emergency-confirmation__button--confirm-kMlO0G",
1475
+ "emergency-confirmation__button--danger": "de-emergency-emergency-confirmation__button--danger-kMlO0G",
1476
+ "emergency-confirmation__card": "de-emergency-emergency-confirmation__card-kMlO0G",
1477
+ "emergency-confirmation__card--danger": "de-emergency-emergency-confirmation__card--danger-kMlO0G",
1478
+ "emergency-confirmation__icon": "de-emergency-emergency-confirmation__icon-kMlO0G",
1479
+ "emergency-confirmation__text": "de-emergency-emergency-confirmation__text-kMlO0G",
1480
+ "emergency-confirmation__title": "de-emergency-emergency-confirmation__title-kMlO0G",
1481
+ "emergency-error-message": "de-emergency-emergency-error-message-kMlO0G",
1482
+ "emergency-error-message__container": "de-emergency-emergency-error-message__container-kMlO0G",
1483
+ "emergency-error-message__content": "de-emergency-emergency-error-message__content-kMlO0G",
1484
+ "emergency-error-message__detail": "de-emergency-emergency-error-message__detail-kMlO0G",
1485
+ "emergency-error-message__details": "de-emergency-emergency-error-message__details-kMlO0G",
1486
+ "emergency-error-message__icon": "de-emergency-emergency-error-message__icon-kMlO0G",
1487
+ "emergency-error-message__text": "de-emergency-emergency-error-message__text-kMlO0G",
1488
+ "emergency-error-message__title": "de-emergency-emergency-error-message__title-kMlO0G",
1489
+ "emergency-fallback": "de-emergency-emergency-fallback-kMlO0G",
1490
+ "emergency-fallback__phone": "de-emergency-emergency-fallback__phone-kMlO0G",
1491
+ "emergency-fallback__text": "de-emergency-emergency-fallback__text-kMlO0G",
1492
+ "emergency-fallback__title": "de-emergency-emergency-fallback__title-kMlO0G",
1493
+ "emergency-feedback": "de-emergency-emergency-feedback-kMlO0G",
1494
+ "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-kMlO0G",
1495
+ "emergency-panel": "de-emergency-emergency-panel-kMlO0G",
1496
+ "emergency-panel__body": "de-emergency-emergency-panel__body-kMlO0G",
1497
+ "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-kMlO0G",
1498
+ "emergency-panel__body-title-container": "de-emergency-emergency-panel__body-title-container-kMlO0G",
1499
+ "emergency-panel__button": "de-emergency-emergency-panel__button-kMlO0G",
1500
+ "emergency-panel__button--emergency": "de-emergency-emergency-panel__button--emergency-kMlO0G",
1501
+ "emergency-panel__button--simulation": "de-emergency-emergency-panel__button--simulation-kMlO0G",
1502
+ "emergency-panel__close-button": "de-emergency-emergency-panel__close-button-kMlO0G",
1503
+ "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-kMlO0G",
1504
+ "emergency-panel__footer": "de-emergency-emergency-panel__footer-kMlO0G",
1505
+ "emergency-panel__header": "de-emergency-emergency-panel__header-kMlO0G",
1506
+ "emergency-panel__title": "de-emergency-emergency-panel__title-kMlO0G",
1507
+ "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-kMlO0G",
1508
+ "emergency-resume-notification-button": "de-emergency-emergency-resume-notification-button-kMlO0G",
1509
+ "emergency-resume-notification-card": "de-emergency-emergency-resume-notification-card-kMlO0G",
1510
+ "emergency-resume-notification-icon": "de-emergency-emergency-resume-notification-icon-kMlO0G",
1511
+ "emergency-resume-notification-message": "de-emergency-emergency-resume-notification-message-kMlO0G",
1512
+ "emergency-resume-notification-overlay": "de-emergency-emergency-resume-notification-overlay-kMlO0G",
1513
+ "emergency-resume-notification-title": "de-emergency-emergency-resume-notification-title-kMlO0G",
1514
+ "emergency-status__abort-btn": "de-emergency-emergency-status__abort-btn-kMlO0G",
1515
+ "emergency-status__body": "de-emergency-emergency-status__body-kMlO0G",
1516
+ "emergency-status__cell": "de-emergency-emergency-status__cell-kMlO0G",
1517
+ "emergency-status__cell-icon": "de-emergency-emergency-status__cell-icon-kMlO0G",
1518
+ "emergency-status__cell-text": "de-emergency-emergency-status__cell-text-kMlO0G",
1519
+ "emergency-status__cell-time": "de-emergency-emergency-status__cell-time-kMlO0G",
1520
+ "emergency-status__grid": "de-emergency-emergency-status__grid-kMlO0G",
1521
+ "emergency-status__header": "de-emergency-emergency-status__header-kMlO0G",
1522
+ "emergency-status__loading": "de-emergency-emergency-status__loading-kMlO0G",
1523
+ "emergency-status__loading-text": "de-emergency-emergency-status__loading-text-kMlO0G",
1524
+ "emergency-status__message-icon": "de-emergency-emergency-status__message-icon-kMlO0G",
1525
+ "emergency-status__message-icon--failed": "de-emergency-emergency-status__message-icon--failed-kMlO0G",
1526
+ "emergency-status__message-icon--success": "de-emergency-emergency-status__message-icon--success-kMlO0G",
1527
+ "emergency-status__message-row": "de-emergency-emergency-status__message-row-kMlO0G",
1528
+ "emergency-status__message-row--failed": "de-emergency-emergency-status__message-row--failed-kMlO0G",
1529
+ "emergency-status__message-row--success": "de-emergency-emergency-status__message-row--success-kMlO0G",
1530
+ "emergency-status__message-sent-at": "de-emergency-emergency-status__message-sent-at-kMlO0G",
1531
+ "emergency-status__message-text": "de-emergency-emergency-status__message-text-kMlO0G",
1532
+ "emergency-status__message-text-container": "de-emergency-emergency-status__message-text-container-kMlO0G",
1533
+ "emergency-status__panel": "de-emergency-emergency-status__panel-kMlO0G",
1534
+ "emergency-status__row": "de-emergency-emergency-status__row-kMlO0G",
1535
+ "emergency-status__row-container": "de-emergency-emergency-status__row-container-kMlO0G",
1536
+ "emergency-status__spinner": "de-emergency-emergency-status__spinner-kMlO0G",
1537
+ "emergency-status-panel__header": "de-emergency-emergency-status-panel__header-kMlO0G",
1538
+ "emergency-status-panel__refresh-btn": "de-emergency-emergency-status-panel__refresh-btn-kMlO0G",
1539
+ "emergency-status-panel__refresh-btn-container": "de-emergency-emergency-status-panel__refresh-btn-container-kMlO0G",
1540
+ "emergency-status-panel__refresh-icon": "de-emergency-emergency-status-panel__refresh-icon-kMlO0G",
1541
+ "emergency-status-panel__title": "de-emergency-emergency-status-panel__title-kMlO0G",
1542
+ "responder-name": "de-emergency-responder-name-kMlO0G",
1543
+ "sent-at--verified": "de-emergency-sent-at--verified-kMlO0G",
1544
+ "simulacrum-success-message": "de-emergency-simulacrum-success-message-kMlO0G",
1545
+ "simulacrum-success-message__container": "de-emergency-simulacrum-success-message__container-kMlO0G",
1546
+ "simulacrum-success-message__content": "de-emergency-simulacrum-success-message__content-kMlO0G",
1547
+ "simulacrum-success-message__detail": "de-emergency-simulacrum-success-message__detail-kMlO0G",
1548
+ "simulacrum-success-message__details": "de-emergency-simulacrum-success-message__details-kMlO0G",
1549
+ "simulacrum-success-message__icon": "de-emergency-simulacrum-success-message__icon-kMlO0G",
1550
+ "simulacrum-success-message__text": "de-emergency-simulacrum-success-message__text-kMlO0G",
1551
+ "simulacrum-success-message__title": "de-emergency-simulacrum-success-message__title-kMlO0G",
1552
+ "sos-img": "de-emergency-sos-img-kMlO0G",
1553
+ "sos-img__container": "de-emergency-sos-img__container-kMlO0G",
1554
+ "status-icon": "de-emergency-status-icon-kMlO0G",
1555
+ "status-icon--checked": "de-emergency-status-icon--checked-kMlO0G",
1556
+ "status-icon--failed": "de-emergency-status-icon--failed-kMlO0G",
1557
+ "status-icon--unchecked": "de-emergency-status-icon--unchecked-kMlO0G"
1558
+ };
1559
+
1560
+ // src/react/classnames.ts
1561
+ /**
1562
+ * Joins local CSS Module class names, omitting names that are not in the
1563
+ * module.
1564
+ */
1565
+ function cls(...names) {
1566
+ return names.filter((name) => Boolean(name)).map((name) => panel_module_default[name] ?? "").filter(Boolean).join(" ");
1567
+ }
1568
+
1569
+ // src/react/components/MessageSurface/index.tsx
1570
+ function MessageSurface(props) {
1571
+ const { variant, icon, title, text, details, className, children } = props;
1572
+ const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
1573
+ const success = variant === "success";
1574
+ return /* @__PURE__ */ jsxs("div", {
1575
+ className: [cls(prefix), className].filter(Boolean).join(" "),
1576
+ role: success ? "status" : "alert",
1577
+ "aria-live": success ? "polite" : "assertive",
1578
+ children: [/* @__PURE__ */ jsxs("div", {
1579
+ className: cls(`${prefix}__container`),
1580
+ children: [/* @__PURE__ */ jsx("div", {
1581
+ className: cls(`${prefix}__icon`),
1582
+ children: /* @__PURE__ */ jsx(Icon, { name: icon })
1583
+ }), /* @__PURE__ */ jsxs("div", {
1584
+ className: cls(`${prefix}__content`),
1585
+ children: [
1586
+ /* @__PURE__ */ jsx("h3", {
1587
+ className: cls(`${prefix}__title`),
1588
+ children: title
1589
+ }),
1590
+ /* @__PURE__ */ jsx("p", {
1591
+ className: cls(`${prefix}__text`),
1592
+ children: text
1593
+ }),
1594
+ /* @__PURE__ */ jsx("div", {
1595
+ className: cls(`${prefix}__details`),
1596
+ children: details.map((detail) => /* @__PURE__ */ jsxs("span", {
1597
+ className: cls(`${prefix}__detail`),
1598
+ children: [/* @__PURE__ */ jsx(Icon, { name: detail.icon }), detail.text]
1599
+ }, detail.text))
1600
+ })
1601
+ ]
1602
+ })]
1603
+ }), children]
1604
+ });
1605
+ }
1606
+
1607
+ // src/react/components/EmergencyError/index.tsx
1608
+ function EmergencyError({ error, onRetry }) {
1609
+ return /* @__PURE__ */ jsx(MessageSurface, {
1610
+ variant: "error",
1611
+ icon: "exclamation-circle",
1612
+ title: copy.errorTitle,
1613
+ text: error.message || copy.errorText,
1614
+ details: [{
1615
+ icon: "info",
1616
+ text: copy.errorDetailInfo
1617
+ }, {
1618
+ icon: "arrow-left",
1619
+ text: copy.errorDetailRetry
1620
+ }],
1621
+ children: /* @__PURE__ */ jsx("button", {
1622
+ type: "button",
1623
+ className: cls("emergency-feedback__dismiss"),
1624
+ onClick: onRetry,
1625
+ children: copy.retry
1626
+ })
1627
+ });
1628
+ }
1629
+
1630
+ // src/react/components/EmergencyFallback/index.tsx
1631
+ function errorCopy(error) {
1632
+ switch (error.code) {
1633
+ case "geolocation_denied": return copy.geolocationDenied;
1634
+ case "geolocation_timeout": return copy.geolocationTimeout;
1635
+ case "geolocation_unsupported": return copy.geolocationUnsupported;
1636
+ default: return copy.geolocationUnavailable;
1637
+ }
1638
+ }
1639
+ function EmergencyFallback({ fallback, error }) {
1640
+ return /* @__PURE__ */ jsxs("div", {
1641
+ className: cls("emergency-fallback"),
1642
+ children: [
1643
+ /* @__PURE__ */ jsx("h4", {
1644
+ className: cls("emergency-fallback__title"),
1645
+ children: copy.fallbackTitle
1646
+ }),
1647
+ /* @__PURE__ */ jsx("p", {
1648
+ className: cls("emergency-fallback__text"),
1649
+ children: errorCopy(error)
1650
+ }),
1651
+ fallback ? /* @__PURE__ */ jsxs("a", {
1652
+ className: cls("emergency-fallback__phone"),
1653
+ href: `tel:${fallback.phone.replace(/[^\d+]/g, "")}`,
1654
+ children: [/* @__PURE__ */ jsx(Icon, { name: "message" }), fallback.label]
1655
+ }) : null
1656
+ ]
1657
+ });
1658
+ }
1659
+
1660
+ // src/react/components/EmergencyFeedback/index.tsx
1661
+ function EmergencyFeedback({ abortResult, simulationResult, onDismiss }) {
1662
+ const result = abortResult ?? simulationResult;
1663
+ if (!result) return null;
1664
+ const aborting = abortResult !== null;
1665
+ const success = result.ok;
1666
+ const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
1667
+ const details = success ? aborting ? [{
1668
+ icon: "message",
1669
+ text: copy.abortSuccessDetails
1670
+ }] : [{
1671
+ icon: "message",
1672
+ text: copy.simulateSuccessDetails
1673
+ }, {
1674
+ icon: "clock",
1675
+ text: copy.simulateSuccessCompleted
1676
+ }] : [{
1677
+ icon: "info",
1678
+ text: copy.errorDetailInfo
1679
+ }, {
1680
+ icon: "arrow-left",
1681
+ text: copy.errorDetailRetry
1682
+ }];
1683
+ return /* @__PURE__ */ jsx(MessageSurface, {
1684
+ variant: success ? "success" : "error",
1685
+ icon: success ? "check-circle" : "alert-triangle",
1686
+ title,
1687
+ text: result.message,
1688
+ details,
1689
+ className: cls("emergency-feedback"),
1690
+ children: /* @__PURE__ */ jsx("button", {
1691
+ type: "button",
1692
+ className: cls("emergency-feedback__dismiss"),
1693
+ onClick: onDismiss,
1694
+ children: copy.feedbackDismiss
1695
+ })
1696
+ });
1697
+ }
1698
+
1699
+ // src/react/components/EmergencyLoading/index.tsx
1700
+ function EmergencyLoading() {
1701
+ return /* @__PURE__ */ jsxs("div", {
1702
+ className: cls("emergency-status__loading"),
1703
+ role: "status",
1704
+ "aria-live": "polite",
1705
+ children: [/* @__PURE__ */ jsx("span", {
1706
+ className: cls("emergency-status__spinner"),
1707
+ "aria-hidden": "true"
1708
+ }), /* @__PURE__ */ jsx("span", {
1709
+ className: cls("emergency-status__loading-text"),
1710
+ children: copy.loading
1711
+ })]
1712
+ });
1713
+ }
1714
+
1715
+ // src/core/date.ts
1716
+ /**
1717
+ * Formats a timestamp as `h:mmam` or `h:mmpm` in local time, without a space
1718
+ * or leading zero.
1719
+ */
1720
+ function formatShortTime(value, locale = "es") {
1721
+ if (value === null) return "";
1722
+ const date = value instanceof Date ? value : new Date(value);
1723
+ if (Number.isNaN(date.getTime())) return "";
1724
+ const parts = new Intl.DateTimeFormat(locale, {
1725
+ hour: "numeric",
1726
+ minute: "2-digit",
1727
+ hour12: true
1728
+ }).formatToParts(date);
1729
+ const hour = parts.find((part) => part.type === "hour")?.value;
1730
+ const minute = parts.find((part) => part.type === "minute")?.value;
1731
+ const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value;
1732
+ if (!hour || !minute) return "";
1733
+ return `${hour}:${minute}${normalizePeriod(dayPeriod)}`;
1734
+ }
1735
+ function normalizePeriod(dayPeriod) {
1736
+ if (!dayPeriod) return "";
1737
+ if (/^p/i.test(dayPeriod)) return "pm";
1738
+ return /^a/i.test(dayPeriod) ? "am" : "";
1739
+ }
1740
+
1741
+ // src/react/components/EmergencyMessageRow/index.tsx
1742
+ /**
1743
+ * Renders one message row per responder.
1744
+ */
1745
+ function EmergencyMessageRow({ responder, locale }) {
1746
+ const message = responder.lastMessage;
1747
+ const hasContent = Boolean(message?.content);
1748
+ const modifier = hasContent ? "emergency-status__message-row--success" : "emergency-status__message-row--failed";
1749
+ const iconModifier = hasContent ? "emergency-status__message-icon--success" : "emergency-status__message-icon--failed";
1750
+ return /* @__PURE__ */ jsxs("div", {
1751
+ className: cls("emergency-status__message-row", modifier),
1752
+ children: [/* @__PURE__ */ jsxs("p", {
1753
+ className: cls("emergency-status__message-text-container"),
1754
+ children: [/* @__PURE__ */ jsx(Icon, {
1755
+ name: "flag",
1756
+ className: cls("emergency-status__message-icon", iconModifier)
1757
+ }), /* @__PURE__ */ jsxs("span", {
1758
+ className: cls("emergency-status__message-text"),
1759
+ children: [
1760
+ /* @__PURE__ */ jsx("strong", { children: copy.statusPrefix }),
1761
+ " ",
1762
+ message?.content ?? ""
1763
+ ]
1764
+ })]
1765
+ }), message ? /* @__PURE__ */ jsxs("p", {
1766
+ className: cls("emergency-status__message-text-container", "sent-at--verified"),
1767
+ children: [/* @__PURE__ */ jsx(Icon, {
1768
+ name: "double-check",
1769
+ className: cls("emergency-status__message-icon")
1770
+ }), message.sentAt ? /* @__PURE__ */ jsx("span", {
1771
+ className: cls("emergency-status__message-sent-at"),
1772
+ children: formatShortTime(message.sentAt, locale)
1773
+ }) : null]
1774
+ }) : null]
1775
+ });
1776
+ }
1777
+
1778
+ // src/react/components/StatusIcon/index.tsx
1779
+ /**
1780
+ * Explicit responder state model:
1781
+ * - `failed`: the send operation failed (red alert in the "Sent" cell only).
1782
+ * - `confirmed`: checked circle.
1783
+ * - otherwise: unchecked circle.
1784
+ */
1785
+ function StatusIcon({ confirmed, failed = false }) {
1786
+ return /* @__PURE__ */ jsx(Icon, {
1787
+ name: failed ? "exclamation-circle" : confirmed ? "check-circle" : "x-circle",
1788
+ className: cls("status-icon", failed ? "status-icon--failed" : confirmed ? "status-icon--checked" : "status-icon--unchecked")
1789
+ });
1790
+ }
1791
+
1792
+ // src/react/components/StatusCell/index.tsx
1793
+ function StatusCell({ confirmed, timestamp, failed = false, isFailedIcon = false, locale }) {
1794
+ const showTime = !failed && timestamp !== null && timestamp !== "";
1795
+ const showIcon = failed ? isFailedIcon : true;
1796
+ return /* @__PURE__ */ jsxs("div", {
1797
+ className: cls("emergency-status__cell"),
1798
+ children: [/* @__PURE__ */ jsx("p", {
1799
+ className: cls("emergency-status__cell-icon"),
1800
+ children: showIcon ? /* @__PURE__ */ jsx(StatusIcon, {
1801
+ confirmed,
1802
+ failed
1803
+ }) : null
1804
+ }), showTime ? /* @__PURE__ */ jsx("p", {
1805
+ className: cls("emergency-status__cell-time"),
1806
+ children: formatShortTime(timestamp, locale)
1807
+ }) : null]
1808
+ });
1809
+ }
1810
+
1811
+ // src/react/components/ResponderRow/index.tsx
1812
+ function ResponderRow({ responder, locale }) {
1813
+ return /* @__PURE__ */ jsxs("div", {
1814
+ className: cls("emergency-status__row-container"),
1815
+ "data-responder-id": responder.phone,
1816
+ children: [/* @__PURE__ */ jsxs("div", {
1817
+ className: cls("emergency-status__row"),
1818
+ children: [
1819
+ /* @__PURE__ */ jsx("div", {
1820
+ className: cls("emergency-status__cell", "responder-name"),
1821
+ children: /* @__PURE__ */ jsx("p", {
1822
+ className: cls("emergency-status__cell-text"),
1823
+ children: responder.name
1824
+ })
1825
+ }),
1826
+ /* @__PURE__ */ jsx(StatusCell, {
1827
+ confirmed: responder.sent,
1828
+ timestamp: responder.sentAt,
1829
+ failed: responder.failed,
1830
+ isFailedIcon: true,
1831
+ locale
1832
+ }),
1833
+ /* @__PURE__ */ jsx(StatusCell, {
1834
+ confirmed: responder.delivered,
1835
+ timestamp: responder.deliveredAt,
1836
+ failed: responder.failed,
1837
+ locale
1838
+ }),
1839
+ /* @__PURE__ */ jsx(StatusCell, {
1840
+ confirmed: responder.read,
1841
+ timestamp: responder.readAt,
1842
+ failed: responder.failed,
1843
+ locale
1844
+ })
1845
+ ]
1846
+ }), /* @__PURE__ */ jsx(EmergencyMessageRow, {
1847
+ responder,
1848
+ locale
1849
+ })]
1850
+ });
1851
+ }
1852
+
1853
+ // src/react/components/EmergencyStatusPanel/index.tsx
1854
+ function EmergencyStatusPanel({ responders, refreshing, locale, onRefresh }) {
1855
+ const columns = [
1856
+ copy.responderColumn,
1857
+ copy.sentColumn,
1858
+ copy.deliveredColumn,
1859
+ copy.readColumn
1860
+ ];
1861
+ return /* @__PURE__ */ jsxs("div", {
1862
+ className: cls("emergency-status__panel"),
1863
+ children: [/* @__PURE__ */ jsx("div", {
1864
+ className: cls("emergency-status-panel__header"),
1865
+ children: /* @__PURE__ */ jsx("h3", {
1866
+ className: cls("emergency-status-panel__title"),
1867
+ children: copy.statusTitle
1868
+ })
1869
+ }), /* @__PURE__ */ jsxs("div", {
1870
+ className: cls("emergency-status__grid"),
1871
+ children: [
1872
+ /* @__PURE__ */ jsx("div", {
1873
+ className: cls("emergency-status__header"),
1874
+ children: columns.map((column) => /* @__PURE__ */ jsx("div", {
1875
+ className: cls("emergency-status__cell"),
1876
+ children: /* @__PURE__ */ jsx("p", {
1877
+ className: cls("emergency-status__cell-text"),
1878
+ children: column
1879
+ })
1880
+ }, column))
1881
+ }),
1882
+ /* @__PURE__ */ jsxs("div", {
1883
+ className: cls("emergency-status__body"),
1884
+ children: [responders.map((responder) => /* @__PURE__ */ jsx(ResponderRow, {
1885
+ responder,
1886
+ locale
1887
+ }, `${responder.phone}-${responder.name}`)), responders.length === 0 ? /* @__PURE__ */ jsx("p", {
1888
+ className: cls("emergency-status__cell-text"),
1889
+ children: copy.respondersEmpty
1890
+ }) : null]
1891
+ }),
1892
+ /* @__PURE__ */ jsxs("div", {
1893
+ className: cls("emergency-status-panel__refresh-btn-container"),
1894
+ children: [/* @__PURE__ */ jsxs("button", {
1895
+ type: "button",
1896
+ className: cls("emergency-status-panel__refresh-btn"),
1897
+ "aria-label": copy.refresh,
1898
+ disabled: refreshing,
1899
+ onClick: onRefresh,
1900
+ children: [/* @__PURE__ */ jsx(Icon, {
1901
+ name: "rotate",
1902
+ className: cls("emergency-status-panel__refresh-icon")
1903
+ }), /* @__PURE__ */ jsx("span", {
1904
+ className: cls("emergency-status-panel__refresh-text"),
1905
+ children: refreshing ? copy.refreshing : copy.refresh
1906
+ })]
1907
+ }), refreshing ? /* @__PURE__ */ jsxs("div", {
1908
+ className: cls("emergency-status__loading"),
1909
+ role: "status",
1910
+ "aria-live": "polite",
1911
+ children: [/* @__PURE__ */ jsx("span", {
1912
+ className: cls("emergency-status__spinner"),
1913
+ "aria-hidden": "true"
1914
+ }), /* @__PURE__ */ jsx("span", {
1915
+ className: cls("emergency-status__loading-text"),
1916
+ children: copy.refreshing
1917
+ })]
1918
+ }) : null]
1919
+ })
1920
+ ]
1921
+ })]
1922
+ });
1923
+ }
1924
+
1925
+ // src/react/components/EmergencyBody/index.tsx
1926
+ function EmergencyBody(props) {
1927
+ const { active, responders, refreshing, loading, locale, geolocationError, error, fallback, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
1928
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1929
+ /* @__PURE__ */ jsx("div", {
1930
+ className: cls("sos-img__container"),
1931
+ id: "sos-img__container",
1932
+ children: /* @__PURE__ */ jsx(ParamedicalMark, {
1933
+ className: cls("sos-img"),
1934
+ title: copy.illustrationAlt
1935
+ })
1936
+ }),
1937
+ active ? null : /* @__PURE__ */ jsx("div", {
1938
+ className: cls("emergency-panel__body-title-container"),
1939
+ children: /* @__PURE__ */ jsx("h4", {
1940
+ className: cls("emergency-panel__body-title"),
1941
+ children: copy.question
1942
+ })
1943
+ }),
1944
+ active ? /* @__PURE__ */ jsx(EmergencyStatusPanel, {
1945
+ responders,
1946
+ refreshing,
1947
+ locale,
1948
+ onRefresh
1949
+ }) : null,
1950
+ loading ? /* @__PURE__ */ jsx(EmergencyLoading, {}) : null,
1951
+ geolocationError ? /* @__PURE__ */ jsx(EmergencyFallback, {
1952
+ fallback,
1953
+ error: geolocationError
1954
+ }) : null,
1955
+ error ? /* @__PURE__ */ jsx(EmergencyError, {
1956
+ error,
1957
+ onRetry
1958
+ }) : null,
1959
+ /* @__PURE__ */ jsx(EmergencyFeedback, {
1960
+ abortResult,
1961
+ simulationResult,
1962
+ onDismiss: onDismissFeedback
1963
+ })
1964
+ ] });
1965
+ }
1966
+
1967
+ // src/react/components/EmergencyConfirmation/index.tsx
1968
+ function EmergencyConfirmation(props) {
1969
+ const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
1970
+ const cardRef = useRef(null);
1971
+ const titleId = useId();
1972
+ const danger = abortConfirmation;
1973
+ const open = abortConfirmation || simulationConfirmation;
1974
+ useEffect(() => {
1975
+ if (open) cardRef.current?.querySelector("button")?.focus();
1976
+ }, [open]);
1977
+ if (!open) return null;
1978
+ const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
1979
+ const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
1980
+ return /* @__PURE__ */ jsx("div", {
1981
+ className: cls("emergency-confirmation"),
1982
+ role: "dialog",
1983
+ "aria-modal": "true",
1984
+ "aria-labelledby": titleId,
1985
+ children: /* @__PURE__ */ jsxs("div", {
1986
+ ref: cardRef,
1987
+ className: cls("emergency-confirmation__card", danger && "emergency-confirmation__card--danger"),
1988
+ children: [
1989
+ /* @__PURE__ */ jsx("div", {
1990
+ className: cls("emergency-confirmation__icon"),
1991
+ children: /* @__PURE__ */ jsx(Icon, { name: danger ? "alert-triangle" : "info" })
1992
+ }),
1993
+ /* @__PURE__ */ jsx("h3", {
1994
+ className: cls("emergency-confirmation__title"),
1995
+ id: titleId,
1996
+ children: title
1997
+ }),
1998
+ /* @__PURE__ */ jsx("p", {
1999
+ className: cls("emergency-confirmation__text"),
2000
+ children: text
2001
+ }),
2002
+ /* @__PURE__ */ jsxs("div", {
2003
+ className: cls("emergency-confirmation__actions"),
2004
+ children: [/* @__PURE__ */ jsx("button", {
2005
+ type: "button",
2006
+ className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2007
+ onClick: danger ? onCancelAbort : onCancelSimulation,
2008
+ children: danger ? copy.abortCancel : copy.simulateCancel
2009
+ }), /* @__PURE__ */ jsx("button", {
2010
+ type: "button",
2011
+ className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2012
+ onClick: danger ? onConfirmAbort : onConfirmSimulation,
2013
+ children: danger ? copy.abortConfirm : copy.simulateConfirm
2014
+ })]
2015
+ })
2016
+ ]
2017
+ })
2018
+ });
2019
+ }
2020
+
2021
+ // src/react/components/EmergencyFooter/index.tsx
2022
+ function EmergencyFooter(props) {
2023
+ const { active, busy, aborting, onActivate, onRequestAbort, onRequestSimulation } = props;
2024
+ if (active) return /* @__PURE__ */ jsx("footer", {
2025
+ className: cls("emergency-panel__footer"),
2026
+ children: /* @__PURE__ */ jsxs("button", {
2027
+ type: "button",
2028
+ className: cls("emergency-status__abort-btn"),
2029
+ "aria-label": copy.abort,
2030
+ disabled: aborting,
2031
+ onClick: onRequestAbort,
2032
+ children: [copy.abort, /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })]
2033
+ })
2034
+ });
2035
+ return /* @__PURE__ */ jsxs("footer", {
2036
+ className: cls("emergency-panel__footer"),
2037
+ children: [/* @__PURE__ */ jsxs("button", {
2038
+ type: "button",
2039
+ className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
2040
+ "aria-label": copy.emergencyAria,
2041
+ disabled: busy,
2042
+ onClick: onActivate,
2043
+ children: [copy.emergency, /* @__PURE__ */ jsx(Icon, { name: "emergency" })]
2044
+ }), /* @__PURE__ */ jsxs("button", {
2045
+ type: "button",
2046
+ className: cls("emergency-panel__button", "emergency-panel__button--simulation"),
2047
+ "aria-label": copy.testAria,
2048
+ disabled: busy,
2049
+ onClick: onRequestSimulation,
2050
+ children: [copy.test, /* @__PURE__ */ jsx(Icon, { name: "list-check" })]
2051
+ })]
2052
+ });
2053
+ }
2054
+
2055
+ // src/react/components/EmergencyHeader/index.tsx
2056
+ function EmergencyHeader({ titleId, onClose }) {
2057
+ return /* @__PURE__ */ jsxs("header", {
2058
+ className: cls("emergency-panel__header"),
2059
+ children: [/* @__PURE__ */ jsx("h2", {
2060
+ className: cls("emergency-panel__title"),
2061
+ id: titleId,
2062
+ children: copy.panelLabel
2063
+ }), /* @__PURE__ */ jsx("button", {
2064
+ type: "button",
2065
+ className: cls("emergency-panel__close-button"),
2066
+ "aria-label": copy.close,
2067
+ onClick: onClose,
2068
+ children: /* @__PURE__ */ jsx(Icon, {
2069
+ name: "close",
2070
+ className: cls("emergency-panel__close-icon")
2071
+ })
2072
+ })]
2073
+ });
2074
+ }
2075
+
2076
+ // src/react/hooks/useEscapeKey.ts
2077
+ function useEscapeKey(active, onEscape) {
2078
+ useEffect(() => {
2079
+ if (!active || typeof document === "undefined") return;
2080
+ const handleKeyDown = (event) => {
2081
+ if (event.key === "Escape") {
2082
+ event.stopPropagation();
2083
+ onEscape();
2084
+ }
2085
+ };
2086
+ document.addEventListener("keydown", handleKeyDown);
2087
+ return () => {
2088
+ document.removeEventListener("keydown", handleKeyDown);
2089
+ };
2090
+ }, [active, onEscape]);
2091
+ }
2092
+
2093
+ // src/react/hooks/useFocusTrap.ts
2094
+ const FOCUSABLE_SELECTOR = [
2095
+ "a[href]",
2096
+ "button:not([disabled])",
2097
+ "input:not([disabled])",
2098
+ "select:not([disabled])",
2099
+ "textarea:not([disabled])",
2100
+ "[tabindex]:not([tabindex=\"-1\"])"
2101
+ ].join(",");
2102
+ /**
2103
+ * Keeps keyboard focus inside the dialog while it is open and restores focus to
2104
+ * the element that opened it when it closes.
2105
+ */
2106
+ function useFocusTrap(ref, active) {
2107
+ useEffect(() => {
2108
+ if (!active || typeof document === "undefined") return;
2109
+ const container = ref.current;
2110
+ if (!container) return;
2111
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2112
+ const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
2113
+ (focusable()[0] ?? container).focus({ preventScroll: true });
2114
+ const handleKeyDown = (event) => {
2115
+ if (event.key !== "Tab") return;
2116
+ const items = focusable();
2117
+ if (items.length === 0) {
2118
+ event.preventDefault();
2119
+ container.focus({ preventScroll: true });
2120
+ return;
2121
+ }
2122
+ const first = items[0];
2123
+ const last = items[items.length - 1];
2124
+ const current = document.activeElement;
2125
+ if (event.shiftKey && (current === first || !container.contains(current))) {
2126
+ event.preventDefault();
2127
+ if (last) last.focus({ preventScroll: true });
2128
+ return;
2129
+ }
2130
+ if (!event.shiftKey && current === last && first) {
2131
+ event.preventDefault();
2132
+ first.focus({ preventScroll: true });
2133
+ }
2134
+ };
2135
+ document.addEventListener("keydown", handleKeyDown, true);
2136
+ return () => {
2137
+ document.removeEventListener("keydown", handleKeyDown, true);
2138
+ if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
2139
+ };
2140
+ }, [active, ref]);
2141
+ }
2142
+
2143
+ // src/react/components/EmergencyPanel/index.tsx
2144
+ function EmergencyPanel(props) {
2145
+ const { open, theme, active, busy, aborting, loading, locale, responders, refreshing, geolocationError, error, fallback, abortResult, simulationResult, abortConfirmation, simulationConfirmation, onClose, onActivate, onRefresh, onRetry, onDismissFeedback, onRequestAbort, onRequestSimulation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2146
+ const titleId = useId();
2147
+ const dialogRef = useRef(null);
2148
+ const closeOnEscape = useCallback(() => {
2149
+ onClose("escape");
2150
+ }, [onClose]);
2151
+ useFocusTrap(dialogRef, open);
2152
+ useEscapeKey(open, closeOnEscape);
2153
+ return /* @__PURE__ */ jsxs("div", {
2154
+ ref: dialogRef,
2155
+ className: cls("emergency-panel", !open && "emergency-panel--hidden"),
2156
+ style: themeToCssVariables(theme),
2157
+ role: "dialog",
2158
+ "aria-modal": "true",
2159
+ "aria-labelledby": titleId,
2160
+ "aria-hidden": !open,
2161
+ inert: !open,
2162
+ tabIndex: -1,
2163
+ children: [
2164
+ /* @__PURE__ */ jsx(EmergencyHeader, {
2165
+ titleId,
2166
+ onClose: () => onClose("close")
2167
+ }),
2168
+ /* @__PURE__ */ jsx("section", {
2169
+ className: cls("emergency-panel__body"),
2170
+ "aria-labelledby": titleId,
2171
+ children: /* @__PURE__ */ jsx(EmergencyBody, {
2172
+ active,
2173
+ responders,
2174
+ refreshing,
2175
+ loading,
2176
+ locale,
2177
+ geolocationError,
2178
+ error,
2179
+ fallback,
2180
+ abortResult,
2181
+ simulationResult,
2182
+ onRefresh,
2183
+ onRetry,
2184
+ onDismissFeedback
2185
+ })
2186
+ }),
2187
+ /* @__PURE__ */ jsx(EmergencyFooter, {
2188
+ active,
2189
+ busy,
2190
+ aborting,
2191
+ onActivate,
2192
+ onRequestAbort,
2193
+ onRequestSimulation
2194
+ }),
2195
+ /* @__PURE__ */ jsx(EmergencyConfirmation, {
2196
+ abortConfirmation,
2197
+ simulationConfirmation,
2198
+ onCancelAbort,
2199
+ onCancelSimulation,
2200
+ onConfirmAbort,
2201
+ onConfirmSimulation
2202
+ })
2203
+ ]
2204
+ });
2205
+ }
2206
+
2207
+ // src/react/components/EmergencyResumeNotification/index.tsx
2208
+ function EmergencyResumeNotification(props) {
2209
+ const { resumeAvailable, onOpen } = props;
2210
+ const titleId = useId();
2211
+ if (!resumeAvailable) return null;
2212
+ return /* @__PURE__ */ jsx("div", {
2213
+ className: cls("emergency-resume-notification-overlay"),
2214
+ role: "dialog",
2215
+ "aria-modal": "true",
2216
+ "aria-labelledby": titleId,
2217
+ children: /* @__PURE__ */ jsxs("div", {
2218
+ className: cls("emergency-resume-notification-card"),
2219
+ children: [
2220
+ /* @__PURE__ */ jsx("div", {
2221
+ className: cls("emergency-resume-notification-icon"),
2222
+ "aria-hidden": "true",
2223
+ children: /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })
2224
+ }),
2225
+ /* @__PURE__ */ jsx("h2", {
2226
+ className: cls("emergency-resume-notification-title"),
2227
+ id: titleId,
2228
+ children: copy.resumeTitle
2229
+ }),
2230
+ /* @__PURE__ */ jsx("p", {
2231
+ className: cls("emergency-resume-notification-message"),
2232
+ children: copy.resumeDescription
2233
+ }),
2234
+ /* @__PURE__ */ jsxs("button", {
2235
+ type: "button",
2236
+ className: cls("emergency-resume-notification-button"),
2237
+ "aria-label": copy.resumeAction,
2238
+ onClick: onOpen,
2239
+ children: [/* @__PURE__ */ jsx(Icon, { name: "eye" }), copy.resumeAction]
2240
+ })
2241
+ ]
2242
+ })
2243
+ });
2244
+ }
2245
+
2246
+ // src/react/context.tsx
2247
+ const EmergencyContext = createContext(null);
2248
+ function EmergencyProvider({ value, children }) {
2249
+ return /* @__PURE__ */ jsx(EmergencyContext.Provider, {
2250
+ value,
2251
+ children
2252
+ });
2253
+ }
2254
+ function useEmergencyContext() {
2255
+ const value = useContext(EmergencyContext);
2256
+ if (value === null) throw new Error("Emergency components must be rendered inside an emergency client.");
2257
+ return value;
2258
+ }
2259
+
2260
+ // src/react/hooks/useEmergencyController.ts
2261
+ function useEmergencyController() {
2262
+ return useEmergencyContext().client;
2263
+ }
2264
+
2265
+ // src/react/hooks/useEmergencyStore.ts
2266
+ function useEmergencyStore(selector) {
2267
+ const { store } = useEmergencyContext();
2268
+ return useStore(store, selector);
2269
+ }
2270
+
2271
+ // src/react/components/EmergencyRoot/index.tsx
2272
+ const BUSY_PHASES = [
2273
+ "initializing",
2274
+ "activating",
2275
+ "simulating",
2276
+ "aborting"
2277
+ ];
2278
+ function EmergencyRoot() {
2279
+ const client = useEmergencyController();
2280
+ const panelOpen = useEmergencyStore((state) => state.panelOpen);
2281
+ const phase = useEmergencyStore((state) => state.phase);
2282
+ const config = useEmergencyStore((state) => state.config);
2283
+ const snapshot = useEmergencyStore((state) => state.snapshot);
2284
+ const responders = useEmergencyStore((state) => state.responders);
2285
+ const resumeAvailable = useEmergencyStore((state) => state.resumeAvailable);
2286
+ const geolocationError = useEmergencyStore((state) => state.geolocationError);
2287
+ const lastError = useEmergencyStore((state) => state.lastError);
2288
+ const abortResult = useEmergencyStore((state) => state.abortResult);
2289
+ const simulationResult = useEmergencyStore((state) => state.simulationResult);
2290
+ const abortConfirmation = useEmergencyStore((state) => state.abortConfirmation);
2291
+ const simulationConfirmation = useEmergencyStore((state) => state.simulationConfirmation);
2292
+ const active = snapshot?.active === true;
2293
+ const busy = BUSY_PHASES.includes(phase.kind);
2294
+ const showError = phase.kind === "error" || lastError !== null && geolocationError === null && abortResult === null && simulationResult === null;
2295
+ const error = phase.kind === "error" ? phase.error : lastError;
2296
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(EmergencyResumeNotification, {
2297
+ resumeAvailable,
2298
+ onOpen: () => client.open("resume")
2299
+ }), /* @__PURE__ */ jsx(EmergencyPanel, {
2300
+ open: panelOpen,
2301
+ theme: config.theme,
2302
+ active,
2303
+ busy,
2304
+ aborting: phase.kind === "aborting",
2305
+ loading: phase.kind === "initializing" || phase.kind === "activating",
2306
+ locale: config.locale ?? "es",
2307
+ responders,
2308
+ refreshing: phase.kind === "refreshing",
2309
+ geolocationError,
2310
+ error: showError ? error : null,
2311
+ fallback: config.fallback ?? null,
2312
+ abortResult,
2313
+ simulationResult,
2314
+ abortConfirmation,
2315
+ simulationConfirmation,
2316
+ onClose: (reason) => client.close(reason),
2317
+ onActivate: () => {
2318
+ client.activate();
2319
+ },
2320
+ onRefresh: () => {
2321
+ client.refresh();
2322
+ },
2323
+ onRetry: () => {
2324
+ client.activate();
2325
+ },
2326
+ onDismissFeedback: () => client.dismissFeedback(),
2327
+ onRequestAbort: () => client.requestAbortConfirmation(),
2328
+ onRequestSimulation: () => client.requestSimulationConfirmation(),
2329
+ onCancelAbort: () => client.cancelAbortConfirmation(),
2330
+ onCancelSimulation: () => client.cancelSimulationConfirmation(),
2331
+ onConfirmAbort: () => {
2332
+ client.abort();
2333
+ },
2334
+ onConfirmSimulation: () => {
2335
+ client.simulate();
2336
+ }
2337
+ })] });
2338
+ }
2339
+
2340
+ // src/mount.tsx
2341
+ function resolveRoot(root) {
2342
+ if (typeof root === "string") {
2343
+ if (typeof document === "undefined") return null;
2344
+ return document.querySelector(root);
2345
+ }
2346
+ return root instanceof HTMLElement ? root : null;
2347
+ }
2348
+ function createFailedClient(error) {
2349
+ const state = {
2350
+ config: {
2351
+ bffBaseUrl: "",
2352
+ code: ""
2353
+ },
2354
+ phase: {
2355
+ kind: "error",
2356
+ error
2357
+ },
2358
+ panelOpen: false,
2359
+ sessionReady: false,
2360
+ resumeAvailable: false,
2361
+ snapshot: null,
2362
+ responders: [],
2363
+ failures: [],
2364
+ messages: [],
2365
+ lastError: error,
2366
+ geolocation: null,
2367
+ geolocationError: null,
2368
+ realtimeStatus: "idle",
2369
+ abortResult: null,
2370
+ simulationResult: null,
2371
+ abortConfirmation: false,
2372
+ simulationConfirmation: false,
2373
+ destroyed: false
2374
+ };
2375
+ const noop = () => void 0;
2376
+ const noopAsync = async () => void 0;
2377
+ return {
2378
+ open: noop,
2379
+ close: noop,
2380
+ destroy: noop,
2381
+ getState: () => state,
2382
+ subscribe: () => () => void 0,
2383
+ on: () => () => void 0,
2384
+ off: noop,
2385
+ ensureSession: noopAsync,
2386
+ resume: noopAsync,
2387
+ dismissResume: noop,
2388
+ activate: noopAsync,
2389
+ simulate: noopAsync,
2390
+ refresh: noopAsync,
2391
+ abort: noopAsync,
2392
+ requestAbortConfirmation: noop,
2393
+ cancelAbortConfirmation: noop,
2394
+ requestSimulationConfirmation: noop,
2395
+ cancelSimulationConfirmation: noop,
2396
+ dismissFeedback: noop
2397
+ };
2398
+ }
2399
+ function attachTrigger(trigger, client) {
2400
+ if (!trigger) return () => void 0;
2401
+ if (typeof trigger === "function") {
2402
+ const cleanup = trigger(() => client.open());
2403
+ return typeof cleanup === "function" ? cleanup : () => void 0;
2404
+ }
2405
+ const element = typeof trigger === "string" ? typeof document === "undefined" ? null : document.querySelector(trigger) : trigger;
2406
+ if (!element) return () => void 0;
2407
+ const listener = (event) => {
2408
+ event.preventDefault();
2409
+ client.open();
2410
+ };
2411
+ element.addEventListener("click", listener);
2412
+ return () => {
2413
+ element.removeEventListener("click", listener);
2414
+ };
2415
+ }
2416
+ function reportConfigurationError(error, options) {
2417
+ if (typeof console !== "undefined" && typeof console.error === "function") console.error("[emergency]", error.message);
2418
+ options.onEvent?.("emergency:failed", {
2419
+ error,
2420
+ operation: "mount"
2421
+ });
2422
+ return createFailedClient(error);
2423
+ }
2424
+ /**
2425
+ * Mounts the emergency client into a host element. Validation failures never
2426
+ * throw into the host page: they produce a typed configuration error, a console
2427
+ * message, and an inert client.
2428
+ */
2429
+ function mount(options) {
2430
+ if (typeof document === "undefined") return reportConfigurationError(createEmergencyError("configuration", "mount() requires a browser environment."), options);
2431
+ const root = resolveRoot(options.root);
2432
+ if (!root) return reportConfigurationError(createEmergencyError("configuration", `The mount root "${typeof options.root === "string" ? options.root : "element"}" was not found.`), options);
2433
+ const validation = validateConfig(options);
2434
+ if (!validation.ok || !validation.config) return reportConfigurationError(validation.error ?? createEmergencyError("configuration", "Invalid emergency configuration."), options);
2435
+ const config = validation.config;
2436
+ const controller = createEmergencyController({ config });
2437
+ const container = document.createElement("div");
2438
+ container.setAttribute("data-de-emergency-container", "");
2439
+ root.appendChild(container);
2440
+ const reactRoot = createRoot(container);
2441
+ reactRoot.render(/* @__PURE__ */ jsx(EmergencyProvider, {
2442
+ value: {
2443
+ store: controller.store,
2444
+ client: controller.client,
2445
+ config
2446
+ },
2447
+ children: /* @__PURE__ */ jsx(EmergencyRoot, {})
2448
+ }));
2449
+ const detachTrigger = attachTrigger(options.trigger, controller.client);
2450
+ const unsubscribers = options.onEvent ? EMERGENCY_EVENT_NAMES.map((name) => controller.events.on(name, (payload) => {
2451
+ options.onEvent?.(name, payload);
2452
+ })) : [];
2453
+ let destroyed = false;
2454
+ controller.client.resume();
2455
+ const destroy = () => {
2456
+ if (destroyed) return;
2457
+ destroyed = true;
2458
+ detachTrigger();
2459
+ for (const unsubscribe of unsubscribers) unsubscribe();
2460
+ controller.client.destroy();
2461
+ container.remove();
2462
+ queueMicrotask(() => reactRoot.unmount());
2463
+ };
2464
+ const client = controller.client;
2465
+ return {
2466
+ open: (reason) => client.open(reason ?? "manual"),
2467
+ close: (reason) => client.close(reason ?? "manual"),
2468
+ getState: () => client.getState(),
2469
+ subscribe: (listener) => client.subscribe(listener),
2470
+ on: (event, handler) => client.on(event, handler),
2471
+ off: (event, handler) => client.off(event, handler),
2472
+ ensureSession: () => client.ensureSession(),
2473
+ resume: () => client.resume(),
2474
+ dismissResume: () => client.dismissResume(),
2475
+ activate: () => client.activate(),
2476
+ simulate: () => client.simulate(),
2477
+ refresh: () => client.refresh(),
2478
+ abort: () => client.abort(),
2479
+ requestAbortConfirmation: () => client.requestAbortConfirmation(),
2480
+ cancelAbortConfirmation: () => client.cancelAbortConfirmation(),
2481
+ requestSimulationConfirmation: () => client.requestSimulationConfirmation(),
2482
+ cancelSimulationConfirmation: () => client.cancelSimulationConfirmation(),
2483
+ dismissFeedback: () => client.dismissFeedback(),
2484
+ destroy
2485
+ };
2486
+ }
2487
+
2488
+ export { EMERGENCY_CODE_PATTERN, EMERGENCY_EVENT_NAMES, EMERGENCY_PHASE_NAMES, EMERGENCY_TRANSITIONS, EmergencyClientError, canTransition, createBffClient, createBrowserGeolocationProvider, createCookieCsrfStrategy, createEmergencyController, createEmergencyError, createMemoryCsrfStrategy, createPusherRealtimeProvider, isEmergencyError, isValidEmergencyCode, mount, normalizeBaseUrl, themeToCssVariables, toEmergencyError, validateConfig };
2489
+ //# sourceMappingURL=index.js.map