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