@guidelytech/react-native 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4634 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/index.tsx
6
+ import { useContext as useContext2, useEffect as useEffect8, useRef as useRef5, useState as useState7 } from "react";
7
+ import { AppState, Dimensions, Linking, Platform as Platform6 } from "react-native";
8
+
9
+ // ../shared/dist/chunk-5IQVBZO6.js
10
+ function fnv1a(input) {
11
+ let h = 2166136261;
12
+ for (let i = 0; i < input.length; i++) {
13
+ h ^= input.charCodeAt(i);
14
+ h = Math.imul(h, 16777619) >>> 0;
15
+ }
16
+ return h >>> 0;
17
+ }
18
+ function rolloutBucket(subject, salt) {
19
+ return fnv1a(`${salt}:${subject}`) % 100;
20
+ }
21
+ var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
22
+ function randomId(size = 16) {
23
+ const g = typeof globalThis !== "undefined" ? globalThis : {};
24
+ let bytes;
25
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
26
+ bytes = new Uint8Array(size);
27
+ g.crypto.getRandomValues(bytes);
28
+ } else {
29
+ bytes = new Uint8Array(size);
30
+ for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
31
+ }
32
+ let out = "";
33
+ for (let i = 0; i < size; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
34
+ return out;
35
+ }
36
+ function getPath(obj, path) {
37
+ if (!path) return obj;
38
+ let cur = obj;
39
+ for (const part of path.split(".")) {
40
+ if (cur == null) return void 0;
41
+ cur = cur[part];
42
+ }
43
+ return cur;
44
+ }
45
+ function matchUrlPattern(pattern, url) {
46
+ if (!pattern) return true;
47
+ if (pattern === url) return true;
48
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "__GLOBSTAR__").replace(/\*/g, "[^/]*").replace(/__GLOBSTAR__/g, ".*");
49
+ const re = new RegExp(`^${escaped}$`);
50
+ return re.test(url) || re.test(stripOrigin(url));
51
+ }
52
+ function stripOrigin(url) {
53
+ try {
54
+ const u = new URL(url);
55
+ return u.pathname + u.search + u.hash;
56
+ } catch {
57
+ return url;
58
+ }
59
+ }
60
+ function safeRegExp(source, flags = "i") {
61
+ if (!source || source.length > 200) return null;
62
+ try {
63
+ return new RegExp(source, flags);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ var DEFAULT_PLATFORMS = ["web"];
69
+ function guidePlatforms(config) {
70
+ return Array.isArray(config.platforms) ? [...config.platforms] : [...DEFAULT_PLATFORMS];
71
+ }
72
+ function supportsPlatform(config, platform) {
73
+ return guidePlatforms(config).includes(platform);
74
+ }
75
+ function guidelyIdFromSelector(value) {
76
+ const v = (value ?? "").trim();
77
+ if (!v) return null;
78
+ if (/^[A-Za-z0-9_][A-Za-z0-9_\-:.]*$/.test(v)) return v;
79
+ const hash = v.match(/^#([A-Za-z0-9_][A-Za-z0-9_\-:.]*)$/);
80
+ if (hash) return hash[1];
81
+ const attr2 = v.match(/^(?:[a-z][a-z0-9]*)?\[\s*data-(?:guidely-id|testid|tour)\s*=\s*(["']?)([^"'\]]+)\1\s*\]$/i);
82
+ if (attr2) return attr2[2].trim();
83
+ return null;
84
+ }
85
+ function screenPath(screen) {
86
+ const s = (screen ?? "").trim();
87
+ if (!s) return "/";
88
+ return s.startsWith("/") ? s : `/${s}`;
89
+ }
90
+ function compareVersions(a, b) {
91
+ const sa = a == null ? "" : String(a).trim().replace(/^v/i, "");
92
+ const sb = b == null ? "" : String(b).trim().replace(/^v/i, "");
93
+ if (!sa || !sb) return null;
94
+ const pa = sa.split(/[.+-]/);
95
+ const pb = sb.split(/[.+-]/);
96
+ const n = Math.max(pa.length, pb.length);
97
+ for (let i = 0; i < n; i++) {
98
+ const x = pa[i] ?? "0";
99
+ const y = pb[i] ?? "0";
100
+ const nx = /^\d+$/.test(x) ? Number(x) : NaN;
101
+ const ny = /^\d+$/.test(y) ? Number(y) : NaN;
102
+ if (!Number.isNaN(nx) && !Number.isNaN(ny)) {
103
+ if (nx !== ny) return nx < ny ? -1 : 1;
104
+ } else if (x !== y) {
105
+ return x < y ? -1 : 1;
106
+ }
107
+ }
108
+ return 0;
109
+ }
110
+ function isQuestionVisible(q, answers) {
111
+ if (!q.showWhen) return true;
112
+ const actual = answers[q.showWhen.questionId];
113
+ if (actual === void 0) return false;
114
+ const expected = q.showWhen.value;
115
+ switch (q.showWhen.op) {
116
+ case "equals":
117
+ return Array.isArray(actual) ? actual.includes(String(expected)) : String(actual) === String(expected);
118
+ case "lte":
119
+ return Number(actual) <= Number(expected);
120
+ case "gte":
121
+ return Number(actual) >= Number(expected);
122
+ case "in": {
123
+ const list = Array.isArray(expected) ? expected : [String(expected)];
124
+ return Array.isArray(actual) ? actual.some((a) => list.includes(a)) : list.includes(String(actual));
125
+ }
126
+ default:
127
+ return false;
128
+ }
129
+ }
130
+ function isChecklistComplete(config, progress) {
131
+ const done = new Set(progress.completedTaskIds);
132
+ return config.tasks.filter((t) => t.required).every((t) => done.has(t.id));
133
+ }
134
+ function isRuleGroup(r) {
135
+ return r.rules !== void 0;
136
+ }
137
+ function has(set, v) {
138
+ if (!set) return false;
139
+ return set instanceof Set ? set.has(v) : set.includes(v);
140
+ }
141
+ function resolveField(rule, ctx) {
142
+ switch (rule.field) {
143
+ case "user.attribute":
144
+ return rule.key ? getPath(ctx.userAttributes, rule.key) : void 0;
145
+ case "account.attribute":
146
+ return rule.key ? getPath(ctx.accountAttributes, rule.key) : void 0;
147
+ case "user.role":
148
+ return ctx.userAttributes.role;
149
+ case "user.plan":
150
+ return ctx.userAttributes.plan ?? ctx.accountAttributes.plan;
151
+ case "url.path":
152
+ return ctx.path;
153
+ case "url.full":
154
+ return ctx.url;
155
+ case "url.query":
156
+ return rule.key ? ctx.query[rule.key] : void 0;
157
+ case "device.type":
158
+ return ctx.device.type;
159
+ case "device.browser":
160
+ return ctx.device.browser;
161
+ case "device.os":
162
+ return ctx.device.os;
163
+ case "visitor.firstVisit":
164
+ return ctx.firstVisit;
165
+ case "visitor.returning":
166
+ return !ctx.firstVisit;
167
+ case "event.seen":
168
+ return rule.key ? has(ctx.eventsSeen, rule.key) : false;
169
+ case "guide.completed":
170
+ return rule.key ? has(ctx.completedGuides, rule.key) : false;
171
+ case "guide.dismissed":
172
+ return rule.key ? has(ctx.dismissedGuides, rule.key) : false;
173
+ case "guide.seen":
174
+ return rule.key ? has(ctx.seenGuides, rule.key) : false;
175
+ case "checklist.completed":
176
+ return rule.key ? has(ctx.completedChecklists, rule.key) : false;
177
+ case "audience.member":
178
+ return rule.key ? has(ctx.audienceIds, rule.key) : false;
179
+ case "datetime.now":
180
+ return ctx.now.toISOString();
181
+ case "datetime.dayOfWeek":
182
+ return ctx.now.getDay();
183
+ case "datetime.hour":
184
+ return ctx.now.getHours();
185
+ case "rollout.percentage":
186
+ return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
187
+ case "environment":
188
+ return ctx.environment;
189
+ case "locale":
190
+ return ctx.locale;
191
+ case "app.platform":
192
+ return ctx.platform ?? "web";
193
+ case "app.screen":
194
+ return ctx.screen;
195
+ case "app.version":
196
+ return ctx.appVersion;
197
+ default:
198
+ return void 0;
199
+ }
200
+ }
201
+ function toNumber(v) {
202
+ if (typeof v === "number") return v;
203
+ if (typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v))) return Number(v);
204
+ if (v instanceof Date) return v.getTime();
205
+ return null;
206
+ }
207
+ function toDateMs(v) {
208
+ if (v instanceof Date) return v.getTime();
209
+ if (typeof v === "string") {
210
+ const t = Date.parse(v);
211
+ return Number.isNaN(t) ? null : t;
212
+ }
213
+ if (typeof v === "number") return v;
214
+ return null;
215
+ }
216
+ function compare(op, actual, expected) {
217
+ switch (op) {
218
+ case "exists":
219
+ return actual !== void 0 && actual !== null && actual !== "";
220
+ case "not_exists":
221
+ return actual === void 0 || actual === null || actual === "";
222
+ case "is_true":
223
+ return actual === true || actual === "true";
224
+ case "is_false":
225
+ return actual === false || actual === "false";
226
+ case "equals":
227
+ if (typeof actual === "boolean") return String(actual) === String(expected);
228
+ return String(actual ?? "") === String(expected ?? "");
229
+ case "not_equals":
230
+ return String(actual ?? "") !== String(expected ?? "");
231
+ case "contains":
232
+ return typeof actual === "string" && actual.toLowerCase().includes(String(expected ?? "").toLowerCase());
233
+ case "not_contains":
234
+ return !(typeof actual === "string" && actual.toLowerCase().includes(String(expected ?? "").toLowerCase()));
235
+ case "starts_with":
236
+ return typeof actual === "string" && actual.startsWith(String(expected ?? ""));
237
+ case "ends_with":
238
+ return typeof actual === "string" && actual.endsWith(String(expected ?? ""));
239
+ case "gt":
240
+ case "gte":
241
+ case "lt":
242
+ case "lte": {
243
+ const a = toNumber(actual);
244
+ const b = toNumber(expected);
245
+ if (a === null || b === null) return false;
246
+ return op === "gt" ? a > b : op === "gte" ? a >= b : op === "lt" ? a < b : a <= b;
247
+ }
248
+ case "in": {
249
+ const list = Array.isArray(expected) ? expected : String(expected ?? "").split(",").map((s) => s.trim());
250
+ return list.includes(String(actual ?? ""));
251
+ }
252
+ case "not_in": {
253
+ const list = Array.isArray(expected) ? expected : String(expected ?? "").split(",").map((s) => s.trim());
254
+ return !list.includes(String(actual ?? ""));
255
+ }
256
+ case "regex": {
257
+ const re = safeRegExp(String(expected ?? ""));
258
+ return !!re && typeof actual === "string" && re.test(actual);
259
+ }
260
+ case "before": {
261
+ const a = toDateMs(actual);
262
+ const b = toDateMs(expected);
263
+ return a !== null && b !== null && a < b;
264
+ }
265
+ case "after": {
266
+ const a = toDateMs(actual);
267
+ const b = toDateMs(expected);
268
+ return a !== null && b !== null && a > b;
269
+ }
270
+ default:
271
+ return false;
272
+ }
273
+ }
274
+ function evaluateRule(rule, ctx) {
275
+ let result;
276
+ if (rule.field === "url.path" || rule.field === "url.full" || rule.field === "app.screen") {
277
+ const actual = String(resolveField(rule, ctx) ?? "");
278
+ if (rule.op === "equals") result = matchUrlPattern(String(rule.value ?? ""), actual);
279
+ else if (rule.op === "not_equals") result = !matchUrlPattern(String(rule.value ?? ""), actual);
280
+ else result = compare(rule.op, actual, rule.value);
281
+ } else if (rule.field === "app.version" && ["equals", "not_equals", "gt", "gte", "lt", "lte"].includes(rule.op)) {
282
+ const c = compareVersions(resolveField(rule, ctx), rule.value);
283
+ if (c === null) result = rule.op === "not_equals";
284
+ else result = rule.op === "equals" ? c === 0 : rule.op === "not_equals" ? c !== 0 : rule.op === "gt" ? c > 0 : rule.op === "gte" ? c >= 0 : rule.op === "lt" ? c < 0 : c <= 0;
285
+ } else {
286
+ result = compare(rule.op, resolveField(rule, ctx), rule.value);
287
+ }
288
+ return rule.negate ? !result : result;
289
+ }
290
+ function evaluateRules(group, ctx) {
291
+ if (!group || group.rules.length === 0) return group?.negate ? false : true;
292
+ const results = group.rules.map((r) => isRuleGroup(r) ? evaluateRules(r, ctx) : evaluateRule(r, ctx));
293
+ const value = group.op === "and" ? results.every(Boolean) : results.some(Boolean);
294
+ return group.negate ? !value : value;
295
+ }
296
+ var SENSITIVE_PROPERTY_PATTERN = /(password|passwd|secret|token|credit|card|cvv|ssn|iban)/i;
297
+ var MAX_PROPERTY_BYTES = 8 * 1024;
298
+ function sanitizeProperties(props) {
299
+ if (!props) return {};
300
+ const out = {};
301
+ let size = 0;
302
+ for (const [k, v] of Object.entries(props)) {
303
+ if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
304
+ if (typeof v === "function" || typeof v === "symbol") continue;
305
+ const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
306
+ size += k.length + (str?.length ?? 0);
307
+ if (size > MAX_PROPERTY_BYTES) break;
308
+ out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
309
+ }
310
+ return out;
311
+ }
312
+ var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
313
+ var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
314
+ var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
315
+ var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
316
+ var VIMEO_ID = /^\d{1,12}$/;
317
+ var VIMEO_HASH = /^[0-9a-f]{6,20}$/i;
318
+ var LOOM_ID = /^[0-9a-f]{32}$/i;
319
+ var WISTIA_ID = /^[a-z0-9]{10}$/i;
320
+ function parseUrl(input) {
321
+ const s = input.trim();
322
+ if (!s) return null;
323
+ try {
324
+ const u = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : `https://${s}`);
325
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
326
+ if (u.username || u.password || u.port && u.port !== "443" && u.port !== "80") return null;
327
+ return u;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ function parseStart(v) {
333
+ if (!v) return 0;
334
+ if (/^\d+$/.test(v)) return Math.min(Number(v), 86400);
335
+ const m = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(v);
336
+ if (!m || !m[0]) return 0;
337
+ return Math.min(Number(m[1] ?? 0) * 3600 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0), 86400);
338
+ }
339
+ function youtube(id, start) {
340
+ if (!YOUTUBE_ID.test(id)) return null;
341
+ return {
342
+ provider: "youtube",
343
+ src: `https://www.youtube-nocookie.com/embed/${id}${start ? `?start=${start}` : ""}`,
344
+ watchUrl: `https://www.youtube.com/watch?v=${id}${start ? `&t=${start}s` : ""}`,
345
+ title: "YouTube video"
346
+ };
347
+ }
348
+ function vimeo(id, hash) {
349
+ if (!VIMEO_ID.test(id) || hash !== null && !VIMEO_HASH.test(hash)) return null;
350
+ const h = hash ? hash.toLowerCase() : null;
351
+ return { provider: "vimeo", src: `https://player.vimeo.com/video/${id}${h ? `?h=${h}` : ""}`, watchUrl: `https://vimeo.com/${id}${h ? `/${h}` : ""}`, title: "Vimeo video" };
352
+ }
353
+ function toEmbedUrl(input) {
354
+ const u = parseUrl(input);
355
+ if (!u) return null;
356
+ const host = u.hostname.toLowerCase().replace(/^www\./, "");
357
+ const parts = u.pathname.split("/").filter(Boolean);
358
+ switch (host) {
359
+ case "youtube.com":
360
+ case "m.youtube.com":
361
+ case "youtube-nocookie.com": {
362
+ const start = parseStart(u.searchParams.get("start") ?? u.searchParams.get("t"));
363
+ if (parts[0] === "watch" && parts.length === 1) return youtube(u.searchParams.get("v") ?? "", start);
364
+ if ((parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "live") && parts.length === 2) return youtube(parts[1], start);
365
+ return null;
366
+ }
367
+ case "youtu.be":
368
+ return parts.length === 1 ? youtube(parts[0], parseStart(u.searchParams.get("t") ?? u.searchParams.get("start"))) : null;
369
+ case "vimeo.com":
370
+ if (parts.length === 1) return vimeo(parts[0], null);
371
+ if (parts.length === 2 && VIMEO_ID.test(parts[0])) return vimeo(parts[0], parts[1]);
372
+ return null;
373
+ case "player.vimeo.com":
374
+ return parts[0] === "video" && parts.length === 2 ? vimeo(parts[1], u.searchParams.get("h")) : null;
375
+ case "loom.com":
376
+ if ((parts[0] === "share" || parts[0] === "embed") && parts.length === 2 && LOOM_ID.test(parts[1])) {
377
+ const id = parts[1].toLowerCase();
378
+ return { provider: "loom", src: `https://www.loom.com/embed/${id}`, watchUrl: `https://www.loom.com/share/${id}`, title: "Loom video" };
379
+ }
380
+ return null;
381
+ default: {
382
+ let id;
383
+ if (host === "fast.wistia.net" || host === "fast.wistia.com") id = parts[0] === "embed" && parts[1] === "iframe" && parts.length === 3 ? parts[2] : void 0;
384
+ else if (host === "wi.st" || host === "wistia.com" || /^[a-z0-9-]+\.wistia\.com$/.test(host)) id = parts[0] === "medias" && parts.length === 2 ? parts[1] : void 0;
385
+ if (!id || !WISTIA_ID.test(id)) return null;
386
+ return { provider: "wistia", src: `https://fast.wistia.net/embed/iframe/${id}`, watchUrl: `https://fast.wistia.net/embed/iframe/${id}`, title: "Wistia video" };
387
+ }
388
+ }
389
+ }
390
+ function isAllowedEmbedSrc(src) {
391
+ if (!src) return false;
392
+ return toEmbedUrl(src)?.src === src;
393
+ }
394
+ function isSafeMediaUrl(url) {
395
+ if (!url) return false;
396
+ try {
397
+ const u = new URL(url);
398
+ return (u.protocol === "https:" || u.protocol === "http:") && !u.username && !u.password;
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+ var MAX_COLLECTION_DEPTH = 3;
404
+ function matchesAudiences(required, member) {
405
+ if (!required || required.length === 0) return true;
406
+ if (!member) return false;
407
+ const has2 = member instanceof Set ? (id) => member.has(id) : (id) => member.includes(id);
408
+ return required.some(has2);
409
+ }
410
+ function resourceCenterSections(collections, articles, member) {
411
+ const byId = new Map(collections.map((c) => [c.id, c]));
412
+ const children = /* @__PURE__ */ new Map();
413
+ for (const c of collections) {
414
+ const parent = c.parentId && c.parentId !== c.id && byId.has(c.parentId) ? c.parentId : null;
415
+ const list = children.get(parent) ?? [];
416
+ list.push(c);
417
+ children.set(parent, list);
418
+ }
419
+ for (const list of children.values()) list.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
420
+ const direct = /* @__PURE__ */ new Map();
421
+ const uncategorized = [];
422
+ for (const a of articles) {
423
+ if (!matchesAudiences(a.audienceIds, member)) continue;
424
+ if (a.collectionId && byId.has(a.collectionId)) {
425
+ const list = direct.get(a.collectionId) ?? [];
426
+ list.push(a);
427
+ direct.set(a.collectionId, list);
428
+ } else uncategorized.push(a);
429
+ }
430
+ const sections = [];
431
+ const seen = /* @__PURE__ */ new Set();
432
+ const visit = (c, path) => {
433
+ if (seen.has(c.id)) return;
434
+ seen.add(c.id);
435
+ if (!matchesAudiences(c.audienceIds, member)) return;
436
+ const here = [...path, c.name];
437
+ const list = direct.get(c.id);
438
+ if (list?.length) sections.push({ collection: c, path: here, depth: here.length - 1, articles: list });
439
+ for (const child of children.get(c.id) ?? []) visit(child, here);
440
+ };
441
+ for (const root of children.get(null) ?? []) visit(root, []);
442
+ if (uncategorized.length) sections.push({ collection: null, path: [], depth: 0, articles: uncategorized });
443
+ return sections;
444
+ }
445
+ function isArticleListed(collections, article, member) {
446
+ if (!matchesAudiences(article.audienceIds, member)) return false;
447
+ const byId = new Map(collections.map((c) => [c.id, c]));
448
+ let cur = article.collectionId ? byId.get(article.collectionId) : void 0;
449
+ for (let guard = 0; cur && guard <= MAX_COLLECTION_DEPTH * 4; guard++) {
450
+ if (!matchesAudiences(cur.audienceIds, member)) return false;
451
+ cur = cur.parentId ? byId.get(cur.parentId) : void 0;
452
+ }
453
+ return true;
454
+ }
455
+
456
+ // ../sdk-core/dist/chunk-GJT7CFAC.js
457
+ var __defProp2 = Object.defineProperty;
458
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
459
+ var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
460
+ var Api = class {
461
+ constructor(baseUrl, key, fetchImpl) {
462
+ __publicField2(this, "baseUrl", baseUrl);
463
+ __publicField2(this, "key", key);
464
+ __publicField2(this, "fetchImpl", fetchImpl);
465
+ }
466
+ url(path, params = {}) {
467
+ const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
468
+ return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
469
+ }
470
+ get fetcher() {
471
+ if (this.fetchImpl) return this.fetchImpl;
472
+ return typeof fetch === "function" ? fetch.bind(globalThis) : null;
473
+ }
474
+ async request(method, path, options = {}) {
475
+ const f = this.fetcher;
476
+ if (!f) throw new Error("fetch is not available");
477
+ const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
478
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
479
+ if (options.etag) headers["If-None-Match"] = options.etag;
480
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
481
+ const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
482
+ try {
483
+ const res = await f(this.url(path, options.params), {
484
+ method,
485
+ headers,
486
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
487
+ keepalive: options.keepalive,
488
+ signal: ac?.signal,
489
+ credentials: "omit",
490
+ mode: "cors"
491
+ });
492
+ let data = null;
493
+ if (res.status !== 304 && res.status !== 204) {
494
+ try {
495
+ data = await res.json();
496
+ } catch {
497
+ data = null;
498
+ }
499
+ }
500
+ return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
501
+ } finally {
502
+ if (timer) clearTimeout(timer);
503
+ }
504
+ }
505
+ get(path, options) {
506
+ return this.request("GET", path, options);
507
+ }
508
+ post(path, body, options) {
509
+ return this.request("POST", path, { body, ...options });
510
+ }
511
+ /**
512
+ * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
513
+ * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
514
+ */
515
+ async stream(path, body, onEvent, headers = {}) {
516
+ const f = this.fetcher;
517
+ if (!f) throw new Error("fetch is not available");
518
+ const res = await f(this.url(path), {
519
+ method: "POST",
520
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
521
+ body: JSON.stringify(body),
522
+ credentials: "omit",
523
+ mode: "cors"
524
+ });
525
+ const type = res.headers?.get?.("content-type") ?? "";
526
+ if (!type.includes("text/event-stream") || !res.body) {
527
+ let data = null;
528
+ try {
529
+ data = await res.json();
530
+ } catch {
531
+ data = null;
532
+ }
533
+ return { status: res.status, ok: res.ok, data, etag: null };
534
+ }
535
+ await readEventStream(res.body, onEvent);
536
+ return { status: res.status, ok: res.ok, data: null, etag: null };
537
+ }
538
+ /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
539
+ beacon(path, body) {
540
+ try {
541
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
542
+ const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
543
+ return navigator.sendBeacon(this.url(path), blob);
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+ };
549
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
550
+ function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
551
+ return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
552
+ }
553
+ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
554
+ for (let i = 0; i < attempts; i++) {
555
+ try {
556
+ const res = await api.post(path, body);
557
+ if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
558
+ } catch {
559
+ }
560
+ if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
561
+ }
562
+ return null;
563
+ }
564
+ async function readEventStream(body, onEvent) {
565
+ const reader = body.getReader();
566
+ const decoder = new TextDecoder();
567
+ let buffer = "";
568
+ const dispatch = (raw) => {
569
+ let event = "message";
570
+ const data = [];
571
+ for (const line of raw.split("\n")) {
572
+ if (!line || line.startsWith(":")) continue;
573
+ const i = line.indexOf(":");
574
+ const field = i < 0 ? line : line.slice(0, i);
575
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
576
+ if (field === "event") event = value;
577
+ else if (field === "data") data.push(value);
578
+ }
579
+ if (!data.length) return;
580
+ try {
581
+ onEvent(event, JSON.parse(data.join("\n")));
582
+ } catch {
583
+ }
584
+ };
585
+ for (; ; ) {
586
+ const { done, value } = await reader.read();
587
+ if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
588
+ let sep;
589
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
590
+ dispatch(buffer.slice(0, sep));
591
+ buffer = buffer.slice(sep + 2);
592
+ }
593
+ if (done) break;
594
+ }
595
+ if (buffer.trim()) dispatch(buffer);
596
+ }
597
+
598
+ // ../sdk-core/dist/chunk-HLR3W63Z.js
599
+ function readJson(store, key, fallback) {
600
+ const raw = store.get(key);
601
+ if (raw == null) return fallback;
602
+ try {
603
+ return JSON.parse(raw);
604
+ } catch {
605
+ return fallback;
606
+ }
607
+ }
608
+ function writeJson(store, key, value) {
609
+ try {
610
+ store.set(key, JSON.stringify(value));
611
+ } catch {
612
+ }
613
+ }
614
+ var KEYS = {
615
+ anon: "guidely:anon",
616
+ seen: "guidely:seen",
617
+ config: (key) => `guidely:config:${key}`,
618
+ etag: (key) => `guidely:etag:${key}`,
619
+ state: (key) => `guidely:state:${key}`,
620
+ user: (key) => `guidely:user:${key}`,
621
+ queue: (key) => `guidely:queue:${key}`,
622
+ session: "guidely:session",
623
+ active: "guidely:active",
624
+ authoring: "guidely:authoring",
625
+ /** Messenger conversations this browser can read: [{ id, token }]. */
626
+ conversations: (key) => `guidely:conversations:${key}`
627
+ };
628
+ var EventQueue = class {
629
+ constructor(opts) {
630
+ __publicField2(this, "opts", opts);
631
+ __publicField2(this, "events", []);
632
+ __publicField2(this, "timer", null);
633
+ __publicField2(this, "retryTimer", null);
634
+ __publicField2(this, "inflight", false);
635
+ __publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
636
+ __publicField2(this, "attempt", 0);
637
+ __publicField2(this, "destroyed", false);
638
+ __publicField2(this, "flushIntervalMs");
639
+ __publicField2(this, "maxQueue");
640
+ __publicField2(this, "batchSize");
641
+ __publicField2(this, "maxAttempts");
642
+ this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
643
+ this.maxQueue = opts.maxQueue ?? 200;
644
+ this.batchSize = opts.batchSize ?? 50;
645
+ this.maxAttempts = opts.maxAttempts ?? 5;
646
+ const persisted = readJson(opts.store, opts.storageKey, []);
647
+ if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
648
+ if (this.events.length) this.schedule();
649
+ }
650
+ get size() {
651
+ return this.events.length;
652
+ }
653
+ /** Pending events (read-only copy, for debugging). */
654
+ peek() {
655
+ return this.events.slice();
656
+ }
657
+ enqueue(event) {
658
+ if (this.destroyed) return;
659
+ if (this.events.some((e) => e.eventId === event.eventId)) return;
660
+ this.events.push(event);
661
+ if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);
662
+ this.persist();
663
+ this.schedule();
664
+ }
665
+ persist() {
666
+ writeJson(this.opts.store, this.opts.storageKey, this.events);
667
+ }
668
+ schedule() {
669
+ if (this.timer || this.retryTimer || this.destroyed) return;
670
+ this.timer = setTimeout(() => {
671
+ this.timer = null;
672
+ void this.flush();
673
+ }, this.flushIntervalMs);
674
+ }
675
+ /** Sends queued events now (one batch at a time). Safe to call at any time. */
676
+ async flush() {
677
+ if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;
678
+ if (this.timer) {
679
+ clearTimeout(this.timer);
680
+ this.timer = null;
681
+ }
682
+ this.inflight = true;
683
+ const batch = this.events.slice(0, this.batchSize);
684
+ this.inflightIds = new Set(batch.map((e) => e.eventId));
685
+ let ok = false;
686
+ let retry = true;
687
+ try {
688
+ const res = await this.opts.send(batch);
689
+ ok = res.ok;
690
+ retry = res.retry ?? !res.ok;
691
+ } catch (err) {
692
+ this.opts.onError?.(err);
693
+ }
694
+ this.inflight = false;
695
+ this.inflightIds.clear();
696
+ if (this.destroyed) return;
697
+ if (ok || !retry) {
698
+ const ids = new Set(batch.map((e) => e.eventId));
699
+ this.events = this.events.filter((e) => !ids.has(e.eventId));
700
+ this.attempt = 0;
701
+ this.persist();
702
+ if (this.events.length) this.schedule();
703
+ return;
704
+ }
705
+ this.attempt += 1;
706
+ if (this.attempt >= this.maxAttempts) {
707
+ this.attempt = 0;
708
+ return;
709
+ }
710
+ this.retryTimer = setTimeout(() => {
711
+ this.retryTimer = null;
712
+ void this.flush();
713
+ }, backoffMs(this.attempt - 1));
714
+ }
715
+ /** Best-effort synchronous flush on unload using `sendBeacon`. */
716
+ flushSync() {
717
+ if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
718
+ const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
719
+ if (!batch.length) return;
720
+ if (this.opts.beacon(batch)) {
721
+ const ids = new Set(batch.map((e) => e.eventId));
722
+ this.events = this.events.filter((e) => !ids.has(e.eventId));
723
+ this.persist();
724
+ }
725
+ }
726
+ clear() {
727
+ this.events = [];
728
+ this.persist();
729
+ }
730
+ destroy() {
731
+ this.destroyed = true;
732
+ if (this.timer) clearTimeout(this.timer);
733
+ if (this.retryTimer) clearTimeout(this.retryTimer);
734
+ this.timer = null;
735
+ this.retryTimer = null;
736
+ this.persist();
737
+ }
738
+ };
739
+ var Emitter = class {
740
+ constructor(onError) {
741
+ __publicField2(this, "onError", onError);
742
+ __publicField2(this, "listeners", /* @__PURE__ */ new Map());
743
+ }
744
+ on(event, cb) {
745
+ let set = this.listeners.get(event);
746
+ if (!set) {
747
+ set = /* @__PURE__ */ new Set();
748
+ this.listeners.set(event, set);
749
+ }
750
+ set.add(cb);
751
+ return () => this.off(event, cb);
752
+ }
753
+ off(event, cb) {
754
+ this.listeners.get(event)?.delete(cb);
755
+ }
756
+ emit(event, ...args) {
757
+ const set = this.listeners.get(event);
758
+ if (!set) return;
759
+ for (const cb of Array.from(set)) {
760
+ try {
761
+ cb(...args);
762
+ } catch (err) {
763
+ this.onError?.(err);
764
+ }
765
+ }
766
+ }
767
+ clear() {
768
+ this.listeners.clear();
769
+ }
770
+ };
771
+ var HOUR = 3600 * 1e3;
772
+ function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
773
+ const s = state ?? { impressions: 0 };
774
+ if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
775
+ if (s.completedAt && freq.mode !== "every_time") return false;
776
+ if (s.dismissedAt) {
777
+ if (!freq.reshowAfterDismiss) return false;
778
+ if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
779
+ }
780
+ switch (freq.mode) {
781
+ case "once":
782
+ return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
783
+ case "once_per_session":
784
+ return !shownThisSession;
785
+ case "interval": {
786
+ if (!s.lastShownAt) return true;
787
+ const hours = freq.intervalHours ?? 24;
788
+ return now - Date.parse(s.lastShownAt) >= hours * HOUR;
789
+ }
790
+ case "every_time":
791
+ default:
792
+ return true;
793
+ }
794
+ }
795
+ function isExclusiveGuide(guide) {
796
+ if (guide.kind === "banner" || guide.kind === "checklist") return false;
797
+ const steps = guide.config.steps;
798
+ if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
799
+ return true;
800
+ }
801
+ function areMutuallyExclusive(a, b) {
802
+ const la = a.config.frequency.mutuallyExclusiveWith;
803
+ const lb = b.config.frequency.mutuallyExclusiveWith;
804
+ return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
805
+ }
806
+ function isWithinSchedule(guide, now = Date.now()) {
807
+ const s = guide.config.survey;
808
+ if (!s) return true;
809
+ if (s.startAt && now < Date.parse(s.startAt)) return false;
810
+ if (s.endAt && now > Date.parse(s.endAt)) return false;
811
+ return true;
812
+ }
813
+
814
+ // src/core/targets.ts
815
+ var TargetTimeoutError = class extends Error {
816
+ constructor(ids) {
817
+ super(`[Guidely] target not found: ${ids.join(", ") || "(no mobile-compatible selector)"}`);
818
+ this.name = "TargetTimeoutError";
819
+ }
820
+ };
821
+ function targetIdsFor(selectors) {
822
+ const ids = [];
823
+ for (const s of selectors) {
824
+ let id = null;
825
+ if (s.kind === "guidely-id" || s.kind === "id") id = s.value.trim().replace(/^#/, "") || null;
826
+ else if (s.kind === "css" || s.kind === "data-attr") id = guidelyIdFromSelector(s.value);
827
+ if (id && !ids.includes(id)) ids.push(id);
828
+ }
829
+ return ids;
830
+ }
831
+ var TargetRegistry = class {
832
+ constructor() {
833
+ __publicField(this, "targets", /* @__PURE__ */ new Map());
834
+ __publicField(this, "changeListeners", /* @__PURE__ */ new Set());
835
+ __publicField(this, "tapListeners", /* @__PURE__ */ new Set());
836
+ __publicField(this, "viewport", null);
837
+ __publicField(this, "screen", null);
838
+ }
839
+ register(id, handle) {
840
+ const list = this.targets.get(id) ?? [];
841
+ list.push(handle);
842
+ this.targets.set(id, list);
843
+ this.changed();
844
+ return () => {
845
+ const cur = this.targets.get(id);
846
+ if (!cur) return;
847
+ const next = cur.filter((h) => h !== handle);
848
+ if (next.length) this.targets.set(id, next);
849
+ else this.targets.delete(id);
850
+ this.changed();
851
+ };
852
+ }
853
+ /** Window size used for the on-screen check. */
854
+ setViewport(width, height) {
855
+ this.viewport = { width, height };
856
+ }
857
+ /** The current screen; targets scoped to other screens are treated as hidden. */
858
+ setScreen(name) {
859
+ if (name === this.screen) return;
860
+ this.screen = name;
861
+ this.changed();
862
+ }
863
+ has(id) {
864
+ return this.targets.has(id);
865
+ }
866
+ ids() {
867
+ return Array.from(this.targets.keys());
868
+ }
869
+ /** Call when a target's layout may have changed (onLayout, scroll). */
870
+ changed() {
871
+ for (const cb of Array.from(this.changeListeners)) {
872
+ try {
873
+ cb();
874
+ } catch {
875
+ }
876
+ }
877
+ }
878
+ onChange(cb) {
879
+ this.changeListeners.add(cb);
880
+ return () => this.changeListeners.delete(cb);
881
+ }
882
+ tapped(id) {
883
+ for (const cb of Array.from(this.tapListeners)) {
884
+ try {
885
+ cb(id);
886
+ } catch {
887
+ }
888
+ }
889
+ }
890
+ onTap(cb) {
891
+ this.tapListeners.add(cb);
892
+ return () => this.tapListeners.delete(cb);
893
+ }
894
+ /** Frame of a visible target (the most recently mounted visible one when an id is used twice), else null. */
895
+ async measure(id) {
896
+ const list = this.targets.get(id) ?? [];
897
+ for (let i = list.length - 1; i >= 0; i--) {
898
+ const handle = list[i];
899
+ if (handle.screen && this.screen && handle.screen !== this.screen) continue;
900
+ const r = await this.measureHandle(handle);
901
+ if (r) return r;
902
+ }
903
+ return null;
904
+ }
905
+ async measureHandle(handle) {
906
+ try {
907
+ const r = await handle.measure();
908
+ if (!r || !(r.width > 0) || !(r.height > 0)) return null;
909
+ const vp = this.viewport;
910
+ if (vp && (r.x + r.width <= 0 || r.y + r.height <= 0 || r.x >= vp.width || r.y >= vp.height)) return null;
911
+ return r;
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+ /** First of `ids` that is mounted and visible. */
917
+ async resolve(ids) {
918
+ for (const id of ids) {
919
+ const rect = await this.measure(id);
920
+ if (rect) return { id, rect };
921
+ }
922
+ return null;
923
+ }
924
+ /** Resolves when one of `ids` is visible; `timeoutMs` 0 waits forever. Re-checks on registry changes and every 300ms. */
925
+ waitFor(ids, options) {
926
+ return new Promise((resolve, reject) => {
927
+ let done = false;
928
+ let checking = false;
929
+ let again = false;
930
+ const timers = [];
931
+ const finish = (fn) => {
932
+ if (done) return;
933
+ done = true;
934
+ off();
935
+ timers.forEach((t) => clearTimeout(t));
936
+ timers.forEach((t) => clearInterval(t));
937
+ options.signal?.removeEventListener?.("abort", onAbort);
938
+ fn();
939
+ };
940
+ const check = async () => {
941
+ if (done) return;
942
+ if (checking) {
943
+ again = true;
944
+ return;
945
+ }
946
+ checking = true;
947
+ const hit = ids.length ? await this.resolve(ids) : null;
948
+ checking = false;
949
+ if (hit) return finish(() => resolve(hit));
950
+ if (again) {
951
+ again = false;
952
+ void check();
953
+ }
954
+ };
955
+ const onAbort = () => finish(() => reject(Object.assign(new Error("aborted"), { name: "AbortError" })));
956
+ if (options.signal?.aborted) return onAbort();
957
+ options.signal?.addEventListener?.("abort", onAbort);
958
+ const off = this.onChange(() => void check());
959
+ timers.push(setInterval(() => void check(), 300));
960
+ if (options.timeoutMs > 0) timers.push(setTimeout(() => finish(() => reject(new TargetTimeoutError(ids))), options.timeoutMs));
961
+ void check();
962
+ });
963
+ }
964
+ };
965
+
966
+ // src/core/controller.ts
967
+ var ANCHORED = /* @__PURE__ */ new Set(["tooltip", "popover", "spotlight", "hotspot"]);
968
+ var isAnchoredStep = (step) => ANCHORED.has(step.type);
969
+ function visibleButtons(step, canGoBack) {
970
+ return canGoBack ? step.buttons : step.buttons.filter((b) => b.action !== "back");
971
+ }
972
+ function screenMatches(pattern, url) {
973
+ if (!pattern) return true;
974
+ if (matchUrlPattern(pattern, url)) return true;
975
+ if (!pattern.includes("?") && !pattern.includes("#")) return matchUrlPattern(pattern, url.split(/[?#]/)[0] ?? url);
976
+ return false;
977
+ }
978
+ function runGuide(guide, o) {
979
+ const steps = guide.config.steps;
980
+ const total = steps.length;
981
+ const onError = o.onError ?? (() => {
982
+ });
983
+ let status = "idle";
984
+ let index = -1;
985
+ let pendingIndex = -1;
986
+ let rendered = false;
987
+ let started = false;
988
+ let stepDone = false;
989
+ let ended = false;
990
+ const history = [];
991
+ const stepCleanups = [];
992
+ const emit = (name, step, stepIndex, properties) => {
993
+ try {
994
+ o.onEvent({ name, stepId: step?.id, stepIndex, properties });
995
+ } catch (err) {
996
+ onError(err);
997
+ }
998
+ };
999
+ const clearStep = () => {
1000
+ stepCleanups.splice(0).forEach((fn) => {
1001
+ try {
1002
+ fn();
1003
+ } catch {
1004
+ }
1005
+ });
1006
+ if (rendered) {
1007
+ rendered = false;
1008
+ try {
1009
+ o.onClear();
1010
+ } catch (err) {
1011
+ onError(err);
1012
+ }
1013
+ }
1014
+ stepDone = false;
1015
+ };
1016
+ const end = (reason) => {
1017
+ if (ended) return;
1018
+ ended = true;
1019
+ const step = steps[index];
1020
+ clearStep();
1021
+ status = "ended";
1022
+ if (reason === "completed") emit("guide_completed", step, index);
1023
+ else if (reason === "dismissed") emit("guide_dismissed", step, index);
1024
+ else if (reason === "skipped") emit("guide_skipped", step, index);
1025
+ try {
1026
+ o.onEnd(reason);
1027
+ } catch (err) {
1028
+ onError(err);
1029
+ }
1030
+ };
1031
+ const completeStep = () => {
1032
+ if (stepDone || index < 0) return;
1033
+ stepDone = true;
1034
+ emit("step_completed", steps[index], index);
1035
+ };
1036
+ const goNext = (overrideStepId) => {
1037
+ const step = steps[index];
1038
+ let target = -1;
1039
+ const ctx = o.ctx();
1040
+ if (step) {
1041
+ for (const branch of step.branches) {
1042
+ if (evaluateRules(branch.when, ctx)) {
1043
+ target = steps.findIndex((s) => s.id === branch.goTo);
1044
+ if (target >= 0) break;
1045
+ }
1046
+ }
1047
+ }
1048
+ if (target < 0 && overrideStepId) target = steps.findIndex((s) => s.id === overrideStepId);
1049
+ if (target < 0) target = index + 1;
1050
+ if (target >= total) return end("completed");
1051
+ history.push(index);
1052
+ showStep(target, 1);
1053
+ };
1054
+ const advance = () => {
1055
+ if (ended) return;
1056
+ completeStep();
1057
+ goNext();
1058
+ };
1059
+ const press = (b) => {
1060
+ try {
1061
+ switch (b.action) {
1062
+ case "next":
1063
+ completeStep();
1064
+ goNext(b.value);
1065
+ break;
1066
+ case "back":
1067
+ controller.back();
1068
+ break;
1069
+ case "dismiss":
1070
+ end("dismissed");
1071
+ break;
1072
+ case "complete":
1073
+ completeStep();
1074
+ end("completed");
1075
+ break;
1076
+ case "skip":
1077
+ end("skipped");
1078
+ break;
1079
+ case "url":
1080
+ completeStep();
1081
+ o.onNavigate(b.value || "");
1082
+ if (index + 1 >= total) end("completed");
1083
+ else goNext();
1084
+ break;
1085
+ case "start_guide":
1086
+ completeStep();
1087
+ end("completed");
1088
+ o.onAction("start_guide", b.value);
1089
+ break;
1090
+ case "event":
1091
+ o.onAction("event", b.value);
1092
+ break;
1093
+ case "open_resource_center":
1094
+ o.onAction("open_resource_center", b.value);
1095
+ break;
1096
+ default:
1097
+ break;
1098
+ }
1099
+ } catch (err) {
1100
+ onError(err);
1101
+ }
1102
+ };
1103
+ const setupAdvance = (step, targetId) => {
1104
+ const modes = step.advanceOn === "any" ? ["element_click", "element_visible", "event", "delay"] : [step.advanceOn];
1105
+ for (const mode of modes) {
1106
+ const id = guidelyIdFromSelector(step.advanceValue) ?? (mode === "element_click" ? targetId : null);
1107
+ if (mode === "element_click" && id) {
1108
+ stepCleanups.push(o.targets.onTap((tapped) => tapped === id && advance()));
1109
+ } else if (mode === "element_visible" && id) {
1110
+ const ac = new AbortController();
1111
+ stepCleanups.push(() => ac.abort());
1112
+ o.targets.waitFor([id], { timeoutMs: 0, signal: ac.signal }).then(() => !ac.signal.aborted && advance(), () => {
1113
+ });
1114
+ } else if (mode === "event" && step.advanceValue) {
1115
+ stepCleanups.push(o.eventBus.on(step.advanceValue, advance));
1116
+ } else if (mode === "delay") {
1117
+ const t = setTimeout(advance, step.advanceDelayMs ?? 3e3);
1118
+ stepCleanups.push(() => clearTimeout(t));
1119
+ }
1120
+ }
1121
+ };
1122
+ const render = (step, targetId, asModal = false) => {
1123
+ if (ended) return;
1124
+ status = "running";
1125
+ try {
1126
+ o.onRender({ step, index, total, canGoBack: history.length > 0 || index > 0, targetId, asModal });
1127
+ rendered = true;
1128
+ } catch (err) {
1129
+ onError(err);
1130
+ if (index + 1 < total) return goNext();
1131
+ return end("skipped");
1132
+ }
1133
+ if (!started) {
1134
+ started = true;
1135
+ emit("guide_started", step, index);
1136
+ }
1137
+ emit("step_viewed", step, index, asModal ? { fallback: "show_as_modal" } : void 0);
1138
+ setupAdvance(step, targetId);
1139
+ };
1140
+ const applyFallback = (step, direction) => {
1141
+ const fallback = step.target?.fallback ?? "skip";
1142
+ emit("step_viewed", step, index, { fallback, targetMissing: true });
1143
+ if (fallback === "show_as_modal") return render(step, null, true);
1144
+ if (fallback === "end_guide") return end("skipped");
1145
+ const nextIndex = index + direction;
1146
+ if (nextIndex < 0) return showStep(0, 1);
1147
+ if (nextIndex >= total) return end("completed");
1148
+ showStep(nextIndex, direction);
1149
+ };
1150
+ function showStep(i, direction = 1) {
1151
+ if (ended) return;
1152
+ clearStep();
1153
+ if (i < 0) i = 0;
1154
+ if (i >= total) return end("completed");
1155
+ index = i;
1156
+ const step = steps[i];
1157
+ if (step.targeting && step.targeting.rules.length && !evaluateRules(step.targeting, o.ctx())) {
1158
+ const nextIndex = i + direction;
1159
+ if (nextIndex >= total) return end("completed");
1160
+ if (nextIndex < 0) return;
1161
+ return showStep(nextIndex, direction);
1162
+ }
1163
+ if (step.url && !screenMatches(step.url, o.currentUrl())) {
1164
+ status = "waiting_url";
1165
+ pendingIndex = i;
1166
+ return;
1167
+ }
1168
+ pendingIndex = -1;
1169
+ if (isAnchoredStep(step) && step.target) {
1170
+ status = "waiting_target";
1171
+ const ids = targetIdsFor(step.target.selectors);
1172
+ if (!ids.length) return applyFallback(step, direction);
1173
+ const ac = new AbortController();
1174
+ stepCleanups.push(() => ac.abort());
1175
+ const timeoutMs = step.target.fallback === "wait" ? 0 : step.target.waitMs;
1176
+ o.targets.waitFor(ids, { timeoutMs, signal: ac.signal }).then(
1177
+ (hit) => {
1178
+ if (ac.signal.aborted || ended) return;
1179
+ render(step, hit.id);
1180
+ },
1181
+ (err) => {
1182
+ if (ac.signal.aborted || ended || err?.name === "AbortError") return;
1183
+ applyFallback(step, direction);
1184
+ }
1185
+ );
1186
+ return;
1187
+ }
1188
+ render(step, null);
1189
+ }
1190
+ const controller = {
1191
+ guide,
1192
+ start() {
1193
+ if (status !== "idle") return;
1194
+ emit("guide_viewed");
1195
+ if (total === 0) return end("completed");
1196
+ let startIndex = o.startAtStep ? steps.findIndex((s) => s.id === o.startAtStep) : 0;
1197
+ if (startIndex < 0) startIndex = 0;
1198
+ showStep(startIndex, 1);
1199
+ },
1200
+ next() {
1201
+ if (ended || index < 0) return;
1202
+ completeStep();
1203
+ goNext();
1204
+ },
1205
+ back() {
1206
+ if (ended) return;
1207
+ const prev = history.pop();
1208
+ if (prev == null) {
1209
+ if (index > 0) showStep(index - 1, -1);
1210
+ return;
1211
+ }
1212
+ showStep(prev, -1);
1213
+ },
1214
+ goTo(stepId) {
1215
+ const i = steps.findIndex((s) => s.id === stepId);
1216
+ if (i >= 0 && !ended) {
1217
+ if (index >= 0) history.push(index);
1218
+ showStep(i, 1);
1219
+ }
1220
+ },
1221
+ dismiss: () => end("dismissed"),
1222
+ complete() {
1223
+ completeStep();
1224
+ end("completed");
1225
+ },
1226
+ skip: () => end("skipped"),
1227
+ press,
1228
+ resume() {
1229
+ if (ended) return;
1230
+ if (status === "waiting_url" && pendingIndex >= 0) return showStep(pendingIndex, 1);
1231
+ const step = steps[index];
1232
+ if (step?.url && status === "running" && !screenMatches(step.url, o.currentUrl())) showStep(index, 1);
1233
+ },
1234
+ destroy() {
1235
+ if (ended) return;
1236
+ ended = true;
1237
+ clearStep();
1238
+ status = "ended";
1239
+ try {
1240
+ o.onEnd("destroyed");
1241
+ } catch (err) {
1242
+ onError(err);
1243
+ }
1244
+ },
1245
+ getState: () => ({ status, stepId: steps[index]?.id ?? null, stepIndex: index, total })
1246
+ };
1247
+ return controller;
1248
+ }
1249
+
1250
+ // src/core/messenger.ts
1251
+ var TOKEN_HEADER = "X-Guidely-Conversation";
1252
+ var MAX_STORED = 50;
1253
+ function createSseParser(onEvent) {
1254
+ let buffer = "";
1255
+ const dispatch = (raw) => {
1256
+ let event = "message";
1257
+ const data = [];
1258
+ for (const line of raw.split("\n")) {
1259
+ if (!line || line.startsWith(":")) continue;
1260
+ const i = line.indexOf(":");
1261
+ const field = i < 0 ? line : line.slice(0, i);
1262
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
1263
+ if (field === "event") event = value;
1264
+ else if (field === "data") data.push(value);
1265
+ }
1266
+ if (!data.length) return;
1267
+ try {
1268
+ onEvent(event, JSON.parse(data.join("\n")));
1269
+ } catch {
1270
+ }
1271
+ };
1272
+ return {
1273
+ feed(chunk) {
1274
+ buffer += chunk.replace(/\r\n?/g, "\n");
1275
+ let sep;
1276
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
1277
+ dispatch(buffer.slice(0, sep));
1278
+ buffer = buffer.slice(sep + 2);
1279
+ }
1280
+ },
1281
+ end() {
1282
+ if (buffer.trim()) dispatch(buffer);
1283
+ buffer = "";
1284
+ }
1285
+ };
1286
+ }
1287
+ function xhrStreamPost() {
1288
+ if (typeof XMLHttpRequest === "undefined") return null;
1289
+ return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
1290
+ const xhr = new XMLHttpRequest();
1291
+ let seen = 0;
1292
+ let streamed = false;
1293
+ const pump = () => {
1294
+ const type = xhr.getResponseHeader("content-type") || "";
1295
+ if (!type.includes("text/event-stream")) return;
1296
+ streamed = true;
1297
+ const text = xhr.responseText || "";
1298
+ if (text.length > seen) {
1299
+ onChunk(text.slice(seen));
1300
+ seen = text.length;
1301
+ }
1302
+ };
1303
+ xhr.open("POST", url);
1304
+ for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
1305
+ xhr.onprogress = pump;
1306
+ xhr.onload = () => {
1307
+ pump();
1308
+ let json = null;
1309
+ if (!streamed) {
1310
+ try {
1311
+ json = JSON.parse(xhr.responseText || "null");
1312
+ } catch {
1313
+ json = null;
1314
+ }
1315
+ }
1316
+ resolve({ status: xhr.status, json, streamed });
1317
+ };
1318
+ xhr.onerror = () => reject(new Error("Network request failed"));
1319
+ xhr.send(JSON.stringify(body));
1320
+ });
1321
+ }
1322
+ function fetchStreamPost(fetchImpl) {
1323
+ return async (url, headers, body, onChunk) => {
1324
+ const res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
1325
+ const type = res.headers.get("content-type") || "";
1326
+ const text = await res.text();
1327
+ if (type.includes("text/event-stream")) {
1328
+ onChunk(text);
1329
+ return { status: res.status, json: null, streamed: true };
1330
+ }
1331
+ let json = null;
1332
+ try {
1333
+ json = JSON.parse(text || "null");
1334
+ } catch {
1335
+ json = null;
1336
+ }
1337
+ return { status: res.status, json, streamed: false };
1338
+ };
1339
+ }
1340
+ var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
1341
+ var Messenger = class {
1342
+ constructor(config, deps) {
1343
+ __publicField(this, "deps", deps);
1344
+ __publicField(this, "snapshot");
1345
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
1346
+ __publicField(this, "threadTimer", null);
1347
+ __publicField(this, "backgroundTimer", null);
1348
+ __publicField(this, "foreground", true);
1349
+ __publicField(this, "destroyed", false);
1350
+ __publicField(this, "poll");
1351
+ this.poll = deps.pollMs ?? { thread: 5e3, background: 6e4 };
1352
+ this.snapshot = { config, open: false, view: { kind: "tab", tab: "home" }, conversations: [], thread: null, unread: 0, articles: null, article: null };
1353
+ void this.refreshConversations();
1354
+ this.startBackgroundPolling();
1355
+ }
1356
+ // -------------------------------------------------------------------------- state
1357
+ getSnapshot() {
1358
+ return this.snapshot;
1359
+ }
1360
+ subscribe(cb) {
1361
+ this.listeners.add(cb);
1362
+ return () => this.listeners.delete(cb);
1363
+ }
1364
+ set(patch) {
1365
+ if (this.destroyed) return;
1366
+ const next = { ...this.snapshot, ...patch };
1367
+ next.unread = next.conversations.filter((c) => c.unread > 0).length;
1368
+ this.snapshot = next;
1369
+ for (const cb of Array.from(this.listeners)) {
1370
+ try {
1371
+ cb();
1372
+ } catch (err) {
1373
+ this.deps.onError(err);
1374
+ }
1375
+ }
1376
+ }
1377
+ setThread(patch) {
1378
+ const t = this.snapshot.thread;
1379
+ if (!t) return;
1380
+ this.set({ thread: { ...t, ...patch } });
1381
+ }
1382
+ updateConfig(config) {
1383
+ this.set({ config });
1384
+ }
1385
+ // -------------------------------------------------------------------------- tokens
1386
+ stored() {
1387
+ const list = readJson(this.deps.store, KEYS.conversations(this.deps.publicKey), []);
1388
+ return Array.isArray(list) ? list.filter((c) => c && typeof c.id === "string" && typeof c.token === "string") : [];
1389
+ }
1390
+ remember(id, token) {
1391
+ writeJson(this.deps.store, KEYS.conversations(this.deps.publicKey), [{ id, token }, ...this.stored().filter((c) => c.id !== id)].slice(0, MAX_STORED));
1392
+ }
1393
+ tokenFor(id) {
1394
+ return this.stored().find((c) => c.id === id)?.token ?? "";
1395
+ }
1396
+ /** Forgets every conversation (logout). */
1397
+ forgetAll() {
1398
+ this.deps.store.remove(KEYS.conversations(this.deps.publicKey));
1399
+ this.stopThreadPolling();
1400
+ this.set({ open: false, view: { kind: "tab", tab: "home" }, conversations: [], thread: null, article: null });
1401
+ }
1402
+ // -------------------------------------------------------------------------- http
1403
+ url(path, params = {}) {
1404
+ const query = Object.entries({ key: this.deps.publicKey, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1405
+ return `${this.deps.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
1406
+ }
1407
+ headers(extra = {}) {
1408
+ return { "X-Guidely-Key": this.deps.publicKey, Accept: "application/json", "Content-Type": "application/json", ...extra };
1409
+ }
1410
+ async request(method, path, opts = {}) {
1411
+ const headers = this.headers(opts.conversationId ? { [TOKEN_HEADER]: this.tokenFor(opts.conversationId) } : {});
1412
+ const res = await this.deps.fetch(this.url(path, opts.params), { method, headers, body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body) });
1413
+ if (!res.ok) throw new Error(`Request failed (${res.status})`);
1414
+ return await res.json();
1415
+ }
1416
+ // -------------------------------------------------------------------------- navigation
1417
+ open(tab) {
1418
+ const wasOpen = this.snapshot.open;
1419
+ this.set({ open: true, ...tab ? { view: { kind: "tab", tab } } : {} });
1420
+ if (!wasOpen) {
1421
+ this.deps.onOpen();
1422
+ void this.refreshConversations();
1423
+ }
1424
+ if (tab === "help" || this.snapshot.view.kind === "tab") this.onView();
1425
+ }
1426
+ close() {
1427
+ this.stopThreadPolling();
1428
+ this.set({ open: false });
1429
+ }
1430
+ setTab(tab) {
1431
+ this.stopThreadPolling();
1432
+ this.set({ view: { kind: "tab", tab }, thread: null });
1433
+ this.onView();
1434
+ }
1435
+ onView() {
1436
+ const v = this.snapshot.view;
1437
+ if (v.kind === "tab" && v.tab === "help" && !this.snapshot.articles) void this.loadArticles();
1438
+ if (v.kind === "tab" && v.tab === "messages") void this.refreshConversations();
1439
+ }
1440
+ /** Opens a new thread (optionally pre-filling the composer). */
1441
+ showNewMessage(text = "") {
1442
+ this.stopThreadPolling();
1443
+ if (!this.snapshot.open) this.deps.onOpen();
1444
+ this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text } });
1445
+ }
1446
+ async openConversation(id) {
1447
+ const known = this.snapshot.conversations.find((c) => c.id === id) ?? null;
1448
+ this.stopThreadPolling();
1449
+ this.set({ open: true, view: { kind: "thread", conversationId: id }, thread: { conversation: known, messages: [], streaming: {}, sending: false, error: null, draft: "" } });
1450
+ try {
1451
+ const res = await this.request("GET", `/conversations/${encodeURIComponent(id)}/messages`, { conversationId: id });
1452
+ this.markRead(res.conversation);
1453
+ this.setThread({ conversation: { ...res.conversation, unread: 0 }, messages: res.messages });
1454
+ } catch (err) {
1455
+ this.setThread({ error: "Couldn't load this conversation." });
1456
+ this.deps.onError(err);
1457
+ }
1458
+ this.scheduleThreadPoll();
1459
+ }
1460
+ back() {
1461
+ const v = this.snapshot.view;
1462
+ if (v.kind === "article") return this.set({ view: v.from, article: null });
1463
+ this.setTab(this.snapshot.thread?.conversation ? "messages" : "home");
1464
+ }
1465
+ // -------------------------------------------------------------------------- conversations
1466
+ upsertConversation(c) {
1467
+ const list = [c, ...this.snapshot.conversations.filter((x) => x.id !== c.id)].sort(byNewest);
1468
+ this.set({ conversations: list });
1469
+ }
1470
+ markRead(c) {
1471
+ this.upsertConversation({ ...c, unread: 0 });
1472
+ }
1473
+ async refreshConversations() {
1474
+ const stored = this.stored();
1475
+ if (!stored.length) return;
1476
+ try {
1477
+ const res = await this.request("POST", "/conversations/lookup", { body: { tokens: stored.map((c) => c.token) } });
1478
+ const known = new Set(res.conversations.map((c) => c.id));
1479
+ if (stored.some((c) => !known.has(c.id))) writeJson(this.deps.store, KEYS.conversations(this.deps.publicKey), stored.filter((c) => known.has(c.id)));
1480
+ const openId = this.snapshot.view.kind === "thread" ? this.snapshot.view.conversationId : null;
1481
+ const list = res.conversations.map(({ token: _token, ...c }) => c.id === openId && this.snapshot.open ? { ...c, unread: 0 } : c).sort(byNewest);
1482
+ this.set({ conversations: list });
1483
+ } catch (err) {
1484
+ this.deps.onError(err);
1485
+ }
1486
+ }
1487
+ /** Sends the composer text: starts a conversation or posts to the open one. Streams Scout's reply. */
1488
+ async send(text) {
1489
+ const body = text.trim().slice(0, 4e3);
1490
+ const thread = this.snapshot.thread;
1491
+ if (!body || !thread || thread.sending) return;
1492
+ this.setThread({ sending: true, error: null, draft: "" });
1493
+ const conversationId = thread.conversation?.id ?? null;
1494
+ const path = conversationId ? `/conversations/${encodeURIComponent(conversationId)}/messages` : "/conversations";
1495
+ const payload = conversationId ? { message: body } : { anonymousId: this.deps.anonymousId(), userId: this.deps.userId(), message: body, pageUrl: this.deps.pageUrl() };
1496
+ const headers = { ...this.headers(conversationId ? { [TOKEN_HEADER]: this.tokenFor(conversationId) } : {}), Accept: "text/event-stream, application/json" };
1497
+ let finished = false;
1498
+ const parser = createSseParser((event, data) => {
1499
+ if (event === "conversation") {
1500
+ const d = data;
1501
+ this.remember(d.conversation.id, d.token);
1502
+ this.markRead(d.conversation);
1503
+ this.set({ view: { kind: "thread", conversationId: d.conversation.id } });
1504
+ this.setThread({ conversation: { ...d.conversation, unread: 0 } });
1505
+ d.messages.forEach((m) => this.addMessage(m));
1506
+ } else if (event === "message") this.addMessage(data);
1507
+ else if (event === "delta") {
1508
+ const d = data;
1509
+ const t = this.snapshot.thread;
1510
+ if (t) this.setThread({ streaming: { ...t.streaming, [d.messageId]: (t.streaming[d.messageId] ?? "") + d.text } });
1511
+ } else if (event === "done") {
1512
+ finished = true;
1513
+ const d = data;
1514
+ if (d.message) this.addMessage(d.message);
1515
+ this.markRead(d.conversation);
1516
+ this.setThread({ conversation: { ...d.conversation, unread: 0 } });
1517
+ } else if (event === "error") {
1518
+ this.setThread({ error: data?.message ?? "Something went wrong" });
1519
+ }
1520
+ });
1521
+ try {
1522
+ const res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
1523
+ parser.end();
1524
+ if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
1525
+ if (!res.streamed && res.json) {
1526
+ const d = res.json;
1527
+ if ("token" in d) {
1528
+ this.remember(d.conversation.id, d.token);
1529
+ this.set({ view: { kind: "thread", conversationId: d.conversation.id } });
1530
+ }
1531
+ this.markRead(d.conversation);
1532
+ this.setThread({ conversation: { ...d.conversation, unread: 0 } });
1533
+ d.messages.forEach((m) => this.addMessage(m));
1534
+ } else if (res.streamed && !finished) this.setThread({ error: "The reply was interrupted. Pull to refresh in a moment." });
1535
+ } catch (err) {
1536
+ this.setThread({ error: "Your message wasn't sent. Check your connection and try again.", draft: body });
1537
+ this.deps.onError(err);
1538
+ } finally {
1539
+ const t = this.snapshot.thread;
1540
+ if (t) this.setThread({ sending: false, streaming: {} });
1541
+ this.scheduleThreadPoll();
1542
+ }
1543
+ }
1544
+ addMessage(m) {
1545
+ const t = this.snapshot.thread;
1546
+ if (!t) return;
1547
+ const exists = t.messages.some((x) => x.id === m.id);
1548
+ const messages = exists ? t.messages.map((x) => x.id === m.id ? m : x) : [...t.messages, m];
1549
+ const streaming = { ...t.streaming };
1550
+ if (m.status !== "pending") delete streaming[m.id];
1551
+ this.setThread({ messages, streaming });
1552
+ if (!exists && m.status !== "pending" && (m.author === "scout" || m.author === "agent") && t.conversation) this.deps.onMessage(m, t.conversation);
1553
+ }
1554
+ async handoff() {
1555
+ const id = this.snapshot.thread?.conversation?.id;
1556
+ if (!id) return;
1557
+ try {
1558
+ const res = await this.request("POST", `/conversations/${encodeURIComponent(id)}/handoff`, { body: {}, conversationId: id });
1559
+ this.markRead(res.conversation);
1560
+ this.setThread({ conversation: { ...res.conversation, unread: 0 } });
1561
+ res.messages.forEach((m) => this.addMessage(m));
1562
+ } catch (err) {
1563
+ this.setThread({ error: "Couldn't reach the team. Try again." });
1564
+ this.deps.onError(err);
1565
+ }
1566
+ }
1567
+ async rate(rating) {
1568
+ const c = this.snapshot.thread?.conversation;
1569
+ if (!c) return;
1570
+ this.setThread({ conversation: { ...c, rating } });
1571
+ try {
1572
+ await this.request("POST", `/conversations/${encodeURIComponent(c.id)}/rating`, { body: { rating }, conversationId: c.id });
1573
+ } catch (err) {
1574
+ this.deps.onError(err);
1575
+ }
1576
+ }
1577
+ startGuide(key) {
1578
+ this.close();
1579
+ this.deps.onStartGuide(key);
1580
+ }
1581
+ // -------------------------------------------------------------------------- help
1582
+ async loadArticles() {
1583
+ try {
1584
+ this.set({ articles: await this.request("GET", "/articles") });
1585
+ } catch (err) {
1586
+ this.deps.onError(err);
1587
+ }
1588
+ }
1589
+ async openArticle(id) {
1590
+ const from = this.snapshot.view.kind === "article" ? this.snapshot.view.from : this.snapshot.view;
1591
+ this.stopThreadPolling();
1592
+ this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
1593
+ try {
1594
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
1595
+ this.set({ article: res.article });
1596
+ } catch (err) {
1597
+ this.deps.onError(err);
1598
+ }
1599
+ }
1600
+ async search(q) {
1601
+ try {
1602
+ return (await this.request("GET", "/resource-center/search", { params: { q } })).articles;
1603
+ } catch {
1604
+ return [];
1605
+ }
1606
+ }
1607
+ // -------------------------------------------------------------------------- polling
1608
+ /** Called with app foreground/background changes; polling only runs in the foreground. */
1609
+ setForeground(active) {
1610
+ this.foreground = active;
1611
+ if (!active) {
1612
+ this.stopThreadPolling();
1613
+ return;
1614
+ }
1615
+ void this.refreshConversations();
1616
+ if (this.snapshot.open && this.snapshot.view.kind === "thread") this.scheduleThreadPoll();
1617
+ }
1618
+ startBackgroundPolling() {
1619
+ this.backgroundTimer = setInterval(() => {
1620
+ if (this.foreground && !(this.snapshot.open && this.snapshot.view.kind === "thread")) void this.refreshConversations();
1621
+ }, this.poll.background);
1622
+ }
1623
+ scheduleThreadPoll() {
1624
+ this.stopThreadPolling();
1625
+ if (this.destroyed || !this.foreground) return;
1626
+ this.threadTimer = setTimeout(() => void this.pollThread(), this.poll.thread);
1627
+ }
1628
+ stopThreadPolling() {
1629
+ if (this.threadTimer) clearTimeout(this.threadTimer);
1630
+ this.threadTimer = null;
1631
+ }
1632
+ async pollThread() {
1633
+ const t = this.snapshot.thread;
1634
+ const id = t?.conversation?.id;
1635
+ if (!t || !id || !this.snapshot.open || t.sending) return this.scheduleThreadPoll();
1636
+ const since = t.messages[t.messages.length - 1]?.createdAt;
1637
+ try {
1638
+ const res = await this.request("GET", `/conversations/${encodeURIComponent(id)}/messages`, { params: { since }, conversationId: id });
1639
+ if (this.snapshot.thread?.conversation?.id !== id) return;
1640
+ this.markRead(res.conversation);
1641
+ this.setThread({ conversation: { ...res.conversation, unread: 0 } });
1642
+ res.messages.forEach((m) => this.addMessage(m));
1643
+ } catch (err) {
1644
+ this.deps.onError(err);
1645
+ }
1646
+ this.scheduleThreadPoll();
1647
+ }
1648
+ destroy() {
1649
+ this.destroyed = true;
1650
+ this.stopThreadPolling();
1651
+ if (this.backgroundTimer) clearInterval(this.backgroundTimer);
1652
+ this.listeners.clear();
1653
+ }
1654
+ };
1655
+
1656
+ // src/core/storage.ts
1657
+ var PersistentStore = class {
1658
+ constructor(backing, onError = () => {
1659
+ }) {
1660
+ __publicField(this, "backing", backing);
1661
+ __publicField(this, "onError", onError);
1662
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
1663
+ __publicField(this, "chain", Promise.resolve());
1664
+ }
1665
+ async hydrate(keys) {
1666
+ if (!this.backing) return;
1667
+ await Promise.all(
1668
+ keys.map(async (k) => {
1669
+ try {
1670
+ const v = await this.backing.getItem(k);
1671
+ if (v != null && !this.cache.has(k)) this.cache.set(k, v);
1672
+ } catch (err) {
1673
+ this.onError(err);
1674
+ }
1675
+ })
1676
+ );
1677
+ }
1678
+ get(key) {
1679
+ return this.cache.get(key) ?? null;
1680
+ }
1681
+ set(key, value) {
1682
+ if (this.cache.get(key) === value) return;
1683
+ this.cache.set(key, value);
1684
+ this.write(() => this.backing?.setItem(key, value));
1685
+ }
1686
+ remove(key) {
1687
+ if (!this.cache.has(key)) return;
1688
+ this.cache.delete(key);
1689
+ this.write(() => this.backing?.removeItem(key));
1690
+ }
1691
+ /** Resolves once every pending write has reached the backing storage. */
1692
+ flush() {
1693
+ return this.chain.then(() => void 0);
1694
+ }
1695
+ write(op) {
1696
+ if (!this.backing) return;
1697
+ this.chain = this.chain.then(async () => {
1698
+ try {
1699
+ await op();
1700
+ } catch (err) {
1701
+ this.onError(err);
1702
+ }
1703
+ });
1704
+ }
1705
+ };
1706
+
1707
+ // src/core/client.ts
1708
+ var SDK_NAME = "@guidelytech/react-native";
1709
+ var SDK_VERSION = "0.5.0";
1710
+ var DEFAULT_API_URL = "https://guidelytech.com";
1711
+ var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1712
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1713
+ var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
1714
+ var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
1715
+ var GuidelyClient = class {
1716
+ constructor(options, bridge) {
1717
+ __publicField(this, "options");
1718
+ __publicField(this, "targets", new TargetRegistry());
1719
+ __publicField(this, "bridge");
1720
+ __publicField(this, "api");
1721
+ __publicField(this, "store");
1722
+ __publicField(this, "emitter");
1723
+ __publicField(this, "queue", null);
1724
+ __publicField(this, "config", null);
1725
+ __publicField(this, "configFetchedAt", 0);
1726
+ __publicField(this, "etag", null);
1727
+ __publicField(this, "anonymousId", "");
1728
+ __publicField(this, "user", null);
1729
+ __publicField(this, "state", emptyState());
1730
+ __publicField(this, "session", newSession());
1731
+ __publicField(this, "consent");
1732
+ __publicField(this, "readyPromise", null);
1733
+ __publicField(this, "hydrated", false);
1734
+ __publicField(this, "pending", []);
1735
+ __publicField(this, "isReady", false);
1736
+ __publicField(this, "destroyed", false);
1737
+ __publicField(this, "active", null);
1738
+ __publicField(this, "activeSurvey", null);
1739
+ __publicField(this, "queued", []);
1740
+ __publicField(this, "flows", /* @__PURE__ */ new Map());
1741
+ __publicField(this, "checklists", /* @__PURE__ */ new Map());
1742
+ __publicField(this, "expanded", /* @__PURE__ */ new Set());
1743
+ __publicField(this, "surveyView", null);
1744
+ __publicField(this, "resourceCenterOpen", false);
1745
+ __publicField(this, "cleanups", []);
1746
+ __publicField(this, "timers", /* @__PURE__ */ new Set());
1747
+ __publicField(this, "visibilityWatchers", /* @__PURE__ */ new Map());
1748
+ __publicField(this, "firedDelays", /* @__PURE__ */ new Set());
1749
+ __publicField(this, "firstVisit", true);
1750
+ __publicField(this, "screenName", "");
1751
+ __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
1752
+ __publicField(this, "backgroundedAt", null);
1753
+ __publicField(this, "messenger", null);
1754
+ __publicField(this, "ui", { steps: [], checklists: [], survey: null, resourceCenterOpen: false, theme: {}, messenger: false });
1755
+ __publicField(this, "uiListeners", /* @__PURE__ */ new Set());
1756
+ __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1757
+ __publicField(this, "eventBus", {
1758
+ on: (name, cb) => {
1759
+ let set = this.customListeners.get(name);
1760
+ if (!set) {
1761
+ set = /* @__PURE__ */ new Set();
1762
+ this.customListeners.set(name, set);
1763
+ }
1764
+ set.add(cb);
1765
+ return () => void set?.delete(cb);
1766
+ }
1767
+ });
1768
+ if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
1769
+ this.options = {
1770
+ ...options,
1771
+ environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
1772
+ apiUrl: options.apiUrl ?? DEFAULT_API_URL,
1773
+ analytics: options.analytics ?? true,
1774
+ configRefreshMs: options.configRefreshMs ?? 5 * 60 * 1e3,
1775
+ debug: options.debug ?? false
1776
+ };
1777
+ this.bridge = bridge;
1778
+ this.consent = options.consent ?? "granted";
1779
+ this.emitter = new Emitter((err) => this.reportError(err));
1780
+ this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
1781
+ this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
1782
+ if (!options.storage) this.log("no `storage` passed: identity and guide history last only for this app session. Pass AsyncStorage to persist them.");
1783
+ this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
1784
+ }
1785
+ // ---------------------------------------------------------------------------
1786
+ // Lifecycle
1787
+ // ---------------------------------------------------------------------------
1788
+ /** Loads persisted state and config, identifies the stored user and evaluates triggers. Idempotent. */
1789
+ init() {
1790
+ if (!this.readyPromise) this.readyPromise = this.bootstrap();
1791
+ return this.readyPromise;
1792
+ }
1793
+ ready() {
1794
+ return this.init();
1795
+ }
1796
+ async bootstrap() {
1797
+ const key = this.options.projectId;
1798
+ try {
1799
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.conversations(key)]);
1800
+ if (this.destroyed) return;
1801
+ this.anonymousId = this.store.get(KEYS.anon) || "";
1802
+ if (!this.anonymousId) {
1803
+ this.anonymousId = `anon_${randomId(20)}`;
1804
+ this.store.set(KEYS.anon, this.anonymousId);
1805
+ }
1806
+ this.firstVisit = !this.store.get(KEYS.seen);
1807
+ this.store.set(KEYS.seen, nowIso());
1808
+ this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(key), {}) };
1809
+ this.user = this.options.user ? { ...this.options.user } : readJson(this.store, KEYS.user(key), null);
1810
+ const saved = readJson(this.store, KEYS.session, null);
1811
+ this.session = saved?.id && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
1812
+ this.persistSession();
1813
+ this.lastAudiences = new Set(this.state.audienceIds);
1814
+ this.queue = new EventQueue({
1815
+ store: this.store,
1816
+ storageKey: KEYS.queue(key),
1817
+ enabled: () => this.analyticsEnabled(),
1818
+ send: async (events) => {
1819
+ const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
1820
+ return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
1821
+ },
1822
+ onError: (err) => this.reportError(err)
1823
+ });
1824
+ this.hydrated = true;
1825
+ const cached = readJson(this.store, KEYS.config(key), null);
1826
+ this.etag = this.store.get(KEYS.etag(key));
1827
+ if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
1828
+ this.syncMessenger();
1829
+ this.pending.splice(0).forEach((fn) => fn());
1830
+ await this.fetchConfig();
1831
+ if (this.destroyed) return;
1832
+ this.wireTimers();
1833
+ if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1834
+ if (this.destroyed) return;
1835
+ this.isReady = true;
1836
+ if (this.config) this.emitter.emit("ready", this.config);
1837
+ this.notify();
1838
+ this.evaluate("ready");
1839
+ } catch (err) {
1840
+ this.reportError(err);
1841
+ this.isReady = true;
1842
+ }
1843
+ }
1844
+ whenHydrated(fn) {
1845
+ if (this.destroyed) return;
1846
+ if (this.hydrated) fn();
1847
+ else this.pending.push(fn);
1848
+ }
1849
+ async fetchConfig() {
1850
+ try {
1851
+ const res = await this.api.get("/config", { etag: this.etag, params: { platform: this.bridge.device.platform } });
1852
+ this.configFetchedAt = Date.now();
1853
+ if (res.status === 304) return true;
1854
+ if (res.ok && res.data && Array.isArray(res.data.guides)) {
1855
+ this.config = forPlatform(res.data, this.bridge.device.platform);
1856
+ this.etag = res.etag;
1857
+ writeJson(this.store, KEYS.config(this.options.projectId), res.data);
1858
+ if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
1859
+ else this.store.remove(KEYS.etag(this.options.projectId));
1860
+ this.syncMessenger();
1861
+ this.emitter.emit("config", this.config);
1862
+ this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
1863
+ return true;
1864
+ }
1865
+ this.reportError(new Error(`[Guidely] config request failed (${res.status})${this.config ? "; using cached config" : ""}`));
1866
+ return false;
1867
+ } catch (err) {
1868
+ this.reportError(new Error(`[Guidely] config request failed: ${err instanceof Error ? err.message : String(err)}${this.config ? "; using cached config" : ""}`));
1869
+ return false;
1870
+ }
1871
+ }
1872
+ wireTimers() {
1873
+ const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
1874
+ this.cleanups.push(() => clearInterval(refresh));
1875
+ const poll = setInterval(() => {
1876
+ if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
1877
+ const url = this.currentUrl();
1878
+ const ctx = this.getContext();
1879
+ for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
1880
+ this.drainQueue(url, ctx);
1881
+ }, 1e3);
1882
+ this.cleanups.push(() => clearInterval(poll));
1883
+ }
1884
+ /** Re-fetches the published config and re-evaluates triggers. */
1885
+ async refresh() {
1886
+ if (this.destroyed) return;
1887
+ const before = this.config?.configVersion;
1888
+ await this.fetchConfig();
1889
+ if (this.config && this.config.configVersion !== before) {
1890
+ this.notify();
1891
+ this.evaluate("ready");
1892
+ }
1893
+ }
1894
+ /** Called by the provider with React Native `AppState` changes. */
1895
+ setAppState(state) {
1896
+ if (this.destroyed || !this.hydrated) return;
1897
+ if (state === "background") {
1898
+ this.backgroundedAt = Date.now();
1899
+ this.session.lastActiveAt = nowIso();
1900
+ this.persistSession();
1901
+ void this.queue?.flush();
1902
+ void this.store.flush();
1903
+ this.messenger?.setForeground(false);
1904
+ } else if (state === "active" && this.backgroundedAt !== null) {
1905
+ this.messenger?.setForeground(true);
1906
+ const away = this.backgroundedAt;
1907
+ this.backgroundedAt = null;
1908
+ if (this.sessionExpired(away)) {
1909
+ this.session = newSession();
1910
+ this.persistSession();
1911
+ this.firedDelays.clear();
1912
+ }
1913
+ if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
1914
+ else if (this.isReady) this.evaluate("ready");
1915
+ }
1916
+ }
1917
+ sessionExpired(lastActiveMs) {
1918
+ const minutes = this.config?.settings?.sessionTimeoutMinutes ?? 30;
1919
+ return !Number.isFinite(lastActiveMs) || Date.now() - lastActiveMs > minutes * 6e4;
1920
+ }
1921
+ /** Tears everything down. Create a new client to start again. */
1922
+ destroy() {
1923
+ if (this.destroyed) return;
1924
+ this.destroyed = true;
1925
+ this.active?.controller.destroy();
1926
+ this.active = null;
1927
+ this.flows.forEach((f) => f.controller.destroy());
1928
+ this.flows.clear();
1929
+ this.queued = [];
1930
+ this.messenger?.destroy();
1931
+ this.messenger = null;
1932
+ this.visibilityWatchers.forEach((stop) => stop());
1933
+ this.visibilityWatchers.clear();
1934
+ this.timers.forEach((t) => clearTimeout(t));
1935
+ this.timers.clear();
1936
+ this.cleanups.splice(0).forEach((fn) => fn());
1937
+ void this.queue?.flush();
1938
+ this.queue?.destroy();
1939
+ this.emitter.clear();
1940
+ this.uiListeners.clear();
1941
+ }
1942
+ // ---------------------------------------------------------------------------
1943
+ // Identity
1944
+ // ---------------------------------------------------------------------------
1945
+ /** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
1946
+ identify(userId, traits, account) {
1947
+ return new Promise(
1948
+ (resolve) => this.whenHydrated(() => {
1949
+ const id = String(userId);
1950
+ const same = this.user?.id === id;
1951
+ this.user = { id, traits: { ...same ? this.user?.traits : {}, ...traits || {} }, account: account ?? (same ? this.user?.account : void 0) };
1952
+ writeJson(this.store, KEYS.user(this.options.projectId), this.user);
1953
+ this.enqueue("identify", { properties: { traits: sanitizeProperties(this.user.traits) } });
1954
+ void this.syncIdentify().then(() => {
1955
+ this.emitter.emit("identify", { userId: id, anonymousId: this.anonymousId });
1956
+ if (this.isReady) this.evaluate("identify");
1957
+ resolve();
1958
+ });
1959
+ })
1960
+ );
1961
+ }
1962
+ async syncIdentify() {
1963
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? this.currentUrl() : void 0 };
1964
+ const res = await postWithRetry(this.api, "/identify", body, 2);
1965
+ if (res?.ok && res.data) this.mergeServerState(res.data);
1966
+ }
1967
+ mergeServerState(data) {
1968
+ const guides = { ...this.state.guides };
1969
+ for (const [id, s] of Object.entries(data.guideState || {})) {
1970
+ const local = guides[id];
1971
+ guides[id] = {
1972
+ impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
1973
+ completedAt: local?.completedAt ?? s.completedAt ?? null,
1974
+ dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
1975
+ lastShownAt: local?.lastShownAt ?? s.lastShownAt ?? null
1976
+ };
1977
+ }
1978
+ const checklists = { ...this.state.checklists };
1979
+ for (const [id, p] of Object.entries(data.checklistProgress || {})) {
1980
+ const local = checklists[id];
1981
+ checklists[id] = {
1982
+ completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
1983
+ completedAt: local?.completedAt ?? p.completedAt ?? null,
1984
+ dismissedAt: local?.dismissedAt ?? p.dismissedAt ?? null
1985
+ };
1986
+ }
1987
+ this.state = {
1988
+ guides,
1989
+ checklists,
1990
+ surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
1991
+ audienceIds: data.audienceIds || [],
1992
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
1993
+ };
1994
+ if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
1995
+ this.persistState();
1996
+ this.notify();
1997
+ }
1998
+ /** Sets or replaces the account (company / organisation) for the current user. */
1999
+ async setAccount(account) {
2000
+ await this.init();
2001
+ if (!this.user?.id) {
2002
+ this.user = { id: "", traits: this.user?.traits, account };
2003
+ writeJson(this.store, KEYS.user(this.options.projectId), this.user);
2004
+ return;
2005
+ }
2006
+ await this.identify(this.user.id, void 0, account);
2007
+ }
2008
+ /** Merges additional traits into the current user. */
2009
+ async updateTraits(traits) {
2010
+ await this.init();
2011
+ if (!this.user?.id) {
2012
+ this.user = { id: "", traits: { ...this.user?.traits || {}, ...traits }, account: this.user?.account };
2013
+ return;
2014
+ }
2015
+ await this.identify(this.user.id, traits, this.user.account);
2016
+ }
2017
+ /** Forgets the user (call on logout): new anonymous id, cleared guide/checklist state, new session. */
2018
+ reset() {
2019
+ this.whenHydrated(() => {
2020
+ this.stopGuide();
2021
+ this.flows.forEach((f) => f.controller.destroy());
2022
+ this.flows.clear();
2023
+ this.checklists.clear();
2024
+ this.surveyView = null;
2025
+ this.activeSurvey = null;
2026
+ this.resourceCenterOpen = false;
2027
+ if (this.messenger) this.messenger.forgetAll();
2028
+ else this.store.remove(KEYS.conversations(this.options.projectId));
2029
+ this.user = null;
2030
+ this.store.remove(KEYS.user(this.options.projectId));
2031
+ this.anonymousId = `anon_${randomId(20)}`;
2032
+ this.store.set(KEYS.anon, this.anonymousId);
2033
+ this.state = emptyState();
2034
+ this.persistState();
2035
+ this.session = newSession();
2036
+ this.persistSession();
2037
+ this.lastAudiences = /* @__PURE__ */ new Set();
2038
+ this.firedDelays.clear();
2039
+ this.firstVisit = true;
2040
+ this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
2041
+ this.notify();
2042
+ if (this.isReady) this.evaluate("identify");
2043
+ });
2044
+ }
2045
+ // ---------------------------------------------------------------------------
2046
+ // Events & screens
2047
+ // ---------------------------------------------------------------------------
2048
+ /** Updates consent at runtime (`granted` flushes buffered events; `denied` drops them). */
2049
+ setConsent(consent) {
2050
+ this.consent = consent;
2051
+ this.whenHydrated(() => {
2052
+ if (consent === "denied") this.queue?.clear();
2053
+ else if (consent === "granted") void this.queue?.flush();
2054
+ });
2055
+ }
2056
+ analyticsEnabled() {
2057
+ return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
2058
+ }
2059
+ collectUrls() {
2060
+ return this.config?.settings?.collectUrls ?? true;
2061
+ }
2062
+ currentUrl() {
2063
+ return screenPath(this.screenName);
2064
+ }
2065
+ enqueue(name, extra = {}) {
2066
+ if (this.consent === "denied" || !this.options.analytics || !this.queue) return null;
2067
+ const event = {
2068
+ eventId: randomId(24),
2069
+ name,
2070
+ occurredAt: nowIso(),
2071
+ anonymousId: this.anonymousId,
2072
+ userId: this.user?.id || null,
2073
+ accountId: this.user?.account?.id || null,
2074
+ sessionId: this.session.id,
2075
+ url: this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
2076
+ ...extra
2077
+ };
2078
+ if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2079
+ this.queue.enqueue(event);
2080
+ this.emitter.emit("track", event);
2081
+ return event;
2082
+ }
2083
+ /** Tracks a custom event. Events drive `event` triggers, `advanceOn: "event"` steps and checklist tasks. */
2084
+ track(name, properties) {
2085
+ if (!name) return;
2086
+ this.whenHydrated(() => {
2087
+ if (!this.session.events.includes(name)) {
2088
+ this.session.events.push(name);
2089
+ this.persistSession();
2090
+ }
2091
+ this.enqueue("custom", { customName: name.slice(0, 120), properties: properties ?? {} });
2092
+ this.customListeners.get(name)?.forEach((cb) => {
2093
+ try {
2094
+ cb();
2095
+ } catch (err) {
2096
+ this.reportError(err);
2097
+ }
2098
+ });
2099
+ this.completeChecklistTasksBy("event", name);
2100
+ if (this.isReady) this.evaluate("event", { eventName: name });
2101
+ });
2102
+ }
2103
+ /**
2104
+ * Records a screen view (the mobile equivalent of a page view) and re-evaluates triggers. URL
2105
+ * patterns, `url_match` triggers, multi-screen steps and `url_visited` tasks match `/<name>`.
2106
+ */
2107
+ screen(name, properties) {
2108
+ if (!name) return;
2109
+ this.whenHydrated(() => {
2110
+ const changed = name !== this.screenName;
2111
+ this.screenName = name;
2112
+ this.targets.setScreen(name);
2113
+ const url = this.currentUrl();
2114
+ if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
2115
+ this.completeChecklistTasksBy("url_visited", url);
2116
+ this.active?.controller.resume();
2117
+ this.flows.forEach((f) => f.controller.resume());
2118
+ if (this.active) {
2119
+ const st = this.active.controller.getState();
2120
+ const g = this.active.guide;
2121
+ if (st.status === "running" && !this.active.manual && g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => screenMatches(p, url))) this.active.controller.destroy();
2122
+ }
2123
+ this.flows.forEach((f) => {
2124
+ const patterns = f.guide.config.urlPatterns;
2125
+ if (!f.manual && patterns.length && !patterns.some((p) => screenMatches(p, url))) f.controller.destroy();
2126
+ });
2127
+ this.firedDelays.clear();
2128
+ if (this.isReady) this.evaluate("navigation");
2129
+ if (changed) void this.queue?.flush();
2130
+ });
2131
+ }
2132
+ /** The current screen name (last value passed to `screen()`). */
2133
+ getScreen() {
2134
+ return this.screenName;
2135
+ }
2136
+ handleTap(targetId) {
2137
+ if (!this.hydrated) return;
2138
+ this.completeChecklistTasksBy("click", targetId);
2139
+ if (this.isReady) this.evaluate("click", { targetId });
2140
+ }
2141
+ // ---------------------------------------------------------------------------
2142
+ // Targeting + triggers
2143
+ // ---------------------------------------------------------------------------
2144
+ /** Builds the targeting context for the current moment. */
2145
+ getContext() {
2146
+ const guides = this.state.guides;
2147
+ const keysOf = (ids) => ids.flatMap((id) => [id, this.findGuide(id)?.key ?? id]);
2148
+ const url = this.currentUrl();
2149
+ const device = this.bridge.device;
2150
+ return {
2151
+ anonymousId: this.anonymousId,
2152
+ userId: this.user?.id || null,
2153
+ userAttributes: this.user?.traits ?? {},
2154
+ accountAttributes: this.user?.account?.traits ?? {},
2155
+ url,
2156
+ path: url,
2157
+ query: {},
2158
+ device: { type: device.deviceType, os: device.platform },
2159
+ firstVisit: this.firstVisit,
2160
+ eventsSeen: new Set(this.session.events),
2161
+ completedGuides: new Set(keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt))),
2162
+ dismissedGuides: new Set(keysOf(Object.keys(guides).filter((id) => guides[id]?.dismissedAt))),
2163
+ seenGuides: new Set(keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0))),
2164
+ completedChecklists: new Set(keysOf(Object.entries(this.state.checklists).filter(([, p]) => !!p.completedAt).map(([id]) => id))),
2165
+ audienceIds: new Set(this.state.audienceIds),
2166
+ now: /* @__PURE__ */ new Date(),
2167
+ environment: this.options.environment,
2168
+ locale: device.locale,
2169
+ platform: device.platform,
2170
+ screen: this.screenName,
2171
+ appVersion: this.options.appVersion ?? device.appVersion
2172
+ };
2173
+ }
2174
+ isTargeted(guide, ctx, url) {
2175
+ const c = guide.config;
2176
+ if (c.urlPatterns.length && !c.urlPatterns.some((p) => screenMatches(p, url))) return false;
2177
+ if (c.trigger.urlPattern && !screenMatches(c.trigger.urlPattern, url)) return false;
2178
+ if (!isWithinSchedule(guide)) return false;
2179
+ if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
2180
+ ctx.rolloutSalt = guide.id;
2181
+ return evaluateRules(c.targeting, ctx);
2182
+ }
2183
+ isEligible(guide, ctx, url) {
2184
+ if (!this.isTargeted(guide, ctx, url)) return false;
2185
+ if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2186
+ return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2187
+ }
2188
+ triggerFires(guide, reason, detail) {
2189
+ const t = guide.config.trigger;
2190
+ switch (t.type) {
2191
+ case "page_load":
2192
+ if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
2193
+ return reason === "ready" || reason === "navigation" || reason === "identify";
2194
+ case "url_match":
2195
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && screenMatches(t.value, this.currentUrl());
2196
+ case "event":
2197
+ return reason === "event" && !!t.value && detail.eventName === t.value;
2198
+ case "element_click":
2199
+ return reason === "click" && !!detail.targetId && guidelyIdFromSelector(t.value) === detail.targetId;
2200
+ case "element_visible":
2201
+ return reason === "visible" && detail.guideId === guide.id;
2202
+ case "delay":
2203
+ return reason === "timer" && detail.guideId === guide.id;
2204
+ case "after_guide":
2205
+ return reason === "after_guide" && !!detail.endedGuide && (detail.endedGuide.id === t.value || detail.endedGuide.key === t.value);
2206
+ case "segment_enter":
2207
+ return reason === "identify" && !!t.value && !!detail.newAudiences?.has(t.value);
2208
+ case "manual":
2209
+ default:
2210
+ return false;
2211
+ }
2212
+ }
2213
+ evaluate(reason, detail = {}) {
2214
+ if (!this.config || this.destroyed) return;
2215
+ const url = this.currentUrl();
2216
+ const ctx = this.getContext();
2217
+ if (reason === "identify") {
2218
+ const current = new Set(this.state.audienceIds);
2219
+ detail.newAudiences = new Set(Array.from(current).filter((a) => !this.lastAudiences.has(a)));
2220
+ this.lastAudiences = current;
2221
+ }
2222
+ const candidates = [];
2223
+ for (const guide of this.config.guides) {
2224
+ const t = guide.config.trigger;
2225
+ if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2226
+ if (reason === "ready" || reason === "navigation") {
2227
+ if (t.type === "element_visible" && t.value) this.armVisibility(guide, ctx, url);
2228
+ if ((t.type === "delay" || t.type === "page_load" && t.delayMs > 0) && !this.firedDelays.has(guide.id)) {
2229
+ if (!this.isEligible(guide, ctx, url)) continue;
2230
+ this.firedDelays.add(guide.id);
2231
+ this.setTimer(() => this.evaluate("timer", { guideId: guide.id }), t.delayMs);
2232
+ continue;
2233
+ }
2234
+ }
2235
+ if (t.type === "page_load" && t.delayMs > 0 && reason !== "timer") continue;
2236
+ if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2237
+ if (!this.triggerFires(guide, reason, detail)) continue;
2238
+ if (!this.isEligible(guide, ctx, url)) continue;
2239
+ candidates.push(guide);
2240
+ }
2241
+ if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
2242
+ if (!candidates.length) return;
2243
+ candidates.sort((a, b) => a.config.frequency.priority - b.config.frequency.priority);
2244
+ for (const guide of candidates) {
2245
+ if (this.isShowing(guide.id)) continue;
2246
+ const delay = guide.config.trigger.type !== "delay" && guide.config.trigger.type !== "page_load" ? guide.config.trigger.delayMs : 0;
2247
+ if (delay > 0) this.setTimer(() => this.tryShow(guide, url, false), delay);
2248
+ else this.tryShow(guide, url, false);
2249
+ }
2250
+ }
2251
+ armVisibility(guide, ctx, url) {
2252
+ const id = guidelyIdFromSelector(guide.config.trigger.value);
2253
+ if (!id || this.visibilityWatchers.has(guide.id) || !this.isEligible(guide, ctx, url)) return;
2254
+ const ac = new AbortController();
2255
+ const stop = () => {
2256
+ ac.abort();
2257
+ this.visibilityWatchers.delete(guide.id);
2258
+ };
2259
+ this.visibilityWatchers.set(guide.id, stop);
2260
+ this.targets.waitFor([id], { timeoutMs: 0, signal: ac.signal }).then(
2261
+ () => {
2262
+ if (ac.signal.aborted) return;
2263
+ stop();
2264
+ this.evaluate("visible", { guideId: guide.id });
2265
+ },
2266
+ () => stop()
2267
+ );
2268
+ }
2269
+ setTimer(fn, ms) {
2270
+ const t = setTimeout(() => {
2271
+ this.timers.delete(t);
2272
+ if (!this.destroyed) fn();
2273
+ }, ms);
2274
+ this.timers.add(t);
2275
+ }
2276
+ isShowing(id) {
2277
+ return this.flows.has(id) || this.checklists.has(id) || this.active?.guide.id === id || this.activeSurvey?.guide.id === id;
2278
+ }
2279
+ hasExclusive() {
2280
+ return this.active?.guide ?? this.activeSurvey?.guide ?? null;
2281
+ }
2282
+ tryShow(guide, url, manual) {
2283
+ if (this.destroyed) return false;
2284
+ if (!isExclusiveGuide(guide)) {
2285
+ if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2286
+ this.show(guide, manual);
2287
+ return true;
2288
+ }
2289
+ const current = this.hasExclusive();
2290
+ if (current) {
2291
+ if (current.id === guide.id) return false;
2292
+ if (manual) this.stopGuide();
2293
+ else {
2294
+ if (!this.queued.some((q) => q.guide.id === guide.id)) this.queued.push({ guide, url, pageScoped: PAGE_SCOPED.has(guide.config.trigger.type) });
2295
+ this.log("queued", guide.key, "behind", current.key);
2296
+ return false;
2297
+ }
2298
+ }
2299
+ const blocked = [...this.flows.values()].map((f) => f.guide).some((g) => areMutuallyExclusive(g, guide));
2300
+ if (blocked && !manual) {
2301
+ if (!this.queued.some((q) => q.guide.id === guide.id)) this.queued.push({ guide, url, pageScoped: true });
2302
+ return false;
2303
+ }
2304
+ this.show(guide, manual);
2305
+ return true;
2306
+ }
2307
+ drainQueue(url, ctx) {
2308
+ if (this.hasExclusive()) return;
2309
+ this.queued = this.queued.filter((q) => !(q.pageScoped && q.url !== url));
2310
+ while (this.queued.length) {
2311
+ const next = this.queued.shift();
2312
+ if (!this.isEligible(next.guide, ctx, url)) continue;
2313
+ if (this.tryShow(next.guide, url, false)) return;
2314
+ }
2315
+ }
2316
+ // ---------------------------------------------------------------------------
2317
+ // Showing guides
2318
+ // ---------------------------------------------------------------------------
2319
+ findGuide(idOrKey) {
2320
+ return this.config?.guides.find((g) => g.id === idOrKey || g.key === idOrKey);
2321
+ }
2322
+ guideState(id) {
2323
+ return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2324
+ }
2325
+ recordImpression(guide) {
2326
+ const s = this.guideState(guide.id);
2327
+ s.impressions += 1;
2328
+ s.lastShownAt = nowIso();
2329
+ if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
2330
+ this.persistState();
2331
+ this.persistSession();
2332
+ this.syncGuideState(guide.id, { impression: true });
2333
+ }
2334
+ recordEnd(guide, reason) {
2335
+ const s = this.guideState(guide.id);
2336
+ if (reason === "completed") s.completedAt = nowIso();
2337
+ else s.dismissedAt = nowIso();
2338
+ this.persistState();
2339
+ this.syncGuideState(guide.id, reason === "completed" ? { completed: true } : { dismissed: true });
2340
+ if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2341
+ }
2342
+ syncGuideState(guideId, update) {
2343
+ void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
2344
+ }
2345
+ show(guide, manual, stepId) {
2346
+ try {
2347
+ if (guide.kind === "checklist" && guide.config.checklist) return this.showChecklist(guide);
2348
+ if (guide.kind === "survey" && guide.config.survey) return this.showSurvey(guide, manual);
2349
+ this.showFlow(guide, manual, stepId);
2350
+ } catch (err) {
2351
+ this.reportError(err);
2352
+ }
2353
+ }
2354
+ showFlow(guide, manual, stepId) {
2355
+ const exclusive = isExclusiveGuide(guide);
2356
+ const flow = { guide, manual, view: null, controller: null };
2357
+ let renders = 0;
2358
+ flow.controller = runGuide(guide, {
2359
+ targets: this.targets,
2360
+ ctx: () => this.getContext(),
2361
+ currentUrl: () => this.currentUrl(),
2362
+ startAtStep: stepId,
2363
+ eventBus: this.eventBus,
2364
+ onError: (err) => this.reportError(err),
2365
+ onEvent: (e) => {
2366
+ const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2367
+ this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2368
+ if (e.name === "guide_started") {
2369
+ this.recordImpression(guide);
2370
+ this.emitter.emit("guide:start", guide);
2371
+ } else if (e.name === "step_viewed" && step) {
2372
+ this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
2373
+ } else if (e.name === "guide_completed") {
2374
+ this.recordEnd(guide, "completed");
2375
+ this.emitter.emit("guide:complete", guide);
2376
+ } else if (e.name === "guide_dismissed") {
2377
+ this.recordEnd(guide, "dismissed");
2378
+ this.emitter.emit("guide:dismiss", guide);
2379
+ } else if (e.name === "guide_skipped") {
2380
+ this.recordEnd(guide, "skipped");
2381
+ this.emitter.emit("guide:skip", guide);
2382
+ }
2383
+ },
2384
+ onRender: (r) => {
2385
+ renders += 1;
2386
+ this.expanded.delete(guide.id);
2387
+ flow.view = { key: `${guide.id}:${r.step.id}:${renders}`, guide, step: r.step, index: r.index, total: r.total, canGoBack: r.canGoBack, targetId: r.targetId, asModal: r.asModal, expanded: false };
2388
+ this.notify();
2389
+ },
2390
+ onClear: () => {
2391
+ flow.view = null;
2392
+ this.notify();
2393
+ },
2394
+ onNavigate: (url) => this.navigate(url),
2395
+ onAction: (action, value) => {
2396
+ if (action === "start_guide" && value) this.setTimer(() => this.startGuide(value), 0);
2397
+ else if (action === "event" && value) this.track(value);
2398
+ else if (action === "open_resource_center") this.openResourceCenter();
2399
+ },
2400
+ onEnd: (reason) => {
2401
+ if (exclusive) {
2402
+ if (this.active === flow) this.active = null;
2403
+ } else if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2404
+ this.notify();
2405
+ if (reason !== "destroyed" && !this.destroyed) this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
2406
+ }
2407
+ });
2408
+ if (exclusive) this.active = flow;
2409
+ else this.flows.set(guide.id, flow);
2410
+ flow.controller.start();
2411
+ if (flow.controller.getState().status === "ended") {
2412
+ if (this.active === flow) this.active = null;
2413
+ if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2414
+ }
2415
+ }
2416
+ showChecklist(guide, open) {
2417
+ const config = guide.config.checklist;
2418
+ if (!config || this.checklists.has(guide.id)) return;
2419
+ const progress = this.getChecklistProgress(guide.id);
2420
+ const first = !this.state.guides[guide.id]?.impressions;
2421
+ this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
2422
+ if (first) this.recordImpression(guide);
2423
+ if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2424
+ this.notify();
2425
+ }
2426
+ showSurvey(guide, manual) {
2427
+ if (!guide.config.survey) return;
2428
+ this.activeSurvey = { guide, manual };
2429
+ this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2430
+ this.recordImpression(guide);
2431
+ this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2432
+ this.emitter.emit("guide:start", guide);
2433
+ this.notify();
2434
+ }
2435
+ finishSurvey(reason) {
2436
+ const s = this.activeSurvey;
2437
+ if (!s) return;
2438
+ this.activeSurvey = null;
2439
+ this.surveyView = null;
2440
+ if (reason === "dismissed") {
2441
+ this.enqueue("survey_dismissed", { guideId: s.guide.id, guideVersion: s.guide.version });
2442
+ this.recordEnd(s.guide, "dismissed");
2443
+ this.emitter.emit("guide:dismiss", s.guide);
2444
+ }
2445
+ this.notify();
2446
+ this.setTimer(() => this.evaluate("after_guide", { endedGuide: s.guide }), 0);
2447
+ }
2448
+ /** Theme for a guide: project theme ← app overrides ← guide appearance. */
2449
+ themeFor(guide) {
2450
+ const a = guide?.config.appearance;
2451
+ return {
2452
+ ...this.config?.theme || {},
2453
+ ...this.options.theme || {},
2454
+ ...a?.primaryColor ? { primaryColor: a.primaryColor } : {},
2455
+ ...a?.backgroundColor ? { backgroundColor: a.backgroundColor } : {},
2456
+ ...a?.textColor ? { textColor: a.textColor } : {},
2457
+ ...a?.borderRadius != null ? { borderRadius: a.borderRadius } : {},
2458
+ dark: a?.theme ?? "auto",
2459
+ overlayOpacity: a?.overlayOpacity
2460
+ };
2461
+ }
2462
+ /**
2463
+ * Opens a URL from a button, link or task: the app's `onNavigate` handler first (deep links and
2464
+ * in-app routes), then http(s) and custom-scheme URLs through the OS.
2465
+ */
2466
+ navigate(url) {
2467
+ if (!url) return;
2468
+ try {
2469
+ if (this.options.onNavigate?.(url) === true) return;
2470
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) this.bridge.openExternal(url);
2471
+ else if (!this.options.onNavigate) this.log(`no onNavigate handler for in-app URL "${url}"`);
2472
+ } catch (err) {
2473
+ this.reportError(err);
2474
+ }
2475
+ }
2476
+ // ---------------------------------------------------------------------------
2477
+ // UI state (consumed by the overlay)
2478
+ // ---------------------------------------------------------------------------
2479
+ getUiState() {
2480
+ return this.ui;
2481
+ }
2482
+ subscribeUi(cb) {
2483
+ this.uiListeners.add(cb);
2484
+ return () => this.uiListeners.delete(cb);
2485
+ }
2486
+ notify() {
2487
+ const steps = [];
2488
+ for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
2489
+ const checklists = [];
2490
+ this.checklists.forEach((w, id) => {
2491
+ const guide = this.findGuide(id);
2492
+ if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
2493
+ });
2494
+ this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
2495
+ for (const cb of Array.from(this.uiListeners)) {
2496
+ try {
2497
+ cb();
2498
+ } catch (err) {
2499
+ this.reportError(err);
2500
+ }
2501
+ }
2502
+ }
2503
+ flowFor(guideId) {
2504
+ return this.active?.guide.id === guideId ? this.active : this.flows.get(guideId);
2505
+ }
2506
+ /** A step button was pressed in the overlay. */
2507
+ pressButton(guideId, button) {
2508
+ this.flowFor(guideId)?.controller.press(button);
2509
+ }
2510
+ /** The close (×) button of a step. */
2511
+ closeStep(guideId) {
2512
+ this.flowFor(guideId)?.controller.dismiss();
2513
+ }
2514
+ /** The dimmed backdrop behind a step was tapped. */
2515
+ backdropPressed(guideId) {
2516
+ const f = this.flowFor(guideId);
2517
+ if (f?.view?.step.dismissOnOutsideClick) f.controller.dismiss();
2518
+ }
2519
+ /** Hotspot dot tapped: show (or hide) its card. */
2520
+ toggleHotspot(guideId) {
2521
+ if (this.expanded.has(guideId)) this.expanded.delete(guideId);
2522
+ else this.expanded.add(guideId);
2523
+ this.notify();
2524
+ }
2525
+ setChecklistOpen(guideId, open) {
2526
+ const w = this.checklists.get(guideId);
2527
+ const guide = this.findGuide(guideId);
2528
+ if (!w || !guide || w.open === open) return;
2529
+ w.open = open;
2530
+ if (open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2531
+ this.notify();
2532
+ }
2533
+ checklistTaskPressed(guideId, taskId) {
2534
+ const guide = this.findGuide(guideId);
2535
+ const task = guide?.config.checklist?.tasks.find((t) => t.id === taskId);
2536
+ if (!guide || !task) return;
2537
+ if (task.completion === "manual") this.markTaskDone(guide, task);
2538
+ if (task.action === "start_guide" && task.actionValue) {
2539
+ this.setChecklistOpen(guide.id, false);
2540
+ this.startGuide(task.actionValue);
2541
+ } else if (task.action === "url" && task.actionValue) this.navigate(task.actionValue);
2542
+ else if (task.action === "event" && task.actionValue) this.track(task.actionValue);
2543
+ }
2544
+ dismissChecklist(guideId) {
2545
+ const guide = this.findGuide(guideId);
2546
+ if (!guide) return;
2547
+ const progress = this.state.checklists[guide.id] ?? (this.state.checklists[guide.id] = { completedTaskIds: [] });
2548
+ progress.dismissedAt = nowIso();
2549
+ this.persistState();
2550
+ this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2551
+ void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
2552
+ this.checklists.delete(guide.id);
2553
+ this.notify();
2554
+ }
2555
+ submitSurvey(guideId, answers) {
2556
+ const s = this.activeSurvey;
2557
+ if (!s || s.guide.id !== guideId || !this.surveyView || this.surveyView.submitted) return;
2558
+ const guide = s.guide;
2559
+ const responseId = randomId(24);
2560
+ this.state.surveysSubmitted.push(guide.id);
2561
+ this.persistState();
2562
+ this.recordEnd(guide, "completed");
2563
+ this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2564
+ void postWithRetry(this.api, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
2565
+ this.emitter.emit("survey:submit", { guide, answers, responseId });
2566
+ this.emitter.emit("guide:complete", guide);
2567
+ this.surveyView = { ...this.surveyView, submitted: true };
2568
+ this.notify();
2569
+ }
2570
+ closeSurvey(guideId) {
2571
+ if (this.activeSurvey?.guide.id !== guideId) return;
2572
+ this.finishSurvey(this.surveyView?.submitted ? "submitted" : "dismissed");
2573
+ }
2574
+ /** The visitor's audiences (from the last identify), for audience-targeted help articles. */
2575
+ getAudienceIds() {
2576
+ return this.state.audienceIds;
2577
+ }
2578
+ getResourceCenter() {
2579
+ const rc = this.config?.resourceCenter;
2580
+ return rc && rc.enabled ? rc : null;
2581
+ }
2582
+ async searchResourceCenter(q) {
2583
+ try {
2584
+ const res = await this.api.get("/resource-center/search", { params: { q } });
2585
+ return res.ok && res.data ? res.data.articles : [];
2586
+ } catch {
2587
+ return [];
2588
+ }
2589
+ }
2590
+ articleViewed(article) {
2591
+ this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
2592
+ }
2593
+ // ---------------------------------------------------------------------------
2594
+ // Checklists
2595
+ // ---------------------------------------------------------------------------
2596
+ getChecklistProgress(idOrKey) {
2597
+ const id = this.findGuide(idOrKey)?.id ?? idOrKey;
2598
+ const p = this.state.checklists[id];
2599
+ return p ? { completedTaskIds: [...p.completedTaskIds], completedAt: p.completedAt ?? null, dismissedAt: p.dismissedAt ?? null } : { completedTaskIds: [], completedAt: null, dismissedAt: null };
2600
+ }
2601
+ completeChecklistTask(idOrKey, taskId) {
2602
+ this.whenHydrated(() => {
2603
+ const guide = this.findGuide(idOrKey);
2604
+ const task = guide?.config.checklist?.tasks.find((t) => t.id === taskId);
2605
+ if (guide && task) this.markTaskDone(guide, task);
2606
+ });
2607
+ }
2608
+ markTaskDone(guide, task) {
2609
+ const config = guide.config.checklist;
2610
+ if (!config) return;
2611
+ const progress = this.state.checklists[guide.id] ?? (this.state.checklists[guide.id] = { completedTaskIds: [] });
2612
+ if (progress.completedTaskIds.includes(task.id)) return;
2613
+ progress.completedTaskIds.push(task.id);
2614
+ this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
2615
+ const complete = isChecklistComplete(config, progress) && !progress.completedAt;
2616
+ if (complete) {
2617
+ progress.completedAt = nowIso();
2618
+ this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
2619
+ }
2620
+ this.persistState();
2621
+ void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
2622
+ this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2623
+ if (complete) {
2624
+ this.emitter.emit("checklist:complete", guide);
2625
+ const w = this.checklists.get(guide.id);
2626
+ if (w) w.open = true;
2627
+ }
2628
+ this.notify();
2629
+ }
2630
+ completeChecklistTasksBy(kind, value) {
2631
+ if (!this.config) return;
2632
+ for (const guide of this.config.guides) {
2633
+ for (const task of guide.config.checklist?.tasks ?? []) {
2634
+ if (task.completion !== kind || !task.completionValue) continue;
2635
+ let hit = false;
2636
+ if (kind === "event") hit = value === task.completionValue;
2637
+ else if (kind === "url_visited") hit = screenMatches(screenPath(task.completionValue), value) || screenMatches(task.completionValue, value);
2638
+ else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2639
+ else if (kind === "click") hit = guidelyIdFromSelector(task.completionValue) === value;
2640
+ if (hit) this.markTaskDone(guide, task);
2641
+ }
2642
+ }
2643
+ }
2644
+ openChecklist(idOrKey) {
2645
+ this.whenHydrated(() => {
2646
+ const guide = this.findGuide(idOrKey);
2647
+ if (!guide?.config.checklist) return;
2648
+ if (!this.checklists.has(guide.id)) {
2649
+ const p = this.state.checklists[guide.id];
2650
+ if (p?.dismissedAt) {
2651
+ delete p.dismissedAt;
2652
+ this.persistState();
2653
+ }
2654
+ this.showChecklist(guide, true);
2655
+ }
2656
+ this.setChecklistOpen(guide.id, true);
2657
+ });
2658
+ }
2659
+ closeChecklist(idOrKey) {
2660
+ const guide = this.findGuide(idOrKey);
2661
+ if (guide) this.setChecklistOpen(guide.id, false);
2662
+ }
2663
+ // ---------------------------------------------------------------------------
2664
+ // Public guide controls
2665
+ // ---------------------------------------------------------------------------
2666
+ /** Starts a guide by id or key, bypassing targeting and frequency rules. */
2667
+ startGuide(idOrKey, options = {}) {
2668
+ const guide = this.findGuide(idOrKey);
2669
+ if (!guide) {
2670
+ this.reportError(new Error(`[Guidely] startGuide: unknown guide "${idOrKey}"`));
2671
+ return false;
2672
+ }
2673
+ if (guide.kind === "checklist") {
2674
+ this.openChecklist(guide.id);
2675
+ return true;
2676
+ }
2677
+ if (this.hasExclusive()) this.stopGuide();
2678
+ this.flows.get(guide.id)?.controller.destroy();
2679
+ this.flows.delete(guide.id);
2680
+ this.show(guide, true, options.stepId);
2681
+ return true;
2682
+ }
2683
+ /** Stops the active guide silently (no dismiss event). */
2684
+ stopGuide() {
2685
+ const a = this.active;
2686
+ if (a) {
2687
+ this.active = null;
2688
+ a.controller.destroy();
2689
+ }
2690
+ if (this.activeSurvey) {
2691
+ this.activeSurvey = null;
2692
+ this.surveyView = null;
2693
+ }
2694
+ this.notify();
2695
+ }
2696
+ /** Dismisses the active guide (records a dismissal). */
2697
+ dismissGuide() {
2698
+ if (this.active) this.active.controller.dismiss();
2699
+ else if (this.activeSurvey) this.finishSurvey("dismissed");
2700
+ }
2701
+ getActiveGuide() {
2702
+ return this.hasExclusive();
2703
+ }
2704
+ getGuides() {
2705
+ return this.config?.guides ?? [];
2706
+ }
2707
+ isGuideCompleted(idOrKey) {
2708
+ const guide = this.findGuide(idOrKey);
2709
+ return !!this.state.guides[guide?.id ?? idOrKey]?.completedAt;
2710
+ }
2711
+ /** Impressions, completion and dismissal for a guide. */
2712
+ getGuideState(idOrKey) {
2713
+ const guide = this.findGuide(idOrKey);
2714
+ return { impressions: 0, ...this.state.guides[guide?.id ?? idOrKey] ?? {} };
2715
+ }
2716
+ openResourceCenter() {
2717
+ if (this.messenger) return this.openMessenger("help");
2718
+ if (!this.getResourceCenter()) {
2719
+ this.log("openResourceCenter: the resource center is not enabled or published for this environment");
2720
+ return;
2721
+ }
2722
+ if (this.resourceCenterOpen) return;
2723
+ this.resourceCenterOpen = true;
2724
+ this.enqueue("resource_center_opened");
2725
+ this.emitter.emit("resource-center:open");
2726
+ this.notify();
2727
+ }
2728
+ closeResourceCenter() {
2729
+ if (this.messenger) return this.closeMessenger();
2730
+ if (!this.resourceCenterOpen) return;
2731
+ this.resourceCenterOpen = false;
2732
+ this.notify();
2733
+ }
2734
+ // ---------------------------------------------------------------------------
2735
+ // Scout messenger
2736
+ // ---------------------------------------------------------------------------
2737
+ /** Creates or removes the messenger to match the config (on only when enabled for this platform). */
2738
+ syncMessenger() {
2739
+ const cfg = this.config?.messenger;
2740
+ if (cfg?.enabled) {
2741
+ if (this.messenger) this.messenger.updateConfig(cfg);
2742
+ else {
2743
+ this.messenger = new Messenger(cfg, {
2744
+ baseUrl: this.options.apiUrl,
2745
+ publicKey: this.options.projectId,
2746
+ store: this.store,
2747
+ fetch: this.options.fetch ?? fetch,
2748
+ streamPost: (this.options.fetch ? null : xhrStreamPost()) ?? fetchStreamPost(this.options.fetch ?? fetch),
2749
+ anonymousId: () => this.anonymousId,
2750
+ userId: () => this.user?.id || null,
2751
+ pageUrl: () => this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
2752
+ onOpen: () => {
2753
+ this.enqueue("messenger_opened");
2754
+ this.emitter.emit("messenger:open");
2755
+ },
2756
+ onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
2757
+ onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
2758
+ onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
2759
+ });
2760
+ this.resourceCenterOpen = false;
2761
+ }
2762
+ } else if (this.messenger) {
2763
+ this.messenger.destroy();
2764
+ this.messenger = null;
2765
+ }
2766
+ this.notify();
2767
+ }
2768
+ /** The Scout messenger store when Scout is on for this platform (rendered by the overlay). */
2769
+ getMessenger() {
2770
+ return this.messenger;
2771
+ }
2772
+ /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
2773
+ openMessenger(tab) {
2774
+ this.whenHydrated(() => this.messenger?.open(tab));
2775
+ }
2776
+ closeMessenger() {
2777
+ this.messenger?.close();
2778
+ }
2779
+ /** Opens a new messenger conversation, optionally pre-filling the message. */
2780
+ showNewMessage(text) {
2781
+ this.whenHydrated(() => this.messenger?.showNewMessage(text));
2782
+ }
2783
+ on(event, cb) {
2784
+ return this.emitter.on(event, cb);
2785
+ }
2786
+ off(event, cb) {
2787
+ this.emitter.off(event, cb);
2788
+ }
2789
+ isClientReady() {
2790
+ return this.isReady;
2791
+ }
2792
+ /** Snapshot for debugging. */
2793
+ getState() {
2794
+ return {
2795
+ ready: this.isReady,
2796
+ destroyed: this.destroyed,
2797
+ anonymousId: this.anonymousId,
2798
+ user: this.user,
2799
+ consent: this.consent,
2800
+ screen: this.screenName,
2801
+ configVersion: this.config?.configVersion ?? null,
2802
+ guides: this.getGuides().map((g) => ({ id: g.id, key: g.key, kind: g.kind })),
2803
+ activeGuide: this.active ? { id: this.active.guide.id, key: this.active.guide.key, ...this.active.controller.getState() } : this.activeSurvey ? { id: this.activeSurvey.guide.id, key: this.activeSurvey.guide.key } : null,
2804
+ queued: this.queued.map((q) => q.guide.key),
2805
+ widgets: [...this.flows.keys(), ...this.checklists.keys()],
2806
+ guideState: this.state.guides,
2807
+ checklists: this.state.checklists,
2808
+ audienceIds: this.state.audienceIds,
2809
+ session: this.session,
2810
+ pendingEvents: this.queue?.size ?? 0,
2811
+ targets: this.targets.ids()
2812
+ };
2813
+ }
2814
+ // ---------------------------------------------------------------------------
2815
+ // Internals
2816
+ // ---------------------------------------------------------------------------
2817
+ persistState() {
2818
+ writeJson(this.store, KEYS.state(this.options.projectId), this.state);
2819
+ }
2820
+ persistSession() {
2821
+ this.session.lastActiveAt = nowIso();
2822
+ writeJson(this.store, KEYS.session, this.session);
2823
+ }
2824
+ reportError(err) {
2825
+ const error = err instanceof Error ? err : new Error(String(err));
2826
+ this.log("error", error.message);
2827
+ try {
2828
+ this.options.onError?.(error);
2829
+ } catch {
2830
+ }
2831
+ this.emitter.emit("error", error);
2832
+ }
2833
+ log(...args) {
2834
+ if (this.options.debug && typeof console !== "undefined") console.log("[Guidely]", ...args);
2835
+ }
2836
+ };
2837
+ function forPlatform(config, platform) {
2838
+ return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
2839
+ }
2840
+
2841
+ // src/ui/Overlay.tsx
2842
+ import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef3, useState as useState6, useSyncExternalStore as useSyncExternalStore2 } from "react";
2843
+ import { Platform as Platform5, Pressable as Pressable8, StatusBar, StyleSheet as StyleSheet9, Text as Text8, useColorScheme, useWindowDimensions, View as View9 } from "react-native";
2844
+
2845
+ // src/ui/Checklist.tsx
2846
+ import { Pressable as Pressable3, ScrollView, StyleSheet as StyleSheet3, Text as Text3, View as View3 } from "react-native";
2847
+
2848
+ // src/ui/primitives.tsx
2849
+ import { Pressable, StyleSheet, Text, View } from "react-native";
2850
+ import { jsx } from "react/jsx-runtime";
2851
+ function GButton({ label, variant = "primary", onPress, palette: p, style, accessibilityHint, disabled }) {
2852
+ const filled = variant === "primary";
2853
+ const outlined = variant === "secondary";
2854
+ return /* @__PURE__ */ jsx(
2855
+ Pressable,
2856
+ {
2857
+ accessibilityRole: "button",
2858
+ accessibilityLabel: label,
2859
+ accessibilityHint,
2860
+ accessibilityState: { disabled: !!disabled },
2861
+ disabled,
2862
+ onPress,
2863
+ hitSlop: 6,
2864
+ style: ({ pressed }) => [
2865
+ styles.btn,
2866
+ { borderRadius: Math.min(p.radius, 10), opacity: disabled ? 0.5 : pressed ? 0.75 : 1 },
2867
+ filled && { backgroundColor: p.primary },
2868
+ outlined && { borderWidth: 1, borderColor: p.border, backgroundColor: p.background },
2869
+ variant === "link" && styles.link,
2870
+ style
2871
+ ],
2872
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.btnText, { color: filled ? p.onPrimary : variant === "link" ? p.primary : variant === "ghost" ? p.muted : p.text, fontFamily: p.fontFamily }, variant === "link" && styles.underline], numberOfLines: 1, children: label })
2873
+ }
2874
+ );
2875
+ }
2876
+ function CloseButton({ onPress, palette: p, label = "Close" }) {
2877
+ return /* @__PURE__ */ jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 10, style: ({ pressed }) => [styles.close, { opacity: pressed ? 0.6 : 1 }], children: /* @__PURE__ */ jsx(Text, { style: [styles.closeText, { color: p.muted }], children: "\xD7" }) });
2878
+ }
2879
+ function Surface({ palette: p, children, style }) {
2880
+ return /* @__PURE__ */ jsx(View, { style: [styles.surface, { backgroundColor: p.background, borderRadius: p.radius, borderColor: p.border }, style], children });
2881
+ }
2882
+ var styles = StyleSheet.create({
2883
+ btn: { minHeight: 36, paddingHorizontal: 14, paddingVertical: 8, alignItems: "center", justifyContent: "center" },
2884
+ link: { paddingHorizontal: 4 },
2885
+ btnText: { fontSize: 14, fontWeight: "600" },
2886
+ underline: { textDecorationLine: "underline" },
2887
+ close: { width: 28, height: 28, alignItems: "center", justifyContent: "center", marginTop: -4, marginRight: -6 },
2888
+ closeText: { fontSize: 24, lineHeight: 26, fontWeight: "400" },
2889
+ surface: {
2890
+ borderWidth: StyleSheet.hairlineWidth,
2891
+ shadowColor: "#000",
2892
+ shadowOpacity: 0.18,
2893
+ shadowRadius: 18,
2894
+ shadowOffset: { width: 0, height: 8 },
2895
+ elevation: 10
2896
+ }
2897
+ });
2898
+
2899
+ // src/ui/RichText.tsx
2900
+ import { useEffect, useMemo, useState } from "react";
2901
+ import { Image, Platform, Pressable as Pressable2, StyleSheet as StyleSheet2, Text as Text2, View as View2 } from "react-native";
2902
+
2903
+ // src/core/html.ts
2904
+ var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0", mdash: "\u2014", ndash: "\u2013", hellip: "\u2026", rsquo: "\u2019", lsquo: "\u2018", rdquo: "\u201D", ldquo: "\u201C", copy: "\xA9", reg: "\xAE", trade: "\u2122", times: "\xD7", rarr: "\u2192", larr: "\u2190", bull: "\u2022" };
2905
+ function decodeEntities(s) {
2906
+ return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e) => {
2907
+ if (e[0] === "#") {
2908
+ const code = e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
2909
+ return Number.isFinite(code) && code > 0 && code < 1114112 ? String.fromCodePoint(code) : m;
2910
+ }
2911
+ return ENTITIES[e.toLowerCase()] ?? m;
2912
+ });
2913
+ }
2914
+ var BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div", "section", "article", "header", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", "ul", "ol", "table", "tr", "pre"]);
2915
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "head", "title", "svg", "template", "iframe", "video"]);
2916
+ var PROVIDER_LABEL = { youtube: "Watch on YouTube", vimeo: "Watch on Vimeo", loom: "Watch on Loom", wistia: "Watch video" };
2917
+ function attr(attrs, name) {
2918
+ const m = attrs.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
2919
+ return m ? decodeEntities(m[2] ?? m[3] ?? m[4] ?? "") : void 0;
2920
+ }
2921
+ function parseHtml(html) {
2922
+ const blocks = [];
2923
+ const style = { bold: 0, italic: 0, underline: 0, code: 0 };
2924
+ const hrefs = [];
2925
+ const lists = [];
2926
+ let current = null;
2927
+ let skipping = null;
2928
+ const media = (block) => {
2929
+ flush();
2930
+ blocks.push({ level: 1, runs: [], ...block });
2931
+ };
2932
+ const flush = () => {
2933
+ if (current) {
2934
+ const runs = current.runs;
2935
+ while (runs.length && !runs[0].text.trim()) runs.shift();
2936
+ while (runs.length && !runs[runs.length - 1].text.trim()) runs.pop();
2937
+ if (runs.length) {
2938
+ runs[0].text = runs[0].text.replace(/^[ \t\n]+/, "");
2939
+ runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/[ \t\n]+$/, "");
2940
+ blocks.push(current);
2941
+ }
2942
+ }
2943
+ current = null;
2944
+ };
2945
+ const open = (kind, level = 1, marker) => {
2946
+ flush();
2947
+ current = { kind, level, marker, runs: [] };
2948
+ };
2949
+ const push = (text) => {
2950
+ if (!text) return;
2951
+ if (!current) current = { kind: "paragraph", level: 1, runs: [] };
2952
+ const run = { text };
2953
+ if (style.bold) run.bold = true;
2954
+ if (style.italic) run.italic = true;
2955
+ if (style.underline) run.underline = true;
2956
+ if (style.code) run.code = true;
2957
+ if (hrefs.length) run.href = hrefs[hrefs.length - 1];
2958
+ const last = current.runs[current.runs.length - 1];
2959
+ if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
2960
+ else current.runs.push(run);
2961
+ };
2962
+ const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
2963
+ let m;
2964
+ while (m = re.exec(html || "")) {
2965
+ const [token, rawTag, attrs = "", text] = m;
2966
+ if (text !== void 0) {
2967
+ if (skipping) continue;
2968
+ push(decodeEntities(text.replace(/\s+/g, " ")));
2969
+ continue;
2970
+ }
2971
+ if (!rawTag) continue;
2972
+ const tag = rawTag.toLowerCase();
2973
+ const closing = token.startsWith("</");
2974
+ if (skipping) {
2975
+ if (closing && tag === skipping) skipping = null;
2976
+ continue;
2977
+ }
2978
+ if (SKIP_TAGS.has(tag)) {
2979
+ if (closing) continue;
2980
+ if (tag === "iframe") {
2981
+ const src = attr(attrs, "src");
2982
+ const embed = isAllowedEmbedSrc(src) ? toEmbedUrl(src) : null;
2983
+ if (embed) media({ kind: "video", href: embed.watchUrl, alt: PROVIDER_LABEL[embed.provider] });
2984
+ } else if (tag === "video") {
2985
+ const src = attr(attrs, "src");
2986
+ if (isSafeMediaUrl(src)) media({ kind: "video", href: src, alt: "Play video" });
2987
+ }
2988
+ if (!token.endsWith("/>")) skipping = tag;
2989
+ continue;
2990
+ }
2991
+ if (closing) {
2992
+ if (tag === "b" || tag === "strong") style.bold = Math.max(0, style.bold - 1);
2993
+ else if (tag === "i" || tag === "em") style.italic = Math.max(0, style.italic - 1);
2994
+ else if (tag === "u") style.underline = Math.max(0, style.underline - 1);
2995
+ else if (tag === "code") style.code = Math.max(0, style.code - 1);
2996
+ else if (tag === "a") hrefs.pop();
2997
+ else if (tag === "ul" || tag === "ol") {
2998
+ lists.pop();
2999
+ flush();
3000
+ } else if (BLOCK_TAGS.has(tag)) flush();
3001
+ continue;
3002
+ }
3003
+ switch (tag) {
3004
+ case "br":
3005
+ push("\n");
3006
+ break;
3007
+ case "b":
3008
+ case "strong":
3009
+ style.bold += 1;
3010
+ break;
3011
+ case "i":
3012
+ case "em":
3013
+ style.italic += 1;
3014
+ break;
3015
+ case "u":
3016
+ style.underline += 1;
3017
+ break;
3018
+ case "code":
3019
+ style.code += 1;
3020
+ break;
3021
+ case "a":
3022
+ hrefs.push(attr(attrs, "href") ?? "");
3023
+ break;
3024
+ case "ul":
3025
+ case "ol":
3026
+ flush();
3027
+ lists.push({ ordered: tag === "ol", n: 0 });
3028
+ break;
3029
+ case "li": {
3030
+ const list = lists[lists.length - 1];
3031
+ if (list) list.n += 1;
3032
+ open("list-item", Math.max(1, lists.length), list?.ordered ? `${list.n}.` : "\u2022");
3033
+ break;
3034
+ }
3035
+ case "h1":
3036
+ case "h2":
3037
+ case "h3":
3038
+ case "h4":
3039
+ case "h5":
3040
+ case "h6":
3041
+ open("heading", Math.min(3, Number(tag[1])));
3042
+ break;
3043
+ case "blockquote":
3044
+ open("quote");
3045
+ break;
3046
+ default:
3047
+ if (BLOCK_TAGS.has(tag)) open("paragraph");
3048
+ else if (tag === "img") {
3049
+ const src = attr(attrs, "src") ?? "";
3050
+ const alt = attr(attrs, "alt") ?? "";
3051
+ if (isSafeMediaUrl(src) || /^data:image\//i.test(src)) {
3052
+ const w = Number(attr(attrs, "width"));
3053
+ const h = Number(attr(attrs, "height"));
3054
+ media({ kind: "image", src, alt, ...w > 0 && h > 0 ? { width: w, height: h } : {} });
3055
+ } else if (alt) push(alt);
3056
+ }
3057
+ }
3058
+ }
3059
+ flush();
3060
+ return blocks;
3061
+ }
3062
+ function htmlToText(html) {
3063
+ return parseHtml(html).map((b) => b.kind === "image" || b.kind === "video" ? b.alt ?? "" : (b.marker ? `${b.marker} ` : "") + b.runs.map((r) => r.text).join("")).filter(Boolean).join("\n");
3064
+ }
3065
+
3066
+ // src/ui/RichText.tsx
3067
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
3068
+ var MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
3069
+ var HEADING_SIZE = [0, 20, 17, 15];
3070
+ function runStyle(run, p) {
3071
+ return {
3072
+ fontWeight: run.bold ? "700" : void 0,
3073
+ fontStyle: run.italic ? "italic" : void 0,
3074
+ textDecorationLine: run.underline || run.href ? "underline" : void 0,
3075
+ fontFamily: run.code ? MONO : void 0,
3076
+ backgroundColor: run.code ? p.surface : void 0,
3077
+ color: run.href ? p.primary : void 0
3078
+ };
3079
+ }
3080
+ function BodyImage({ block, palette: p }) {
3081
+ const [ratio, setRatio] = useState(block.width && block.height ? block.width / block.height : 16 / 9);
3082
+ useEffect(() => {
3083
+ if (block.width && block.height) return;
3084
+ let live = true;
3085
+ Image.getSize(block.src, (w, h) => live && w > 0 && h > 0 && setRatio(w / h), () => {
3086
+ });
3087
+ return () => {
3088
+ live = false;
3089
+ };
3090
+ }, [block.src, block.width, block.height]);
3091
+ return /* @__PURE__ */ jsx2(Image, { source: { uri: block.src }, accessible: true, accessibilityRole: "image", accessibilityLabel: block.alt || void 0, resizeMode: "contain", style: [styles2.image, { aspectRatio: ratio, backgroundColor: p.surface }] });
3092
+ }
3093
+ function BodyVideo({ block, palette: p, onLink }) {
3094
+ return /* @__PURE__ */ jsxs(Pressable2, { accessibilityRole: "link", accessibilityLabel: block.alt, onPress: () => onLink(block.href), style: ({ pressed }) => [styles2.video, { borderColor: p.border, backgroundColor: p.surface, opacity: pressed ? 0.7 : 1 }], children: [
3095
+ /* @__PURE__ */ jsx2(Text2, { style: [styles2.play, { color: p.primary }], children: "\u25B6" }),
3096
+ /* @__PURE__ */ jsx2(Text2, { style: { color: p.primary, fontFamily: p.fontFamily, fontWeight: "600" }, children: block.alt })
3097
+ ] });
3098
+ }
3099
+ function RichText({ html, palette: p, onLink, size = 15 }) {
3100
+ const blocks = useMemo(() => parseHtml(html), [html]);
3101
+ if (!blocks.length) return null;
3102
+ const base = { color: p.text, fontSize: size, lineHeight: Math.round(size * 1.42), fontFamily: p.fontFamily };
3103
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.wrap, children: blocks.map((b, i) => {
3104
+ if (b.kind === "image") return /* @__PURE__ */ jsx2(BodyImage, { block: b, palette: p }, i);
3105
+ if (b.kind === "video") return /* @__PURE__ */ jsx2(BodyVideo, { block: b, palette: p, onLink }, i);
3106
+ const runs = b.runs.map((r, j) => /* @__PURE__ */ jsx2(Text2, { style: runStyle(r, p), onPress: r.href ? () => onLink(r.href) : void 0, accessibilityRole: r.href ? "link" : void 0, children: r.text }, j));
3107
+ if (b.kind === "list-item") {
3108
+ return /* @__PURE__ */ jsxs(View2, { style: [styles2.li, { paddingLeft: (b.level - 1) * 14 }], children: [
3109
+ /* @__PURE__ */ jsx2(Text2, { style: [base, styles2.marker], children: b.marker }),
3110
+ /* @__PURE__ */ jsx2(Text2, { style: [base, styles2.flex], children: runs })
3111
+ ] }, i);
3112
+ }
3113
+ if (b.kind === "heading") {
3114
+ const fs = HEADING_SIZE[b.level] ?? 15;
3115
+ return /* @__PURE__ */ jsx2(Text2, { accessibilityRole: "header", style: [base, { fontSize: fs, lineHeight: Math.round(fs * 1.3), fontWeight: "700" }], children: runs }, i);
3116
+ }
3117
+ return /* @__PURE__ */ jsx2(Text2, { style: [base, b.kind === "quote" && { borderLeftWidth: 3, borderLeftColor: p.border, paddingLeft: 8, color: p.muted }], children: runs }, i);
3118
+ }) });
3119
+ }
3120
+ var styles2 = StyleSheet2.create({
3121
+ wrap: { gap: 6 },
3122
+ li: { flexDirection: "row", gap: 6 },
3123
+ marker: { minWidth: 14 },
3124
+ flex: { flex: 1 },
3125
+ image: { width: "100%", borderRadius: 6 },
3126
+ video: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 12, paddingHorizontal: 14, borderWidth: 1, borderRadius: 8 },
3127
+ play: { fontSize: 14 }
3128
+ });
3129
+
3130
+ // src/ui/Checklist.tsx
3131
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
3132
+ function Checklist({ view, client, palette: p, geometry, showLauncher, launcherBottom }) {
3133
+ const config = view.guide.config.checklist;
3134
+ if (!config) return null;
3135
+ const done = new Set(view.progress.completedTaskIds);
3136
+ const total = config.tasks.length;
3137
+ const completed = config.tasks.filter((t) => done.has(t.id)).length;
3138
+ const finished = !!view.progress.completedAt;
3139
+ const left = config.position === "bottom-left";
3140
+ const { insets, viewport } = geometry;
3141
+ const side = left ? { left: insets.left + 16 } : { right: insets.right + 16 };
3142
+ const width = Math.min(viewport.width - insets.left - insets.right - 32, 360);
3143
+ const nextOpen = config.ordered ? config.tasks.find((t) => !done.has(t.id))?.id : void 0;
3144
+ const bottom = insets.bottom + launcherBottom;
3145
+ return /* @__PURE__ */ jsxs2(View3, { pointerEvents: "box-none", style: StyleSheet3.absoluteFill, children: [
3146
+ view.open ? /* @__PURE__ */ jsx3(Surface, { palette: p, style: [styles3.panel, side, { width, bottom: bottom + (showLauncher ? 56 : 0), maxHeight: viewport.height - insets.top - bottom - 80 }], children: /* @__PURE__ */ jsxs2(View3, { accessibilityViewIsModal: true, style: styles3.panelInner, children: [
3147
+ /* @__PURE__ */ jsxs2(View3, { style: styles3.head, children: [
3148
+ /* @__PURE__ */ jsxs2(View3, { style: styles3.flex, children: [
3149
+ /* @__PURE__ */ jsx3(Text3, { accessibilityRole: "header", style: [styles3.title, { color: p.text, fontFamily: p.fontFamily }], children: finished ? config.completionTitle : config.launcherLabel }),
3150
+ /* @__PURE__ */ jsxs2(Text3, { style: [styles3.sub, { color: p.muted }], children: [
3151
+ completed,
3152
+ " of ",
3153
+ total,
3154
+ " complete"
3155
+ ] })
3156
+ ] }),
3157
+ /* @__PURE__ */ jsx3(CloseButton, { palette: p, onPress: () => client.setChecklistOpen(view.guide.id, false), label: "Close checklist" })
3158
+ ] }),
3159
+ /* @__PURE__ */ jsx3(View3, { style: [styles3.bar, { backgroundColor: p.surface }], accessibilityRole: "progressbar", accessibilityValue: { min: 0, max: total, now: completed }, children: /* @__PURE__ */ jsx3(View3, { style: [styles3.fill, { backgroundColor: p.primary, width: `${total ? completed / total * 100 : 0}%` }] }) }),
3160
+ finished && config.completionBody ? /* @__PURE__ */ jsx3(RichText, { html: config.completionBody, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
3161
+ /* @__PURE__ */ jsx3(ScrollView, { style: styles3.tasks, contentContainerStyle: styles3.tasksContent, children: config.tasks.map((task) => {
3162
+ const isDone = done.has(task.id);
3163
+ const locked = !!nextOpen && !isDone && task.id !== nextOpen;
3164
+ return /* @__PURE__ */ jsxs2(
3165
+ Pressable3,
3166
+ {
3167
+ disabled: locked,
3168
+ accessibilityRole: "button",
3169
+ accessibilityState: { checked: isDone, disabled: locked },
3170
+ accessibilityLabel: `${task.title}${isDone ? ", done" : ""}`,
3171
+ onPress: () => client.checklistTaskPressed(view.guide.id, task.id),
3172
+ style: ({ pressed }) => [styles3.task, { borderColor: p.border, opacity: locked ? 0.5 : pressed ? 0.7 : 1 }],
3173
+ children: [
3174
+ /* @__PURE__ */ jsx3(View3, { style: [styles3.check, isDone ? { backgroundColor: p.primary, borderColor: p.primary } : { borderColor: p.border }], children: isDone ? /* @__PURE__ */ jsx3(Text3, { style: [styles3.tick, { color: p.onPrimary }], children: "\u2713" }) : null }),
3175
+ /* @__PURE__ */ jsxs2(View3, { style: styles3.flex, children: [
3176
+ /* @__PURE__ */ jsxs2(Text3, { style: [styles3.taskTitle, { color: p.text, fontFamily: p.fontFamily }, isDone && { textDecorationLine: "line-through", color: p.muted }], children: [
3177
+ task.title,
3178
+ !task.required ? /* @__PURE__ */ jsx3(Text3, { style: { color: p.muted, fontWeight: "400" }, children: " (optional)" }) : null
3179
+ ] }),
3180
+ task.description ? /* @__PURE__ */ jsx3(Text3, { style: [styles3.taskDesc, { color: p.muted }], children: task.description }) : null
3181
+ ] })
3182
+ ]
3183
+ },
3184
+ task.id
3185
+ );
3186
+ }) }),
3187
+ config.dismissible ? /* @__PURE__ */ jsx3(GButton, { label: finished ? "Close for good" : "Dismiss checklist", variant: "ghost", palette: p, onPress: () => client.dismissChecklist(view.guide.id), style: styles3.dismiss }) : null
3188
+ ] }) }) : null,
3189
+ showLauncher ? /* @__PURE__ */ jsxs2(
3190
+ Pressable3,
3191
+ {
3192
+ accessibilityRole: "button",
3193
+ accessibilityLabel: `${config.launcherLabel}, ${completed} of ${total} complete`,
3194
+ accessibilityState: { expanded: view.open },
3195
+ onPress: () => client.setChecklistOpen(view.guide.id, !view.open),
3196
+ style: ({ pressed }) => [styles3.launcher, side, { bottom, backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }],
3197
+ children: [
3198
+ /* @__PURE__ */ jsx3(View3, { style: [styles3.count, { borderColor: p.onPrimary }], children: /* @__PURE__ */ jsx3(Text3, { style: [styles3.countText, { color: p.onPrimary }], children: finished ? "\u2713" : `${completed}/${total}` }) }),
3199
+ /* @__PURE__ */ jsx3(Text3, { style: [styles3.launcherText, { color: p.onPrimary, fontFamily: p.fontFamily }], numberOfLines: 1, children: config.launcherLabel })
3200
+ ]
3201
+ }
3202
+ ) : null
3203
+ ] });
3204
+ }
3205
+ var styles3 = StyleSheet3.create({
3206
+ flex: { flex: 1 },
3207
+ panel: { position: "absolute" },
3208
+ panelInner: { padding: 16, gap: 10, flexShrink: 1 },
3209
+ head: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
3210
+ title: { fontSize: 17, fontWeight: "700" },
3211
+ sub: { fontSize: 13, marginTop: 2 },
3212
+ bar: { height: 6, borderRadius: 3, overflow: "hidden" },
3213
+ fill: { height: 6, borderRadius: 3 },
3214
+ tasks: { flexGrow: 0 },
3215
+ tasksContent: { gap: 8 },
3216
+ task: { flexDirection: "row", gap: 10, alignItems: "flex-start", borderWidth: StyleSheet3.hairlineWidth, borderRadius: 10, padding: 10 },
3217
+ check: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, alignItems: "center", justifyContent: "center", marginTop: 1 },
3218
+ tick: { fontSize: 13, fontWeight: "800" },
3219
+ taskTitle: { fontSize: 15, fontWeight: "600" },
3220
+ taskDesc: { fontSize: 13, marginTop: 2 },
3221
+ dismiss: { alignSelf: "center" },
3222
+ launcher: {
3223
+ position: "absolute",
3224
+ flexDirection: "row",
3225
+ alignItems: "center",
3226
+ gap: 8,
3227
+ paddingVertical: 10,
3228
+ paddingHorizontal: 14,
3229
+ borderRadius: 24,
3230
+ shadowColor: "#000",
3231
+ shadowOpacity: 0.22,
3232
+ shadowRadius: 12,
3233
+ shadowOffset: { width: 0, height: 4 },
3234
+ elevation: 8,
3235
+ maxWidth: 240
3236
+ },
3237
+ count: { minWidth: 30, height: 22, borderRadius: 11, borderWidth: 1.5, alignItems: "center", justifyContent: "center", paddingHorizontal: 4 },
3238
+ countText: { fontSize: 11, fontWeight: "700" },
3239
+ launcherText: { fontSize: 14, fontWeight: "700", flexShrink: 1 }
3240
+ });
3241
+
3242
+ // src/ui/Messenger.tsx
3243
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2, useSyncExternalStore } from "react";
3244
+ import { ActivityIndicator, Keyboard, Modal, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView2, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
3245
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3246
+ function useMessenger(store) {
3247
+ return useSyncExternalStore(
3248
+ useCallback((cb) => store.subscribe(cb), [store]),
3249
+ () => store.getSnapshot(),
3250
+ () => store.getSnapshot()
3251
+ );
3252
+ }
3253
+ function useKeyboardHeight() {
3254
+ const [h, setH] = useState2(0);
3255
+ useEffect2(() => {
3256
+ if (Platform2.OS !== "ios") return;
3257
+ const show = Keyboard.addListener("keyboardWillShow", (e) => setH(e.endCoordinates.height));
3258
+ const hide = Keyboard.addListener("keyboardWillHide", () => setH(0));
3259
+ return () => {
3260
+ show.remove();
3261
+ hide.remove();
3262
+ };
3263
+ }, []);
3264
+ return h;
3265
+ }
3266
+ function ago(iso) {
3267
+ const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1e3);
3268
+ if (s < 60) return "now";
3269
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
3270
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
3271
+ return `${Math.floor(s / 86400)}d`;
3272
+ }
3273
+ function MessengerLauncher({ store, palette: p, style }) {
3274
+ const snap = useMessenger(store);
3275
+ if (snap.open) return null;
3276
+ const label = snap.unread ? `Open ${snap.config.name}, ${snap.unread} unread` : `Open ${snap.config.name}`;
3277
+ return /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => store.open(), style: ({ pressed }) => [styles4.launcher, style, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
3278
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.launcherIcon, { color: p.onPrimary }], children: "\u{1F4AC}" }),
3279
+ snap.unread ? /* @__PURE__ */ jsx4(View4, { style: styles4.badge, children: /* @__PURE__ */ jsx4(Text4, { style: styles4.badgeText, children: snap.unread }) }) : null
3280
+ ] });
3281
+ }
3282
+ function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
3283
+ const snap = useMessenger(store);
3284
+ const keyboard = useKeyboardHeight();
3285
+ const v = snap.view;
3286
+ const title = v.kind === "thread" ? snap.thread?.conversation?.subject || "New conversation" : v.kind === "article" ? "" : v.tab === "home" ? snap.config.name : v.tab === "messages" ? "Messages" : "Help";
3287
+ return /* @__PURE__ */ jsx4(Modal, { visible: snap.open, animationType: "slide", presentationStyle: Platform2.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => v.kind === "tab" ? store.close() : store.back(), children: /* @__PURE__ */ jsxs3(View4, { style: [styles4.sheet, { backgroundColor: p.background, paddingTop: Platform2.OS === "ios" ? 8 : topInset, paddingBottom: keyboard }], children: [
3288
+ /* @__PURE__ */ jsxs3(View4, { style: [styles4.header, { borderColor: p.border }], children: [
3289
+ v.kind !== "tab" ? /* @__PURE__ */ jsx4(Pressable4, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => store.back(), hitSlop: 10, children: /* @__PURE__ */ jsx4(Text4, { style: [styles4.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
3290
+ /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "header", numberOfLines: 1, style: [styles4.title, { color: p.text, fontFamily: p.fontFamily }], children: title }),
3291
+ /* @__PURE__ */ jsx4(CloseButton, { palette: p, onPress: () => store.close(), label: `Close ${snap.config.name}` })
3292
+ ] }),
3293
+ /* @__PURE__ */ jsx4(View4, { style: styles4.flex, children: v.kind === "thread" ? /* @__PURE__ */ jsx4(Thread, { store, palette: p, bottomInset: keyboard ? 0 : bottomInset }) : v.kind === "article" ? /* @__PURE__ */ jsx4(Article, { article: snap.article, palette: p, client }) : v.tab === "home" ? /* @__PURE__ */ jsx4(Home, { store, palette: p }) : v.tab === "messages" ? /* @__PURE__ */ jsx4(Conversations, { store, palette: p }) : /* @__PURE__ */ jsx4(Help, { store, client, palette: p }) }),
3294
+ v.kind === "tab" && !keyboard ? /* @__PURE__ */ jsx4(Tabs, { store, active: v.tab, palette: p, bottomInset }) : null
3295
+ ] }) });
3296
+ }
3297
+ function Tabs({ store, active, palette: p, bottomInset }) {
3298
+ const unread = useMessenger(store).unread;
3299
+ const tabs = [
3300
+ ["home", "\u2302", "Home"],
3301
+ ["messages", "\u2709\uFE0E", "Messages"],
3302
+ ["help", "?", "Help"]
3303
+ ];
3304
+ return /* @__PURE__ */ jsx4(View4, { accessibilityRole: "tablist", style: [styles4.tabs, { borderColor: p.border, paddingBottom: Math.max(8, bottomInset) }], children: tabs.map(([tab, icon, label]) => {
3305
+ const on = tab === active;
3306
+ return /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "tab", accessibilityState: { selected: on }, accessibilityLabel: tab === "messages" && unread ? `${label}, ${unread} unread` : label, onPress: () => store.setTab(tab), style: styles4.tab, children: [
3307
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.tabIcon, { color: on ? p.primary : p.muted }], children: icon }),
3308
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.tabLabel, { color: on ? p.primary : p.muted }], children: label }),
3309
+ tab === "messages" && unread ? /* @__PURE__ */ jsx4(View4, { style: [styles4.dot, styles4.tabDot] }) : null
3310
+ ] }, tab);
3311
+ }) });
3312
+ }
3313
+ function Home({ store, palette: p }) {
3314
+ const snap = useMessenger(store);
3315
+ const recent = snap.conversations[0];
3316
+ const [q, setQ] = useState2("");
3317
+ return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3318
+ /* @__PURE__ */ jsxs3(View4, { style: [styles4.hero, { backgroundColor: p.primary, borderRadius: p.radius }], children: [
3319
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.heroTitle, { color: p.onPrimary }], children: snap.config.greeting || `Hi! I'm ${snap.config.name}.` }),
3320
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.heroSub, { color: p.onPrimary }], children: snap.config.aiAvailable ? `${snap.config.name} answers right away${snap.config.handoffEnabled ? ", and the team is here when you need a person" : ""}.` : "The team usually replies within a few hours." })
3321
+ ] }),
3322
+ /* @__PURE__ */ jsx4(GButton, { label: "Send us a message", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth }),
3323
+ recent ? /* @__PURE__ */ jsxs3(View4, { style: styles4.section, children: [
3324
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.sectionTitle, { color: p.muted }], children: "RECENT" }),
3325
+ /* @__PURE__ */ jsx4(ConversationRow, { c: recent, palette: p, onPress: () => void store.openConversation(recent.id) })
3326
+ ] }) : null,
3327
+ /* @__PURE__ */ jsxs3(View4, { style: styles4.section, children: [
3328
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.sectionTitle, { color: p.muted }], children: "SEARCH FOR HELP" }),
3329
+ /* @__PURE__ */ jsx4(
3330
+ TextInput,
3331
+ {
3332
+ value: q,
3333
+ onChangeText: setQ,
3334
+ onSubmitEditing: () => {
3335
+ pendingSearch.current = q;
3336
+ store.setTab("help");
3337
+ },
3338
+ returnKeyType: "search",
3339
+ placeholder: "Search articles",
3340
+ placeholderTextColor: p.muted,
3341
+ accessibilityLabel: "Search help articles",
3342
+ style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
3343
+ }
3344
+ )
3345
+ ] })
3346
+ ] });
3347
+ }
3348
+ var pendingSearch = { current: "" };
3349
+ function ConversationRow({ c, palette: p, onPress }) {
3350
+ return /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityLabel: `${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`, onPress, style: ({ pressed }) => [styles4.row, { borderColor: p.border, opacity: pressed ? 0.7 : 1 }], children: [
3351
+ /* @__PURE__ */ jsxs3(View4, { style: styles4.flex, children: [
3352
+ /* @__PURE__ */ jsx4(Text4, { numberOfLines: 1, style: [styles4.rowTitle, { color: p.text }], children: c.subject || "Conversation" }),
3353
+ /* @__PURE__ */ jsx4(Text4, { numberOfLines: 1, style: [styles4.rowSub, { color: p.muted }], children: c.lastMessagePreview || (c.status === "closed" ? "Closed" : " ") })
3354
+ ] }),
3355
+ /* @__PURE__ */ jsxs3(View4, { style: styles4.rowEnd, children: [
3356
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.time, { color: p.muted }], children: ago(c.lastMessageAt) }),
3357
+ c.unread ? /* @__PURE__ */ jsx4(View4, { style: [styles4.dot, { backgroundColor: p.primary }] }) : null
3358
+ ] })
3359
+ ] });
3360
+ }
3361
+ function Conversations({ store, palette: p }) {
3362
+ const snap = useMessenger(store);
3363
+ return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, children: [
3364
+ snap.conversations.length ? snap.conversations.map((c) => /* @__PURE__ */ jsx4(ConversationRow, { c, palette: p, onPress: () => void store.openConversation(c.id) }, c.id)) : /* @__PURE__ */ jsx4(Text4, { style: [styles4.empty, { color: p.muted }], children: "No conversations yet." }),
3365
+ /* @__PURE__ */ jsx4(GButton, { label: "New conversation", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth })
3366
+ ] });
3367
+ }
3368
+ function Bubble({ m, text, name, palette: p, onArticle, onGuide }) {
3369
+ if (m.author === "system")
3370
+ return /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text });
3371
+ const mine = m.author === "user";
3372
+ return /* @__PURE__ */ jsxs3(View4, { style: [styles4.bubbleWrap, mine ? styles4.mine : styles4.theirs], children: [
3373
+ !mine ? /* @__PURE__ */ jsx4(Text4, { style: [styles4.author, { color: p.muted }], children: m.author === "agent" ? m.authorName || "Team" : name }) : null,
3374
+ /* @__PURE__ */ jsx4(View4, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text ? /* @__PURE__ */ jsx4(TypingDots, { color: p.muted }) : /* @__PURE__ */ jsx4(Text4, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text }) }),
3375
+ m.citations.length || m.suggestedGuides.length ? /* @__PURE__ */ jsxs3(View4, { style: styles4.chips, children: [
3376
+ m.citations.map((c) => /* @__PURE__ */ jsx4(Pressable4, { accessibilityRole: "link", accessibilityLabel: `Article: ${c.title}`, onPress: () => onArticle(c.articleId), style: [styles4.chip, { borderColor: p.border }], children: /* @__PURE__ */ jsxs3(Text4, { style: [styles4.chipText, { color: p.text }], children: [
3377
+ "\u{1F4C4} ",
3378
+ c.title
3379
+ ] }) }, c.articleId)),
3380
+ m.suggestedGuides.map((g) => /* @__PURE__ */ jsx4(Pressable4, { accessibilityRole: "button", accessibilityLabel: `Show me: ${g.name}`, onPress: () => onGuide(g.key), style: [styles4.chip, { borderColor: p.primary }], children: /* @__PURE__ */ jsxs3(Text4, { style: [styles4.chipText, { color: p.primary }], children: [
3381
+ "\u25B6 Show me: ",
3382
+ g.name
3383
+ ] }) }, g.key))
3384
+ ] }) : null
3385
+ ] });
3386
+ }
3387
+ function TypingDots({ color }) {
3388
+ return /* @__PURE__ */ jsx4(ActivityIndicator, { size: "small", color, accessibilityLabel: "Typing" });
3389
+ }
3390
+ function Thread({ store, palette: p, bottomInset }) {
3391
+ const snap = useMessenger(store);
3392
+ const t = snap.thread;
3393
+ const [text, setText] = useState2(t?.draft ?? "");
3394
+ const scroll = useRef(null);
3395
+ const convo = t?.conversation ?? null;
3396
+ useEffect2(() => {
3397
+ if (t?.draft) setText(t.draft);
3398
+ }, [t?.draft]);
3399
+ const streamingIds = useMemo2(() => Object.keys(t?.streaming ?? {}).filter((id) => !t?.messages.some((m) => m.id === id)), [t]);
3400
+ if (!t) return null;
3401
+ const send = () => {
3402
+ const value = text;
3403
+ setText("");
3404
+ void store.send(value);
3405
+ };
3406
+ const canHandoff = snap.config.handoffEnabled && snap.config.aiAvailable && convo?.status === "bot" && !t.sending;
3407
+ return /* @__PURE__ */ jsxs3(View4, { style: styles4.flex, children: [
3408
+ /* @__PURE__ */ jsxs3(ScrollView2, { ref: scroll, contentContainerStyle: styles4.thread, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [
3409
+ !t.messages.length && !t.sending ? /* @__PURE__ */ jsx4(Text4, { style: [styles4.empty, { color: p.muted }], children: snap.config.greeting || "Ask us anything." }) : null,
3410
+ t.messages.map((m) => /* @__PURE__ */ jsx4(Bubble, { m, text: t.streaming[m.id] ?? m.body, name: snap.config.name, palette: p, onArticle: (id) => void store.openArticle(id), onGuide: (k) => store.startGuide(k) }, m.id)),
3411
+ streamingIds.map((id) => /* @__PURE__ */ jsx4(Bubble, { m: { id, author: "scout", body: "", citations: [], suggestedGuides: [], status: "pending", createdAt: (/* @__PURE__ */ new Date()).toISOString() }, text: t.streaming[id] ?? "", name: snap.config.name, palette: p, onArticle: () => {
3412
+ }, onGuide: () => {
3413
+ } }, id)),
3414
+ t.sending && !streamingIds.length && snap.config.aiAvailable && t.messages.length ? /* @__PURE__ */ jsx4(TypingDots, { color: p.muted }) : null,
3415
+ t.error ? /* @__PURE__ */ jsx4(Text4, { accessibilityLiveRegion: "polite", style: styles4.error, children: t.error }) : null,
3416
+ convo?.status === "closed" && convo.rating == null ? /* @__PURE__ */ jsx4(Rating, { palette: p, onRate: (n) => void store.rate(n) }) : null,
3417
+ convo?.status === "closed" && convo.rating != null ? /* @__PURE__ */ jsx4(Text4, { style: [styles4.system, { color: p.muted }], children: "Thanks for rating this conversation." }) : null
3418
+ ] }),
3419
+ canHandoff ? /* @__PURE__ */ jsx4(GButton, { label: "Talk to a person", variant: "secondary", palette: p, onPress: () => void store.handoff(), style: styles4.handoff }) : null,
3420
+ /* @__PURE__ */ jsxs3(View4, { style: [styles4.composer, { borderColor: p.border, paddingBottom: Math.max(10, bottomInset) }], children: [
3421
+ /* @__PURE__ */ jsx4(
3422
+ TextInput,
3423
+ {
3424
+ value: text,
3425
+ onChangeText: (v) => setText(v.slice(0, 4e3)),
3426
+ placeholder: convo ? "Reply\u2026" : "Write a message\u2026",
3427
+ placeholderTextColor: p.muted,
3428
+ multiline: true,
3429
+ accessibilityLabel: "Message",
3430
+ style: [styles4.input, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(18, p.radius + 6) }]
3431
+ }
3432
+ ),
3433
+ /* @__PURE__ */ jsx4(GButton, { label: "Send", palette: p, onPress: send, disabled: !text.trim() || t.sending })
3434
+ ] })
3435
+ ] });
3436
+ }
3437
+ function Rating({ palette: p, onRate }) {
3438
+ return /* @__PURE__ */ jsxs3(View4, { style: styles4.rating, children: [
3439
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.ratingTitle, { color: p.text }], children: "How did we do?" }),
3440
+ /* @__PURE__ */ jsx4(View4, { accessibilityRole: "radiogroup", style: styles4.stars, children: [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ jsx4(Pressable4, { accessibilityRole: "radio", accessibilityLabel: `${n} of 5`, onPress: () => onRate(n), hitSlop: 6, children: /* @__PURE__ */ jsx4(Text4, { style: [styles4.star, { color: p.primary }], children: "\u2605" }) }, n)) })
3441
+ ] });
3442
+ }
3443
+ function Help({ store, client, palette: p }) {
3444
+ const snap = useMessenger(store);
3445
+ const rc = client.getResourceCenter();
3446
+ const [q, setQ] = useState2(pendingSearch.current);
3447
+ const [results, setResults] = useState2(null);
3448
+ const [faq, setFaq] = useState2(null);
3449
+ useEffect2(() => {
3450
+ pendingSearch.current = "";
3451
+ }, []);
3452
+ useEffect2(() => {
3453
+ const query = q.trim();
3454
+ if (query.length < 2) return setResults(null);
3455
+ let alive = true;
3456
+ const t = setTimeout(() => void store.search(query).then((r) => alive && setResults(r)), 250);
3457
+ return () => {
3458
+ alive = false;
3459
+ clearTimeout(t);
3460
+ };
3461
+ }, [q, store]);
3462
+ const guides = client.getGuides();
3463
+ const recommended = (rc?.recommendedGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
3464
+ const articles = snap.articles;
3465
+ const row = (key, title, sub, onPress, icon = "\u203A") => /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityLabel: title, onPress, style: ({ pressed }) => [styles4.row, { borderColor: p.border, opacity: pressed ? 0.7 : 1 }], children: [
3466
+ /* @__PURE__ */ jsxs3(View4, { style: styles4.flex, children: [
3467
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.rowTitle, { color: p.text }], children: title }),
3468
+ sub ? /* @__PURE__ */ jsx4(Text4, { numberOfLines: 2, style: [styles4.rowSub, { color: p.muted }], children: sub }) : null
3469
+ ] }),
3470
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.chev, { color: p.muted }], children: icon })
3471
+ ] }, key);
3472
+ const open = (a) => a.externalUrl ? client.navigate(a.externalUrl) : void store.openArticle(a.id);
3473
+ const section = (title, children) => /* @__PURE__ */ jsxs3(View4, { style: styles4.section, children: [
3474
+ /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "header", style: [styles4.sectionTitle, { color: p.muted }], children: title.toUpperCase() }),
3475
+ children
3476
+ ] }, title);
3477
+ const audiences = client.getAudienceIds();
3478
+ const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
3479
+ const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
3480
+ return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3481
+ /* @__PURE__ */ jsx4(TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
3482
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx4(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsxs3(Fragment, { children: [
3483
+ recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
3484
+ !articles ? /* @__PURE__ */ jsx4(ActivityIndicator, { color: p.muted }) : null,
3485
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
3486
+ rc?.faqs.length ? section(
3487
+ "FAQ",
3488
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
3489
+ /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
3490
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
3491
+ /* @__PURE__ */ jsx4(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
3492
+ ] }),
3493
+ faq === f.id ? /* @__PURE__ */ jsx4(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
3494
+ ] }, f.id))
3495
+ ) : null,
3496
+ rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
3497
+ ] })
3498
+ ] });
3499
+ }
3500
+ function Article({ article, palette: p, client }) {
3501
+ if (!article) return /* @__PURE__ */ jsx4(ActivityIndicator, { style: styles4.loading, color: p.muted });
3502
+ return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, children: [
3503
+ /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "header", style: [styles4.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
3504
+ /* @__PURE__ */ jsx4(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
3505
+ ] });
3506
+ }
3507
+ var styles4 = StyleSheet4.create({
3508
+ flex: { flex: 1 },
3509
+ launcher: { position: "absolute", width: 54, height: 54, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
3510
+ launcherIcon: { fontSize: 24 },
3511
+ badge: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center", paddingHorizontal: 5, borderWidth: 2, borderColor: "#ffffff" },
3512
+ badgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" },
3513
+ sheet: { flex: 1 },
3514
+ header: { flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: StyleSheet4.hairlineWidth },
3515
+ back: { fontSize: 16, fontWeight: "600" },
3516
+ title: { flex: 1, fontSize: 18, fontWeight: "700" },
3517
+ tabs: { flexDirection: "row", borderTopWidth: StyleSheet4.hairlineWidth, paddingTop: 8 },
3518
+ tab: { flex: 1, alignItems: "center", gap: 2 },
3519
+ tabIcon: { fontSize: 18 },
3520
+ tabLabel: { fontSize: 11, fontWeight: "600" },
3521
+ tabDot: { position: "absolute", top: 0, right: "34%", backgroundColor: "#dc2626" },
3522
+ dot: { width: 8, height: 8, borderRadius: 4 },
3523
+ content: { padding: 16, gap: 16 },
3524
+ hero: { padding: 18, gap: 6 },
3525
+ heroTitle: { fontSize: 20, fontWeight: "800" },
3526
+ heroSub: { fontSize: 14, opacity: 0.9 },
3527
+ fullWidth: { alignSelf: "stretch" },
3528
+ section: { gap: 8 },
3529
+ sectionTitle: { fontSize: 12, fontWeight: "700", letterSpacing: 0.6 },
3530
+ search: { height: 42, paddingHorizontal: 12, fontSize: 15 },
3531
+ row: { flexDirection: "row", alignItems: "center", gap: 10, borderWidth: StyleSheet4.hairlineWidth, borderRadius: 10, paddingVertical: 12, paddingHorizontal: 12 },
3532
+ rowTitle: { fontSize: 15, fontWeight: "600" },
3533
+ rowSub: { fontSize: 13, marginTop: 2 },
3534
+ rowEnd: { alignItems: "flex-end", gap: 6 },
3535
+ time: { fontSize: 12 },
3536
+ chev: { fontSize: 18 },
3537
+ empty: { textAlign: "center", paddingVertical: 24, fontSize: 14 },
3538
+ faq: { flexDirection: "column", alignItems: "stretch" },
3539
+ faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
3540
+ thread: { padding: 16, gap: 12 },
3541
+ system: { textAlign: "center", fontSize: 12, paddingHorizontal: 24 },
3542
+ bubbleWrap: { maxWidth: "86%", gap: 4 },
3543
+ mine: { alignSelf: "flex-end", alignItems: "flex-end" },
3544
+ theirs: { alignSelf: "flex-start", alignItems: "flex-start" },
3545
+ author: { fontSize: 12, fontWeight: "600", marginLeft: 4 },
3546
+ bubble: { paddingHorizontal: 14, paddingVertical: 10 },
3547
+ bubbleText: { fontSize: 15, lineHeight: 21 },
3548
+ chips: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
3549
+ chip: { borderWidth: 1, borderRadius: 14, paddingHorizontal: 10, paddingVertical: 5 },
3550
+ chipText: { fontSize: 13, fontWeight: "600" },
3551
+ error: { color: "#dc2626", fontSize: 13, textAlign: "center" },
3552
+ handoff: { alignSelf: "center", marginBottom: 8 },
3553
+ composer: { flexDirection: "row", alignItems: "flex-end", gap: 8, paddingHorizontal: 12, paddingTop: 10, borderTopWidth: StyleSheet4.hairlineWidth },
3554
+ input: { flex: 1, minHeight: 40, maxHeight: 120, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 10, fontSize: 15 },
3555
+ rating: { alignItems: "center", gap: 6, paddingVertical: 8 },
3556
+ ratingTitle: { fontSize: 15, fontWeight: "600" },
3557
+ stars: { flexDirection: "row", gap: 8 },
3558
+ star: { fontSize: 30 },
3559
+ articleTitle: { fontSize: 22, fontWeight: "700" },
3560
+ loading: { marginTop: 40 }
3561
+ });
3562
+
3563
+ // src/ui/ResourceCenter.tsx
3564
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
3565
+ import { Modal as Modal2, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView3, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
3566
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3567
+ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3568
+ const rc = client.getResourceCenter();
3569
+ const [query, setQuery] = useState3("");
3570
+ const [results, setResults] = useState3(null);
3571
+ const [article, setArticle] = useState3(null);
3572
+ const [faq, setFaq] = useState3(null);
3573
+ useEffect3(() => {
3574
+ if (!open) {
3575
+ setQuery("");
3576
+ setResults(null);
3577
+ setArticle(null);
3578
+ }
3579
+ }, [open]);
3580
+ useEffect3(() => {
3581
+ const q = query.trim();
3582
+ if (q.length < 2) {
3583
+ setResults(null);
3584
+ return;
3585
+ }
3586
+ let alive = true;
3587
+ const t = setTimeout(() => {
3588
+ void client.searchResourceCenter(q).then((r) => alive && setResults(r));
3589
+ }, 250);
3590
+ return () => {
3591
+ alive = false;
3592
+ clearTimeout(t);
3593
+ };
3594
+ }, [client, query]);
3595
+ const guides = client.getGuides();
3596
+ const recommended = useMemo3(() => (rc?.recommendedGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind !== "checklist"), [rc, guides]);
3597
+ const checklists = useMemo3(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
3598
+ if (!rc) return null;
3599
+ const close = () => client.closeResourceCenter();
3600
+ const openArticle = (a) => {
3601
+ if (a.externalUrl) return client.navigate(a.externalUrl);
3602
+ client.articleViewed(a);
3603
+ setArticle(a);
3604
+ };
3605
+ const row = (key, title, subtitle, onPress, icon = "\u203A") => /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityLabel: title, onPress, style: ({ pressed }) => [styles5.row, { borderColor: p.border, opacity: pressed ? 0.7 : 1 }], children: [
3606
+ /* @__PURE__ */ jsxs4(View5, { style: styles5.flex, children: [
3607
+ /* @__PURE__ */ jsx5(Text5, { style: [styles5.rowTitle, { color: p.text, fontFamily: p.fontFamily }], children: title }),
3608
+ subtitle ? /* @__PURE__ */ jsx5(Text5, { style: [styles5.rowSub, { color: p.muted }], numberOfLines: 2, children: subtitle }) : null
3609
+ ] }),
3610
+ /* @__PURE__ */ jsx5(Text5, { style: [styles5.chev, { color: p.muted }], children: icon })
3611
+ ] }, key);
3612
+ const section = (title, children) => /* @__PURE__ */ jsxs4(View5, { style: styles5.section, children: [
3613
+ /* @__PURE__ */ jsx5(Text5, { accessibilityRole: "header", style: [styles5.sectionTitle, { color: p.muted }], children: title.toUpperCase() }),
3614
+ children
3615
+ ] }, title);
3616
+ const audiences = client.getAudienceIds();
3617
+ const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
3618
+ const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
3619
+ return /* @__PURE__ */ jsx5(Modal2, { visible: open, animationType: "slide", presentationStyle: Platform3.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ jsxs4(View5, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: Platform3.OS === "ios" ? 8 : topInset }], children: [
3620
+ /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { borderColor: p.border }], children: [
3621
+ article ? /* @__PURE__ */ jsx5(Pressable5, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ jsx5(Text5, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
3622
+ /* @__PURE__ */ jsx5(Text5, { accessibilityRole: "header", style: [styles5.title, { color: p.text, fontFamily: p.fontFamily }], numberOfLines: 1, children: article ? "" : rc.title }),
3623
+ /* @__PURE__ */ jsx5(CloseButton, { palette: p, onPress: close, label: "Close help" })
3624
+ ] }),
3625
+ article ? /* @__PURE__ */ jsxs4(ScrollView3, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], children: [
3626
+ /* @__PURE__ */ jsx5(Text5, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
3627
+ /* @__PURE__ */ jsx5(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
3628
+ ] }) : /* @__PURE__ */ jsxs4(ScrollView3, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
3629
+ /* @__PURE__ */ jsx5(
3630
+ TextInput2,
3631
+ {
3632
+ value: query,
3633
+ onChangeText: setQuery,
3634
+ placeholder: "Search help articles",
3635
+ placeholderTextColor: p.muted,
3636
+ accessibilityLabel: "Search help articles",
3637
+ clearButtonMode: "while-editing",
3638
+ returnKeyType: "search",
3639
+ style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
3640
+ }
3641
+ ),
3642
+ listed ? section(
3643
+ `Results for \u201C${query.trim()}\u201D`,
3644
+ listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ jsx5(Text5, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
3645
+ ) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
3646
+ recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
3647
+ checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
3648
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
3649
+ rc.faqs.length ? section(
3650
+ "FAQ",
3651
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
3652
+ /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
3653
+ /* @__PURE__ */ jsx5(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
3654
+ /* @__PURE__ */ jsx5(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
3655
+ ] }),
3656
+ faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
3657
+ ] }, f.id))
3658
+ ) : null,
3659
+ rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
3660
+ ] })
3661
+ ] })
3662
+ ] }) });
3663
+ }
3664
+ var styles5 = StyleSheet5.create({
3665
+ flex: { flex: 1 },
3666
+ sheet: { flex: 1 },
3667
+ header: { flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: StyleSheet5.hairlineWidth },
3668
+ back: { fontSize: 16, fontWeight: "600" },
3669
+ title: { flex: 1, fontSize: 18, fontWeight: "700" },
3670
+ content: { padding: 16, gap: 18 },
3671
+ search: { height: 42, paddingHorizontal: 12, fontSize: 15 },
3672
+ section: { gap: 8 },
3673
+ sectionTitle: { fontSize: 12, fontWeight: "700", letterSpacing: 0.6 },
3674
+ row: { flexDirection: "row", alignItems: "center", gap: 10, borderWidth: StyleSheet5.hairlineWidth, borderRadius: 10, paddingVertical: 12, paddingHorizontal: 12 },
3675
+ rowTitle: { fontSize: 15, fontWeight: "600" },
3676
+ rowSub: { fontSize: 13, marginTop: 2 },
3677
+ chev: { fontSize: 18 },
3678
+ faq: { flexDirection: "column", alignItems: "stretch" },
3679
+ faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
3680
+ articleTitle: { fontSize: 22, fontWeight: "700" }
3681
+ });
3682
+
3683
+ // src/ui/StepLayer.tsx
3684
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
3685
+ import { AccessibilityInfo, Animated, Easing, Pressable as Pressable6, StyleSheet as StyleSheet7, View as View7 } from "react-native";
3686
+
3687
+ // src/core/position.ts
3688
+ var MARGIN = 12;
3689
+ var ARROW_INSET = 18;
3690
+ var opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
3691
+ function positionCard(target, size, viewport, insets, placement, offset) {
3692
+ const [rawSide, align] = placement.split("-");
3693
+ const fits = (side2) => {
3694
+ switch (side2) {
3695
+ case "bottom":
3696
+ return target.y + target.height + offset + size.height <= viewport.height - insets.bottom - MARGIN;
3697
+ case "top":
3698
+ return target.y - offset - size.height >= insets.top + MARGIN;
3699
+ case "right":
3700
+ return target.x + target.width + offset + size.width <= viewport.width - insets.right - MARGIN;
3701
+ case "left":
3702
+ return target.x - offset - size.width >= insets.left + MARGIN;
3703
+ }
3704
+ };
3705
+ const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
3706
+ const space = { top: target.y - insets.top, bottom: viewport.height - insets.bottom - (target.y + target.height) };
3707
+ const side = order.find(fits) ?? (space.bottom >= space.top ? "bottom" : "top");
3708
+ const clamp2 = (v, min, max) => Math.max(min, Math.min(max, v));
3709
+ if (side === "top" || side === "bottom") {
3710
+ const cx = target.x + target.width / 2;
3711
+ let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
3712
+ x2 = clamp2(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
3713
+ let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
3714
+ y2 = clamp2(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
3715
+ return { x: x2, y: y2, side, arrow: clamp2(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
3716
+ }
3717
+ const cy = target.y + target.height / 2;
3718
+ let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
3719
+ y = clamp2(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
3720
+ const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
3721
+ return { x, y, side, arrow: clamp2(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
3722
+ }
3723
+
3724
+ // src/ui/StepCard.tsx
3725
+ import { Image as Image2, StyleSheet as StyleSheet6, Text as Text6, View as View6 } from "react-native";
3726
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3727
+ function StepCard({ view, client, palette: p, width, style, compact }) {
3728
+ const { step, guide } = view;
3729
+ const buttons = visibleButtons(step, view.canGoBack);
3730
+ const body = step.type === "custom" ? step.customHtml || step.body : step.body;
3731
+ const showProgress = step.showProgress && view.total > 1;
3732
+ const onLink = (url) => client.navigate(url);
3733
+ return /* @__PURE__ */ jsx6(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ jsxs5(View6, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
3734
+ step.imageUrl ? /* @__PURE__ */ jsx6(Image2, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
3735
+ step.title ? /* @__PURE__ */ jsxs5(View6, { style: styles6.header, children: [
3736
+ /* @__PURE__ */ jsx6(Text6, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
3737
+ step.showClose ? /* @__PURE__ */ jsx6(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
3738
+ ] }) : null,
3739
+ body ? (
3740
+ // Without a title the close button floats beside the body instead of taking a row.
3741
+ /* @__PURE__ */ jsx6(View6, { style: !step.title && step.showClose ? styles6.bodyBesideClose : void 0, children: /* @__PURE__ */ jsx6(RichText, { html: body, palette: p, onLink }) })
3742
+ ) : null,
3743
+ !step.title && step.showClose ? /* @__PURE__ */ jsx6(View6, { style: compact ? styles6.floatingCloseCompact : styles6.floatingClose, children: /* @__PURE__ */ jsx6(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) }) : null,
3744
+ showProgress || buttons.length ? /* @__PURE__ */ jsxs5(View6, { style: styles6.footer, children: [
3745
+ showProgress ? /* @__PURE__ */ jsxs5(Text6, { style: [styles6.progress, { color: p.muted }], accessibilityLabel: `Step ${view.index + 1} of ${view.total}`, children: [
3746
+ view.index + 1,
3747
+ " of ",
3748
+ view.total
3749
+ ] }) : /* @__PURE__ */ jsx6(View6, { style: styles6.flex }),
3750
+ /* @__PURE__ */ jsx6(View6, { style: styles6.buttons, children: buttons.map((b) => /* @__PURE__ */ jsx6(GButton, { label: b.label, variant: b.variant, palette: p, onPress: () => client.pressButton(guide.id, b) }, b.id)) })
3751
+ ] }) : null
3752
+ ] }) });
3753
+ }
3754
+ function announcement(view) {
3755
+ const parts = [view.step.title, htmlToText(view.step.body)].filter(Boolean);
3756
+ if (view.total > 1) parts.push(`Step ${view.index + 1} of ${view.total}`);
3757
+ return parts.join(". ");
3758
+ }
3759
+ var styles6 = StyleSheet6.create({
3760
+ inner: { padding: 16, gap: 10 },
3761
+ compact: { paddingHorizontal: 14, paddingVertical: 10, gap: 6 },
3762
+ image: { width: "100%", height: 140, marginBottom: 2 },
3763
+ header: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
3764
+ title: { flex: 1, fontSize: 17, fontWeight: "700", lineHeight: 22 },
3765
+ flex: { flex: 1 },
3766
+ bodyBesideClose: { paddingRight: 24 },
3767
+ floatingClose: { position: "absolute", top: 16, right: 16 },
3768
+ floatingCloseCompact: { position: "absolute", top: 10, right: 14 },
3769
+ footer: { flexDirection: "row", alignItems: "center", gap: 8, marginTop: 4, flexWrap: "wrap" },
3770
+ progress: { flex: 1, fontSize: 12, fontWeight: "500" },
3771
+ buttons: { flexDirection: "row", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }
3772
+ });
3773
+
3774
+ // src/ui/StepLayer.tsx
3775
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3776
+ function useTargetRect(client, targetId, origin) {
3777
+ const [rect, setRect] = useState4(null);
3778
+ useEffect4(() => {
3779
+ if (!targetId) {
3780
+ setRect(null);
3781
+ return;
3782
+ }
3783
+ let alive = true;
3784
+ const measure = async () => {
3785
+ const r = await client.targets.measure(targetId);
3786
+ if (!alive) return;
3787
+ setRect((prev) => {
3788
+ const next = r ? { x: r.x - origin.x, y: r.y - origin.y, width: r.width, height: r.height } : null;
3789
+ if (prev && next && Math.abs(prev.x - next.x) < 0.5 && Math.abs(prev.y - next.y) < 0.5 && prev.width === next.width && prev.height === next.height) return prev;
3790
+ return next;
3791
+ });
3792
+ };
3793
+ void measure();
3794
+ const off = client.targets.onChange(() => void measure());
3795
+ const timer = setInterval(() => void measure(), 250);
3796
+ return () => {
3797
+ alive = false;
3798
+ off();
3799
+ clearInterval(timer);
3800
+ };
3801
+ }, [client, targetId, origin.x, origin.y]);
3802
+ return rect;
3803
+ }
3804
+ function useFadeIn(key) {
3805
+ const opacity = useRef2(new Animated.Value(0)).current;
3806
+ useEffect4(() => {
3807
+ opacity.setValue(0);
3808
+ Animated.timing(opacity, { toValue: 1, duration: 160, easing: Easing.out(Easing.quad), useNativeDriver: true }).start();
3809
+ }, [key, opacity]);
3810
+ return opacity;
3811
+ }
3812
+ function StepLayer({ view, client, palette: p, geometry }) {
3813
+ const { step } = view;
3814
+ const rect = useTargetRect(client, view.asModal ? null : view.targetId, geometry.origin);
3815
+ const opacity = useFadeIn(view.key);
3816
+ const { viewport, insets } = geometry;
3817
+ const cardWidth = Math.min(viewport.width - 32, step.width ?? 340);
3818
+ useEffect4(() => {
3819
+ if (step.type === "hotspot" && !view.expanded) return;
3820
+ AccessibilityInfo.announceForAccessibility?.(announcement(view));
3821
+ }, [view.key, view.expanded]);
3822
+ const backdrop = (children) => /* @__PURE__ */ jsx7(Pressable6, { accessible: false, style: [StyleSheet7.absoluteFill, { backgroundColor: p.overlay }], onPress: () => client.backdropPressed(view.guide.id), children });
3823
+ const anchored = !view.asModal && ["tooltip", "popover", "spotlight", "hotspot"].includes(step.type);
3824
+ if (!anchored && step.type === "banner") {
3825
+ const top = step.position.startsWith("top");
3826
+ return /* @__PURE__ */ jsx7(Animated.View, { pointerEvents: "box-none", style: [styles7.banner, top ? { top: insets.top + 8 } : { bottom: insets.bottom + 8 }, { left: insets.left + 8, right: insets.right + 8, opacity }], children: /* @__PURE__ */ jsx7(StepCard, { view, client, palette: p, width: viewport.width - insets.left - insets.right - 16, compact: true }) });
3827
+ }
3828
+ if (!anchored && step.type === "slideout") {
3829
+ const wide = viewport.width >= 600;
3830
+ const right = !step.position.endsWith("left");
3831
+ const width = wide ? Math.min(380, cardWidth) : viewport.width - insets.left - insets.right - 24;
3832
+ return /* @__PURE__ */ jsx7(Animated.View, { pointerEvents: "box-none", style: [styles7.abs, { bottom: insets.bottom + 12, opacity }, wide ? right ? { right: insets.right + 16 } : { left: insets.left + 16 } : { left: insets.left + 12 }], children: /* @__PURE__ */ jsx7(StepCard, { view, client, palette: p, width }) });
3833
+ }
3834
+ if (!anchored) {
3835
+ return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, styles7.center, { opacity }], children: [
3836
+ step.showOverlay ? backdrop() : null,
3837
+ /* @__PURE__ */ jsx7(StepCard, { view, client, palette: p, width: Math.min(viewport.width - 40, step.width ?? 360) })
3838
+ ] });
3839
+ }
3840
+ if (!rect) return null;
3841
+ if (step.type === "hotspot") {
3842
+ const size = step.hotspot?.size ?? 14;
3843
+ return /* @__PURE__ */ jsxs6(View7, { pointerEvents: "box-none", style: StyleSheet7.absoluteFill, children: [
3844
+ /* @__PURE__ */ jsx7(Hotspot, { x: rect.x + rect.width - size / 2, y: rect.y - size / 2, size, pulse: step.hotspot?.pulse ?? true, color: p.primary, onPress: () => client.toggleHotspot(view.guide.id), label: step.title || "Show tip" }),
3845
+ view.expanded ? /* @__PURE__ */ jsx7(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth }) : null
3846
+ ] });
3847
+ }
3848
+ if (step.type === "spotlight") {
3849
+ const pad = step.spotlightPadding;
3850
+ const hole = { x: rect.x - pad, y: rect.y - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 };
3851
+ const r = Math.max(0, Math.min(12, p.radius, hole.width / 2, hole.height / 2));
3852
+ const shade = { backgroundColor: p.overlay };
3853
+ const press = () => client.backdropPressed(view.guide.id);
3854
+ const corners = [
3855
+ [hole.x, hole.y, -r, -r],
3856
+ [hole.x + hole.width - r, hole.y, -2 * r, -r],
3857
+ [hole.x, hole.y + hole.height - r, -r, -2 * r],
3858
+ [hole.x + hole.width - r, hole.y + hole.height - r, -2 * r, -2 * r]
3859
+ ];
3860
+ return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
3861
+ /* @__PURE__ */ jsx7(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: 0, height: Math.max(0, hole.y) }] }),
3862
+ /* @__PURE__ */ jsx7(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: hole.y + hole.height, bottom: 0 }] }),
3863
+ /* @__PURE__ */ jsx7(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, width: Math.max(0, hole.x), top: hole.y, height: hole.height }] }),
3864
+ /* @__PURE__ */ jsx7(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: hole.x + hole.width, right: 0, top: hole.y, height: hole.height }] }),
3865
+ r > 0 ? corners.map(([left, top, dx, dy], i) => /* @__PURE__ */ jsx7(View7, { pointerEvents: "none", style: [styles7.abs, { left, top, width: r, height: r, overflow: "hidden" }], children: /* @__PURE__ */ jsx7(View7, { style: [styles7.abs, { left: dx, top: dy, width: 4 * r, height: 4 * r, borderRadius: 2 * r, borderWidth: r, borderColor: p.overlay }] }) }, i)) : null,
3866
+ /* @__PURE__ */ jsx7(View7, { pointerEvents: "none", style: [styles7.abs, { left: hole.x, top: hole.y, width: hole.width, height: hole.height, borderRadius: r, borderWidth: 2, borderColor: p.primary }] }),
3867
+ /* @__PURE__ */ jsx7(Anchored, { view, client, palette: p, geometry, rect: hole, width: cardWidth })
3868
+ ] });
3869
+ }
3870
+ return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
3871
+ step.showOverlay ? backdrop() : null,
3872
+ /* @__PURE__ */ jsx7(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth })
3873
+ ] });
3874
+ }
3875
+ var ARROW = 12;
3876
+ function Anchored({ view, client, palette: p, geometry, rect, width }) {
3877
+ const [height, setHeight] = useState4(0);
3878
+ const onLayout = (e) => setHeight(Math.round(e.nativeEvent.layout.height));
3879
+ const offset = view.step.offset + ARROW / 2;
3880
+ const pos = positionCard(rect, { width, height: height || 120 }, geometry.viewport, geometry.insets, view.step.placement, offset);
3881
+ return /* @__PURE__ */ jsxs6(View7, { pointerEvents: "box-none", style: [styles7.abs, { left: pos.x, top: pos.y, width, opacity: height ? 1 : 0 }], onLayout, children: [
3882
+ /* @__PURE__ */ jsx7(Arrow, { side: pos.side, at: pos.arrow, color: p.background, border: p.border }),
3883
+ /* @__PURE__ */ jsx7(StepCard, { view, client, palette: p, width })
3884
+ ] });
3885
+ }
3886
+ function Arrow({ side, at, color, border }) {
3887
+ const half = ARROW / 2;
3888
+ const place = side === "bottom" ? { top: -half, left: at - half } : side === "top" ? { bottom: -half, left: at - half } : side === "right" ? { left: -half, top: at - half } : { right: -half, top: at - half };
3889
+ return /* @__PURE__ */ jsx7(View7, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
3890
+ }
3891
+ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
3892
+ const scale = useRef2(new Animated.Value(0)).current;
3893
+ const [reduceMotion, setReduceMotion] = useState4(false);
3894
+ useEffect4(() => {
3895
+ let alive = true;
3896
+ AccessibilityInfo.isReduceMotionEnabled?.().then((v) => alive && setReduceMotion(v), () => {
3897
+ });
3898
+ return () => {
3899
+ alive = false;
3900
+ };
3901
+ }, []);
3902
+ useEffect4(() => {
3903
+ if (!pulse || reduceMotion) return;
3904
+ const loop = Animated.loop(Animated.timing(scale, { toValue: 1, duration: 1400, easing: Easing.out(Easing.ease), useNativeDriver: true }));
3905
+ loop.start();
3906
+ return () => loop.stop();
3907
+ }, [pulse, reduceMotion, scale]);
3908
+ return /* @__PURE__ */ jsxs6(Pressable6, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
3909
+ pulse && !reduceMotion ? /* @__PURE__ */ jsx7(
3910
+ Animated.View,
3911
+ {
3912
+ pointerEvents: "none",
3913
+ style: [styles7.abs, { width: size, height: size, borderRadius: size / 2, backgroundColor: color, opacity: scale.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] }), transform: [{ scale: scale.interpolate({ inputRange: [0, 1], outputRange: [1, 2.6] }) }] }]
3914
+ }
3915
+ ) : null,
3916
+ /* @__PURE__ */ jsx7(View7, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
3917
+ ] });
3918
+ }
3919
+ var styles7 = StyleSheet7.create({
3920
+ abs: { position: "absolute" },
3921
+ center: { alignItems: "center", justifyContent: "center" },
3922
+ banner: { position: "absolute" },
3923
+ arrow: { width: ARROW, height: ARROW, transform: [{ rotate: "45deg" }], borderWidth: StyleSheet7.hairlineWidth }
3924
+ });
3925
+
3926
+ // src/ui/Survey.tsx
3927
+ import { useEffect as useEffect5, useState as useState5 } from "react";
3928
+ import {
3929
+ AccessibilityInfo as AccessibilityInfo2,
3930
+ KeyboardAvoidingView,
3931
+ Platform as Platform4,
3932
+ Pressable as Pressable7,
3933
+ ScrollView as ScrollView4,
3934
+ StyleSheet as StyleSheet8,
3935
+ Text as Text7,
3936
+ TextInput as TextInput3,
3937
+ View as View8
3938
+ } from "react-native";
3939
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3940
+ function Survey({
3941
+ view,
3942
+ client,
3943
+ palette: p,
3944
+ geometry
3945
+ }) {
3946
+ const config = view.guide.config.survey;
3947
+ const [answers, setAnswers] = useState5({});
3948
+ const [error, setError] = useState5("");
3949
+ useEffect5(() => {
3950
+ setAnswers({});
3951
+ setError("");
3952
+ AccessibilityInfo2.announceForAccessibility?.(
3953
+ `Survey: ${config?.questions[0]?.title ?? view.guide.name}`
3954
+ );
3955
+ }, [view.key]);
3956
+ if (!config) return null;
3957
+ const { insets, viewport } = geometry;
3958
+ const width = Math.min(viewport.width - insets.left - insets.right - 24, 420);
3959
+ const centered = config.position === "center";
3960
+ const set = (id, value) => setAnswers((prev) => {
3961
+ const next = { ...prev };
3962
+ if (value === void 0) delete next[id];
3963
+ else next[id] = value;
3964
+ for (const q of config.questions)
3965
+ if (!isQuestionVisible(q, next) && next[q.id] !== void 0) delete next[q.id];
3966
+ return next;
3967
+ });
3968
+ const submit = () => {
3969
+ const missing = config.questions.filter(
3970
+ (q) => q.required && isQuestionVisible(q, answers) && answers[q.id] === void 0
3971
+ );
3972
+ if (missing.length) {
3973
+ setError(`Please answer: ${missing.map((q) => q.title).join(", ")}`);
3974
+ return;
3975
+ }
3976
+ setError("");
3977
+ client.submitSurvey(view.guide.id, { ...answers });
3978
+ };
3979
+ const close = () => client.closeSurvey(view.guide.id);
3980
+ return (
3981
+ // KeyboardAvoidingView owns its bottom padding, so the safe-area padding lives on an inner view.
3982
+ /* @__PURE__ */ jsxs7(
3983
+ KeyboardAvoidingView,
3984
+ {
3985
+ pointerEvents: "box-none",
3986
+ behavior: Platform4.OS === "ios" ? "padding" : void 0,
3987
+ style: StyleSheet8.absoluteFill,
3988
+ children: [
3989
+ centered ? /* @__PURE__ */ jsx8(
3990
+ Pressable7,
3991
+ {
3992
+ accessible: false,
3993
+ style: [StyleSheet8.absoluteFill, { backgroundColor: p.overlay }],
3994
+ onPress: close
3995
+ }
3996
+ ) : null,
3997
+ /* @__PURE__ */ jsx8(
3998
+ View8,
3999
+ {
4000
+ pointerEvents: "box-none",
4001
+ style: [
4002
+ styles8.fill,
4003
+ centered ? styles8.center : styles8.bottom,
4004
+ !centered && { paddingBottom: insets.bottom + 12 }
4005
+ ],
4006
+ children: /* @__PURE__ */ jsx8(
4007
+ Surface,
4008
+ {
4009
+ palette: p,
4010
+ style: { width, maxHeight: viewport.height - insets.top - insets.bottom - 40 },
4011
+ children: view.submitted ? /* @__PURE__ */ jsxs7(View8, { style: styles8.inner, children: [
4012
+ /* @__PURE__ */ jsx8(
4013
+ Text7,
4014
+ {
4015
+ accessibilityRole: "header",
4016
+ style: [styles8.title, { color: p.text, fontFamily: p.fontFamily }],
4017
+ children: config.thankYouTitle
4018
+ }
4019
+ ),
4020
+ config.thankYouBody ? /* @__PURE__ */ jsx8(
4021
+ RichText,
4022
+ {
4023
+ html: config.thankYouBody,
4024
+ palette: p,
4025
+ onLink: (u) => client.navigate(u)
4026
+ }
4027
+ ) : null,
4028
+ /* @__PURE__ */ jsx8(GButton, { label: "Done", palette: p, onPress: close, style: styles8.end })
4029
+ ] }) : /* @__PURE__ */ jsxs7(ScrollView4, { contentContainerStyle: styles8.inner, keyboardShouldPersistTaps: "handled", children: [
4030
+ config.questions.filter((q) => isQuestionVisible(q, answers)).map((q, i) => /* @__PURE__ */ jsxs7(View8, { style: styles8.question, children: [
4031
+ /* @__PURE__ */ jsxs7(View8, { style: styles8.head, children: [
4032
+ /* @__PURE__ */ jsxs7(Text7, { style: [styles8.qTitle, { color: p.text, fontFamily: p.fontFamily }], children: [
4033
+ q.title,
4034
+ !q.required ? /* @__PURE__ */ jsx8(Text7, { style: { color: p.muted, fontWeight: "400" }, children: " (optional)" }) : null
4035
+ ] }),
4036
+ i === 0 ? /* @__PURE__ */ jsx8(CloseButton, { palette: p, onPress: close, label: "Dismiss survey" }) : null
4037
+ ] }),
4038
+ q.description ? /* @__PURE__ */ jsx8(Text7, { style: [styles8.desc, { color: p.muted }], children: q.description }) : null,
4039
+ /* @__PURE__ */ jsx8(
4040
+ QuestionInput,
4041
+ {
4042
+ q,
4043
+ value: answers[q.id],
4044
+ onChange: (v) => set(q.id, v),
4045
+ palette: p
4046
+ }
4047
+ )
4048
+ ] }, q.id)),
4049
+ error ? /* @__PURE__ */ jsx8(Text7, { accessibilityLiveRegion: "polite", style: [styles8.error], children: error }) : null,
4050
+ /* @__PURE__ */ jsx8(GButton, { label: "Submit", palette: p, onPress: submit, style: styles8.end })
4051
+ ] })
4052
+ }
4053
+ )
4054
+ }
4055
+ )
4056
+ ]
4057
+ }
4058
+ )
4059
+ );
4060
+ }
4061
+ function QuestionInput({
4062
+ q,
4063
+ value,
4064
+ onChange,
4065
+ palette: p
4066
+ }) {
4067
+ const chip = (selected) => [
4068
+ styles8.chip,
4069
+ {
4070
+ borderColor: selected ? p.primary : p.border,
4071
+ backgroundColor: selected ? p.primary : p.background,
4072
+ borderRadius: Math.min(8, p.radius)
4073
+ }
4074
+ ];
4075
+ const chipText = (selected) => [
4076
+ styles8.chipText,
4077
+ { color: selected ? p.onPrimary : p.text }
4078
+ ];
4079
+ switch (q.type) {
4080
+ case "nps":
4081
+ case "csat": {
4082
+ const min = q.type === "nps" ? 0 : 1;
4083
+ const max = q.type === "nps" ? 10 : q.scaleMax ?? 5;
4084
+ const values = Array.from({ length: max - min + 1 }, (_, i) => min + i);
4085
+ return /* @__PURE__ */ jsxs7(View8, { children: [
4086
+ /* @__PURE__ */ jsx8(View8, { style: styles8.scale, accessibilityRole: "radiogroup", accessibilityLabel: q.title, children: values.map((n) => /* @__PURE__ */ jsx8(
4087
+ Pressable7,
4088
+ {
4089
+ accessibilityRole: "radio",
4090
+ accessibilityState: { selected: value === n },
4091
+ accessibilityLabel: String(n),
4092
+ onPress: () => onChange(n),
4093
+ style: [...chip(value === n), styles8.scaleItem],
4094
+ children: /* @__PURE__ */ jsx8(Text7, { style: chipText(value === n), children: n })
4095
+ },
4096
+ n
4097
+ )) }),
4098
+ q.lowLabel || q.highLabel ? /* @__PURE__ */ jsxs7(View8, { style: styles8.labels, children: [
4099
+ /* @__PURE__ */ jsx8(Text7, { style: [styles8.small, { color: p.muted }], children: q.lowLabel }),
4100
+ /* @__PURE__ */ jsx8(Text7, { style: [styles8.small, { color: p.muted }], children: q.highLabel })
4101
+ ] }) : null
4102
+ ] });
4103
+ }
4104
+ case "rating": {
4105
+ const max = q.scaleMax ?? 5;
4106
+ return /* @__PURE__ */ jsx8(View8, { style: styles8.stars, accessibilityRole: "radiogroup", accessibilityLabel: q.title, children: Array.from({ length: max }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsx8(
4107
+ Pressable7,
4108
+ {
4109
+ accessibilityRole: "radio",
4110
+ accessibilityLabel: `${n} of ${max}`,
4111
+ accessibilityState: { selected: value === n },
4112
+ onPress: () => onChange(n),
4113
+ hitSlop: 4,
4114
+ children: /* @__PURE__ */ jsx8(
4115
+ Text7,
4116
+ {
4117
+ style: [
4118
+ styles8.star,
4119
+ { color: typeof value === "number" && n <= value ? p.primary : p.border }
4120
+ ],
4121
+ children: "\u2605"
4122
+ }
4123
+ )
4124
+ },
4125
+ n
4126
+ )) });
4127
+ }
4128
+ case "thumbs":
4129
+ return /* @__PURE__ */ jsx8(View8, { style: styles8.row, accessibilityRole: "radiogroup", children: [
4130
+ [true, "\u{1F44D}", "Thumbs up"],
4131
+ [false, "\u{1F44E}", "Thumbs down"]
4132
+ ].map(([val, icon, label]) => /* @__PURE__ */ jsx8(
4133
+ Pressable7,
4134
+ {
4135
+ accessibilityRole: "radio",
4136
+ accessibilityLabel: label,
4137
+ accessibilityState: { selected: value === val },
4138
+ onPress: () => onChange(val),
4139
+ style: [...chip(value === val), styles8.thumb],
4140
+ children: /* @__PURE__ */ jsx8(Text7, { style: styles8.thumbText, children: icon })
4141
+ },
4142
+ String(val)
4143
+ )) });
4144
+ case "single_choice":
4145
+ case "multiple_choice": {
4146
+ const multi = q.type === "multiple_choice";
4147
+ const selected = new Set(
4148
+ multi ? value ?? [] : value !== void 0 ? [String(value)] : []
4149
+ );
4150
+ return /* @__PURE__ */ jsx8(View8, { style: styles8.choices, children: q.options.map((o) => {
4151
+ const on = selected.has(o.id);
4152
+ return /* @__PURE__ */ jsxs7(
4153
+ Pressable7,
4154
+ {
4155
+ accessibilityRole: multi ? "checkbox" : "radio",
4156
+ accessibilityState: { checked: on },
4157
+ accessibilityLabel: o.label,
4158
+ onPress: () => {
4159
+ if (!multi) return onChange(o.id);
4160
+ const next = new Set(selected);
4161
+ if (on) next.delete(o.id);
4162
+ else next.add(o.id);
4163
+ onChange(next.size ? Array.from(next) : void 0);
4164
+ },
4165
+ style: [
4166
+ styles8.choice,
4167
+ { borderColor: on ? p.primary : p.border, borderRadius: Math.min(10, p.radius) }
4168
+ ],
4169
+ children: [
4170
+ /* @__PURE__ */ jsx8(
4171
+ View8,
4172
+ {
4173
+ style: [
4174
+ styles8.dot,
4175
+ multi && styles8.box,
4176
+ {
4177
+ borderColor: on ? p.primary : p.border,
4178
+ backgroundColor: on ? p.primary : "transparent"
4179
+ }
4180
+ ]
4181
+ }
4182
+ ),
4183
+ /* @__PURE__ */ jsx8(Text7, { style: [styles8.choiceText, { color: p.text }], children: o.label })
4184
+ ]
4185
+ },
4186
+ o.id
4187
+ );
4188
+ }) });
4189
+ }
4190
+ case "text":
4191
+ default:
4192
+ return /* @__PURE__ */ jsx8(
4193
+ TextInput3,
4194
+ {
4195
+ accessibilityLabel: q.title,
4196
+ multiline: true,
4197
+ value: typeof value === "string" ? value : "",
4198
+ onChangeText: (t) => onChange(t ? t.slice(0, 5e3) : void 0),
4199
+ placeholder: q.placeholder || "",
4200
+ placeholderTextColor: p.muted,
4201
+ style: [
4202
+ styles8.input,
4203
+ {
4204
+ color: p.text,
4205
+ borderColor: p.border,
4206
+ borderRadius: Math.min(10, p.radius),
4207
+ backgroundColor: p.surface
4208
+ }
4209
+ ]
4210
+ }
4211
+ );
4212
+ }
4213
+ }
4214
+ var styles8 = StyleSheet8.create({
4215
+ fill: { flex: 1 },
4216
+ center: { alignItems: "center", justifyContent: "center" },
4217
+ bottom: { alignItems: "center", justifyContent: "flex-end" },
4218
+ inner: { padding: 16, gap: 14 },
4219
+ title: { fontSize: 17, fontWeight: "700" },
4220
+ end: { alignSelf: "flex-end" },
4221
+ question: { gap: 8 },
4222
+ head: { flexDirection: "row", gap: 8, alignItems: "flex-start" },
4223
+ qTitle: { flex: 1, fontSize: 15, fontWeight: "600", lineHeight: 20 },
4224
+ desc: { fontSize: 13 },
4225
+ scale: { flexDirection: "row", flexWrap: "wrap", gap: 4 },
4226
+ scaleItem: { flexGrow: 1, flexBasis: 26 },
4227
+ chip: {
4228
+ borderWidth: 1,
4229
+ minHeight: 36,
4230
+ alignItems: "center",
4231
+ justifyContent: "center",
4232
+ paddingHorizontal: 4
4233
+ },
4234
+ chipText: { fontSize: 14, fontWeight: "600" },
4235
+ labels: { flexDirection: "row", justifyContent: "space-between", marginTop: 4 },
4236
+ small: { fontSize: 12 },
4237
+ stars: { flexDirection: "row", gap: 6 },
4238
+ star: { fontSize: 30 },
4239
+ row: { flexDirection: "row", gap: 10 },
4240
+ thumb: { width: 56, height: 44 },
4241
+ thumbText: { fontSize: 22 },
4242
+ choices: { gap: 8 },
4243
+ choice: {
4244
+ flexDirection: "row",
4245
+ alignItems: "center",
4246
+ gap: 10,
4247
+ borderWidth: 1,
4248
+ paddingVertical: 10,
4249
+ paddingHorizontal: 12
4250
+ },
4251
+ dot: { width: 18, height: 18, borderRadius: 9, borderWidth: 2 },
4252
+ box: { borderRadius: 4 },
4253
+ choiceText: { fontSize: 15, flex: 1 },
4254
+ input: { minHeight: 80, borderWidth: 1, padding: 10, fontSize: 15, textAlignVertical: "top" },
4255
+ error: { color: "#dc2626", fontSize: 13 }
4256
+ });
4257
+
4258
+ // src/ui/theme.ts
4259
+ var DARK = { background: "#18181b", text: "#f4f4f5", muted: "#a1a1aa", border: "#3f3f46", surface: "#27272a" };
4260
+ function luminance(hex) {
4261
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex);
4262
+ if (!m) return 1;
4263
+ const n = parseInt(m[1], 16);
4264
+ const ch = [n >> 16 & 255, n >> 8 & 255, n & 255].map((c) => {
4265
+ const s = c / 255;
4266
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
4267
+ });
4268
+ return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
4269
+ }
4270
+ function mix(hex, alpha) {
4271
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
4272
+ return /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${a}` : hex;
4273
+ }
4274
+ function nativeFont(stack) {
4275
+ const first = stack?.split(",")[0]?.trim().replace(/^["']|["']$/g, "");
4276
+ if (!first || /^(system-ui|-apple-system|sans-serif|serif|monospace|inter|ui-sans-serif)$/i.test(first)) return void 0;
4277
+ return first;
4278
+ }
4279
+ function palette(theme, systemScheme, explicit) {
4280
+ const primary = theme.primaryColor ?? "#4f46e5";
4281
+ const wantsDark = theme.dark === "dark" || theme.dark === "auto" && systemScheme === "dark";
4282
+ const dark = wantsDark && !explicit?.backgroundColor;
4283
+ const background = dark ? DARK.background : theme.backgroundColor ?? "#ffffff";
4284
+ const text = dark ? DARK.text : theme.textColor ?? "#111827";
4285
+ return {
4286
+ primary,
4287
+ onPrimary: luminance(primary) > 0.45 ? "#111827" : "#ffffff",
4288
+ background,
4289
+ text,
4290
+ muted: dark ? DARK.muted : mix(text, 0.65),
4291
+ border: dark ? DARK.border : mix(text, 0.12),
4292
+ surface: dark ? DARK.surface : mix(text, 0.05),
4293
+ radius: theme.borderRadius ?? 12,
4294
+ overlay: `rgba(15, 23, 42, ${theme.overlayOpacity ?? 0.55})`,
4295
+ fontFamily: nativeFont(theme.fontFamily)
4296
+ };
4297
+ }
4298
+
4299
+ // src/ui/Overlay.tsx
4300
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
4301
+ function defaultInsets(width, height) {
4302
+ if (Platform5.OS === "ios") {
4303
+ const notch = Math.max(width, height) >= 812 && !Platform5.isPad;
4304
+ const landscape = width > height;
4305
+ return { top: landscape ? 0 : notch ? 50 : 20, bottom: notch ? landscape ? 21 : 34 : 0, left: landscape && notch ? 44 : 0, right: landscape && notch ? 44 : 0 };
4306
+ }
4307
+ return { top: StatusBar.currentHeight ?? 24, bottom: 16, left: 0, right: 0 };
4308
+ }
4309
+ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
4310
+ const ui = useSyncExternalStore2(
4311
+ useCallback2((cb) => client.subscribeUi(cb), [client]),
4312
+ () => client.getUiState(),
4313
+ () => client.getUiState()
4314
+ );
4315
+ const { width, height } = useWindowDimensions();
4316
+ const scheme = useColorScheme();
4317
+ const ref = useRef3(null);
4318
+ const [origin, setOrigin] = useState6({ x: 0, y: 0 });
4319
+ useEffect6(() => {
4320
+ client.targets.setViewport(width, height);
4321
+ }, [client, width, height]);
4322
+ const measureOrigin = useCallback2(() => {
4323
+ ref.current?.measureInWindow?.((x, y) => {
4324
+ if (Number.isFinite(x) && Number.isFinite(y)) setOrigin((o) => o.x === x && o.y === y ? o : { x, y });
4325
+ });
4326
+ }, []);
4327
+ const insets = { ...defaultInsets(width, height), ...insetsProp };
4328
+ const geometry = { origin, viewport: { width, height }, insets };
4329
+ const basePalette = palette(ui.theme, scheme);
4330
+ const messenger = ui.messenger ? client.getMessenger() : null;
4331
+ const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
4332
+ const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
4333
+ const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
4334
+ return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
4335
+ ui.checklists.map((c) => /* @__PURE__ */ jsx9(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
4336
+ showRcLauncher ? /* @__PURE__ */ jsx9(
4337
+ Pressable8,
4338
+ {
4339
+ accessibilityRole: "button",
4340
+ accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
4341
+ onPress: () => client.openResourceCenter(),
4342
+ style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
4343
+ children: /* @__PURE__ */ jsx9(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
4344
+ }
4345
+ ) : null,
4346
+ messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ jsx9(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
4347
+ ui.survey ? /* @__PURE__ */ jsx9(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
4348
+ ui.steps.map((s) => /* @__PURE__ */ jsx9(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
4349
+ messenger ? /* @__PURE__ */ jsx9(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ jsx9(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
4350
+ ] });
4351
+ }
4352
+ var styles9 = StyleSheet9.create({
4353
+ rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
4354
+ rcText: { fontSize: 22, fontWeight: "800" }
4355
+ });
4356
+
4357
+ // src/ui/Target.tsx
4358
+ import { createContext, useCallback as useCallback3, useContext, useEffect as useEffect7, useRef as useRef4 } from "react";
4359
+ import { View as View10 } from "react-native";
4360
+ import { jsx as jsx10 } from "react/jsx-runtime";
4361
+ var GuidelyContext = createContext(null);
4362
+ var GuidelyScreenContext = createContext(null);
4363
+ function GuidelyScreenScope({ name, children }) {
4364
+ return /* @__PURE__ */ jsx10(GuidelyScreenContext.Provider, { value: name, children });
4365
+ }
4366
+ function guidelyScreenLayout({ route, children }) {
4367
+ return /* @__PURE__ */ jsx10(GuidelyScreenScope, { name: route.name, children });
4368
+ }
4369
+ var TAP_SLOP = 10;
4370
+ function useGuidelyTarget(id) {
4371
+ const client = useContext(GuidelyContext);
4372
+ const screen = useContext(GuidelyScreenContext);
4373
+ const ref = useRef4(null);
4374
+ const start = useRef4(null);
4375
+ useEffect7(() => {
4376
+ if (!client || !id) return;
4377
+ return client.targets.register(id, {
4378
+ screen,
4379
+ measure: () => new Promise((resolve) => {
4380
+ const node = ref.current;
4381
+ if (!node || typeof node.measureInWindow !== "function") return resolve(null);
4382
+ node.measureInWindow((x, y, width, height) => resolve(Number.isFinite(x) ? { x, y, width, height } : null));
4383
+ })
4384
+ });
4385
+ }, [client, id, screen]);
4386
+ const onLayout = useCallback3(() => client?.targets.changed(), [client]);
4387
+ const onTouchStart = useCallback3((e) => {
4388
+ start.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
4389
+ }, []);
4390
+ const onTouchEnd = useCallback3(
4391
+ (e) => {
4392
+ const s = start.current;
4393
+ start.current = null;
4394
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
4395
+ },
4396
+ [client, id]
4397
+ );
4398
+ return { ref, collapsable: false, onLayout, onTouchStart, onTouchEnd };
4399
+ }
4400
+ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
4401
+ const t = useGuidelyTarget(id);
4402
+ return /* @__PURE__ */ jsx10(
4403
+ View10,
4404
+ {
4405
+ ...rest,
4406
+ ref: t.ref,
4407
+ collapsable: false,
4408
+ onLayout: (e) => {
4409
+ t.onLayout();
4410
+ onLayout?.(e);
4411
+ },
4412
+ onTouchStart: (e) => {
4413
+ t.onTouchStart(e);
4414
+ onTouchStart?.(e);
4415
+ },
4416
+ onTouchEnd: (e) => {
4417
+ t.onTouchEnd(e);
4418
+ onTouchEnd?.(e);
4419
+ },
4420
+ children
4421
+ }
4422
+ );
4423
+ }
4424
+
4425
+ // src/index.tsx
4426
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
4427
+ function deviceLocale() {
4428
+ try {
4429
+ return Intl.DateTimeFormat().resolvedOptions().locale;
4430
+ } catch {
4431
+ return void 0;
4432
+ }
4433
+ }
4434
+ function defaultBridge(appVersion) {
4435
+ const { width, height } = Dimensions.get("window");
4436
+ return {
4437
+ device: {
4438
+ platform: Platform6.OS === "android" ? "android" : "ios",
4439
+ deviceType: Platform6.OS === "ios" && Platform6.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
4440
+ locale: deviceLocale(),
4441
+ appVersion
4442
+ },
4443
+ openExternal: (url) => void Linking.openURL(url).catch(() => {
4444
+ })
4445
+ };
4446
+ }
4447
+ function GuidelyProvider({ children, user, insets, launcherOffset = 72, ...options }) {
4448
+ const [client, setClient] = useState7(null);
4449
+ const optionsRef = useRef5(options);
4450
+ optionsRef.current = options;
4451
+ const { projectId, environment, apiUrl, storage } = options;
4452
+ useEffect8(() => {
4453
+ if (!projectId) return;
4454
+ let c;
4455
+ try {
4456
+ const latest = optionsRef.current;
4457
+ c = new GuidelyClient(
4458
+ { ...latest, projectId, environment, apiUrl, storage, onNavigate: (url) => optionsRef.current.onNavigate?.(url), onError: (err) => optionsRef.current.onError?.(err) },
4459
+ defaultBridge(latest.appVersion)
4460
+ );
4461
+ } catch (err) {
4462
+ optionsRef.current.onError?.(err instanceof Error ? err : new Error(String(err)));
4463
+ return;
4464
+ }
4465
+ setClient(c);
4466
+ void c.init();
4467
+ const sub = AppState.addEventListener("change", (s) => c.setAppState(s));
4468
+ return () => {
4469
+ sub.remove();
4470
+ c.destroy();
4471
+ setClient((cur) => cur === c ? null : cur);
4472
+ };
4473
+ }, [projectId, environment, apiUrl, storage]);
4474
+ const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
4475
+ const hadUser = useRef5(false);
4476
+ useEffect8(() => {
4477
+ if (!client) return;
4478
+ if (user && user.id) {
4479
+ hadUser.current = true;
4480
+ void client.identify(user.id, user.traits, user.account);
4481
+ } else if (hadUser.current) {
4482
+ hadUser.current = false;
4483
+ client.reset();
4484
+ }
4485
+ }, [client, userKey]);
4486
+ return /* @__PURE__ */ jsxs9(GuidelyContext.Provider, { value: client, children: [
4487
+ children,
4488
+ client ? /* @__PURE__ */ jsx11(GuidelyOverlay, { client, insets, launcherOffset }) : null
4489
+ ] });
4490
+ }
4491
+ var NOOP_RESULTS = {
4492
+ init: Promise.resolve(),
4493
+ ready: Promise.resolve(),
4494
+ identify: Promise.resolve(),
4495
+ setAccount: Promise.resolve(),
4496
+ updateTraits: Promise.resolve(),
4497
+ refresh: Promise.resolve(),
4498
+ startGuide: false,
4499
+ getActiveGuide: null,
4500
+ getGuides: [],
4501
+ getChecklistProgress: { completedTaskIds: [], completedAt: null, dismissedAt: null },
4502
+ getGuideState: { impressions: 0 },
4503
+ getMessenger: null,
4504
+ isGuideCompleted: false,
4505
+ isClientReady: false,
4506
+ getScreen: "",
4507
+ on: () => {
4508
+ },
4509
+ subscribeUi: () => () => {
4510
+ },
4511
+ getState: { ready: false, destroyed: false }
4512
+ };
4513
+ var noopClient = new Proxy({}, {
4514
+ get(_target, prop) {
4515
+ if (prop === "options") return { projectId: "" };
4516
+ if (typeof prop !== "string") return void 0;
4517
+ return () => NOOP_RESULTS[prop];
4518
+ }
4519
+ });
4520
+ function useGuidely() {
4521
+ return useContext2(GuidelyContext) ?? noopClient;
4522
+ }
4523
+ function useGuidelyReady() {
4524
+ const client = useContext2(GuidelyContext);
4525
+ const [ready, setReady] = useState7(() => !!client?.isClientReady());
4526
+ useEffect8(() => {
4527
+ if (!client) return setReady(false);
4528
+ setReady(client.isClientReady());
4529
+ const off = client.on("ready", () => setReady(true));
4530
+ void client.ready().then(() => setReady(client.isClientReady()));
4531
+ return off;
4532
+ }, [client]);
4533
+ return ready;
4534
+ }
4535
+ function useGuidelyIdentify(userId, traits, account) {
4536
+ const client = useContext2(GuidelyContext);
4537
+ const key = JSON.stringify([userId ?? null, traits ?? null, account ?? null]);
4538
+ useEffect8(() => {
4539
+ if (client && userId) void client.identify(userId, traits, account);
4540
+ }, [client, key]);
4541
+ }
4542
+ function useGuidelyEvent(event, handler) {
4543
+ const client = useContext2(GuidelyContext);
4544
+ const ref = useRef5(handler);
4545
+ ref.current = handler;
4546
+ useEffect8(() => {
4547
+ if (!client) return;
4548
+ return client.on(event, ((...args) => ref.current(...args)));
4549
+ }, [client, event]);
4550
+ }
4551
+ function useGuidelyScreen(name, properties) {
4552
+ const client = useContext2(GuidelyContext);
4553
+ useEffect8(() => {
4554
+ if (client && name) client.screen(name, properties);
4555
+ }, [client, name]);
4556
+ }
4557
+ function useGuidelyNavigation(navigationRef, options = {}) {
4558
+ const client = useContext2(GuidelyContext);
4559
+ const nameRef = useRef5(options.screenName);
4560
+ nameRef.current = options.screenName;
4561
+ const report = () => {
4562
+ const route = navigationRef.getCurrentRoute?.();
4563
+ if (client && route?.name) client.screen(nameRef.current ? nameRef.current(route) : route.name);
4564
+ };
4565
+ useEffect8(report, [client]);
4566
+ return { onReady: report, onStateChange: report };
4567
+ }
4568
+ function useUiSelector(select, fallback) {
4569
+ const client = useContext2(GuidelyContext);
4570
+ const selectRef = useRef5(select);
4571
+ selectRef.current = select;
4572
+ const [value, setValue] = useState7(() => client ? select(client) : fallback);
4573
+ useEffect8(() => {
4574
+ if (!client) return;
4575
+ let last = JSON.stringify(selectRef.current(client));
4576
+ setValue(selectRef.current(client));
4577
+ const update = () => {
4578
+ const next = selectRef.current(client);
4579
+ const key = JSON.stringify(next);
4580
+ if (key !== last) {
4581
+ last = key;
4582
+ setValue(next);
4583
+ }
4584
+ };
4585
+ const offs = [client.subscribeUi(update), client.on("checklist:task", update), client.on("identify", update), client.on("guide:complete", update), client.on("guide:dismiss", update), client.on("ready", update)];
4586
+ return () => offs.forEach((off) => off());
4587
+ }, [client]);
4588
+ return value;
4589
+ }
4590
+ function useChecklistProgress(idOrKey) {
4591
+ return useUiSelector((c) => c.getChecklistProgress(idOrKey), { completedTaskIds: [], completedAt: null, dismissedAt: null });
4592
+ }
4593
+ function useGuideState(idOrKey) {
4594
+ return useUiSelector(
4595
+ (c) => {
4596
+ const s = c.getGuideState(idOrKey);
4597
+ return { impressions: s.impressions, completed: !!s.completedAt, dismissed: !!s.dismissedAt };
4598
+ },
4599
+ { impressions: 0, completed: false, dismissed: false }
4600
+ );
4601
+ }
4602
+ function useActiveGuide() {
4603
+ return useUiSelector((c) => c.getActiveGuide(), null);
4604
+ }
4605
+ export {
4606
+ DEFAULT_API_URL,
4607
+ GuidelyClient,
4608
+ GuidelyOverlay,
4609
+ GuidelyProvider,
4610
+ GuidelyScreenScope,
4611
+ GuidelyTarget,
4612
+ Messenger,
4613
+ SDK_NAME,
4614
+ SDK_VERSION,
4615
+ TargetRegistry,
4616
+ createSseParser,
4617
+ guidelyScreenLayout,
4618
+ htmlToText,
4619
+ noopClient,
4620
+ parseHtml,
4621
+ positionCard,
4622
+ targetIdsFor,
4623
+ useActiveGuide,
4624
+ useChecklistProgress,
4625
+ useGuideState,
4626
+ useGuidely,
4627
+ useGuidelyEvent,
4628
+ useGuidelyIdentify,
4629
+ useGuidelyNavigation,
4630
+ useGuidelyReady,
4631
+ useGuidelyScreen,
4632
+ useGuidelyTarget
4633
+ };
4634
+ //# sourceMappingURL=index.js.map