@dataemergencia/emergency 0.0.0-stage → 0.1.0

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