@dataemergencia/emergency 0.0.0-stage → 0.2.0

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