@dataemergencia/emergency 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2557 @@
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: "Ha ocurrido un 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. Habilita el acceso a la ubicación en tu navegador e inténtalo de nuevo.",
417
+ geolocationUnavailable: "Tu ubicación no está disponible en este momento. Verifica que el GPS esté activado e inténtalo de nuevo.",
418
+ geolocationTimeout: "La ubicación tardó demasiado. Muévete a un lugar con mejor señal e inténtalo de nuevo.",
419
+ geolocationUnsupported: "Este dispositivo no permite compartir la ubicación. Inténtalo desde otro navegador o dispositivo.",
420
+ resumeTitle: "Emergencia Activa Detectada",
421
+ resumeDescription: "Se ha detectado una emergencia activa en el sistema. Por favor, revisa el estado de la emergencia inmediatamente.",
422
+ resumeAction: "Abrir Panel de Emergencia",
423
+ partialFailure: "Algunos contactos no pudieron ser notificados. Revisa la lista.",
424
+ simulatedBanner: "Mensaje de prueba: no hay una emergencia real activa.",
425
+ activeBanner: "Emergencia activa. Tus contactos fueron notificados.",
426
+ emergencyActiveError: "Ya tienes una emergencia activa. Revisa el estado antes de continuar.",
427
+ respondersEmpty: "Aún no hay contactos notificados."
428
+ };
429
+
430
+ // src/core/events.ts
431
+ /**
432
+ * Typed, dependency-free event bus used by the controller and by `mount()`.
433
+ * DOM custom events are not used so the core stays framework agnostic.
434
+ */
435
+ const EMERGENCY_EVENT_NAMES = [
436
+ "session:ready",
437
+ "emergency:activated",
438
+ "emergency:simulated",
439
+ "emergency:resumed",
440
+ "emergency:updated",
441
+ "emergency:aborted",
442
+ "emergency:failed",
443
+ "emergency:fallback",
444
+ "panel:opened",
445
+ "panel:closed"
446
+ ];
447
+ var EmergencyEventEmitter = class {
448
+ handlers = /* @__PURE__ */ new Map();
449
+ on(event, handler) {
450
+ const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
451
+ set.add(handler);
452
+ this.handlers.set(event, set);
453
+ return () => {
454
+ this.off(event, handler);
455
+ };
456
+ }
457
+ off(event, handler) {
458
+ const set = this.handlers.get(event);
459
+ if (!set) return;
460
+ set.delete(handler);
461
+ if (set.size === 0) this.handlers.delete(event);
462
+ }
463
+ emit(event, payload) {
464
+ const set = this.handlers.get(event);
465
+ if (!set) return;
466
+ for (const handler of set) try {
467
+ handler(payload);
468
+ } catch {}
469
+ }
470
+ clear() {
471
+ this.handlers.clear();
472
+ }
473
+ };
474
+ function createEventEmitter() {
475
+ return new EmergencyEventEmitter();
476
+ }
477
+
478
+ // src/core/geolocation.ts
479
+ function mapFailure(error) {
480
+ const mapped = createEmergencyError(error.code === 1 ? "geolocation_denied" : error.code === 3 ? "geolocation_timeout" : "geolocation_unavailable", error.message ?? "The current position could not be determined.");
481
+ return Object.assign(new Error(mapped.message), { emergencyError: mapped });
482
+ }
483
+ function createBrowserGeolocationProvider() {
484
+ return { getCurrentPosition(timeoutMs) {
485
+ return new Promise((resolve, reject) => {
486
+ if (typeof navigator === "undefined" || typeof navigator.geolocation === "undefined" || typeof navigator.geolocation.getCurrentPosition !== "function") {
487
+ reject(Object.assign(/* @__PURE__ */ new Error("Geolocation is not supported in this environment."), { emergencyError: createEmergencyError("geolocation_unsupported", "Geolocation is not supported in this environment.") }));
488
+ return;
489
+ }
490
+ navigator.geolocation.getCurrentPosition((position) => {
491
+ resolve({
492
+ latitude: position.coords.latitude,
493
+ longitude: position.coords.longitude,
494
+ accuracy: position.coords.accuracy
495
+ });
496
+ }, (error) => {
497
+ reject(mapFailure(error));
498
+ }, {
499
+ enableHighAccuracy: true,
500
+ timeout: timeoutMs
501
+ });
502
+ });
503
+ } };
504
+ }
505
+
506
+ // src/core/mappers.ts
507
+ function responderStatus(payload) {
508
+ if (payload.failed) return "failed";
509
+ if (payload.read) return "read";
510
+ if (payload.delivered) return "delivered";
511
+ if (payload.sent) return "sent";
512
+ return "pending";
513
+ }
514
+ function toResponder(payload) {
515
+ const last = payload.last_messages?.at(-1) ?? null;
516
+ return {
517
+ phone: payload.phone,
518
+ name: payload.name,
519
+ status: responderStatus(payload),
520
+ sent: payload.sent,
521
+ delivered: payload.delivered,
522
+ read: payload.read,
523
+ failed: payload.failed,
524
+ messageId: payload.message_id,
525
+ sentAt: payload.sent_at ?? null,
526
+ deliveredAt: payload.delivered_at ?? null,
527
+ readAt: payload.read_at ?? null,
528
+ lastMessage: last ? {
529
+ content: last.content ?? "",
530
+ sentAt: last.sent_at ?? null
531
+ } : null
532
+ };
533
+ }
534
+ function toFailure(payload) {
535
+ return {
536
+ phone: payload.phone,
537
+ message: payload.message,
538
+ status: payload.status ?? null
539
+ };
540
+ }
541
+ function resultToSnapshot(result, code) {
542
+ return {
543
+ emergencyId: result.emergency_id ?? "",
544
+ code,
545
+ active: true,
546
+ simulated: result.simulated,
547
+ responders: result.responders.map(toResponder),
548
+ createdAt: null,
549
+ updatedAt: null,
550
+ raw: { ...result }
551
+ };
552
+ }
553
+ function readString(raw, keys) {
554
+ for (const key of keys) {
555
+ const value = raw[key];
556
+ if (typeof value === "string" && value !== "") return value;
557
+ }
558
+ return null;
559
+ }
560
+ function readBoolean(raw, keys) {
561
+ for (const key of keys) {
562
+ const value = raw[key];
563
+ if (typeof value === "boolean") return value;
564
+ }
565
+ return null;
566
+ }
567
+ /**
568
+ * Maps the upstream emergency payload (proxied by the BFF) into the snapshot the
569
+ * UI renders. Unknown fields survive in `raw`, so the client never loses data
570
+ * the BFF adds later.
571
+ */
572
+ function payloadToSnapshot(raw, options = {}) {
573
+ const respondersValue = raw["responders"];
574
+ const responders = Array.isArray(respondersValue) ? respondersValue.filter(isResponderPayloadLike).map(toResponder) : [];
575
+ return {
576
+ emergencyId: readString(raw, ["emergency_id", "id"]) ?? "",
577
+ code: options.code ?? readString(raw, ["initiator_id", "code"]),
578
+ active: readBoolean(raw, ["is_active", "active"]) ?? true,
579
+ simulated: options.simulated ?? readBoolean(raw, ["is_simulated", "simulated"]) ?? false,
580
+ responders,
581
+ createdAt: readString(raw, ["created_at", "createdAt"]),
582
+ updatedAt: readString(raw, ["updated_at", "updatedAt"]),
583
+ raw
584
+ };
585
+ }
586
+ function isResponderPayloadLike(value) {
587
+ if (!isRecord(value)) return false;
588
+ return typeof value["phone"] === "string" && typeof value["name"] === "string";
589
+ }
590
+ function themeToCssVariables(theme) {
591
+ if (!theme) return {};
592
+ const variables = {};
593
+ for (const [key, variable] of [
594
+ ["accent", "--de-emergency-red"],
595
+ ["accentContrast", "--de-emergency-on-red"],
596
+ ["surface", "--de-emergency-white"],
597
+ ["text", "--de-emergency-text"],
598
+ ["muted", "--de-emergency-text-secondary"],
599
+ ["radius", "--de-emergency-radius"]
600
+ ]) {
601
+ const value = theme[key];
602
+ if (value) variables[variable] = value;
603
+ }
604
+ return variables;
605
+ }
606
+
607
+ // src/core/realtime.ts
608
+ function channelForPhone(pattern, phone) {
609
+ return pattern.replace("{phone}", phone.replace(/^\+/, ""));
610
+ }
611
+ function toStatus(state) {
612
+ switch (state) {
613
+ case "connected": return "connected";
614
+ case "connecting":
615
+ case "initialized": return "connecting";
616
+ case "unavailable":
617
+ case "failed": return "unavailable";
618
+ case "disconnected": return "disconnected";
619
+ default: return "idle";
620
+ }
621
+ }
622
+ function createPusherRealtimeProvider() {
623
+ return { subscribe(config, phones, handlers) {
624
+ if (typeof window === "undefined" || !config.pusher.key || !config.pusher.cluster) return null;
625
+ let pusher;
626
+ try {
627
+ pusher = new Pusher(config.pusher.key, {
628
+ cluster: config.pusher.cluster,
629
+ forceTLS: true
630
+ });
631
+ } catch {
632
+ handlers.onStatus("unavailable");
633
+ return null;
634
+ }
635
+ handlers.onStatus("connecting");
636
+ pusher.connection.bind("state_change", (change) => {
637
+ handlers.onStatus(toStatus(change.current));
638
+ });
639
+ const channels = [...new Set(phones.filter((phone) => phone !== ""))].map((phone) => {
640
+ const channelName = channelForPhone(config.pusher.channel_pattern, phone);
641
+ pusher.subscribe(channelName).bind_global((eventName, data) => {
642
+ handlers.onEvent(channelName, eventName, data);
643
+ });
644
+ return channelName;
645
+ });
646
+ return { disconnect() {
647
+ for (const channelName of channels) pusher.unsubscribe(channelName);
648
+ pusher.disconnect();
649
+ } };
650
+ } };
651
+ }
652
+
653
+ // src/core/state-machine.ts
654
+ const EMERGENCY_PHASE_NAMES = [
655
+ "idle",
656
+ "initializing",
657
+ "ready",
658
+ "activating",
659
+ "simulating",
660
+ "active",
661
+ "refreshing",
662
+ "aborting",
663
+ "error"
664
+ ];
665
+ /**
666
+ * Every allowed transition. `idle` is reachable from every phase so `destroy()`
667
+ * and a hard reset always leave the store in a known state.
668
+ */
669
+ const EMERGENCY_TRANSITIONS = {
670
+ idle: ["initializing"],
671
+ initializing: [
672
+ "idle",
673
+ "ready",
674
+ "active",
675
+ "error"
676
+ ],
677
+ ready: [
678
+ "idle",
679
+ "initializing",
680
+ "activating",
681
+ "simulating",
682
+ "active",
683
+ "refreshing",
684
+ "error"
685
+ ],
686
+ activating: [
687
+ "idle",
688
+ "ready",
689
+ "active",
690
+ "error"
691
+ ],
692
+ simulating: [
693
+ "idle",
694
+ "ready",
695
+ "error"
696
+ ],
697
+ active: [
698
+ "idle",
699
+ "ready",
700
+ "refreshing",
701
+ "aborting",
702
+ "error"
703
+ ],
704
+ refreshing: [
705
+ "idle",
706
+ "active",
707
+ "error"
708
+ ],
709
+ aborting: [
710
+ "idle",
711
+ "ready",
712
+ "active",
713
+ "error"
714
+ ],
715
+ error: [
716
+ "idle",
717
+ "initializing",
718
+ "ready"
719
+ ]
720
+ };
721
+ function canTransition(from, to) {
722
+ return EMERGENCY_TRANSITIONS[from].includes(to);
723
+ }
724
+
725
+ // src/core/store.ts
726
+ function createEmergencyStore(config) {
727
+ return createStore(() => ({
728
+ config,
729
+ phase: { kind: "idle" },
730
+ panelOpen: false,
731
+ sessionReady: false,
732
+ resumeAvailable: false,
733
+ snapshot: null,
734
+ responders: [],
735
+ failures: [],
736
+ messages: [],
737
+ lastError: null,
738
+ geolocation: null,
739
+ geolocationError: null,
740
+ realtimeStatus: "idle",
741
+ abortResult: null,
742
+ simulationResult: null,
743
+ abortConfirmation: false,
744
+ simulationConfirmation: false,
745
+ destroyed: false
746
+ }));
747
+ }
748
+
749
+ // src/core/controller.ts
750
+ const RECONCILE_THROTTLE_MS = 1500;
751
+ function geolocationFailure(error) {
752
+ const attached = isRecord(error) ? error["emergencyError"] : void 0;
753
+ if (attached) return toEmergencyError(attached, "geolocation_unavailable");
754
+ return toEmergencyError(error, "geolocation_unavailable");
755
+ }
756
+ function createEmergencyController(deps) {
757
+ const config = deps.config;
758
+ const bff = deps.bff ?? createBffClient({
759
+ bffBaseUrl: config.bffBaseUrl,
760
+ timeoutMs: config.requestTimeoutMs
761
+ });
762
+ const geolocation = deps.geolocation ?? createBrowserGeolocationProvider();
763
+ const realtimeProvider = deps.realtime ?? createPusherRealtimeProvider();
764
+ const events = deps.events ?? createEventEmitter();
765
+ const store = createEmergencyStore(config);
766
+ let sessionPromise = null;
767
+ let refreshing = false;
768
+ let realtimeSubscription = null;
769
+ let realtimeConfig = null;
770
+ let subscribedPhones = [];
771
+ let messageSequence = 0;
772
+ let lastReconcileAt = 0;
773
+ let reconcileTimer = null;
774
+ const get = () => store.getState();
775
+ function patch(partial) {
776
+ store.setState(partial);
777
+ }
778
+ function transition(next) {
779
+ const current = get().phase;
780
+ if (current.kind === next.kind) {
781
+ patch({ phase: next });
782
+ return true;
783
+ }
784
+ if (!canTransition(current.kind, next.kind)) {
785
+ events.emit("emergency:failed", {
786
+ error: createEmergencyError("invalid_transition", `Cannot move from ${current.kind} to ${next.kind}.`),
787
+ operation: "mount"
788
+ });
789
+ return false;
790
+ }
791
+ patch({ phase: next });
792
+ return true;
793
+ }
794
+ function pushMessage(kind, text) {
795
+ messageSequence += 1;
796
+ const message = {
797
+ id: `${Date.now()}-${messageSequence}`,
798
+ kind,
799
+ text,
800
+ createdAt: Date.now()
801
+ };
802
+ patch({ messages: [...get().messages, message].slice(-20) });
803
+ }
804
+ function fail(error, operation, options = {}) {
805
+ patch({ lastError: error });
806
+ if (options.message !== null) pushMessage("error", options.message ?? error.message);
807
+ if (options.phase !== null) transition(options.phase ?? {
808
+ kind: "error",
809
+ error
810
+ });
811
+ events.emit("emergency:failed", {
812
+ error,
813
+ operation
814
+ });
815
+ }
816
+ function emitFallback(error) {
817
+ events.emit("emergency:fallback", {
818
+ error,
819
+ fallback: config.fallback ?? null
820
+ });
821
+ }
822
+ function stopRealtime() {
823
+ realtimeSubscription?.disconnect();
824
+ realtimeSubscription = null;
825
+ subscribedPhones = [];
826
+ }
827
+ function startRealtime(realtime, responders) {
828
+ if (!realtime) return;
829
+ realtimeConfig = realtime;
830
+ const phones = [...new Set(responders.map((responder) => responder.phone))];
831
+ if (realtimeSubscription && phones.length === subscribedPhones.length && phones.every((phone, index) => phone === subscribedPhones[index])) return;
832
+ realtimeSubscription?.disconnect();
833
+ subscribedPhones = phones;
834
+ if (phones.length === 0) {
835
+ patch({ realtimeStatus: "unavailable" });
836
+ return;
837
+ }
838
+ realtimeSubscription = realtimeProvider.subscribe(realtime, phones, {
839
+ onStatus(status) {
840
+ const previous = get().realtimeStatus;
841
+ patch({ realtimeStatus: status });
842
+ if (status === "connected" && previous !== "connected") scheduleReconcile();
843
+ },
844
+ onEvent() {
845
+ scheduleReconcile();
846
+ }
847
+ });
848
+ if (!realtimeSubscription) patch({ realtimeStatus: "unavailable" });
849
+ }
850
+ function scheduleReconcile() {
851
+ if (reconcileTimer !== null) return;
852
+ const wait = Math.max(0, RECONCILE_THROTTLE_MS - (Date.now() - lastReconcileAt));
853
+ reconcileTimer = setTimeout(() => {
854
+ reconcileTimer = null;
855
+ lastReconcileAt = Date.now();
856
+ refresh(true);
857
+ }, wait);
858
+ }
859
+ async function performSession() {
860
+ const phase = get().phase.kind;
861
+ if (phase === "idle" || phase === "error") transition({ kind: "initializing" });
862
+ try {
863
+ realtimeConfig = (await bff.config()).realtime;
864
+ const session = await bff.session(config.code);
865
+ patch({
866
+ sessionReady: true,
867
+ lastError: null
868
+ });
869
+ if (get().phase.kind === "initializing") transition({ kind: "ready" });
870
+ events.emit("session:ready", {
871
+ code: session.code,
872
+ expiresIn: session.expires_in
873
+ });
874
+ } catch (error) {
875
+ const normalized = toEmergencyError(error, "network");
876
+ patch({ sessionReady: false });
877
+ fail(normalized, "session", { phase: {
878
+ kind: "error",
879
+ error: normalized
880
+ } });
881
+ }
882
+ }
883
+ async function ensureSession() {
884
+ if (get().destroyed || get().sessionReady) return;
885
+ if (!sessionPromise) sessionPromise = performSession().finally(() => {
886
+ sessionPromise = null;
887
+ });
888
+ return sessionPromise;
889
+ }
890
+ async function hydrateCurrent() {
891
+ try {
892
+ const current = await bff.current();
893
+ realtimeConfig = current.realtime;
894
+ if (current.active && current.emergency) {
895
+ const snapshot = payloadToSnapshot(current.emergency, { code: config.code });
896
+ patch({
897
+ snapshot,
898
+ responders: snapshot.responders,
899
+ resumeAvailable: !get().panelOpen,
900
+ failures: [],
901
+ lastError: null,
902
+ geolocationError: null
903
+ });
904
+ transition({
905
+ kind: "active",
906
+ emergencyId: snapshot.emergencyId,
907
+ simulated: snapshot.simulated
908
+ });
909
+ startRealtime(current.realtime, snapshot.responders);
910
+ events.emit("emergency:resumed", { snapshot });
911
+ return;
912
+ }
913
+ patch({
914
+ resumeAvailable: false,
915
+ snapshot: null,
916
+ responders: []
917
+ });
918
+ if (get().phase.kind !== "ready") transition({ kind: "ready" });
919
+ } catch (error) {
920
+ const normalized = toEmergencyError(error, "network");
921
+ patch({ resumeAvailable: false });
922
+ if (normalized.code === "unauthenticated") return;
923
+ fail(normalized, "resume", { phase: get().phase.kind === "active" ? null : {
924
+ kind: "error",
925
+ error: normalized
926
+ } });
927
+ }
928
+ }
929
+ async function resume() {
930
+ if (get().destroyed) return;
931
+ await ensureSession();
932
+ if (!get().sessionReady) return;
933
+ await hydrateCurrent();
934
+ }
935
+ async function activate() {
936
+ if (get().destroyed) return;
937
+ const phase = get().phase.kind;
938
+ if (phase === "activating" || phase === "active") {
939
+ open("activate");
940
+ return;
941
+ }
942
+ open("activate");
943
+ await ensureSession();
944
+ if (!get().sessionReady || !transition({ kind: "activating" })) return;
945
+ patch({
946
+ lastError: null,
947
+ geolocationError: null,
948
+ abortResult: null
949
+ });
950
+ let position;
951
+ try {
952
+ position = await geolocation.getCurrentPosition(config.geolocationTimeoutMs ?? 8e3);
953
+ patch({ geolocation: position });
954
+ } catch (error) {
955
+ const normalized = geolocationFailure(error);
956
+ patch({
957
+ geolocation: null,
958
+ geolocationError: normalized
959
+ });
960
+ transition({ kind: "ready" });
961
+ events.emit("emergency:failed", {
962
+ error: normalized,
963
+ operation: "geolocation"
964
+ });
965
+ emitFallback(normalized);
966
+ return;
967
+ }
968
+ try {
969
+ const result = await bff.activate({ geolocation: position });
970
+ const snapshot = resultToSnapshot(result, config.code);
971
+ const responders = result.responders.map(toResponder);
972
+ const failures = result.failures.map(toFailure);
973
+ const hydrated = {
974
+ ...snapshot,
975
+ responders
976
+ };
977
+ patch({
978
+ snapshot: hydrated,
979
+ responders,
980
+ failures,
981
+ resumeAvailable: false,
982
+ lastError: null
983
+ });
984
+ transition({
985
+ kind: "active",
986
+ emergencyId: hydrated.emergencyId,
987
+ simulated: result.simulated
988
+ });
989
+ pushMessage(failures.length > 0 ? "warning" : "success", failures.length > 0 ? copy.partialFailure : copy.activeBanner);
990
+ events.emit("emergency:activated", { snapshot: hydrated });
991
+ startRealtime(realtimeConfig, responders);
992
+ } catch (error) {
993
+ const normalized = toEmergencyError(error, "network");
994
+ if (normalized.code === "emergency_already_active") {
995
+ pushMessage("info", copy.emergencyActiveError);
996
+ await hydrateCurrent();
997
+ return;
998
+ }
999
+ fail(normalized, "activate", { phase: { kind: "ready" } });
1000
+ emitFallback(normalized);
1001
+ }
1002
+ }
1003
+ async function simulate() {
1004
+ if (get().destroyed) return;
1005
+ const snapshot = get().snapshot;
1006
+ if (snapshot?.active && !snapshot.simulated) {
1007
+ pushMessage("warning", copy.emergencyActiveError);
1008
+ return;
1009
+ }
1010
+ if ([
1011
+ "simulating",
1012
+ "activating",
1013
+ "aborting"
1014
+ ].includes(get().phase.kind)) return;
1015
+ await ensureSession();
1016
+ if (!get().sessionReady || !transition({ kind: "simulating" })) return;
1017
+ patch({
1018
+ lastError: null,
1019
+ simulationResult: null
1020
+ });
1021
+ try {
1022
+ const result = await bff.simulacrum();
1023
+ const responders = result.responders.map(toResponder);
1024
+ const failures = result.failures.map(toFailure);
1025
+ const simulationResult = {
1026
+ ok: true,
1027
+ message: failures.length > 0 ? copy.partialFailure : copy.simulateResultOk,
1028
+ at: Date.now()
1029
+ };
1030
+ patch({
1031
+ responders,
1032
+ failures,
1033
+ simulationResult,
1034
+ simulationConfirmation: false
1035
+ });
1036
+ transition({ kind: "ready" });
1037
+ pushMessage(failures.length > 0 ? "warning" : "success", simulationResult.message);
1038
+ events.emit("emergency:simulated", {
1039
+ responders,
1040
+ failures
1041
+ });
1042
+ } catch (error) {
1043
+ const normalized = toEmergencyError(error, "network");
1044
+ patch({ simulationResult: {
1045
+ ok: false,
1046
+ message: copy.simulateResultFail,
1047
+ at: Date.now()
1048
+ } });
1049
+ transition({ kind: "ready" });
1050
+ fail(normalized, "simulate", { phase: null });
1051
+ }
1052
+ }
1053
+ async function refresh(silent = false) {
1054
+ const snapshot = get().snapshot;
1055
+ if (get().destroyed || !snapshot || snapshot.simulated || snapshot.emergencyId === "") return;
1056
+ if (refreshing) return;
1057
+ refreshing = true;
1058
+ if (!silent) transition({
1059
+ kind: "refreshing",
1060
+ emergencyId: snapshot.emergencyId
1061
+ });
1062
+ try {
1063
+ const result = await bff.refresh(snapshot.emergencyId);
1064
+ const next = payloadToSnapshot(result.emergency, {
1065
+ code: config.code,
1066
+ simulated: snapshot.simulated
1067
+ });
1068
+ const responders = result.responders.length > 0 ? result.responders.map(toResponder) : next.responders.length > 0 ? next.responders : get().responders;
1069
+ const updated = {
1070
+ ...next,
1071
+ responders
1072
+ };
1073
+ patch({
1074
+ snapshot: updated,
1075
+ responders,
1076
+ lastError: null
1077
+ });
1078
+ if (get().phase.kind === "refreshing") transition({
1079
+ kind: "active",
1080
+ emergencyId: updated.emergencyId,
1081
+ simulated: updated.simulated
1082
+ });
1083
+ startRealtime(realtimeConfig, responders);
1084
+ events.emit("emergency:updated", { snapshot: updated });
1085
+ } catch (error) {
1086
+ const normalized = toEmergencyError(error, "network");
1087
+ if (get().phase.kind === "refreshing") transition({
1088
+ kind: "active",
1089
+ emergencyId: snapshot.emergencyId,
1090
+ simulated: snapshot.simulated
1091
+ });
1092
+ fail(normalized, "refresh", {
1093
+ phase: null,
1094
+ message: silent ? null : void 0
1095
+ });
1096
+ } finally {
1097
+ refreshing = false;
1098
+ }
1099
+ }
1100
+ async function abort() {
1101
+ if (get().destroyed) return;
1102
+ const snapshot = get().snapshot;
1103
+ if (get().phase.kind !== "active" || !snapshot) return;
1104
+ if (!get().abortConfirmation) {
1105
+ patch({ abortConfirmation: true });
1106
+ return;
1107
+ }
1108
+ if (snapshot.simulated || snapshot.emergencyId === "") {
1109
+ patch({
1110
+ snapshot: null,
1111
+ responders: [],
1112
+ resumeAvailable: false,
1113
+ abortConfirmation: false
1114
+ });
1115
+ transition({ kind: "ready" });
1116
+ pushMessage("info", "No hay una emergencia real activa.");
1117
+ return;
1118
+ }
1119
+ if (!transition({
1120
+ kind: "aborting",
1121
+ emergencyId: snapshot.emergencyId
1122
+ })) return;
1123
+ patch({
1124
+ lastError: null,
1125
+ abortResult: null
1126
+ });
1127
+ try {
1128
+ const result = await bff.abort(snapshot.emergencyId);
1129
+ const responders = result.responders.map(toResponder);
1130
+ const abortResult = {
1131
+ ok: true,
1132
+ message: copy.abortResultOk,
1133
+ at: Date.now()
1134
+ };
1135
+ patch({
1136
+ abortResult,
1137
+ responders,
1138
+ failures: result.failures.map(toFailure),
1139
+ abortConfirmation: false,
1140
+ snapshot: null,
1141
+ resumeAvailable: false
1142
+ });
1143
+ stopRealtime();
1144
+ transition({ kind: "ready" });
1145
+ pushMessage("success", abortResult.message);
1146
+ events.emit("emergency:aborted", {
1147
+ emergencyId: snapshot.emergencyId,
1148
+ responders
1149
+ });
1150
+ } catch (error) {
1151
+ const normalized = toEmergencyError(error, "network");
1152
+ patch({
1153
+ abortResult: {
1154
+ ok: false,
1155
+ message: copy.abortResultFail,
1156
+ at: Date.now()
1157
+ },
1158
+ abortConfirmation: false
1159
+ });
1160
+ transition({
1161
+ kind: "active",
1162
+ emergencyId: snapshot.emergencyId,
1163
+ simulated: snapshot.simulated
1164
+ });
1165
+ fail(normalized, "abort", { phase: null });
1166
+ }
1167
+ }
1168
+ function open(reason = "manual") {
1169
+ if (get().destroyed) return;
1170
+ patch({
1171
+ panelOpen: true,
1172
+ resumeAvailable: false
1173
+ });
1174
+ events.emit("panel:opened", { reason });
1175
+ }
1176
+ function close(reason = "close") {
1177
+ if (!get().panelOpen) return;
1178
+ patch({
1179
+ panelOpen: false,
1180
+ abortConfirmation: false,
1181
+ simulationConfirmation: false
1182
+ });
1183
+ events.emit("panel:closed", { reason });
1184
+ }
1185
+ function reconcileNow() {
1186
+ const snapshot = get().snapshot;
1187
+ if (!snapshot || snapshot.simulated || snapshot.emergencyId === "") return;
1188
+ lastReconcileAt = Date.now();
1189
+ refresh(true);
1190
+ }
1191
+ function onVisibilityChange() {
1192
+ if (typeof document !== "undefined" && document.visibilityState === "visible") scheduleReconcile();
1193
+ }
1194
+ function registerReconcileListeners() {
1195
+ if (typeof window === "undefined") return () => void 0;
1196
+ window.addEventListener("focus", scheduleReconcile);
1197
+ window.addEventListener("online", reconcileNow);
1198
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
1199
+ return () => {
1200
+ window.removeEventListener("focus", scheduleReconcile);
1201
+ window.removeEventListener("online", reconcileNow);
1202
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
1203
+ };
1204
+ }
1205
+ const removeReconcileListeners = registerReconcileListeners();
1206
+ function destroy() {
1207
+ if (get().destroyed) return;
1208
+ removeReconcileListeners();
1209
+ if (reconcileTimer !== null) {
1210
+ clearTimeout(reconcileTimer);
1211
+ reconcileTimer = null;
1212
+ }
1213
+ bff.destroy();
1214
+ realtimeSubscription?.disconnect();
1215
+ realtimeSubscription = null;
1216
+ realtimeConfig = null;
1217
+ subscribedPhones = [];
1218
+ patch({
1219
+ destroyed: true,
1220
+ phase: { kind: "idle" },
1221
+ panelOpen: false,
1222
+ sessionReady: false,
1223
+ resumeAvailable: false,
1224
+ snapshot: null,
1225
+ responders: [],
1226
+ failures: [],
1227
+ messages: [],
1228
+ lastError: null,
1229
+ geolocation: null,
1230
+ geolocationError: null,
1231
+ realtimeStatus: "idle",
1232
+ abortResult: null,
1233
+ simulationResult: null,
1234
+ abortConfirmation: false,
1235
+ simulationConfirmation: false
1236
+ });
1237
+ events.emit("panel:closed", { reason: "destroy" });
1238
+ events.clear();
1239
+ }
1240
+ return {
1241
+ client: {
1242
+ open,
1243
+ close,
1244
+ destroy,
1245
+ getState: get,
1246
+ subscribe: (listener) => store.subscribe(listener),
1247
+ on: (event, handler) => events.on(event, handler),
1248
+ off: (event, handler) => events.off(event, handler),
1249
+ ensureSession,
1250
+ resume,
1251
+ dismissResume: () => patch({ resumeAvailable: false }),
1252
+ activate,
1253
+ simulate,
1254
+ refresh: () => refresh(false),
1255
+ abort,
1256
+ requestAbortConfirmation: () => {
1257
+ if (get().phase.kind === "active") patch({ abortConfirmation: true });
1258
+ },
1259
+ cancelAbortConfirmation: () => patch({ abortConfirmation: false }),
1260
+ requestSimulationConfirmation: () => {
1261
+ if (!get().snapshot?.active) patch({ simulationConfirmation: true });
1262
+ },
1263
+ cancelSimulationConfirmation: () => patch({ simulationConfirmation: false }),
1264
+ dismissFeedback: () => {
1265
+ const state = get();
1266
+ patch({
1267
+ abortResult: null,
1268
+ simulationResult: null,
1269
+ geolocationError: null,
1270
+ ...state.geolocationError !== null || state.abortResult?.ok === false || state.simulationResult?.ok === false || state.phase.kind === "ready" && state.lastError !== null ? { lastError: null } : {}
1271
+ });
1272
+ }
1273
+ },
1274
+ store,
1275
+ events
1276
+ };
1277
+ }
1278
+
1279
+ // src/core/fonts.ts
1280
+ /**
1281
+ * The exact Google Fonts query segment for each family. Family order matters:
1282
+ * with every family enabled the generated href is the documented stylesheet
1283
+ * URL, byte for byte.
1284
+ */
1285
+ const FONT_FAMILY_PARAMS = {
1286
+ googleSans: "family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700",
1287
+ montserrat: "family=Montserrat:ital,wght@0,100..900;1,100..900",
1288
+ poppins: "family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900"
1289
+ };
1290
+ const FONT_FAMILY_ORDER = [
1291
+ "googleSans",
1292
+ "montserrat",
1293
+ "poppins"
1294
+ ];
1295
+ const FONT_STYLESHEET_BASE = "https://fonts.googleapis.com/css2";
1296
+ const FONT_ORIGIN = "https://fonts.googleapis.com";
1297
+ const FONT_STATIC_ORIGIN = "https://fonts.gstatic.com";
1298
+ /** Marks every link the client injects so repeated mounts stay idempotent. */
1299
+ const FONT_LINK_ATTRIBUTE = "data-de-emergency-fonts";
1300
+ function normalizeFontLoading(input) {
1301
+ const flags = input?.fonts ?? {};
1302
+ return {
1303
+ load: input?.load ?? true,
1304
+ fonts: {
1305
+ googleSans: flags.googleSans ?? true,
1306
+ montserrat: flags.montserrat ?? true,
1307
+ poppins: flags.poppins ?? true
1308
+ }
1309
+ };
1310
+ }
1311
+ /**
1312
+ * Builds the stylesheet URL for the enabled families, or `null` when the
1313
+ * master switch is off or no family is enabled. A disabled family is left out
1314
+ * of the request entirely.
1315
+ */
1316
+ function buildFontStylesheetHref(input) {
1317
+ const { load, fonts } = normalizeFontLoading(input);
1318
+ if (!load) return null;
1319
+ const params = FONT_FAMILY_ORDER.filter((family) => fonts[family]).map((family) => FONT_FAMILY_PARAMS[family]);
1320
+ if (params.length === 0) return null;
1321
+ return `${FONT_STYLESHEET_BASE}?${params.join("&")}&display=swap`;
1322
+ }
1323
+ function createFontLink(rel, href, crossorigin = false) {
1324
+ const link = document.createElement("link");
1325
+ link.rel = rel;
1326
+ link.href = href;
1327
+ if (crossorigin) link.crossOrigin = "anonymous";
1328
+ link.setAttribute(FONT_LINK_ATTRIBUTE, "");
1329
+ return link;
1330
+ }
1331
+ /**
1332
+ * Injects the preconnect and stylesheet links the panel needs, once per page.
1333
+ * The first successful mount wins: later mounts, including mounts from a
1334
+ * duplicated copy of the library, reuse the links already present. The links
1335
+ * are page-level assets and are intentionally not removed on `destroy()`.
1336
+ */
1337
+ function installEmergencyFonts(input) {
1338
+ if (typeof document === "undefined") return;
1339
+ const href = buildFontStylesheetHref(input);
1340
+ if (href === null) return;
1341
+ if (document.querySelector(`link[${"data-de-emergency-fonts"}]`)) return;
1342
+ (document.head ?? document.documentElement).append(createFontLink("preconnect", FONT_ORIGIN), createFontLink("preconnect", FONT_STATIC_ORIGIN, true), createFontLink("stylesheet", href));
1343
+ }
1344
+
1345
+ // src/core/config.ts
1346
+ const EMERGENCY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9]{5}$/;
1347
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
1348
+ "localhost",
1349
+ "127.0.0.1",
1350
+ "[::1]"
1351
+ ]);
1352
+ function isValidEmergencyCode(code) {
1353
+ return EMERGENCY_CODE_PATTERN.test(code);
1354
+ }
1355
+ function isValidFallback(value) {
1356
+ if (!isRecord(value)) return false;
1357
+ return typeof value["phone"] === "string" && value["phone"].trim() !== "" && typeof value["label"] === "string" && value["label"].trim() !== "";
1358
+ }
1359
+ function normalizeBaseUrl(baseUrl) {
1360
+ return baseUrl.trim().replace(/\/+$/, "");
1361
+ }
1362
+ /**
1363
+ * Requests are sent with credentials and the CSRF token, so a plaintext or
1364
+ * attacker-controlled base URL would leak both. Same-origin paths (`/prefix`)
1365
+ * and HTTPS are accepted; plain HTTP is only accepted for loopback hosts.
1366
+ */
1367
+ function validateBaseUrl(baseUrl) {
1368
+ if (baseUrl.startsWith("/")) {
1369
+ if (baseUrl.startsWith("//")) return "The BFF base URL must not be protocol-relative.";
1370
+ return null;
1371
+ }
1372
+ let parsed;
1373
+ try {
1374
+ parsed = new URL(baseUrl);
1375
+ } catch {
1376
+ return "The BFF base URL must be an absolute URL or a same-origin path.";
1377
+ }
1378
+ if (parsed.username !== "" || parsed.password !== "") return "The BFF base URL must not embed credentials.";
1379
+ if (parsed.protocol === "https:") return null;
1380
+ if (parsed.protocol === "http:" && (LOOPBACK_HOSTS.has(parsed.hostname) || parsed.hostname.endsWith(".localhost"))) return null;
1381
+ return "The BFF base URL must use https: (http: is only allowed for localhost).";
1382
+ }
1383
+ function validateConfig(config) {
1384
+ const bffBaseUrl = typeof config.bffBaseUrl === "string" ? normalizeBaseUrl(config.bffBaseUrl) : "";
1385
+ const code = typeof config.code === "string" ? config.code.trim() : "";
1386
+ if (bffBaseUrl === "") return {
1387
+ ok: false,
1388
+ config: null,
1389
+ error: createEmergencyError("configuration", "A BFF base URL is required to mount the emergency client.")
1390
+ };
1391
+ const baseUrlError = validateBaseUrl(bffBaseUrl);
1392
+ if (baseUrlError !== null) return {
1393
+ ok: false,
1394
+ config: null,
1395
+ error: createEmergencyError("configuration", baseUrlError)
1396
+ };
1397
+ if (!isValidEmergencyCode(code)) return {
1398
+ ok: false,
1399
+ config: null,
1400
+ error: createEmergencyError("configuration", "The identity code must be 6 characters and start with a letter.", { details: { code } })
1401
+ };
1402
+ if (config.fallback !== void 0 && !isValidFallback(config.fallback)) return {
1403
+ ok: false,
1404
+ config: null,
1405
+ error: createEmergencyError("configuration", "The fallback requires a non-empty phone and label.")
1406
+ };
1407
+ return {
1408
+ ok: true,
1409
+ error: null,
1410
+ config: {
1411
+ ...config,
1412
+ bffBaseUrl,
1413
+ code,
1414
+ locale: config.locale ?? "es",
1415
+ geolocationTimeoutMs: config.geolocationTimeoutMs ?? 8e3,
1416
+ requestTimeoutMs: config.requestTimeoutMs ?? 15e3,
1417
+ fontLoading: normalizeFontLoading(config.fontLoading)
1418
+ }
1419
+ };
1420
+ }
1421
+
1422
+ // src/react/components/icons/paramedical-mark.tsx
1423
+ /**
1424
+ * Neutral inline illustration used by the idle panel.
1425
+ */
1426
+ function ParamedicalMark({ className, title }) {
1427
+ return /* @__PURE__ */ jsxs("svg", {
1428
+ xmlns: "http://www.w3.org/2000/svg",
1429
+ version: "1.0",
1430
+ viewBox: "0 0 192 192",
1431
+ width: "192",
1432
+ height: "192",
1433
+ id: "Star of life",
1434
+ className,
1435
+ "aria-label": title,
1436
+ children: [
1437
+ /* @__PURE__ */ jsx("defs", { id: "defs2886" }),
1438
+ /* @__PURE__ */ jsxs("g", {
1439
+ id: "cross",
1440
+ children: [/* @__PURE__ */ jsx("path", {
1441
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1442
+ id: "outline",
1443
+ style: {
1444
+ fill: "none",
1445
+ stroke: "#3333bb",
1446
+ strokeWidth: 6
1447
+ }
1448
+ }), /* @__PURE__ */ jsx("path", {
1449
+ d: "m 74,133 0,52 44,0 0,-52 44,26 22,-38 L 140,96 184,70 162,32 117,58 117,7 74,7 74,58 30,32 8,70 52,96 8,121 30,159 z",
1450
+ id: "inner",
1451
+ style: {
1452
+ fill: "#0000bb",
1453
+ fillRule: "evenodd",
1454
+ stroke: "#ffffff",
1455
+ strokeWidth: 2
1456
+ }
1457
+ })]
1458
+ }),
1459
+ /* @__PURE__ */ jsxs("g", {
1460
+ id: "symbols",
1461
+ style: {
1462
+ fill: "#ffffff",
1463
+ fillRule: "evenodd",
1464
+ stroke: "#3333bb",
1465
+ strokeWidth: 2
1466
+ },
1467
+ children: [/* @__PURE__ */ jsx("path", {
1468
+ d: "m 93,166 c 0,7 6,7 6,0 l 3,-138 c 0,-8 -12,-8 -12,0 l 3,135 0,3 z",
1469
+ id: "rod"
1470
+ }), /* @__PURE__ */ jsxs("g", {
1471
+ id: "snake",
1472
+ children: [/* @__PURE__ */ jsx("path", {
1473
+ d: "m 100,153 c 7,4 3,15 -3,15 2,-5 3,-9 3,-15 z m 1,-40 c 10,9 4,22 -3,26 -6,3 -6,7 -6,15 -9,-10 -4,-19 4,-24 5,-4 5,-10 5,-17 z m 1,-30 c 0,11 -17,14 -17,22 0,3 1,9 6,12 0,-11 9,-12 15,-18 7,-7 7,-19 -4,-28 0,4 0,8 0,12 z M 90,77 C 80,69 76,55 80,46 c 3,-10 20,-11 20,-3 0,8 -20,3 -13,16 l 3,5 0,13 z",
1474
+ id: "body"
1475
+ }), /* @__PURE__ */ jsx("path", {
1476
+ d: "m 95,43 c 7,-3 -7,-3 0,0 z",
1477
+ id: "eye",
1478
+ style: {
1479
+ fill: "#3333bb",
1480
+ fillRule: "evenodd",
1481
+ stroke: "none"
1482
+ }
1483
+ })]
1484
+ })]
1485
+ })
1486
+ ]
1487
+ });
1488
+ }
1489
+
1490
+ // src/react/components/Icon/index.tsx
1491
+ /** Icons whose artwork does not fit the default 24x24 grid need their own canvas. */
1492
+ const viewBoxes = { emergency: "0 0 640 640" };
1493
+ /** Icons rendered as a solid shape instead of a stroked outline. */
1494
+ const filledIcons = /* @__PURE__ */ new Set(["emergency"]);
1495
+ const paths = {
1496
+ close: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }),
1497
+ "alert-triangle": /* @__PURE__ */ jsxs(Fragment, { children: [
1498
+ /* @__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" }),
1499
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1500
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1501
+ ] }),
1502
+ "check-circle": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1503
+ cx: "12",
1504
+ cy: "12",
1505
+ r: "9"
1506
+ }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }),
1507
+ "x-circle": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1508
+ cx: "12",
1509
+ cy: "12",
1510
+ r: "9"
1511
+ }), /* @__PURE__ */ jsx("path", { d: "m9 9 6 6M15 9l-6 6" })] }),
1512
+ "exclamation-circle": /* @__PURE__ */ jsxs(Fragment, { children: [
1513
+ /* @__PURE__ */ jsx("circle", {
1514
+ cx: "12",
1515
+ cy: "12",
1516
+ r: "9"
1517
+ }),
1518
+ /* @__PURE__ */ jsx("path", { d: "M12 7v6" }),
1519
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1520
+ ] }),
1521
+ flag: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M5 3v18" }), /* @__PURE__ */ jsx("path", { d: "M5 4h12l-2.5 4L17 12H5" })] }),
1522
+ "double-check": /* @__PURE__ */ jsx("path", { d: "m2 13 4 4 8-9M11 16l1 1 9-10" }),
1523
+ 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" })] }),
1524
+ "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" })] }),
1525
+ 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", {
1526
+ cx: "12",
1527
+ cy: "12",
1528
+ r: "2.5"
1529
+ })] }),
1530
+ 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" })] }),
1531
+ clock: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
1532
+ cx: "12",
1533
+ cy: "12",
1534
+ r: "9"
1535
+ }), /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })] }),
1536
+ info: /* @__PURE__ */ jsxs(Fragment, { children: [
1537
+ /* @__PURE__ */ jsx("circle", {
1538
+ cx: "12",
1539
+ cy: "12",
1540
+ r: "9"
1541
+ }),
1542
+ /* @__PURE__ */ jsx("path", { d: "M12 11v6" }),
1543
+ /* @__PURE__ */ jsx("path", { d: "M12 7h.01" })
1544
+ ] }),
1545
+ "arrow-left": /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M19 12H5" }), /* @__PURE__ */ jsx("path", { d: "m12 19-7-7 7-7" })] }),
1546
+ emergency: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("path", { d: "M272.5 96C272.5 78.3 286.8 64 304.5 64L336.5 64C354.2 64 368.5 78.3 368.5 96L368.5 236.9L490.5 166.5C505.8 157.7 525.4 162.9 534.2 178.2L550.2 205.9C559 221.2 553.8 240.8 538.5 249.6L416.5 320L538.5 390.4C553.8 399.2 559.1 418.8 550.2 434.1L534.2 461.8C525.4 477.1 505.8 482.4 490.5 473.5L368.5 403.1L368.5 544C368.5 561.7 354.2 576 336.5 576L304.5 576C286.8 576 272.5 561.7 272.5 544L272.5 403.1L150.5 473.5C135.2 482.3 115.6 477.1 106.8 461.8L90.8 434.1C82 418.8 87.2 399.2 102.5 390.4L224.5 320L102.5 249.6C87.2 240.8 82 221.2 90.8 205.9L106.8 178.2C115.6 162.9 135.2 157.7 150.5 166.5L272.5 236.9L272.5 96z" }) })
1547
+ };
1548
+ function Icon({ name, className, title }) {
1549
+ const stroked = !filledIcons.has(name);
1550
+ return /* @__PURE__ */ jsxs("svg", {
1551
+ className,
1552
+ viewBox: viewBoxes[name] ?? "0 0 24 24",
1553
+ width: "1rem",
1554
+ height: "1rem",
1555
+ fill: stroked ? "none" : "currentColor",
1556
+ stroke: stroked ? "currentColor" : "none",
1557
+ strokeWidth: stroked ? 2 : void 0,
1558
+ strokeLinecap: "round",
1559
+ strokeLinejoin: "round",
1560
+ "aria-hidden": title ? void 0 : "true",
1561
+ role: title ? "img" : void 0,
1562
+ focusable: "false",
1563
+ children: [title ? /* @__PURE__ */ jsx("title", { children: title }) : null, paths[name]]
1564
+ });
1565
+ }
1566
+
1567
+ // src/styles/panel.module.css
1568
+ var panel_module_default = {
1569
+ "de-emergency-fade-in": "de-emergency-de-emergency-fade-in-kMlO0G",
1570
+ "de-emergency-pulse": "de-emergency-de-emergency-pulse-kMlO0G",
1571
+ "de-emergency-pulse-error": "de-emergency-de-emergency-pulse-error-kMlO0G",
1572
+ "de-emergency-slide-in": "de-emergency-de-emergency-slide-in-kMlO0G",
1573
+ "de-emergency-slide-up": "de-emergency-de-emergency-slide-up-kMlO0G",
1574
+ "de-emergency-spin": "de-emergency-de-emergency-spin-kMlO0G",
1575
+ "emergency-confirmation": "de-emergency-emergency-confirmation-kMlO0G",
1576
+ "emergency-confirmation__body": "de-emergency-emergency-confirmation__body-kMlO0G",
1577
+ "emergency-confirmation__button": "de-emergency-emergency-confirmation__button-kMlO0G",
1578
+ "emergency-confirmation__button--cancel": "de-emergency-emergency-confirmation__button--cancel-kMlO0G",
1579
+ "emergency-confirmation__button--confirm": "de-emergency-emergency-confirmation__button--confirm-kMlO0G",
1580
+ "emergency-confirmation__button--danger": "de-emergency-emergency-confirmation__button--danger-kMlO0G",
1581
+ "emergency-confirmation__card": "de-emergency-emergency-confirmation__card-kMlO0G",
1582
+ "emergency-confirmation__footer": "de-emergency-emergency-confirmation__footer-kMlO0G",
1583
+ "emergency-confirmation__icon": "de-emergency-emergency-confirmation__icon-kMlO0G",
1584
+ "emergency-confirmation__icon--danger": "de-emergency-emergency-confirmation__icon--danger-kMlO0G",
1585
+ "emergency-confirmation__icon--information": "de-emergency-emergency-confirmation__icon--information-kMlO0G",
1586
+ "emergency-confirmation__text": "de-emergency-emergency-confirmation__text-kMlO0G",
1587
+ "emergency-confirmation__title": "de-emergency-emergency-confirmation__title-kMlO0G",
1588
+ "emergency-error-message": "de-emergency-emergency-error-message-kMlO0G",
1589
+ "emergency-error-message__container": "de-emergency-emergency-error-message__container-kMlO0G",
1590
+ "emergency-error-message__content": "de-emergency-emergency-error-message__content-kMlO0G",
1591
+ "emergency-error-message__detail": "de-emergency-emergency-error-message__detail-kMlO0G",
1592
+ "emergency-error-message__details": "de-emergency-emergency-error-message__details-kMlO0G",
1593
+ "emergency-error-message__icon": "de-emergency-emergency-error-message__icon-kMlO0G",
1594
+ "emergency-error-message__text": "de-emergency-emergency-error-message__text-kMlO0G",
1595
+ "emergency-error-message__title": "de-emergency-emergency-error-message__title-kMlO0G",
1596
+ "emergency-feedback": "de-emergency-emergency-feedback-kMlO0G",
1597
+ "emergency-feedback__dismiss": "de-emergency-emergency-feedback__dismiss-kMlO0G",
1598
+ "emergency-panel": "de-emergency-emergency-panel-kMlO0G",
1599
+ "emergency-panel__body": "de-emergency-emergency-panel__body-kMlO0G",
1600
+ "emergency-panel__body-title": "de-emergency-emergency-panel__body-title-kMlO0G",
1601
+ "emergency-panel__body-title-container": "de-emergency-emergency-panel__body-title-container-kMlO0G",
1602
+ "emergency-panel__button": "de-emergency-emergency-panel__button-kMlO0G",
1603
+ "emergency-panel__button--emergency": "de-emergency-emergency-panel__button--emergency-kMlO0G",
1604
+ "emergency-panel__button--simulation": "de-emergency-emergency-panel__button--simulation-kMlO0G",
1605
+ "emergency-panel__close-button": "de-emergency-emergency-panel__close-button-kMlO0G",
1606
+ "emergency-panel__close-icon": "de-emergency-emergency-panel__close-icon-kMlO0G",
1607
+ "emergency-panel__footer": "de-emergency-emergency-panel__footer-kMlO0G",
1608
+ "emergency-panel__header": "de-emergency-emergency-panel__header-kMlO0G",
1609
+ "emergency-panel__title": "de-emergency-emergency-panel__title-kMlO0G",
1610
+ "emergency-panel--hidden": "de-emergency-emergency-panel--hidden-kMlO0G",
1611
+ "emergency-resume-notification-button": "de-emergency-emergency-resume-notification-button-kMlO0G",
1612
+ "emergency-resume-notification-card": "de-emergency-emergency-resume-notification-card-kMlO0G",
1613
+ "emergency-resume-notification-icon": "de-emergency-emergency-resume-notification-icon-kMlO0G",
1614
+ "emergency-resume-notification-message": "de-emergency-emergency-resume-notification-message-kMlO0G",
1615
+ "emergency-resume-notification-overlay": "de-emergency-emergency-resume-notification-overlay-kMlO0G",
1616
+ "emergency-resume-notification-title": "de-emergency-emergency-resume-notification-title-kMlO0G",
1617
+ "emergency-status__abort-btn": "de-emergency-emergency-status__abort-btn-kMlO0G",
1618
+ "emergency-status__body": "de-emergency-emergency-status__body-kMlO0G",
1619
+ "emergency-status__cell": "de-emergency-emergency-status__cell-kMlO0G",
1620
+ "emergency-status__cell-icon": "de-emergency-emergency-status__cell-icon-kMlO0G",
1621
+ "emergency-status__cell-text": "de-emergency-emergency-status__cell-text-kMlO0G",
1622
+ "emergency-status__cell-time": "de-emergency-emergency-status__cell-time-kMlO0G",
1623
+ "emergency-status__grid": "de-emergency-emergency-status__grid-kMlO0G",
1624
+ "emergency-status__header": "de-emergency-emergency-status__header-kMlO0G",
1625
+ "emergency-status__loading": "de-emergency-emergency-status__loading-kMlO0G",
1626
+ "emergency-status__loading-text": "de-emergency-emergency-status__loading-text-kMlO0G",
1627
+ "emergency-status__panel": "de-emergency-emergency-status__panel-kMlO0G",
1628
+ "emergency-status__row": "de-emergency-emergency-status__row-kMlO0G",
1629
+ "emergency-status__row-container": "de-emergency-emergency-status__row-container-kMlO0G",
1630
+ "emergency-status__spinner": "de-emergency-emergency-status__spinner-kMlO0G",
1631
+ "emergency-status-panel__header": "de-emergency-emergency-status-panel__header-kMlO0G",
1632
+ "emergency-status-panel__refresh-btn": "de-emergency-emergency-status-panel__refresh-btn-kMlO0G",
1633
+ "emergency-status-panel__refresh-btn-container": "de-emergency-emergency-status-panel__refresh-btn-container-kMlO0G",
1634
+ "emergency-status-panel__refresh-icon": "de-emergency-emergency-status-panel__refresh-icon-kMlO0G",
1635
+ "emergency-status-panel__title": "de-emergency-emergency-status-panel__title-kMlO0G",
1636
+ "responder-name": "de-emergency-responder-name-kMlO0G",
1637
+ "simulacrum-success-message": "de-emergency-simulacrum-success-message-kMlO0G",
1638
+ "simulacrum-success-message__container": "de-emergency-simulacrum-success-message__container-kMlO0G",
1639
+ "simulacrum-success-message__content": "de-emergency-simulacrum-success-message__content-kMlO0G",
1640
+ "simulacrum-success-message__detail": "de-emergency-simulacrum-success-message__detail-kMlO0G",
1641
+ "simulacrum-success-message__details": "de-emergency-simulacrum-success-message__details-kMlO0G",
1642
+ "simulacrum-success-message__icon": "de-emergency-simulacrum-success-message__icon-kMlO0G",
1643
+ "simulacrum-success-message__text": "de-emergency-simulacrum-success-message__text-kMlO0G",
1644
+ "simulacrum-success-message__title": "de-emergency-simulacrum-success-message__title-kMlO0G",
1645
+ "sos-img": "de-emergency-sos-img-kMlO0G",
1646
+ "sos-img__container": "de-emergency-sos-img__container-kMlO0G",
1647
+ "status-icon": "de-emergency-status-icon-kMlO0G",
1648
+ "status-icon--checked": "de-emergency-status-icon--checked-kMlO0G",
1649
+ "status-icon--failed": "de-emergency-status-icon--failed-kMlO0G",
1650
+ "status-icon--unchecked": "de-emergency-status-icon--unchecked-kMlO0G"
1651
+ };
1652
+
1653
+ // src/react/classnames.ts
1654
+ /**
1655
+ * Joins local CSS Module class names, omitting names that are not in the
1656
+ * module.
1657
+ */
1658
+ function cls(...names) {
1659
+ return names.filter((name) => Boolean(name)).map((name) => panel_module_default[name] ?? "").filter(Boolean).join(" ");
1660
+ }
1661
+
1662
+ // src/react/components/MessageSurface/index.tsx
1663
+ function MessageSurface(props) {
1664
+ const { variant, icon, title, text, details, className, children } = props;
1665
+ const prefix = variant === "success" ? "simulacrum-success-message" : "emergency-error-message";
1666
+ const success = variant === "success";
1667
+ return /* @__PURE__ */ jsxs("div", {
1668
+ className: [cls(prefix), className].filter(Boolean).join(" "),
1669
+ role: success ? "status" : "alert",
1670
+ "aria-live": success ? "polite" : "assertive",
1671
+ children: [/* @__PURE__ */ jsxs("div", {
1672
+ className: cls(`${prefix}__container`),
1673
+ children: [/* @__PURE__ */ jsx("div", {
1674
+ className: cls(`${prefix}__icon`),
1675
+ children: /* @__PURE__ */ jsx(Icon, {
1676
+ name: icon,
1677
+ className: "status-icon"
1678
+ })
1679
+ }), /* @__PURE__ */ jsxs("div", {
1680
+ className: cls(`${prefix}__content`),
1681
+ children: [
1682
+ /* @__PURE__ */ jsx("h3", {
1683
+ className: cls(`${prefix}__title`),
1684
+ children: title
1685
+ }),
1686
+ /* @__PURE__ */ jsx("p", {
1687
+ className: cls(`${prefix}__text`),
1688
+ children: text
1689
+ }),
1690
+ /* @__PURE__ */ jsx("div", {
1691
+ className: cls(`${prefix}__details`),
1692
+ children: details.map((detail) => /* @__PURE__ */ jsxs("span", {
1693
+ className: cls(`${prefix}__detail`),
1694
+ children: [/* @__PURE__ */ jsx(Icon, { name: detail.icon }), detail.text]
1695
+ }, detail.text))
1696
+ })
1697
+ ]
1698
+ })]
1699
+ }), children]
1700
+ });
1701
+ }
1702
+
1703
+ // src/react/components/EmergencyError/index.tsx
1704
+ function EmergencyError({ error, onRetry }) {
1705
+ return /* @__PURE__ */ jsx(MessageSurface, {
1706
+ variant: "error",
1707
+ icon: "exclamation-circle",
1708
+ title: copy.errorTitle,
1709
+ text: error.message || copy.errorText,
1710
+ details: [{
1711
+ icon: "info",
1712
+ text: copy.errorDetailInfo
1713
+ }, {
1714
+ icon: "arrow-left",
1715
+ text: copy.errorDetailRetry
1716
+ }],
1717
+ children: /* @__PURE__ */ jsx("button", {
1718
+ type: "button",
1719
+ className: cls("emergency-feedback__dismiss"),
1720
+ onClick: onRetry,
1721
+ children: copy.retry
1722
+ })
1723
+ });
1724
+ }
1725
+
1726
+ // src/react/components/EmergencyFeedback/index.tsx
1727
+ /** Time the feedback stays on screen before dismissing itself. */
1728
+ const FEEDBACK_AUTO_DISMISS_MS = 3e3;
1729
+ /** Time the geolocation feedback stays on screen before dismissing itself. */
1730
+ const GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS = 5e3;
1731
+ function geolocationText(error) {
1732
+ switch (error.code) {
1733
+ case "geolocation_denied": return copy.geolocationDenied;
1734
+ case "geolocation_timeout": return copy.geolocationTimeout;
1735
+ case "geolocation_unsupported": return copy.geolocationUnsupported;
1736
+ default: return copy.geolocationUnavailable;
1737
+ }
1738
+ }
1739
+ function EmergencyFeedback({ abortResult, simulationResult, geolocationError, activationError, onDismiss }) {
1740
+ const result = abortResult ?? simulationResult;
1741
+ const feedbackActivationError = result ? null : activationError ?? null;
1742
+ const feedbackGeolocationError = result || feedbackActivationError ? null : geolocationError;
1743
+ if (!result && !feedbackActivationError && !feedbackGeolocationError) return null;
1744
+ return /* @__PURE__ */ jsx(FeedbackSurface, {
1745
+ result,
1746
+ aborting: abortResult !== null,
1747
+ geolocationError: feedbackGeolocationError,
1748
+ activationError: feedbackActivationError,
1749
+ onDismiss
1750
+ });
1751
+ }
1752
+ function FeedbackSurface({ result, aborting, geolocationError, activationError, onDismiss }) {
1753
+ const dismissRef = useRef(onDismiss);
1754
+ useEffect(() => {
1755
+ dismissRef.current = onDismiss;
1756
+ }, [onDismiss]);
1757
+ useEffect(() => {
1758
+ const timeout = setTimeout(() => dismissRef.current(), geolocationError || activationError ? GEOLOCATION_FEEDBACK_AUTO_DISMISS_MS : FEEDBACK_AUTO_DISMISS_MS);
1759
+ return () => clearTimeout(timeout);
1760
+ }, [
1761
+ result?.at,
1762
+ geolocationError,
1763
+ activationError
1764
+ ]);
1765
+ const success = result?.ok === true;
1766
+ const title = success ? aborting ? copy.abortSuccessTitle : copy.simulateSuccessTitle : copy.errorTitle;
1767
+ const text = success ? result?.message ?? "" : geolocationError ? geolocationText(geolocationError) : activationError ? activationError.message || copy.errorText : result?.message ?? copy.errorText;
1768
+ const details = success ? aborting ? [{
1769
+ icon: "message",
1770
+ text: copy.abortSuccessDetails
1771
+ }] : [{
1772
+ icon: "message",
1773
+ text: copy.simulateSuccessDetails
1774
+ }, {
1775
+ icon: "clock",
1776
+ text: copy.simulateSuccessCompleted
1777
+ }] : [{
1778
+ icon: "info",
1779
+ text: copy.errorDetailInfo
1780
+ }, {
1781
+ icon: "arrow-left",
1782
+ text: copy.errorDetailRetry
1783
+ }];
1784
+ return /* @__PURE__ */ jsx(MessageSurface, {
1785
+ variant: success ? "success" : "error",
1786
+ icon: success ? "check-circle" : "alert-triangle",
1787
+ title,
1788
+ text,
1789
+ details,
1790
+ className: cls("emergency-feedback"),
1791
+ children: /* @__PURE__ */ jsx("button", {
1792
+ type: "button",
1793
+ className: cls("emergency-feedback__dismiss"),
1794
+ onClick: onDismiss,
1795
+ children: copy.feedbackDismiss
1796
+ })
1797
+ });
1798
+ }
1799
+
1800
+ // src/react/components/EmergencyLoading/index.tsx
1801
+ function EmergencyLoading() {
1802
+ return /* @__PURE__ */ jsxs("div", {
1803
+ className: cls("emergency-status__loading"),
1804
+ role: "status",
1805
+ "aria-live": "polite",
1806
+ children: [/* @__PURE__ */ jsx("span", {
1807
+ className: cls("emergency-status__spinner"),
1808
+ "aria-hidden": "true"
1809
+ }), /* @__PURE__ */ jsx("span", {
1810
+ className: cls("emergency-status__loading-text"),
1811
+ children: copy.loading
1812
+ })]
1813
+ });
1814
+ }
1815
+
1816
+ // src/core/date.ts
1817
+ /**
1818
+ * Formats a timestamp as `h:mmam` or `h:mmpm` in local time, without a space
1819
+ * or leading zero.
1820
+ */
1821
+ function formatShortTime(value, locale = "es") {
1822
+ if (value === null) return "";
1823
+ const date = value instanceof Date ? value : new Date(value);
1824
+ if (Number.isNaN(date.getTime())) return "";
1825
+ const parts = new Intl.DateTimeFormat(locale, {
1826
+ hour: "numeric",
1827
+ minute: "2-digit",
1828
+ hour12: true
1829
+ }).formatToParts(date);
1830
+ const hour = parts.find((part) => part.type === "hour")?.value;
1831
+ const minute = parts.find((part) => part.type === "minute")?.value;
1832
+ const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value;
1833
+ if (!hour || !minute) return "";
1834
+ return `${hour}:${minute}${normalizePeriod(dayPeriod)}`;
1835
+ }
1836
+ function normalizePeriod(dayPeriod) {
1837
+ if (!dayPeriod) return "";
1838
+ if (/^p/i.test(dayPeriod)) return "pm";
1839
+ return /^a/i.test(dayPeriod) ? "am" : "";
1840
+ }
1841
+
1842
+ // src/react/components/StatusIcon/index.tsx
1843
+ /**
1844
+ * Explicit responder state model:
1845
+ * - `failed`: the send operation failed (red alert in the "Sent" cell only).
1846
+ * - `confirmed`: checked circle.
1847
+ * - otherwise: unchecked circle.
1848
+ */
1849
+ function StatusIcon({ confirmed, failed = false }) {
1850
+ return /* @__PURE__ */ jsx(Icon, {
1851
+ name: failed ? "exclamation-circle" : confirmed ? "check-circle" : "x-circle",
1852
+ className: cls("status-icon", failed ? "status-icon--failed" : confirmed ? "status-icon--checked" : "status-icon--unchecked")
1853
+ });
1854
+ }
1855
+
1856
+ // src/react/components/StatusCell/index.tsx
1857
+ function StatusCell({ confirmed, timestamp, failed = false, isFailedIcon = false, locale }) {
1858
+ const showTime = !failed && timestamp !== null && timestamp !== "";
1859
+ const showIcon = failed ? isFailedIcon : true;
1860
+ return /* @__PURE__ */ jsxs("div", {
1861
+ className: cls("emergency-status__cell"),
1862
+ children: [/* @__PURE__ */ jsx("p", {
1863
+ className: cls("emergency-status__cell-icon"),
1864
+ children: showIcon ? /* @__PURE__ */ jsx(StatusIcon, {
1865
+ confirmed,
1866
+ failed
1867
+ }) : null
1868
+ }), showTime ? /* @__PURE__ */ jsx("p", {
1869
+ className: cls("emergency-status__cell-time"),
1870
+ children: formatShortTime(timestamp, locale)
1871
+ }) : null]
1872
+ });
1873
+ }
1874
+
1875
+ // src/react/components/ResponderRow/index.tsx
1876
+ function ResponderRow({ responder, locale }) {
1877
+ return /* @__PURE__ */ jsx("div", {
1878
+ className: cls("emergency-status__row-container"),
1879
+ "data-responder-id": responder.phone,
1880
+ children: /* @__PURE__ */ jsxs("div", {
1881
+ className: cls("emergency-status__row"),
1882
+ children: [
1883
+ /* @__PURE__ */ jsx("div", {
1884
+ className: cls("emergency-status__cell", "responder-name"),
1885
+ children: /* @__PURE__ */ jsx("p", {
1886
+ className: cls("emergency-status__cell-text"),
1887
+ children: responder.name
1888
+ })
1889
+ }),
1890
+ /* @__PURE__ */ jsx(StatusCell, {
1891
+ confirmed: responder.sent,
1892
+ timestamp: responder.sentAt,
1893
+ failed: responder.failed,
1894
+ isFailedIcon: true,
1895
+ locale
1896
+ }),
1897
+ /* @__PURE__ */ jsx(StatusCell, {
1898
+ confirmed: responder.delivered,
1899
+ timestamp: responder.deliveredAt,
1900
+ failed: responder.failed,
1901
+ locale
1902
+ }),
1903
+ /* @__PURE__ */ jsx(StatusCell, {
1904
+ confirmed: responder.read,
1905
+ timestamp: responder.readAt,
1906
+ failed: responder.failed,
1907
+ locale
1908
+ })
1909
+ ]
1910
+ })
1911
+ });
1912
+ }
1913
+
1914
+ // src/react/components/EmergencyStatusPanel/index.tsx
1915
+ function EmergencyStatusPanel({ responders, refreshing, locale, onRefresh }) {
1916
+ const columns = [
1917
+ copy.responderColumn,
1918
+ copy.sentColumn,
1919
+ copy.deliveredColumn,
1920
+ copy.readColumn
1921
+ ];
1922
+ return /* @__PURE__ */ jsxs("div", {
1923
+ className: cls("emergency-status__panel"),
1924
+ children: [/* @__PURE__ */ jsx("div", {
1925
+ className: cls("emergency-status-panel__header"),
1926
+ children: /* @__PURE__ */ jsx("h3", {
1927
+ className: cls("emergency-status-panel__title"),
1928
+ children: copy.statusTitle
1929
+ })
1930
+ }), /* @__PURE__ */ jsxs("div", {
1931
+ className: cls("emergency-status__grid"),
1932
+ children: [
1933
+ /* @__PURE__ */ jsx("div", {
1934
+ className: cls("emergency-status__header"),
1935
+ children: columns.map((column) => /* @__PURE__ */ jsx("div", {
1936
+ className: cls("emergency-status__cell"),
1937
+ children: /* @__PURE__ */ jsx("p", {
1938
+ className: cls("emergency-status__cell-text"),
1939
+ children: column
1940
+ })
1941
+ }, column))
1942
+ }),
1943
+ /* @__PURE__ */ jsxs("div", {
1944
+ className: cls("emergency-status__body"),
1945
+ children: [responders.map((responder) => /* @__PURE__ */ jsx(ResponderRow, {
1946
+ responder,
1947
+ locale
1948
+ }, `${responder.phone}-${responder.name}`)), responders.length === 0 ? /* @__PURE__ */ jsx("p", {
1949
+ className: cls("emergency-status__cell-text"),
1950
+ children: copy.respondersEmpty
1951
+ }) : null]
1952
+ }),
1953
+ /* @__PURE__ */ jsxs("div", {
1954
+ className: cls("emergency-status-panel__refresh-btn-container"),
1955
+ children: [/* @__PURE__ */ jsxs("button", {
1956
+ type: "button",
1957
+ className: cls("emergency-status-panel__refresh-btn"),
1958
+ "aria-label": copy.refresh,
1959
+ disabled: refreshing,
1960
+ onClick: onRefresh,
1961
+ children: [/* @__PURE__ */ jsx(Icon, {
1962
+ name: "rotate",
1963
+ className: cls("emergency-status-panel__refresh-icon")
1964
+ }), /* @__PURE__ */ jsx("span", {
1965
+ className: cls("emergency-status-panel__refresh-text"),
1966
+ children: refreshing ? copy.refreshing : copy.refresh
1967
+ })]
1968
+ }), refreshing ? /* @__PURE__ */ jsxs("div", {
1969
+ className: cls("emergency-status__loading"),
1970
+ role: "status",
1971
+ "aria-live": "polite",
1972
+ children: [/* @__PURE__ */ jsx("span", {
1973
+ className: cls("emergency-status__spinner"),
1974
+ "aria-hidden": "true"
1975
+ }), /* @__PURE__ */ jsx("span", {
1976
+ className: cls("emergency-status__loading-text"),
1977
+ children: copy.refreshing
1978
+ })]
1979
+ }) : null]
1980
+ })
1981
+ ]
1982
+ })]
1983
+ });
1984
+ }
1985
+
1986
+ // src/react/components/EmergencyBody/index.tsx
1987
+ function EmergencyBody(props) {
1988
+ const { active, responders, refreshing, loading, locale, geolocationError, activationError, error, abortResult, simulationResult, onRefresh, onRetry, onDismissFeedback } = props;
1989
+ const showingFeedback = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
1990
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1991
+ !loading && !showingFeedback ? /* @__PURE__ */ jsx("div", {
1992
+ className: cls("sos-img__container"),
1993
+ id: "sos-img__container",
1994
+ children: /* @__PURE__ */ jsx(ParamedicalMark, {
1995
+ className: cls("sos-img"),
1996
+ title: copy.illustrationAlt
1997
+ })
1998
+ }) : null,
1999
+ active || loading || showingFeedback ? null : /* @__PURE__ */ jsx("div", {
2000
+ className: cls("emergency-panel__body-title-container"),
2001
+ children: /* @__PURE__ */ jsx("h4", {
2002
+ className: cls("emergency-panel__body-title"),
2003
+ children: copy.question
2004
+ })
2005
+ }),
2006
+ active && !showingFeedback ? /* @__PURE__ */ jsx(EmergencyStatusPanel, {
2007
+ responders,
2008
+ refreshing,
2009
+ locale,
2010
+ onRefresh
2011
+ }) : null,
2012
+ loading ? /* @__PURE__ */ jsx(EmergencyLoading, {}) : null,
2013
+ error ? /* @__PURE__ */ jsx(EmergencyError, {
2014
+ error,
2015
+ onRetry
2016
+ }) : null,
2017
+ /* @__PURE__ */ jsx(EmergencyFeedback, {
2018
+ abortResult,
2019
+ simulationResult,
2020
+ geolocationError,
2021
+ activationError,
2022
+ onDismiss: onDismissFeedback
2023
+ })
2024
+ ] });
2025
+ }
2026
+
2027
+ // src/react/components/EmergencyConfirmation/index.tsx
2028
+ function EmergencyConfirmation(props) {
2029
+ const { abortConfirmation, simulationConfirmation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2030
+ const cardRef = useRef(null);
2031
+ const titleId = useId();
2032
+ const danger = abortConfirmation;
2033
+ const open = abortConfirmation || simulationConfirmation;
2034
+ useEffect(() => {
2035
+ if (open) cardRef.current?.querySelector("button")?.focus();
2036
+ }, [open]);
2037
+ if (!open) return null;
2038
+ const title = danger ? copy.abortConfirmTitle : copy.simulateConfirmTitle;
2039
+ const text = danger ? copy.abortConfirmText : copy.simulateConfirmText;
2040
+ const tone = danger ? "danger" : "information";
2041
+ return /* @__PURE__ */ jsx("div", {
2042
+ className: cls("emergency-confirmation"),
2043
+ role: "dialog",
2044
+ "aria-modal": "true",
2045
+ "aria-labelledby": titleId,
2046
+ children: /* @__PURE__ */ jsxs("div", {
2047
+ ref: cardRef,
2048
+ className: cls("emergency-confirmation__card"),
2049
+ children: [/* @__PURE__ */ jsxs("div", {
2050
+ className: cls("emergency-confirmation__body"),
2051
+ children: [
2052
+ /* @__PURE__ */ jsx("div", {
2053
+ className: cls("emergency-confirmation__icon", `emergency-confirmation__icon--${tone}`),
2054
+ children: /* @__PURE__ */ jsx(Icon, { name: danger ? "alert-triangle" : "info" })
2055
+ }),
2056
+ /* @__PURE__ */ jsx("h5", {
2057
+ className: cls("emergency-confirmation__title"),
2058
+ id: titleId,
2059
+ children: title
2060
+ }),
2061
+ /* @__PURE__ */ jsx("p", {
2062
+ className: cls("emergency-confirmation__text"),
2063
+ children: text
2064
+ })
2065
+ ]
2066
+ }), /* @__PURE__ */ jsxs("div", {
2067
+ className: cls("emergency-confirmation__footer"),
2068
+ children: [/* @__PURE__ */ jsx("button", {
2069
+ type: "button",
2070
+ className: cls("emergency-confirmation__button", "emergency-confirmation__button--cancel"),
2071
+ onClick: danger ? onCancelAbort : onCancelSimulation,
2072
+ children: danger ? copy.abortCancel : copy.simulateCancel
2073
+ }), /* @__PURE__ */ jsx("button", {
2074
+ type: "button",
2075
+ className: cls("emergency-confirmation__button", danger ? "emergency-confirmation__button--danger" : "emergency-confirmation__button--confirm"),
2076
+ onClick: danger ? onConfirmAbort : onConfirmSimulation,
2077
+ children: danger ? copy.abortConfirm : copy.simulateConfirm
2078
+ })]
2079
+ })]
2080
+ })
2081
+ });
2082
+ }
2083
+
2084
+ // src/react/components/EmergencyFooter/index.tsx
2085
+ function EmergencyFooter(props) {
2086
+ const { active, busy, aborting, feedbackVisible, onActivate, onRequestAbort, onRequestSimulation } = props;
2087
+ if (active) return /* @__PURE__ */ jsx("footer", {
2088
+ className: cls("emergency-panel__footer"),
2089
+ children: /* @__PURE__ */ jsxs("button", {
2090
+ type: "button",
2091
+ className: cls("emergency-status__abort-btn"),
2092
+ "aria-label": copy.abort,
2093
+ disabled: aborting || feedbackVisible,
2094
+ onClick: onRequestAbort,
2095
+ children: [copy.abort, /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })]
2096
+ })
2097
+ });
2098
+ return /* @__PURE__ */ jsxs("footer", {
2099
+ className: cls("emergency-panel__footer"),
2100
+ children: [/* @__PURE__ */ jsxs("button", {
2101
+ type: "button",
2102
+ className: cls("emergency-panel__button", "emergency-panel__button--emergency"),
2103
+ "aria-label": copy.emergencyAria,
2104
+ disabled: busy || feedbackVisible,
2105
+ onClick: onActivate,
2106
+ children: [copy.emergency, /* @__PURE__ */ jsx(Icon, { name: "emergency" })]
2107
+ }), /* @__PURE__ */ jsxs("button", {
2108
+ type: "button",
2109
+ className: cls("emergency-panel__button", "emergency-panel__button--simulation"),
2110
+ "aria-label": copy.testAria,
2111
+ disabled: busy || feedbackVisible,
2112
+ onClick: onRequestSimulation,
2113
+ children: [copy.test, /* @__PURE__ */ jsx(Icon, { name: "list-check" })]
2114
+ })]
2115
+ });
2116
+ }
2117
+
2118
+ // src/react/components/EmergencyHeader/index.tsx
2119
+ function EmergencyHeader({ titleId, onClose }) {
2120
+ return /* @__PURE__ */ jsxs("header", {
2121
+ className: cls("emergency-panel__header"),
2122
+ children: [/* @__PURE__ */ jsx("h2", {
2123
+ className: cls("emergency-panel__title"),
2124
+ id: titleId,
2125
+ children: copy.panelLabel
2126
+ }), /* @__PURE__ */ jsx("button", {
2127
+ type: "button",
2128
+ className: cls("emergency-panel__close-button"),
2129
+ "aria-label": copy.close,
2130
+ onClick: onClose,
2131
+ children: /* @__PURE__ */ jsx(Icon, {
2132
+ name: "close",
2133
+ className: cls("emergency-panel__close-icon")
2134
+ })
2135
+ })]
2136
+ });
2137
+ }
2138
+
2139
+ // src/react/hooks/useEscapeKey.ts
2140
+ function useEscapeKey(active, onEscape) {
2141
+ useEffect(() => {
2142
+ if (!active || typeof document === "undefined") return;
2143
+ const handleKeyDown = (event) => {
2144
+ if (event.key === "Escape") {
2145
+ event.stopPropagation();
2146
+ onEscape();
2147
+ }
2148
+ };
2149
+ document.addEventListener("keydown", handleKeyDown);
2150
+ return () => {
2151
+ document.removeEventListener("keydown", handleKeyDown);
2152
+ };
2153
+ }, [active, onEscape]);
2154
+ }
2155
+
2156
+ // src/react/hooks/useFocusTrap.ts
2157
+ const FOCUSABLE_SELECTOR = [
2158
+ "a[href]",
2159
+ "button:not([disabled])",
2160
+ "input:not([disabled])",
2161
+ "select:not([disabled])",
2162
+ "textarea:not([disabled])",
2163
+ "[tabindex]:not([tabindex=\"-1\"])"
2164
+ ].join(",");
2165
+ /**
2166
+ * Keeps keyboard focus inside the dialog while it is open and restores focus to
2167
+ * the element that opened it when it closes.
2168
+ */
2169
+ function useFocusTrap(ref, active) {
2170
+ useEffect(() => {
2171
+ if (!active || typeof document === "undefined") return;
2172
+ const container = ref.current;
2173
+ if (!container) return;
2174
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2175
+ const focusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("hidden") && element.getAttribute("aria-hidden") !== "true");
2176
+ (focusable()[0] ?? container).focus({ preventScroll: true });
2177
+ const handleKeyDown = (event) => {
2178
+ if (event.key !== "Tab") return;
2179
+ const items = focusable();
2180
+ if (items.length === 0) {
2181
+ event.preventDefault();
2182
+ container.focus({ preventScroll: true });
2183
+ return;
2184
+ }
2185
+ const first = items[0];
2186
+ const last = items[items.length - 1];
2187
+ const current = document.activeElement;
2188
+ if (event.shiftKey && (current === first || !container.contains(current))) {
2189
+ event.preventDefault();
2190
+ if (last) last.focus({ preventScroll: true });
2191
+ return;
2192
+ }
2193
+ if (!event.shiftKey && current === last && first) {
2194
+ event.preventDefault();
2195
+ first.focus({ preventScroll: true });
2196
+ }
2197
+ };
2198
+ document.addEventListener("keydown", handleKeyDown, true);
2199
+ return () => {
2200
+ document.removeEventListener("keydown", handleKeyDown, true);
2201
+ if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
2202
+ };
2203
+ }, [active, ref]);
2204
+ }
2205
+
2206
+ // src/react/components/EmergencyPanel/index.tsx
2207
+ function EmergencyPanel(props) {
2208
+ const { open, theme, active, busy, aborting, loading, locale, responders, refreshing, geolocationError, activationError, error, abortResult, simulationResult, abortConfirmation, simulationConfirmation, onClose, onActivate, onRefresh, onRetry, onDismissFeedback, onRequestAbort, onRequestSimulation, onCancelAbort, onCancelSimulation, onConfirmAbort, onConfirmSimulation } = props;
2209
+ const titleId = useId();
2210
+ const dialogRef = useRef(null);
2211
+ const feedbackVisible = geolocationError !== null || Boolean(activationError) || abortResult !== null || simulationResult !== null;
2212
+ const closeOnEscape = useCallback(() => {
2213
+ onClose("escape");
2214
+ }, [onClose]);
2215
+ useFocusTrap(dialogRef, open);
2216
+ useEscapeKey(open, closeOnEscape);
2217
+ return /* @__PURE__ */ jsxs("div", {
2218
+ ref: dialogRef,
2219
+ className: cls("emergency-panel", !open && "emergency-panel--hidden"),
2220
+ style: themeToCssVariables(theme),
2221
+ role: "dialog",
2222
+ "aria-modal": "true",
2223
+ "aria-labelledby": titleId,
2224
+ "aria-hidden": !open,
2225
+ inert: !open,
2226
+ tabIndex: -1,
2227
+ children: [
2228
+ /* @__PURE__ */ jsx(EmergencyHeader, {
2229
+ titleId,
2230
+ onClose: () => onClose("close")
2231
+ }),
2232
+ /* @__PURE__ */ jsx("section", {
2233
+ className: cls("emergency-panel__body"),
2234
+ "aria-labelledby": titleId,
2235
+ children: /* @__PURE__ */ jsx(EmergencyBody, {
2236
+ active,
2237
+ responders,
2238
+ refreshing,
2239
+ loading,
2240
+ locale,
2241
+ geolocationError,
2242
+ activationError,
2243
+ error,
2244
+ abortResult,
2245
+ simulationResult,
2246
+ onRefresh,
2247
+ onRetry,
2248
+ onDismissFeedback
2249
+ })
2250
+ }),
2251
+ /* @__PURE__ */ jsx(EmergencyFooter, {
2252
+ active,
2253
+ busy,
2254
+ aborting,
2255
+ feedbackVisible,
2256
+ onActivate,
2257
+ onRequestAbort,
2258
+ onRequestSimulation
2259
+ }),
2260
+ /* @__PURE__ */ jsx(EmergencyConfirmation, {
2261
+ abortConfirmation,
2262
+ simulationConfirmation,
2263
+ onCancelAbort,
2264
+ onCancelSimulation,
2265
+ onConfirmAbort,
2266
+ onConfirmSimulation
2267
+ })
2268
+ ]
2269
+ });
2270
+ }
2271
+
2272
+ // src/react/components/EmergencyResumeNotification/index.tsx
2273
+ function EmergencyResumeNotification(props) {
2274
+ const { resumeAvailable, onOpen } = props;
2275
+ const titleId = useId();
2276
+ if (!resumeAvailable) return null;
2277
+ return /* @__PURE__ */ jsx("div", {
2278
+ className: cls("emergency-resume-notification-overlay"),
2279
+ role: "dialog",
2280
+ "aria-modal": "true",
2281
+ "aria-labelledby": titleId,
2282
+ children: /* @__PURE__ */ jsxs("div", {
2283
+ className: cls("emergency-resume-notification-card"),
2284
+ children: [
2285
+ /* @__PURE__ */ jsx("div", {
2286
+ className: cls("emergency-resume-notification-icon"),
2287
+ "aria-hidden": "true",
2288
+ children: /* @__PURE__ */ jsx(Icon, { name: "alert-triangle" })
2289
+ }),
2290
+ /* @__PURE__ */ jsx("h2", {
2291
+ className: cls("emergency-resume-notification-title"),
2292
+ id: titleId,
2293
+ children: copy.resumeTitle
2294
+ }),
2295
+ /* @__PURE__ */ jsx("p", {
2296
+ className: cls("emergency-resume-notification-message"),
2297
+ children: copy.resumeDescription
2298
+ }),
2299
+ /* @__PURE__ */ jsxs("button", {
2300
+ type: "button",
2301
+ className: cls("emergency-resume-notification-button"),
2302
+ "aria-label": copy.resumeAction,
2303
+ onClick: onOpen,
2304
+ children: [/* @__PURE__ */ jsx(Icon, { name: "eye" }), copy.resumeAction]
2305
+ })
2306
+ ]
2307
+ })
2308
+ });
2309
+ }
2310
+
2311
+ // src/react/context.tsx
2312
+ const EmergencyContext = createContext(null);
2313
+ function EmergencyProvider({ value, children }) {
2314
+ return /* @__PURE__ */ jsx(EmergencyContext.Provider, {
2315
+ value,
2316
+ children
2317
+ });
2318
+ }
2319
+ function useEmergencyContext() {
2320
+ const value = useContext(EmergencyContext);
2321
+ if (value === null) throw new Error("Emergency components must be rendered inside an emergency client.");
2322
+ return value;
2323
+ }
2324
+
2325
+ // src/react/hooks/useEmergencyController.ts
2326
+ function useEmergencyController() {
2327
+ return useEmergencyContext().client;
2328
+ }
2329
+
2330
+ // src/react/hooks/useEmergencyStore.ts
2331
+ function useEmergencyStore(selector) {
2332
+ const { store } = useEmergencyContext();
2333
+ return useStore(store, selector);
2334
+ }
2335
+
2336
+ // src/react/components/EmergencyRoot/index.tsx
2337
+ const BUSY_PHASES = [
2338
+ "initializing",
2339
+ "activating",
2340
+ "simulating",
2341
+ "aborting"
2342
+ ];
2343
+ function EmergencyRoot() {
2344
+ const client = useEmergencyController();
2345
+ const panelOpen = useEmergencyStore((state) => state.panelOpen);
2346
+ const phase = useEmergencyStore((state) => state.phase);
2347
+ const config = useEmergencyStore((state) => state.config);
2348
+ const snapshot = useEmergencyStore((state) => state.snapshot);
2349
+ const responders = useEmergencyStore((state) => state.responders);
2350
+ const resumeAvailable = useEmergencyStore((state) => state.resumeAvailable);
2351
+ const geolocationError = useEmergencyStore((state) => state.geolocationError);
2352
+ const lastError = useEmergencyStore((state) => state.lastError);
2353
+ const abortResult = useEmergencyStore((state) => state.abortResult);
2354
+ const simulationResult = useEmergencyStore((state) => state.simulationResult);
2355
+ const abortConfirmation = useEmergencyStore((state) => state.abortConfirmation);
2356
+ const simulationConfirmation = useEmergencyStore((state) => state.simulationConfirmation);
2357
+ const active = snapshot?.active === true;
2358
+ const busy = BUSY_PHASES.includes(phase.kind);
2359
+ const feedbackActive = geolocationError !== null || abortResult !== null || simulationResult !== null;
2360
+ const activationError = phase.kind === "ready" && !feedbackActive ? lastError : null;
2361
+ const showError = phase.kind === "error" || lastError !== null && !feedbackActive && activationError === null;
2362
+ const error = phase.kind === "error" ? phase.error : lastError;
2363
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(EmergencyResumeNotification, {
2364
+ resumeAvailable,
2365
+ onOpen: () => client.open("resume")
2366
+ }), /* @__PURE__ */ jsx(EmergencyPanel, {
2367
+ open: panelOpen,
2368
+ theme: config.theme,
2369
+ active,
2370
+ busy,
2371
+ aborting: phase.kind === "aborting",
2372
+ loading: phase.kind === "initializing" || phase.kind === "activating",
2373
+ locale: config.locale ?? "es",
2374
+ responders,
2375
+ refreshing: phase.kind === "refreshing",
2376
+ geolocationError,
2377
+ activationError,
2378
+ error: showError ? error : null,
2379
+ abortResult,
2380
+ simulationResult,
2381
+ abortConfirmation,
2382
+ simulationConfirmation,
2383
+ onClose: (reason) => client.close(reason),
2384
+ onActivate: () => {
2385
+ client.activate();
2386
+ },
2387
+ onRefresh: () => {
2388
+ client.refresh();
2389
+ },
2390
+ onRetry: () => {
2391
+ client.activate();
2392
+ },
2393
+ onDismissFeedback: () => client.dismissFeedback(),
2394
+ onRequestAbort: () => client.requestAbortConfirmation(),
2395
+ onRequestSimulation: () => client.requestSimulationConfirmation(),
2396
+ onCancelAbort: () => client.cancelAbortConfirmation(),
2397
+ onCancelSimulation: () => client.cancelSimulationConfirmation(),
2398
+ onConfirmAbort: () => {
2399
+ client.abort();
2400
+ },
2401
+ onConfirmSimulation: () => {
2402
+ client.simulate();
2403
+ }
2404
+ })] });
2405
+ }
2406
+
2407
+ // src/mount.tsx
2408
+ function resolveRoot(root) {
2409
+ if (typeof root === "string") {
2410
+ if (typeof document === "undefined") return null;
2411
+ return document.querySelector(root);
2412
+ }
2413
+ return root instanceof HTMLElement ? root : null;
2414
+ }
2415
+ function createFailedClient(error) {
2416
+ const state = {
2417
+ config: {
2418
+ bffBaseUrl: "",
2419
+ code: ""
2420
+ },
2421
+ phase: {
2422
+ kind: "error",
2423
+ error
2424
+ },
2425
+ panelOpen: false,
2426
+ sessionReady: false,
2427
+ resumeAvailable: false,
2428
+ snapshot: null,
2429
+ responders: [],
2430
+ failures: [],
2431
+ messages: [],
2432
+ lastError: error,
2433
+ geolocation: null,
2434
+ geolocationError: null,
2435
+ realtimeStatus: "idle",
2436
+ abortResult: null,
2437
+ simulationResult: null,
2438
+ abortConfirmation: false,
2439
+ simulationConfirmation: false,
2440
+ destroyed: false
2441
+ };
2442
+ const noop = () => void 0;
2443
+ const noopAsync = async () => void 0;
2444
+ return {
2445
+ open: noop,
2446
+ close: noop,
2447
+ destroy: noop,
2448
+ getState: () => state,
2449
+ subscribe: () => () => void 0,
2450
+ on: () => () => void 0,
2451
+ off: noop,
2452
+ ensureSession: noopAsync,
2453
+ resume: noopAsync,
2454
+ dismissResume: noop,
2455
+ activate: noopAsync,
2456
+ simulate: noopAsync,
2457
+ refresh: noopAsync,
2458
+ abort: noopAsync,
2459
+ requestAbortConfirmation: noop,
2460
+ cancelAbortConfirmation: noop,
2461
+ requestSimulationConfirmation: noop,
2462
+ cancelSimulationConfirmation: noop,
2463
+ dismissFeedback: noop
2464
+ };
2465
+ }
2466
+ function attachTrigger(trigger, client) {
2467
+ if (!trigger) return () => void 0;
2468
+ if (typeof trigger === "function") {
2469
+ const cleanup = trigger(() => client.open());
2470
+ return typeof cleanup === "function" ? cleanup : () => void 0;
2471
+ }
2472
+ const element = typeof trigger === "string" ? typeof document === "undefined" ? null : document.querySelector(trigger) : trigger;
2473
+ if (!element) return () => void 0;
2474
+ const listener = (event) => {
2475
+ event.preventDefault();
2476
+ client.open();
2477
+ };
2478
+ element.addEventListener("click", listener);
2479
+ return () => {
2480
+ element.removeEventListener("click", listener);
2481
+ };
2482
+ }
2483
+ function reportConfigurationError(error, options) {
2484
+ if (typeof console !== "undefined" && typeof console.error === "function") console.error("[emergency]", error.message);
2485
+ options.onEvent?.("emergency:failed", {
2486
+ error,
2487
+ operation: "mount"
2488
+ });
2489
+ return createFailedClient(error);
2490
+ }
2491
+ /**
2492
+ * Mounts the emergency client into a host element. Validation failures never
2493
+ * throw into the host page: they produce a typed configuration error, a console
2494
+ * message, and an inert client.
2495
+ */
2496
+ function mount(options) {
2497
+ if (typeof document === "undefined") return reportConfigurationError(createEmergencyError("configuration", "mount() requires a browser environment."), options);
2498
+ const root = resolveRoot(options.root);
2499
+ if (!root) return reportConfigurationError(createEmergencyError("configuration", `The mount root "${typeof options.root === "string" ? options.root : "element"}" was not found.`), options);
2500
+ const validation = validateConfig(options);
2501
+ if (!validation.ok || !validation.config) return reportConfigurationError(validation.error ?? createEmergencyError("configuration", "Invalid emergency configuration."), options);
2502
+ const config = validation.config;
2503
+ installEmergencyFonts(config.fontLoading);
2504
+ const controller = createEmergencyController({ config });
2505
+ const container = document.createElement("div");
2506
+ container.setAttribute("data-de-emergency-container", "");
2507
+ root.appendChild(container);
2508
+ const reactRoot = createRoot(container);
2509
+ reactRoot.render(/* @__PURE__ */ jsx(EmergencyProvider, {
2510
+ value: {
2511
+ store: controller.store,
2512
+ client: controller.client,
2513
+ config
2514
+ },
2515
+ children: /* @__PURE__ */ jsx(EmergencyRoot, {})
2516
+ }));
2517
+ const detachTrigger = attachTrigger(options.trigger, controller.client);
2518
+ const unsubscribers = options.onEvent ? EMERGENCY_EVENT_NAMES.map((name) => controller.events.on(name, (payload) => {
2519
+ options.onEvent?.(name, payload);
2520
+ })) : [];
2521
+ let destroyed = false;
2522
+ controller.client.resume();
2523
+ const destroy = () => {
2524
+ if (destroyed) return;
2525
+ destroyed = true;
2526
+ detachTrigger();
2527
+ for (const unsubscribe of unsubscribers) unsubscribe();
2528
+ controller.client.destroy();
2529
+ container.remove();
2530
+ queueMicrotask(() => reactRoot.unmount());
2531
+ };
2532
+ const client = controller.client;
2533
+ return {
2534
+ open: (reason) => client.open(reason ?? "manual"),
2535
+ close: (reason) => client.close(reason ?? "manual"),
2536
+ getState: () => client.getState(),
2537
+ subscribe: (listener) => client.subscribe(listener),
2538
+ on: (event, handler) => client.on(event, handler),
2539
+ off: (event, handler) => client.off(event, handler),
2540
+ ensureSession: () => client.ensureSession(),
2541
+ resume: () => client.resume(),
2542
+ dismissResume: () => client.dismissResume(),
2543
+ activate: () => client.activate(),
2544
+ simulate: () => client.simulate(),
2545
+ refresh: () => client.refresh(),
2546
+ abort: () => client.abort(),
2547
+ requestAbortConfirmation: () => client.requestAbortConfirmation(),
2548
+ cancelAbortConfirmation: () => client.cancelAbortConfirmation(),
2549
+ requestSimulationConfirmation: () => client.requestSimulationConfirmation(),
2550
+ cancelSimulationConfirmation: () => client.cancelSimulationConfirmation(),
2551
+ dismissFeedback: () => client.dismissFeedback(),
2552
+ destroy
2553
+ };
2554
+ }
2555
+
2556
+ 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 };
2557
+ //# sourceMappingURL=index.js.map