@guidelytech/react-native 0.10.0 → 0.12.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 CHANGED
@@ -3,9 +3,9 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.tsx
6
- import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
7
- import { AppState as AppState2, Dimensions, Linking, Platform as Platform6 } from "react-native";
8
- import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify2 } from "@guidelytech/flags";
6
+ import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
7
+ import { AppState as AppState2, Dimensions, Linking, Platform as Platform6, StyleSheet as StyleSheet10, View as View12 } from "react-native";
8
+ import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify3 } from "@guidelytech/flags";
9
9
 
10
10
  // src/flags.tsx
11
11
  import { createFlagsClient, createStaticFlags, jsonEquals, stableStringify } from "@guidelytech/flags";
@@ -96,7 +96,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
96
96
  return /* @__PURE__ */ jsx(FlagsContext.Provider, { value, children });
97
97
  }
98
98
 
99
- // ../shared/dist/chunk-UW6NRYRY.js
99
+ // ../shared/dist/chunk-XJXXVV3C.js
100
100
  function fnv1a(input) {
101
101
  let h = 2166136261;
102
102
  for (let i = 0; i < input.length; i++) {
@@ -276,6 +276,8 @@ function resolveField(rule, ctx) {
276
276
  return ctx.now.getHours();
277
277
  case "rollout.percentage":
278
278
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
279
+ case "experiment.bucket":
280
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
279
281
  case "environment":
280
282
  return ctx.environment;
281
283
  case "locale":
@@ -286,6 +288,9 @@ function resolveField(rule, ctx) {
286
288
  return ctx.screen;
287
289
  case "app.version":
288
290
  return ctx.appVersion;
291
+ case "page.views":
292
+ case "feature.clicks":
293
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
289
294
  default:
290
295
  return void 0;
291
296
  }
@@ -394,14 +399,14 @@ function sanitizeProperties(props) {
394
399
  for (const [k, v] of Object.entries(props)) {
395
400
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
396
401
  if (typeof v === "function" || typeof v === "symbol") continue;
397
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
398
- size += k.length + (str?.length ?? 0);
402
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
403
+ size += k.length + (str2?.length ?? 0);
399
404
  if (size > MAX_PROPERTY_BYTES) break;
400
405
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
401
406
  }
402
407
  return out;
403
408
  }
404
- var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
409
+ var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
405
410
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
406
411
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
407
412
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
@@ -552,9 +557,151 @@ function isArticleListed(collections, article, member) {
552
557
  }
553
558
  return true;
554
559
  }
560
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
561
+ function orderedModules(modules) {
562
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
563
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
564
+ return [...known, ...missing2];
565
+ }
566
+ function visibleAnnouncements(items, member, now = Date.now()) {
567
+ return (items ?? []).filter((a) => {
568
+ const t = Date.parse(a.publishedAt);
569
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
570
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
571
+ }
572
+ function unreadAnnouncements(items, lastSeenAt) {
573
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
574
+ if (Number.isNaN(seen)) return items.length;
575
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
576
+ }
555
577
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
578
+ var DEFAULT_LOCALE = "en";
579
+ function normalizeLocale(input) {
580
+ if (typeof input !== "string") return "";
581
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
582
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
583
+ return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
584
+ }
585
+ function baseLanguage(locale) {
586
+ return normalizeLocale(locale).split("-")[0] ?? "";
587
+ }
588
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
589
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
590
+ const avail = available.map(normalizeLocale).filter(Boolean);
591
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
592
+ for (const raw of requested) {
593
+ const want = normalizeLocale(raw);
594
+ if (!want) continue;
595
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
596
+ if (exact) return exact;
597
+ const base = baseLanguage(want);
598
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
599
+ if (sameBase) return sameBase;
600
+ }
601
+ return def;
602
+ }
603
+ function visitorLocales(explicit, traits, platform = []) {
604
+ const out = [];
605
+ const push = (v) => {
606
+ const n = normalizeLocale(v);
607
+ if (n && !out.includes(n)) out.push(n);
608
+ };
609
+ push(explicit);
610
+ push(traits?.language);
611
+ push(traits?.locale);
612
+ for (const p of platform) push(p);
613
+ return out;
614
+ }
615
+ function guideLocales(config) {
616
+ const t = config.localization?.translations ?? {};
617
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
618
+ }
619
+ function pick(map, key, fallback) {
620
+ const v = map?.[key];
621
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
622
+ }
623
+ function applyGuideTranslations(config, map) {
624
+ if (!map || !Object.keys(map).length) return config;
625
+ return {
626
+ ...config,
627
+ steps: config.steps.map((s) => ({
628
+ ...s,
629
+ title: pick(map, `steps.${s.id}.title`, s.title),
630
+ body: pick(map, `steps.${s.id}.body`, s.body),
631
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
632
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
633
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
634
+ })),
635
+ checklist: config.checklist ? {
636
+ ...config.checklist,
637
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
638
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
639
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
640
+ tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
641
+ } : config.checklist,
642
+ survey: config.survey ? {
643
+ ...config.survey,
644
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
645
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
646
+ questions: config.survey.questions.map((q) => ({
647
+ ...q,
648
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
649
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
650
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
651
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
652
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
653
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
654
+ }))
655
+ } : config.survey
656
+ };
657
+ }
658
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
659
+ const translations = guide.config.localization?.translations ?? {};
660
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
661
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
662
+ if (!key) return { ...guide, locale };
663
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
664
+ }
665
+ function resourceCenterLocales(rc) {
666
+ const set = new Set(Object.keys(rc.translations ?? {}));
667
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
668
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
669
+ return [...set];
670
+ }
671
+ function translationFor(map, locale) {
672
+ if (!map) return void 0;
673
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
674
+ return key ? map[key] : void 0;
675
+ }
676
+ function localizeArticle(a, locale) {
677
+ const t = translationFor(a.translations, locale);
678
+ if (!t) return a;
679
+ const body = t.body?.trim() ? t.body : a.body;
680
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
681
+ }
682
+ function localizeAnnouncement(a, locale) {
683
+ const t = translationFor(a.translations, locale);
684
+ if (!t) return a;
685
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
686
+ }
687
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
688
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
689
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
690
+ const map = translationFor(rc.translations, locale);
691
+ return {
692
+ ...rc,
693
+ locale,
694
+ title: pick(map, "title", rc.title),
695
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
696
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
697
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
698
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
699
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
700
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
701
+ };
702
+ }
556
703
 
557
- // ../sdk-core/dist/chunk-C4WWSF67.js
704
+ // ../sdk-core/dist/chunk-XML74K2P.js
558
705
  var __defProp2 = Object.defineProperty;
559
706
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
560
707
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -852,6 +999,8 @@ var KEYS = {
852
999
  queue: (key) => `guidely:queue:${key}`,
853
1000
  /** Survey answers and guide/checklist progress not yet accepted by the server. */
854
1001
  outbox: (key) => `guidely:outbox:${key}`,
1002
+ /** Agent Analytics turns not yet accepted by the server. */
1003
+ agentOutbox: (key) => `guidely:agents:${key}`,
855
1004
  session: "guidely:session",
856
1005
  active: "guidely:active",
857
1006
  authoring: "guidely:authoring",
@@ -861,10 +1010,12 @@ var KEYS = {
861
1010
  /** Messenger conversations this browser can read: [{ id, token }]. */
862
1011
  conversations: (key) => `guidely:conversations:${key}`,
863
1012
  /** Announcement ids opened in the messenger's News tab. */
864
- newsSeen: (key) => `guidely:news:${key}`
1013
+ newsSeen: (key) => `guidely:news:${key}`,
1014
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
1015
+ launcher: (key) => `guidely:launcher:${key}`
865
1016
  };
866
1017
 
867
- // ../sdk-core/dist/chunk-GDJB466J.js
1018
+ // ../sdk-core/dist/chunk-2E2YUQAG.js
868
1019
  var EventQueue = class {
869
1020
  constructor(opts) {
870
1021
  __publicField2(this, "opts", opts);
@@ -1140,12 +1291,32 @@ function areMutuallyExclusive(a, b) {
1140
1291
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1141
1292
  }
1142
1293
  function isWithinSchedule(guide, now = Date.now()) {
1143
- const s = guide.config.survey;
1144
- if (!s) return true;
1145
- if (s.startAt && now < Date.parse(s.startAt)) return false;
1146
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1294
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1295
+ if (!w) continue;
1296
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1297
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1298
+ }
1299
+ return true;
1300
+ }
1301
+ var MINUTE = 60 * 1e3;
1302
+ function countsTowardThrottle(guide) {
1303
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1304
+ }
1305
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1306
+ if (!settings || !settings.enabled) return true;
1307
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1308
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1309
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1310
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1147
1311
  return true;
1148
1312
  }
1313
+ function pruneLaunches(launches, settings, now = Date.now()) {
1314
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1315
+ return launches.filter((l) => {
1316
+ const t = Date.parse(l);
1317
+ return !Number.isNaN(t) && now - t < keepMs;
1318
+ }).slice(-100);
1319
+ }
1149
1320
  function dismissedAfterMerge(local, server) {
1150
1321
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1151
1322
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -1154,6 +1325,158 @@ function dismissedAfterMerge(local, server) {
1154
1325
  // src/core/client.ts
1155
1326
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
1156
1327
 
1328
+ // src/core/launcherPlacement.ts
1329
+ var LAUNCHER_PLACEMENT_KEY = "guidely:messenger.launcher";
1330
+ var LAUNCHER_MARGIN = 16;
1331
+ var LAUNCHER_DRAG_THRESHOLD = 8;
1332
+ var clamp2 = (v, min, max) => Math.min(max, Math.max(min, v));
1333
+ function sideX(f, side) {
1334
+ return side === "left" ? f.insets.left + LAUNCHER_MARGIN : f.width - f.insets.right - LAUNCHER_MARGIN - f.size;
1335
+ }
1336
+ function verticalRange(f) {
1337
+ const max = f.height - f.defaultBottom - f.size;
1338
+ return { min: Math.min(max, f.insets.top + LAUNCHER_MARGIN), max };
1339
+ }
1340
+ function clampToSafeArea(f, p) {
1341
+ const left = f.insets.left + LAUNCHER_MARGIN;
1342
+ const top = f.insets.top + LAUNCHER_MARGIN;
1343
+ return {
1344
+ x: clamp2(p.x, left, Math.max(left, f.width - f.insets.right - LAUNCHER_MARGIN - f.size)),
1345
+ y: clamp2(p.y, top, Math.max(top, f.height - f.insets.bottom - LAUNCHER_MARGIN - f.size))
1346
+ };
1347
+ }
1348
+ function snapPlacement(f, p) {
1349
+ const { min, max } = verticalRange(f);
1350
+ const side = p.x + f.size / 2 < f.width / 2 ? "left" : "right";
1351
+ return { side, fraction: max > min ? (max - clamp2(p.y, min, max)) / (max - min) : 0 };
1352
+ }
1353
+ function placementPoint(f, placement) {
1354
+ const { min, max } = verticalRange(f);
1355
+ return { x: sideX(f, placement.side), y: max - clamp2(placement.fraction, 0, 1) * (max - min) };
1356
+ }
1357
+ function parsePlacement(raw) {
1358
+ if (!raw) return null;
1359
+ try {
1360
+ const v = JSON.parse(raw);
1361
+ if (!v || typeof v !== "object" || v.side !== "left" && v.side !== "right" || typeof v.fraction !== "number" || !Number.isFinite(v.fraction)) return null;
1362
+ return { side: v.side, fraction: clamp2(v.fraction, 0, 1) };
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1367
+ function serializePlacement(p) {
1368
+ return JSON.stringify({ side: p.side, fraction: Math.round(clamp2(p.fraction, 0, 1) * 1e4) / 1e4 });
1369
+ }
1370
+
1371
+ // src/core/autocapture.ts
1372
+ var TAP_SLOP = 10;
1373
+ var TAP_MAX_MS = 1e3;
1374
+ var HEATMAP_MAX_POINTS = 40;
1375
+ function quoteValue(value) {
1376
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1377
+ }
1378
+ function attrSelector(name, value) {
1379
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1380
+ }
1381
+ function cleanText(text) {
1382
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1383
+ return t ? t.slice(0, 100) : void 0;
1384
+ }
1385
+ function tapProperties(el, x, y, platform, maskAll) {
1386
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1387
+ const text = maskAll ? void 0 : cleanText(el.text);
1388
+ if (text) props.text = text;
1389
+ return props;
1390
+ }
1391
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1392
+ function heatmapPoint(x, y, el, rect, rage) {
1393
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1394
+ const s = el?.selector.slice(0, 120);
1395
+ if (s) p.s = s;
1396
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1397
+ p.rx = r2((x - rect.x) / rect.width);
1398
+ p.ry = r2((y - rect.y) / rect.height);
1399
+ }
1400
+ return p;
1401
+ }
1402
+ var RageClickDetector = class {
1403
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1404
+ __publicField(this, "threshold", threshold);
1405
+ __publicField(this, "windowMs", windowMs);
1406
+ __publicField(this, "radius", radius);
1407
+ __publicField(this, "clicks", []);
1408
+ __publicField(this, "fired", false);
1409
+ }
1410
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1411
+ get burst() {
1412
+ return this.fired ? this.clicks.length : 0;
1413
+ }
1414
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1415
+ add(x, y, t) {
1416
+ const first = this.clicks[0];
1417
+ const last = this.clicks[this.clicks.length - 1];
1418
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1419
+ this.clicks = [];
1420
+ this.fired = false;
1421
+ }
1422
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1423
+ this.clicks.push({ x, y, t });
1424
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1425
+ this.fired = true;
1426
+ return this.clicks.length;
1427
+ }
1428
+ };
1429
+ var HeatmapBuffer = class {
1430
+ constructor() {
1431
+ /** Screen path the buffered points belong to. */
1432
+ __publicField(this, "url", "");
1433
+ __publicField(this, "points", []);
1434
+ }
1435
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1436
+ add(url, point, burst) {
1437
+ this.url = url;
1438
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1439
+ this.points.push(point);
1440
+ return this.points.length >= HEATMAP_MAX_POINTS;
1441
+ }
1442
+ /** Empties the buffer, returning what was in it. */
1443
+ take() {
1444
+ const points = this.points;
1445
+ this.points = [];
1446
+ return points.length ? { url: this.url, points } : null;
1447
+ }
1448
+ };
1449
+ var TapTracker = class {
1450
+ constructor() {
1451
+ __publicField(this, "start", null);
1452
+ __publicField(this, "moved", 0);
1453
+ }
1454
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1455
+ begin(x, y, t, touches, id) {
1456
+ if (this.start || touches > 1) {
1457
+ this.start = null;
1458
+ return;
1459
+ }
1460
+ this.start = { x, y, t, id };
1461
+ this.moved = 0;
1462
+ }
1463
+ move(x, y, id) {
1464
+ const s = this.start;
1465
+ if (s && (id === void 0 || s.id === void 0 || id === s.id)) this.moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1466
+ }
1467
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1468
+ end(x, y, t, id) {
1469
+ const s = this.start;
1470
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1471
+ this.start = null;
1472
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1473
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1474
+ }
1475
+ cancel() {
1476
+ this.start = null;
1477
+ }
1478
+ };
1479
+
1157
1480
  // src/core/targets.ts
1158
1481
  var TargetTimeoutError = class extends Error {
1159
1482
  constructor(ids) {
@@ -1256,6 +1579,35 @@ var TargetRegistry = class {
1256
1579
  return null;
1257
1580
  }
1258
1581
  }
1582
+ /**
1583
+ * The registered target on the current screen whose visible frame contains the point (window
1584
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1585
+ * `timeoutMs` are skipped.
1586
+ */
1587
+ async hitTest(x, y, timeoutMs = 500) {
1588
+ const candidates = [];
1589
+ this.targets.forEach((list, id) => {
1590
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1591
+ });
1592
+ if (!candidates.length) return null;
1593
+ const rects = await Promise.all(
1594
+ candidates.map(
1595
+ ({ handle }) => new Promise((resolve) => {
1596
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1597
+ void this.measureHandle(handle).then((r) => {
1598
+ clearTimeout(timer);
1599
+ resolve(r);
1600
+ });
1601
+ })
1602
+ )
1603
+ );
1604
+ let best = null;
1605
+ rects.forEach((r, i) => {
1606
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1607
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1608
+ });
1609
+ return best;
1610
+ }
1259
1611
  /** First of `ids` that is mounted and visible. */
1260
1612
  async resolve(ids) {
1261
1613
  for (const id of ids) {
@@ -2013,7 +2365,8 @@ var Messenger = class {
2013
2365
  this.stopThreadPolling();
2014
2366
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
2015
2367
  try {
2016
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2368
+ const locale = this.deps.locale?.();
2369
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
2017
2370
  this.set({ article: res.article });
2018
2371
  } catch (err) {
2019
2372
  this.deps.onError(err);
@@ -2128,7 +2481,7 @@ var PersistentStore = class {
2128
2481
 
2129
2482
  // src/core/client.ts
2130
2483
  var SDK_NAME = "@guidelytech/react-native";
2131
- var SDK_VERSION = "0.10.0";
2484
+ var SDK_VERSION = "0.12.0";
2132
2485
  var DEFAULT_API_URL = "https://guidelytech.com";
2133
2486
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2134
2487
  var CACHED_START_MS = 1500;
@@ -2142,6 +2495,8 @@ function settlesWithin(promise, ms) {
2142
2495
  promise.then(done, done);
2143
2496
  });
2144
2497
  }
2498
+ var appVersionKey = (key) => `guidely:app:${key}`;
2499
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2145
2500
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2146
2501
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
2147
2502
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -2173,6 +2528,7 @@ var GuidelyClient = class {
2173
2528
  __publicField(this, "activeSurvey", null);
2174
2529
  __publicField(this, "queued", []);
2175
2530
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2531
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
2176
2532
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
2177
2533
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
2178
2534
  __publicField(this, "surveyView", null);
@@ -2192,6 +2548,27 @@ var GuidelyClient = class {
2192
2548
  __publicField(this, "flags");
2193
2549
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
2194
2550
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2551
+ // Product analytics (autocapture).
2552
+ __publicField(this, "rage", new RageClickDetector());
2553
+ __publicField(this, "heatmap", new HeatmapBuffer());
2554
+ /** Taps are processed in order (target frames are measured asynchronously). */
2555
+ __publicField(this, "tapChain", Promise.resolve());
2556
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2557
+ __publicField(this, "screenStartedAt", 0);
2558
+ __publicField(this, "screenLeft", true);
2559
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2560
+ __publicField(this, "openedAt", Date.now());
2561
+ __publicField(this, "launchChecked", false);
2562
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2563
+ __publicField(this, "hadIdentity", false);
2564
+ /** Language set with `setLocale()` (or the `locale` option). */
2565
+ __publicField(this, "locale", null);
2566
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2567
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2568
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2569
+ __publicField(this, "localizedRc", null);
2570
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2571
+ __publicField(this, "announcementsSeenAtOpen", null);
2195
2572
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2196
2573
  __publicField(this, "eventBus", {
2197
2574
  on: (name, cb) => {
@@ -2221,6 +2598,7 @@ var GuidelyClient = class {
2221
2598
  if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
2222
2599
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
2223
2600
  if (options.user) this.user = { ...options.user };
2601
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
2224
2602
  this.flags = this.createFlags(options.flags);
2225
2603
  }
2226
2604
  createFlags(opts) {
@@ -2269,9 +2647,10 @@ var GuidelyClient = class {
2269
2647
  async bootstrap() {
2270
2648
  const key = this.options.projectId;
2271
2649
  try {
2272
- 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.outbox(key), KEYS.conversations(key)]);
2650
+ 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.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY]);
2273
2651
  if (this.destroyed) return;
2274
2652
  this.anonymousId = this.store.get(KEYS.anon) || "";
2653
+ this.hadIdentity = !!this.anonymousId;
2275
2654
  if (!this.anonymousId) {
2276
2655
  this.anonymousId = `anon_${randomId(20)}`;
2277
2656
  this.store.set(KEYS.anon, this.anonymousId);
@@ -2308,6 +2687,7 @@ var GuidelyClient = class {
2308
2687
  if (!this.config) await fresh;
2309
2688
  else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
2310
2689
  if (this.destroyed) return;
2690
+ this.checkLaunch();
2311
2691
  this.wireTimers();
2312
2692
  if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
2313
2693
  if (this.destroyed) return;
@@ -2337,6 +2717,7 @@ var GuidelyClient = class {
2337
2717
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2338
2718
  else this.store.remove(KEYS.etag(this.options.projectId));
2339
2719
  this.syncMessenger();
2720
+ if (this.hydrated) this.checkLaunch();
2340
2721
  this.emitter.emit("config", this.config);
2341
2722
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2342
2723
  return true;
@@ -2357,7 +2738,7 @@ var GuidelyClient = class {
2357
2738
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2358
2739
  const url = this.currentUrl();
2359
2740
  const ctx = this.getContext();
2360
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2741
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2361
2742
  this.drainQueue(url, ctx);
2362
2743
  }, 1e3);
2363
2744
  this.cleanups.push(() => clearInterval(poll));
@@ -2381,7 +2762,9 @@ var GuidelyClient = class {
2381
2762
  setAppState(state) {
2382
2763
  if (this.destroyed || !this.hydrated) return;
2383
2764
  if (state === "background") {
2765
+ const wasForeground = this.backgroundedAt === null;
2384
2766
  this.backgroundedAt = Date.now();
2767
+ if (wasForeground) this.wentToBackground();
2385
2768
  this.session.lastActiveAt = nowIso();
2386
2769
  this.persistSession();
2387
2770
  void this.queue?.flush();
@@ -2398,6 +2781,7 @@ var GuidelyClient = class {
2398
2781
  this.persistSession();
2399
2782
  this.firedDelays.clear();
2400
2783
  }
2784
+ this.cameToForeground();
2401
2785
  void this.flags.refresh();
2402
2786
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2403
2787
  else if (this.isReady) this.evaluate("ready");
@@ -2410,6 +2794,7 @@ var GuidelyClient = class {
2410
2794
  /** Tears everything down. Create a new client to start again. */
2411
2795
  destroy() {
2412
2796
  if (this.destroyed) return;
2797
+ this.flushHeatmap();
2413
2798
  this.destroyed = true;
2414
2799
  this.active?.controller.destroy();
2415
2800
  this.active = null;
@@ -2461,6 +2846,7 @@ var GuidelyClient = class {
2461
2846
  for (const [id, s] of Object.entries(data.guideState || {})) {
2462
2847
  const local = guides[id];
2463
2848
  guides[id] = {
2849
+ ...local,
2464
2850
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2465
2851
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2466
2852
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2482,7 +2868,10 @@ var GuidelyClient = class {
2482
2868
  checklists,
2483
2869
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2484
2870
  audienceIds: data.audienceIds || [],
2485
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2871
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2872
+ // Device-local only (the server doesn't hold them).
2873
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2874
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2486
2875
  };
2487
2876
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2488
2877
  this.persistState();
@@ -2574,6 +2963,8 @@ var GuidelyClient = class {
2574
2963
  ...extra
2575
2964
  };
2576
2965
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2966
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2967
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2577
2968
  this.queue.enqueue(event);
2578
2969
  this.emitter.emit("track", event);
2579
2970
  return event;
@@ -2606,11 +2997,19 @@ var GuidelyClient = class {
2606
2997
  if (!name) return;
2607
2998
  this.whenHydrated(() => {
2608
2999
  const changed = name !== this.screenName;
3000
+ if (changed) {
3001
+ this.leaveScreen();
3002
+ this.flushHeatmap();
3003
+ }
2609
3004
  this.screenName = name;
2610
3005
  this.targets.setScreen(name);
2611
3006
  if (changed) this.syncFlagAttributes();
2612
3007
  const url = this.currentUrl();
2613
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
3008
+ if (changed) {
3009
+ this.screenStartedAt = Date.now();
3010
+ this.screenLeft = this.backgroundedAt !== null;
3011
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, ...this.deviceProperties() } });
3012
+ }
2614
3013
  this.completeChecklistTasksBy("url_visited", url);
2615
3014
  this.active?.controller.resume();
2616
3015
  this.flows.forEach((f) => f.controller.resume());
@@ -2638,6 +3037,149 @@ var GuidelyClient = class {
2638
3037
  if (this.isReady) this.evaluate("click", { targetId });
2639
3038
  }
2640
3039
  // ---------------------------------------------------------------------------
3040
+ // Product analytics (autocapture)
3041
+ // ---------------------------------------------------------------------------
3042
+ captureAllowed() {
3043
+ return !this.destroyed && this.analyticsEnabled();
3044
+ }
3045
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3046
+ autocaptureOn(key) {
3047
+ const o = this.options.autocapture;
3048
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3049
+ }
3050
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3051
+ pageviewsOn() {
3052
+ const o = this.options.autocapture;
3053
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3054
+ }
3055
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3056
+ isTapCaptureOn() {
3057
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3058
+ }
3059
+ /**
3060
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3061
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3062
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3063
+ */
3064
+ recordTap(tap) {
3065
+ try {
3066
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3067
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3068
+ if (!on.clicks && !on.rage && !on.heat) return;
3069
+ const t = tap.t ?? Date.now();
3070
+ const url = this.screenName ? this.currentUrl() : "";
3071
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3072
+ } catch (err) {
3073
+ this.reportError(err);
3074
+ }
3075
+ }
3076
+ async processTap(tap, t, url, on) {
3077
+ let el = tap.element ?? null;
3078
+ let rect = null;
3079
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3080
+ if (target) {
3081
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3082
+ rect = target.rect;
3083
+ }
3084
+ if (this.destroyed) return;
3085
+ const platform = this.bridge.device.platform;
3086
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3087
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3088
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3089
+ const count = this.rage.add(tap.x, tap.y, t);
3090
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3091
+ if (!on.heat) return;
3092
+ const burst = this.rage.burst;
3093
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3094
+ if (this.destroyed) return;
3095
+ if (this.heatmap.url !== url) this.flushHeatmap();
3096
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3097
+ }
3098
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3099
+ flushHeatmap() {
3100
+ const batch = this.heatmap.take();
3101
+ if (!batch || !this.captureAllowed()) return;
3102
+ const collect = this.collectUrls() && !!batch.url;
3103
+ const { width, height } = this.windowSize();
3104
+ this.enqueue("heatmap", {
3105
+ url: collect ? batch.url : void 0,
3106
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3107
+ });
3108
+ }
3109
+ windowSize() {
3110
+ try {
3111
+ const s = this.bridge.windowSize?.();
3112
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3113
+ } catch {
3114
+ }
3115
+ return { width: 0, height: 0 };
3116
+ }
3117
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3118
+ leaveScreen() {
3119
+ if (!this.screenName || this.screenLeft) return;
3120
+ this.screenLeft = true;
3121
+ if (!this.autocaptureOn("pageLeave")) return;
3122
+ const url = this.currentUrl();
3123
+ const collect = this.collectUrls();
3124
+ this.enqueue("page_leave", {
3125
+ url: collect ? url : void 0,
3126
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3127
+ });
3128
+ }
3129
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3130
+ appInfo() {
3131
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3132
+ const build = this.options.appBuild;
3133
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3134
+ }
3135
+ /**
3136
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3137
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3138
+ */
3139
+ checkLaunch() {
3140
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3141
+ this.launchChecked = true;
3142
+ try {
3143
+ const on = this.autocaptureOn("appLifecycle");
3144
+ const info = this.appInfo();
3145
+ if (info.appVersion && this.options.storage) {
3146
+ const key = appVersionKey(this.options.projectId);
3147
+ const stored = readJson(this.store, key, null);
3148
+ if (!stored?.appVersion) {
3149
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3150
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3151
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3152
+ }
3153
+ writeJson(this.store, key, info);
3154
+ }
3155
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3156
+ } catch (err) {
3157
+ this.reportError(err);
3158
+ }
3159
+ }
3160
+ wentToBackground() {
3161
+ try {
3162
+ this.leaveScreen();
3163
+ this.flushHeatmap();
3164
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3165
+ } catch (err) {
3166
+ this.reportError(err);
3167
+ }
3168
+ }
3169
+ cameToForeground() {
3170
+ try {
3171
+ this.openedAt = Date.now();
3172
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3173
+ if (this.screenName && this.screenLeft) {
3174
+ this.screenStartedAt = Date.now();
3175
+ this.screenLeft = false;
3176
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, ...this.deviceProperties(), resumed: true } });
3177
+ }
3178
+ } catch (err) {
3179
+ this.reportError(err);
3180
+ }
3181
+ }
3182
+ // ---------------------------------------------------------------------------
2641
3183
  // Targeting + triggers
2642
3184
  // ---------------------------------------------------------------------------
2643
3185
  /** Builds the targeting context for the current moment. */
@@ -2684,6 +3226,50 @@ var GuidelyClient = class {
2684
3226
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2685
3227
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2686
3228
  }
3229
+ /** Guide throttling: may this guide launch automatically now? */
3230
+ throttleAllows(guide) {
3231
+ if (!countsTowardThrottle(guide)) return true;
3232
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3233
+ }
3234
+ recordLaunch(guide, manual) {
3235
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3236
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3237
+ }
3238
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3239
+ candidates() {
3240
+ const guides = this.config?.guides ?? [];
3241
+ const holdouts = this.config?.guideHoldouts ?? [];
3242
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3243
+ }
3244
+ holdoutFor(guide) {
3245
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3246
+ }
3247
+ // ---------------------------------------------------------------------------
3248
+ // Localization
3249
+ // ---------------------------------------------------------------------------
3250
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3251
+ locales() {
3252
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3253
+ }
3254
+ defaultLocale() {
3255
+ return this.config?.settings?.localization?.defaultLocale || "en";
3256
+ }
3257
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3258
+ localize(guide) {
3259
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3260
+ this.guideLocales.set(guide.id, localized.locale);
3261
+ return localized;
3262
+ }
3263
+ /**
3264
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3265
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3266
+ */
3267
+ setLocale(locale) {
3268
+ const next = locale ? normalizeLocale(locale) || null : null;
3269
+ if (next === this.locale) return;
3270
+ this.locale = next;
3271
+ this.notify();
3272
+ }
2687
3273
  triggerFires(guide, reason, detail) {
2688
3274
  const t = guide.config.trigger;
2689
3275
  switch (t.type) {
@@ -2719,7 +3305,7 @@ var GuidelyClient = class {
2719
3305
  this.lastAudiences = current;
2720
3306
  }
2721
3307
  const candidates = [];
2722
- for (const guide of this.config.guides) {
3308
+ for (const guide of this.candidates()) {
2723
3309
  const t = guide.config.trigger;
2724
3310
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2725
3311
  if (reason === "ready" || reason === "navigation") {
@@ -2735,6 +3321,10 @@ var GuidelyClient = class {
2735
3321
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2736
3322
  if (!this.triggerFires(guide, reason, detail)) continue;
2737
3323
  if (!this.isEligible(guide, ctx, url)) continue;
3324
+ if (!this.throttleAllows(guide)) {
3325
+ this.log("throttled", guide.key);
3326
+ continue;
3327
+ }
2738
3328
  candidates.push(guide);
2739
3329
  }
2740
3330
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2780,6 +3370,12 @@ var GuidelyClient = class {
2780
3370
  }
2781
3371
  tryShow(guide, url, manual) {
2782
3372
  if (this.destroyed) return false;
3373
+ if (!manual && !this.throttleAllows(guide)) return false;
3374
+ if (this.holdoutFor(guide)) {
3375
+ if (this.hasExclusive() && !manual) return false;
3376
+ this.recordImpression(guide, manual);
3377
+ return true;
3378
+ }
2783
3379
  if (!isExclusiveGuide(guide)) {
2784
3380
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2785
3381
  this.show(guide, manual);
@@ -2821,14 +3417,18 @@ var GuidelyClient = class {
2821
3417
  guideState(id) {
2822
3418
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2823
3419
  }
2824
- recordImpression(guide) {
3420
+ recordImpression(guide, manual = false) {
2825
3421
  const s = this.guideState(guide.id);
2826
3422
  s.impressions += 1;
2827
3423
  s.lastShownAt = nowIso();
2828
3424
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3425
+ this.recordLaunch(guide, manual);
2829
3426
  this.persistState();
2830
3427
  this.persistSession();
2831
- this.syncGuideState(guide.id, { impression: true });
3428
+ const holdout = this.holdoutFor(guide);
3429
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3430
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3431
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2832
3432
  }
2833
3433
  recordEnd(guide, reason) {
2834
3434
  const s = this.guideState(guide.id);
@@ -2856,7 +3456,8 @@ var GuidelyClient = class {
2856
3456
  this.reportError(err);
2857
3457
  }
2858
3458
  }
2859
- showFlow(guide, manual, stepId) {
3459
+ showFlow(raw, manual, stepId) {
3460
+ const guide = this.localize(raw);
2860
3461
  const exclusive = isExclusiveGuide(guide);
2861
3462
  const flow = { guide, manual, view: null, controller: null };
2862
3463
  let renders = 0;
@@ -2871,7 +3472,7 @@ var GuidelyClient = class {
2871
3472
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2872
3473
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2873
3474
  if (e.name === "guide_started") {
2874
- this.recordImpression(guide);
3475
+ this.recordImpression(guide, manual);
2875
3476
  this.emitter.emit("guide:start", guide);
2876
3477
  } else if (e.name === "step_viewed" && step) {
2877
3478
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2918,21 +3519,24 @@ var GuidelyClient = class {
2918
3519
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2919
3520
  }
2920
3521
  }
2921
- showChecklist(guide, open) {
3522
+ showChecklist(raw, open) {
3523
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3524
+ const guide = this.localize(raw);
2922
3525
  const config = guide.config.checklist;
2923
- if (!config || this.checklists.has(guide.id)) return;
3526
+ if (!config) return;
2924
3527
  const progress = this.getChecklistProgress(guide.id);
2925
3528
  const first = !this.state.guides[guide.id]?.impressions;
2926
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3529
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2927
3530
  if (first) this.recordImpression(guide);
2928
3531
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2929
3532
  this.notify();
2930
3533
  }
2931
- showSurvey(guide, manual) {
2932
- if (!guide.config.survey) return;
3534
+ showSurvey(raw, manual) {
3535
+ if (!raw.config.survey) return;
3536
+ const guide = this.localize(raw);
2933
3537
  this.activeSurvey = { guide, manual };
2934
3538
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2935
- this.recordImpression(guide);
3539
+ this.recordImpression(guide, manual);
2936
3540
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2937
3541
  this.emitter.emit("guide:start", guide);
2938
3542
  this.notify();
@@ -2992,10 +3596,7 @@ var GuidelyClient = class {
2992
3596
  const steps = [];
2993
3597
  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) });
2994
3598
  const checklists = [];
2995
- this.checklists.forEach((w, id) => {
2996
- const guide = this.findGuide(id);
2997
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
2998
- });
3599
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
2999
3600
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
3000
3601
  for (const cb of Array.from(this.uiListeners)) {
3001
3602
  try {
@@ -3080,10 +3681,58 @@ var GuidelyClient = class {
3080
3681
  getAudienceIds() {
3081
3682
  return this.state.audienceIds;
3082
3683
  }
3684
+ /** The published resource center in the visitor's language, or null when it is off. */
3083
3685
  getResourceCenter() {
3084
- const rc = this.config?.resourceCenter;
3686
+ const rc = this.localizedResourceCenter();
3085
3687
  return rc && rc.enabled ? rc : null;
3086
3688
  }
3689
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3690
+ localizedResourceCenter() {
3691
+ const rc = this.config?.resourceCenter;
3692
+ if (!rc) return null;
3693
+ const locales = this.locales();
3694
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3695
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3696
+ return this.localizedRc.value;
3697
+ }
3698
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3699
+ matchesTargeting(rules) {
3700
+ if (!rules) return true;
3701
+ try {
3702
+ return evaluateRules(rules, this.getContext());
3703
+ } catch (err) {
3704
+ this.reportError(err);
3705
+ return false;
3706
+ }
3707
+ }
3708
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3709
+ getResourceCenterModules() {
3710
+ const rc = this.getResourceCenter();
3711
+ if (!rc) return [];
3712
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3713
+ }
3714
+ /**
3715
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3716
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3717
+ */
3718
+ getHelpModules() {
3719
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3720
+ return modules.filter((id) => id !== "announcements");
3721
+ }
3722
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3723
+ getAnnouncements() {
3724
+ const rc = this.getResourceCenter();
3725
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3726
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3727
+ }
3728
+ /** "What's new" entries published since the visitor last opened the resource center. */
3729
+ getUnreadAnnouncements() {
3730
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3731
+ }
3732
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3733
+ isAnnouncementNew(a) {
3734
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3735
+ }
3087
3736
  async searchResourceCenter(q) {
3088
3737
  try {
3089
3738
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -3092,6 +3741,11 @@ var GuidelyClient = class {
3092
3741
  return [];
3093
3742
  }
3094
3743
  }
3744
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3745
+ articleLocale() {
3746
+ const locale = this.localizedResourceCenter()?.locale;
3747
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3748
+ }
3095
3749
  articleViewed(article) {
3096
3750
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
3097
3751
  }
@@ -3226,6 +3880,11 @@ var GuidelyClient = class {
3226
3880
  }
3227
3881
  if (this.resourceCenterOpen) return;
3228
3882
  this.resourceCenterOpen = true;
3883
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3884
+ if (this.getAnnouncements().length) {
3885
+ this.state.announcementsSeenAt = nowIso();
3886
+ this.persistState();
3887
+ }
3229
3888
  this.enqueue("resource_center_opened");
3230
3889
  this.emitter.emit("resource-center:open");
3231
3890
  this.notify();
@@ -3261,7 +3920,8 @@ var GuidelyClient = class {
3261
3920
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
3262
3921
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
3263
3922
  onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3264
- health: this.api.health
3923
+ health: this.api.health,
3924
+ locale: () => this.articleLocale()
3265
3925
  });
3266
3926
  this.resourceCenterOpen = false;
3267
3927
  }
@@ -3275,6 +3935,14 @@ var GuidelyClient = class {
3275
3935
  getMessenger() {
3276
3936
  return this.messenger;
3277
3937
  }
3938
+ /** Where the visitor dragged the messenger bubble on this device, or null to use the configured spot. */
3939
+ getMessengerLauncherPlacement() {
3940
+ return parsePlacement(this.store.get(LAUNCHER_PLACEMENT_KEY));
3941
+ }
3942
+ /** Remembers the bubble's spot on this device (in memory only when no `storage` was passed). */
3943
+ setMessengerLauncherPlacement(placement) {
3944
+ this.store.set(LAUNCHER_PLACEMENT_KEY, serializePlacement(placement));
3945
+ }
3278
3946
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
3279
3947
  openMessenger(tab) {
3280
3948
  this.whenHydrated(() => this.messenger?.open(tab));
@@ -3305,6 +3973,11 @@ var GuidelyClient = class {
3305
3973
  onFlagsChange(cb) {
3306
3974
  return this.flags.subscribe(cb);
3307
3975
  }
3976
+ /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
3977
+ deviceProperties() {
3978
+ const d = this.bridge.device;
3979
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
3980
+ }
3308
3981
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
3309
3982
  flagContext() {
3310
3983
  const d = this.bridge.device;
@@ -3395,7 +4068,191 @@ var GuidelyClient = class {
3395
4068
  }
3396
4069
  };
3397
4070
  function forPlatform(config, platform) {
3398
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4071
+ const keep = (g) => supportsPlatform(g.config, platform);
4072
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4073
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4074
+ }
4075
+ function withoutCode(guide) {
4076
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4077
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4078
+ }
4079
+
4080
+ // src/ui/Autocapture.tsx
4081
+ import { useMemo as useMemo5, useRef as useRef5 } from "react";
4082
+ import { View as View10 } from "react-native";
4083
+
4084
+ // src/core/viewTree.ts
4085
+ var HOST_TEXT = 6;
4086
+ var MAX_ANCESTORS = 500;
4087
+ var MAX_TEXT_NODES = 300;
4088
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4089
+ var TEXT_INPUT_HOST = /TextInput/i;
4090
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4091
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4092
+ function fiberFromEvent(event) {
4093
+ try {
4094
+ const e = event;
4095
+ if (isFiber(e?._targetInst)) return e._targetInst;
4096
+ const target = e?.target;
4097
+ if (!target || typeof target !== "object") return null;
4098
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4099
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4100
+ } catch {
4101
+ }
4102
+ return null;
4103
+ }
4104
+ function componentName(type, depth = 0) {
4105
+ if (!type || depth > 3) return null;
4106
+ if (typeof type === "function") {
4107
+ const f = type;
4108
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4109
+ return n || null;
4110
+ }
4111
+ if (typeof type === "object") {
4112
+ const o = type;
4113
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4114
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4115
+ }
4116
+ return null;
4117
+ }
4118
+ var isHost = (f) => typeof f.type === "string";
4119
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4120
+ function isInteractive(props) {
4121
+ if (!props || typeof props !== "object") return null;
4122
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4123
+ const role = props.accessibilityRole ?? props.role;
4124
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4125
+ }
4126
+ function firstHost(f) {
4127
+ let cur = f;
4128
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4129
+ return null;
4130
+ }
4131
+ function idSelector(f) {
4132
+ const host = firstHost(f);
4133
+ const sources = [f.memoizedProps, host?.memoizedProps];
4134
+ for (const key of ["nativeID", "testID"]) {
4135
+ for (const p of sources) {
4136
+ const v = p && typeof p === "object" ? p[key] : void 0;
4137
+ if (typeof v === "string" && v) return attrSelector(key, v);
4138
+ }
4139
+ }
4140
+ return null;
4141
+ }
4142
+ function pathSelector(f) {
4143
+ const parts = [];
4144
+ let cur = f;
4145
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4146
+ if (isHost(cur)) continue;
4147
+ const name = componentName(cur.type);
4148
+ if (!name || NOISE.test(name)) continue;
4149
+ let same = 0;
4150
+ let index = 0;
4151
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4152
+ if (s.type !== cur.type) continue;
4153
+ same += 1;
4154
+ if (s === cur) index = same;
4155
+ }
4156
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4157
+ }
4158
+ return parts.join(" > ").slice(0, 300) || "View";
4159
+ }
4160
+ function subtreeText(f, opts) {
4161
+ const out = [];
4162
+ let visited = 0;
4163
+ let masked = false;
4164
+ const walk = (node) => {
4165
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4166
+ visited += 1;
4167
+ if (opts.isMask?.(n.type)) {
4168
+ masked = true;
4169
+ return;
4170
+ }
4171
+ if (isTextInput(n)) continue;
4172
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4173
+ else walk(n.child);
4174
+ }
4175
+ };
4176
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4177
+ walk(f.child);
4178
+ return { text: out.join(" "), masked };
4179
+ }
4180
+ function measureHost(host) {
4181
+ return new Promise((resolve) => {
4182
+ let settled = false;
4183
+ const done = (r) => {
4184
+ if (settled) return;
4185
+ settled = true;
4186
+ clearTimeout(timer);
4187
+ resolve(r);
4188
+ };
4189
+ const timer = setTimeout(() => done(null), 500);
4190
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4191
+ try {
4192
+ const sn = host.stateNode;
4193
+ const pub = sn?.canonical?.publicInstance ?? sn;
4194
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4195
+ const ui = globalThis.nativeFabricUIManager;
4196
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4197
+ } catch {
4198
+ }
4199
+ done(null);
4200
+ });
4201
+ }
4202
+ function describe(f, masked, opts, withText) {
4203
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4204
+ const host = firstHost(f);
4205
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4206
+ if (withText && !masked) {
4207
+ const sub = subtreeText(f, opts);
4208
+ if (!sub.masked) {
4209
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4210
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4211
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4212
+ if (text) el.text = text;
4213
+ }
4214
+ }
4215
+ if (host) el.measure = () => measureHost(host);
4216
+ return el;
4217
+ }
4218
+ function resolveFiber(target, opts = {}) {
4219
+ const none = { ignored: false, element: null, hit: null };
4220
+ if (!target) return none;
4221
+ try {
4222
+ let press = null;
4223
+ let role = null;
4224
+ let hitFiber = null;
4225
+ let stopped = false;
4226
+ let masked = false;
4227
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4228
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4229
+ if (opts.isMask?.(f.type)) masked = true;
4230
+ if (stopped) continue;
4231
+ if (isTextInput(f)) {
4232
+ stopped = true;
4233
+ continue;
4234
+ }
4235
+ if (isHost(f)) continue;
4236
+ const name = componentName(f.type);
4237
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4238
+ const kind = isInteractive(f.memoizedProps);
4239
+ if (kind === "press") {
4240
+ press = f;
4241
+ stopped = true;
4242
+ } else if (kind === "role" && !role) role = f;
4243
+ }
4244
+ const element = press ?? role;
4245
+ return {
4246
+ ignored: false,
4247
+ element: element ? describe(element, masked, opts, true) : null,
4248
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4249
+ };
4250
+ } catch {
4251
+ return none;
4252
+ }
4253
+ }
4254
+ function resolveTapTarget(event, opts = {}) {
4255
+ return resolveFiber(fiberFromEvent(event), opts);
3399
4256
  }
3400
4257
 
3401
4258
  // src/ui/Overlay.tsx
@@ -3919,9 +4776,9 @@ var styles3 = StyleSheet3.create({
3919
4776
  });
3920
4777
 
3921
4778
  // src/ui/Messenger.tsx
3922
- import { useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
3923
- import { ActivityIndicator, Keyboard, Modal as Modal2, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView3, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
3924
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
4779
+ import { Fragment as Fragment2, useCallback, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
4780
+ import { AccessibilityInfo, ActivityIndicator, Animated, Easing, Keyboard, Modal as Modal2, PanResponder, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView3, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
4781
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
3925
4782
  function useMessenger(store) {
3926
4783
  return useSyncExternalStore2(
3927
4784
  useCallback((cb) => store.subscribe(cb), [store]),
@@ -3949,14 +4806,89 @@ function ago(iso) {
3949
4806
  if (s < 86400) return `${Math.floor(s / 3600)}h`;
3950
4807
  return `${Math.floor(s / 86400)}d`;
3951
4808
  }
3952
- function MessengerLauncher({ store, palette: p, style }) {
4809
+ var LAUNCHER_SIZE = 54;
4810
+ function useReduceMotion() {
4811
+ const [on, setOn] = useState3(false);
4812
+ useEffect3(() => {
4813
+ let live = true;
4814
+ AccessibilityInfo.isReduceMotionEnabled?.().then((v) => live && setOn(v)).catch(() => {
4815
+ });
4816
+ const sub = AccessibilityInfo.addEventListener?.("reduceMotionChanged", setOn);
4817
+ return () => {
4818
+ live = false;
4819
+ sub?.remove();
4820
+ };
4821
+ }, []);
4822
+ return on;
4823
+ }
4824
+ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
3953
4825
  const snap = useMessenger(store);
4826
+ const reduceMotion = useReduceMotion();
4827
+ const [placement, setPlacement] = useState3(() => client.getMessengerLauncherPlacement());
4828
+ const pan = useRef2(new Animated.ValueXY()).current;
4829
+ const offset = useRef2({ x: 0, y: 0 });
4830
+ const dragging = useRef2(false);
4831
+ const live = useRef2({ frame, defaultSide, reduceMotion });
4832
+ live.current = { frame, defaultSide, reduceMotion };
4833
+ const { width, height, size, defaultBottom } = frame;
4834
+ const { top, bottom, left, right } = frame.insets;
4835
+ useLayoutEffect(() => {
4836
+ if (dragging.current) return;
4837
+ const f = live.current.frame;
4838
+ const anchor = placementPoint(f, { side: defaultSide, fraction: 0 });
4839
+ const at = placement ? placementPoint(f, placement) : anchor;
4840
+ offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
4841
+ pan.setValue(offset.current);
4842
+ }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
4843
+ const responder = useMemo3(() => {
4844
+ const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
4845
+ const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
4846
+ let start = { x: 0, y: 0 };
4847
+ const follow = (g) => {
4848
+ const a = anchor();
4849
+ const at = clampToSafeArea(live.current.frame, { x: a.x + start.x + g.dx, y: a.y + start.y + g.dy });
4850
+ return { x: at.x - a.x, y: at.y - a.y };
4851
+ };
4852
+ const drop = (g) => {
4853
+ const a = anchor();
4854
+ const d = follow(g);
4855
+ const next = snapPlacement(live.current.frame, { x: a.x + d.x, y: a.y + d.y });
4856
+ const at = placementPoint(live.current.frame, next);
4857
+ const to = { x: at.x - a.x, y: at.y - a.y };
4858
+ offset.current = to;
4859
+ client.setMessengerLauncherPlacement(next);
4860
+ const done = ({ finished }) => {
4861
+ if (finished) dragging.current = false;
4862
+ setPlacement(next);
4863
+ };
4864
+ if (live.current.reduceMotion) {
4865
+ pan.setValue(to);
4866
+ done({ finished: true });
4867
+ } else Animated.timing(pan, { toValue: to, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start(done);
4868
+ };
4869
+ return PanResponder.create({
4870
+ // Taps stay with the Pressable; only a real drag is taken (which cancels its press).
4871
+ onMoveShouldSetPanResponderCapture: (_, g) => moved(g),
4872
+ onMoveShouldSetPanResponder: (_, g) => moved(g),
4873
+ onPanResponderGrant: () => {
4874
+ dragging.current = true;
4875
+ start = offset.current;
4876
+ pan.stopAnimation((v) => {
4877
+ if (Number.isFinite(v?.x) && Number.isFinite(v?.y)) start = v;
4878
+ });
4879
+ },
4880
+ onPanResponderMove: (_, g) => pan.setValue(follow(g)),
4881
+ onPanResponderRelease: (_, g) => drop(g),
4882
+ onPanResponderTerminate: (_, g) => drop(g),
4883
+ onPanResponderTerminationRequest: () => false
4884
+ });
4885
+ }, [client, pan]);
3954
4886
  if (snap.open) return null;
3955
4887
  const label = snap.unread ? `Open ${snap.config.name}, ${snap.unread} unread` : `Open ${snap.config.name}`;
3956
- 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: [
4888
+ return /* @__PURE__ */ jsx5(Animated.View, { ...responder.panHandlers, style: [styles4.launcherWrap, style, { transform: pan.getTranslateTransform() }], children: /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => !dragging.current && store.open(), style: ({ pressed }) => [styles4.launcher, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
3957
4889
  /* @__PURE__ */ jsx5(Text4, { style: [styles4.launcherIcon, { color: p.onPrimary }], children: "\u{1F4AC}" }),
3958
4890
  snap.unread ? /* @__PURE__ */ jsx5(View4, { style: styles4.badge, children: /* @__PURE__ */ jsx5(Text4, { style: styles4.badgeText, children: snap.unread }) }) : null
3959
- ] });
4891
+ ] }) });
3960
4892
  }
3961
4893
  function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
3962
4894
  const snap = useMessenger(store);
@@ -4155,25 +5087,41 @@ function Help({ store, client, palette: p }) {
4155
5087
  ] }, title);
4156
5088
  const audiences = client.getAudienceIds();
4157
5089
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
4158
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
5090
+ const modules = client.getHelpModules();
5091
+ const searchOn = modules.includes("search");
5092
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
5093
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
5094
+ const renderModule = (id) => {
5095
+ switch (id) {
5096
+ case "guides":
5097
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
5098
+ case "checklists":
5099
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
5100
+ case "articles":
5101
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
5102
+ !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
5103
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
5104
+ ] });
5105
+ case "faqs":
5106
+ return rc?.faqs.length ? section(
5107
+ "FAQ",
5108
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
5109
+ /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
5110
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
5111
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5112
+ ] }),
5113
+ faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5114
+ ] }, f.id))
5115
+ ) : null;
5116
+ case "links":
5117
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5118
+ default:
5119
+ return null;
5120
+ }
5121
+ };
4159
5122
  return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
4160
- /* @__PURE__ */ jsx5(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) }] }),
4161
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
4162
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
4163
- !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
4164
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
4165
- rc?.faqs.length ? section(
4166
- "FAQ",
4167
- rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4168
- /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4169
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4170
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4171
- ] }),
4172
- faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4173
- ] }, f.id))
4174
- ) : null,
4175
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4176
- ] })
5123
+ searchOn ? /* @__PURE__ */ jsx5(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) }] }) : null,
5124
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsx5(Fragment3, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx5(Fragment2, { children: renderModule(id) }, id)) })
4177
5125
  ] });
4178
5126
  }
4179
5127
  function Article({ article, palette: p, client }) {
@@ -4185,7 +5133,8 @@ function Article({ article, palette: p, client }) {
4185
5133
  }
4186
5134
  var styles4 = StyleSheet4.create({
4187
5135
  flex: { flex: 1 },
4188
- 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 } },
5136
+ launcherWrap: { position: "absolute", width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, elevation: 8 },
5137
+ launcher: { width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
4189
5138
  launcherIcon: { fontSize: 24 },
4190
5139
  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" },
4191
5140
  badgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" },
@@ -4240,9 +5189,9 @@ var styles4 = StyleSheet4.create({
4240
5189
  });
4241
5190
 
4242
5191
  // src/ui/ResourceCenter.tsx
4243
- import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
5192
+ import { Fragment as Fragment4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
4244
5193
  import { Modal as Modal3, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView4, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
4245
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5194
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
4246
5195
  function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4247
5196
  const rc = client.getResourceCenter();
4248
5197
  const [query, setQuery] = useState4("");
@@ -4276,6 +5225,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4276
5225
  const checklists = useMemo4(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
4277
5226
  if (!rc) return null;
4278
5227
  const close = () => client.closeResourceCenter();
5228
+ const modules = client.getResourceCenterModules();
5229
+ const announcements = client.getAnnouncements();
4279
5230
  const openArticle = (a) => {
4280
5231
  if (a.externalUrl) return client.navigate(a.externalUrl);
4281
5232
  client.articleViewed(a);
@@ -4294,7 +5245,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4294
5245
  ] }, title);
4295
5246
  const audiences = client.getAudienceIds();
4296
5247
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
4297
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5248
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5249
+ const link = (label, onPress) => /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5250
+ const announcement2 = (a) => {
5251
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5252
+ const isNew = client.isAnnouncementNew(a);
5253
+ return /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5254
+ /* @__PURE__ */ jsxs4(View5, { style: styles5.faqHead, children: [
5255
+ /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5256
+ isNew ? /* @__PURE__ */ jsx6(Text5, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5257
+ ] }),
5258
+ a.body ? /* @__PURE__ */ jsx6(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5259
+ a.guideId || linked || a.url ? /* @__PURE__ */ jsxs4(View5, { style: styles5.links, children: [
5260
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5261
+ linked ? link("Read more", () => openArticle(linked)) : null,
5262
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5263
+ ] }) : null
5264
+ ] }, a.id);
5265
+ };
5266
+ const renderModule = (id) => {
5267
+ switch (id) {
5268
+ case "announcements":
5269
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5270
+ case "guides":
5271
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5272
+ case "checklists":
5273
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5274
+ case "articles":
5275
+ return sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))));
5276
+ case "faqs":
5277
+ return rc.faqs.length ? section(
5278
+ "FAQ",
5279
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5280
+ /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5281
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5282
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5283
+ ] }),
5284
+ faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5285
+ ] }, f.id))
5286
+ ) : null;
5287
+ case "links":
5288
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5289
+ default:
5290
+ return null;
5291
+ }
5292
+ };
4298
5293
  return /* @__PURE__ */ jsx6(Modal3, { 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: [
4299
5294
  /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { borderColor: p.border }], children: [
4300
5295
  article ? /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -4305,7 +5300,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4305
5300
  /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
4306
5301
  /* @__PURE__ */ jsx6(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
4307
5302
  ] }) : /* @__PURE__ */ jsxs4(ScrollView4, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
4308
- /* @__PURE__ */ jsx6(
5303
+ modules.includes("search") ? /* @__PURE__ */ jsx6(
4309
5304
  TextInput2,
4310
5305
  {
4311
5306
  value: query,
@@ -4317,26 +5312,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4317
5312
  returnKeyType: "search",
4318
5313
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
4319
5314
  }
4320
- ),
5315
+ ) : null,
4321
5316
  listed ? section(
4322
5317
  `Results for \u201C${query.trim()}\u201D`,
4323
5318
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
4324
- ) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
4325
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
4326
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
4327
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
4328
- rc.faqs.length ? section(
4329
- "FAQ",
4330
- rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
4331
- /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
4332
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
4333
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4334
- ] }),
4335
- faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4336
- ] }, f.id))
4337
- ) : null,
4338
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4339
- ] })
5319
+ ) : /* @__PURE__ */ jsx6(Fragment5, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx6(Fragment4, { children: renderModule(id) }, id)) })
4340
5320
  ] })
4341
5321
  ] }) });
4342
5322
  }
@@ -4356,12 +5336,15 @@ var styles5 = StyleSheet5.create({
4356
5336
  chev: { fontSize: 18 },
4357
5337
  faq: { flexDirection: "column", alignItems: "stretch" },
4358
5338
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4359
- articleTitle: { fontSize: 22, fontWeight: "700" }
5339
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5340
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5341
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5342
+ link: { fontSize: 14, fontWeight: "600" }
4360
5343
  });
4361
5344
 
4362
5345
  // src/ui/StepLayer.tsx
4363
5346
  import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
4364
- import { AccessibilityInfo, Animated, Easing, Pressable as Pressable6, StyleSheet as StyleSheet7, View as View7 } from "react-native";
5347
+ import { AccessibilityInfo as AccessibilityInfo2, Animated as Animated2, Easing as Easing2, Pressable as Pressable6, StyleSheet as StyleSheet7, View as View7 } from "react-native";
4365
5348
 
4366
5349
  // src/core/position.ts
4367
5350
  var MARGIN = 12;
@@ -4384,20 +5367,20 @@ function positionCard(target, size, viewport, insets, placement, offset) {
4384
5367
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
4385
5368
  const space = { top: target.y - insets.top, bottom: viewport.height - insets.bottom - (target.y + target.height) };
4386
5369
  const side = order.find(fits) ?? (space.bottom >= space.top ? "bottom" : "top");
4387
- const clamp2 = (v, min, max) => Math.max(min, Math.min(max, v));
5370
+ const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
4388
5371
  if (side === "top" || side === "bottom") {
4389
5372
  const cx = target.x + target.width / 2;
4390
5373
  let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
4391
- x2 = clamp2(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
5374
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
4392
5375
  let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
4393
- y2 = clamp2(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
4394
- return { x: x2, y: y2, side, arrow: clamp2(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
5376
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5377
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
4395
5378
  }
4396
5379
  const cy = target.y + target.height / 2;
4397
5380
  let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
4398
- y = clamp2(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5381
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
4399
5382
  const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
4400
- return { x, y, side, arrow: clamp2(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
5383
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
4401
5384
  }
4402
5385
 
4403
5386
  // src/ui/StepCard.tsx
@@ -4410,7 +5393,19 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4410
5393
  const showProgress = step.showProgress && view.total > 1;
4411
5394
  const onLink = (url) => client.navigate(url);
4412
5395
  return /* @__PURE__ */ jsx7(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: [
4413
- step.imageUrl ? /* @__PURE__ */ jsx7(Image2, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5396
+ step.imageUrl ? (
5397
+ // An empty description marks the image as decorative: screen readers skip it.
5398
+ /* @__PURE__ */ jsx7(
5399
+ Image2,
5400
+ {
5401
+ source: { uri: step.imageUrl },
5402
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5403
+ resizeMode: "cover",
5404
+ accessibilityIgnoresInvertColors: true,
5405
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5406
+ }
5407
+ )
5408
+ ) : null,
4414
5409
  step.title ? /* @__PURE__ */ jsxs5(View6, { style: styles6.header, children: [
4415
5410
  /* @__PURE__ */ jsx7(Text6, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4416
5411
  step.showClose ? /* @__PURE__ */ jsx7(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -4481,10 +5476,10 @@ function useTargetRect(client, targetId, origin) {
4481
5476
  return rect;
4482
5477
  }
4483
5478
  function useFadeIn(key) {
4484
- const opacity = useRef3(new Animated.Value(0)).current;
5479
+ const opacity = useRef3(new Animated2.Value(0)).current;
4485
5480
  useEffect5(() => {
4486
5481
  opacity.setValue(0);
4487
- Animated.timing(opacity, { toValue: 1, duration: 160, easing: Easing.out(Easing.quad), useNativeDriver: true }).start();
5482
+ Animated2.timing(opacity, { toValue: 1, duration: 160, easing: Easing2.out(Easing2.quad), useNativeDriver: true }).start();
4488
5483
  }, [key, opacity]);
4489
5484
  return opacity;
4490
5485
  }
@@ -4496,22 +5491,22 @@ function StepLayer({ view, client, palette: p, geometry }) {
4496
5491
  const cardWidth = Math.min(viewport.width - 32, step.width ?? 340);
4497
5492
  useEffect5(() => {
4498
5493
  if (step.type === "hotspot" && !view.expanded) return;
4499
- AccessibilityInfo.announceForAccessibility?.(announcement(view));
5494
+ AccessibilityInfo2.announceForAccessibility?.(announcement(view));
4500
5495
  }, [view.key, view.expanded]);
4501
5496
  const backdrop = (children) => /* @__PURE__ */ jsx8(Pressable6, { accessible: false, style: [StyleSheet7.absoluteFill, { backgroundColor: p.overlay }], onPress: () => client.backdropPressed(view.guide.id), children });
4502
5497
  const anchored = !view.asModal && ["tooltip", "popover", "spotlight", "hotspot"].includes(step.type);
4503
5498
  if (!anchored && step.type === "banner") {
4504
5499
  const top = step.position.startsWith("top");
4505
- return /* @__PURE__ */ jsx8(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__ */ jsx8(StepCard, { view, client, palette: p, width: viewport.width - insets.left - insets.right - 16, compact: true }) });
5500
+ return /* @__PURE__ */ jsx8(Animated2.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__ */ jsx8(StepCard, { view, client, palette: p, width: viewport.width - insets.left - insets.right - 16, compact: true }) });
4506
5501
  }
4507
5502
  if (!anchored && step.type === "slideout") {
4508
5503
  const wide = viewport.width >= 600;
4509
5504
  const right = !step.position.endsWith("left");
4510
5505
  const width = wide ? Math.min(380, cardWidth) : viewport.width - insets.left - insets.right - 24;
4511
- return /* @__PURE__ */ jsx8(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__ */ jsx8(StepCard, { view, client, palette: p, width }) });
5506
+ return /* @__PURE__ */ jsx8(Animated2.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__ */ jsx8(StepCard, { view, client, palette: p, width }) });
4512
5507
  }
4513
5508
  if (!anchored) {
4514
- return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, styles7.center, { opacity }], children: [
5509
+ return /* @__PURE__ */ jsxs6(Animated2.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, styles7.center, { opacity }], children: [
4515
5510
  step.showOverlay ? backdrop() : null,
4516
5511
  /* @__PURE__ */ jsx8(StepCard, { view, client, palette: p, width: Math.min(viewport.width - 40, step.width ?? 360) })
4517
5512
  ] });
@@ -4536,7 +5531,7 @@ function StepLayer({ view, client, palette: p, geometry }) {
4536
5531
  [hole.x, hole.y + hole.height - r, -r, -2 * r],
4537
5532
  [hole.x + hole.width - r, hole.y + hole.height - r, -2 * r, -2 * r]
4538
5533
  ];
4539
- return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
5534
+ return /* @__PURE__ */ jsxs6(Animated2.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
4540
5535
  /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: 0, height: Math.max(0, hole.y) }] }),
4541
5536
  /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: hole.y + hole.height, bottom: 0 }] }),
4542
5537
  /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, width: Math.max(0, hole.x), top: hole.y, height: hole.height }] }),
@@ -4546,7 +5541,7 @@ function StepLayer({ view, client, palette: p, geometry }) {
4546
5541
  /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect: hole, width: cardWidth })
4547
5542
  ] });
4548
5543
  }
4549
- return /* @__PURE__ */ jsxs6(Animated.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
5544
+ return /* @__PURE__ */ jsxs6(Animated2.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
4550
5545
  step.showOverlay ? backdrop() : null,
4551
5546
  /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth })
4552
5547
  ] });
@@ -4568,11 +5563,11 @@ function Arrow({ side, at, color, border }) {
4568
5563
  return /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
4569
5564
  }
4570
5565
  function Hotspot({ x, y, size, pulse, color, onPress, label }) {
4571
- const scale = useRef3(new Animated.Value(0)).current;
5566
+ const scale = useRef3(new Animated2.Value(0)).current;
4572
5567
  const [reduceMotion, setReduceMotion] = useState5(false);
4573
5568
  useEffect5(() => {
4574
5569
  let alive = true;
4575
- AccessibilityInfo.isReduceMotionEnabled?.().then((v) => alive && setReduceMotion(v), () => {
5570
+ AccessibilityInfo2.isReduceMotionEnabled?.().then((v) => alive && setReduceMotion(v), () => {
4576
5571
  });
4577
5572
  return () => {
4578
5573
  alive = false;
@@ -4580,13 +5575,13 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
4580
5575
  }, []);
4581
5576
  useEffect5(() => {
4582
5577
  if (!pulse || reduceMotion) return;
4583
- const loop = Animated.loop(Animated.timing(scale, { toValue: 1, duration: 1400, easing: Easing.out(Easing.ease), useNativeDriver: true }));
5578
+ const loop = Animated2.loop(Animated2.timing(scale, { toValue: 1, duration: 1400, easing: Easing2.out(Easing2.ease), useNativeDriver: true }));
4584
5579
  loop.start();
4585
5580
  return () => loop.stop();
4586
5581
  }, [pulse, reduceMotion, scale]);
4587
5582
  return /* @__PURE__ */ jsxs6(Pressable6, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
4588
5583
  pulse && !reduceMotion ? /* @__PURE__ */ jsx8(
4589
- Animated.View,
5584
+ Animated2.View,
4590
5585
  {
4591
5586
  pointerEvents: "none",
4592
5587
  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] }) }] }]
@@ -4605,7 +5600,7 @@ var styles7 = StyleSheet7.create({
4605
5600
  // src/ui/Survey.tsx
4606
5601
  import { useEffect as useEffect6, useState as useState6 } from "react";
4607
5602
  import {
4608
- AccessibilityInfo as AccessibilityInfo2,
5603
+ AccessibilityInfo as AccessibilityInfo3,
4609
5604
  KeyboardAvoidingView,
4610
5605
  Platform as Platform4,
4611
5606
  Pressable as Pressable7,
@@ -4628,7 +5623,7 @@ function Survey({
4628
5623
  useEffect6(() => {
4629
5624
  setAnswers({});
4630
5625
  setError("");
4631
- AccessibilityInfo2.announceForAccessibility?.(
5626
+ AccessibilityInfo3.announceForAccessibility?.(
4632
5627
  `Survey: ${config?.questions[0]?.title ?? view.guide.name}`
4633
5628
  );
4634
5629
  }, [view.key]);
@@ -5008,21 +6003,36 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5008
6003
  const basePalette = palette(ui.theme, scheme);
5009
6004
  const messenger = ui.messenger ? client.getMessenger() : null;
5010
6005
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
6006
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5011
6007
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5012
6008
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
6009
+ const messengerBottom = insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0);
5013
6010
  return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
5014
6011
  ui.checklists.map((c) => /* @__PURE__ */ jsx10(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
5015
- showRcLauncher ? /* @__PURE__ */ jsx10(
6012
+ showRcLauncher ? /* @__PURE__ */ jsxs8(
5016
6013
  Pressable8,
5017
6014
  {
5018
6015
  accessibilityRole: "button",
5019
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
6016
+ accessibilityLabel: rcLauncherLabel(client),
5020
6017
  onPress: () => client.openResourceCenter(),
5021
6018
  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 }],
5022
- children: /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
6019
+ children: [
6020
+ /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
6021
+ unread ? /* @__PURE__ */ jsx10(View9, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ jsx10(Text8, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
6022
+ ]
6023
+ }
6024
+ ) : null,
6025
+ messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ jsx10(
6026
+ MessengerLauncher,
6027
+ {
6028
+ store: messenger,
6029
+ client,
6030
+ palette: basePalette,
6031
+ style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: messengerBottom }],
6032
+ frame: { width, height, insets, size: LAUNCHER_SIZE, defaultBottom: messengerBottom },
6033
+ defaultSide: messengerLeft ? "left" : "right"
5023
6034
  }
5024
6035
  ) : null,
5025
- messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ jsx10(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
5026
6036
  ui.survey ? /* @__PURE__ */ jsx10(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
5027
6037
  ui.steps.map((s) => /* @__PURE__ */ jsx10(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
5028
6038
  messenger ? /* @__PURE__ */ jsx10(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ jsx10(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
@@ -5030,27 +6040,75 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5030
6040
  }
5031
6041
  var styles9 = StyleSheet9.create({
5032
6042
  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 } },
5033
- rcText: { fontSize: 22, fontWeight: "800" }
6043
+ rcText: { fontSize: 22, fontWeight: "800" },
6044
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
6045
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
5034
6046
  });
6047
+ function rcLauncherLabel(client) {
6048
+ const label = client.getResourceCenter()?.launcher.label || "Help";
6049
+ const unread = client.getUnreadAnnouncements();
6050
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
6051
+ }
5035
6052
 
5036
- // src/ui/Target.tsx
5037
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef5 } from "react";
5038
- import { View as View10 } from "react-native";
6053
+ // src/ui/Autocapture.tsx
5039
6054
  import { jsx as jsx11 } from "react/jsx-runtime";
6055
+ function GuidelyMask({ children, ...rest }) {
6056
+ return /* @__PURE__ */ jsx11(View10, { ...rest, children });
6057
+ }
6058
+ var RESOLVE = {
6059
+ isIgnored: (type) => type === GuidelyOverlay,
6060
+ isMask: (type) => type === GuidelyMask
6061
+ };
6062
+ function useTapObserver(client) {
6063
+ const clientRef = useRef5(client);
6064
+ clientRef.current = client;
6065
+ return useMemo5(() => {
6066
+ const tracker = new TapTracker();
6067
+ const guard = (fn) => (e) => {
6068
+ try {
6069
+ fn(e);
6070
+ } catch {
6071
+ tracker.cancel();
6072
+ }
6073
+ };
6074
+ return {
6075
+ onTouchStartCapture: guard((e) => {
6076
+ const n = e.nativeEvent;
6077
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
6078
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
6079
+ }),
6080
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
6081
+ onTouchEndCapture: guard((e) => {
6082
+ const n = e.nativeEvent;
6083
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
6084
+ const c = clientRef.current;
6085
+ if (!tap || !c) return;
6086
+ const r = resolveTapTarget(e, RESOLVE);
6087
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
6088
+ }),
6089
+ onTouchCancelCapture: guard(() => tracker.cancel())
6090
+ };
6091
+ }, []);
6092
+ }
6093
+
6094
+ // src/ui/Target.tsx
6095
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef6 } from "react";
6096
+ import { View as View11 } from "react-native";
6097
+ import { jsx as jsx12 } from "react/jsx-runtime";
5040
6098
  var GuidelyContext = createContext2(null);
5041
6099
  var GuidelyScreenContext = createContext2(null);
5042
6100
  function GuidelyScreenScope({ name, children }) {
5043
- return /* @__PURE__ */ jsx11(GuidelyScreenContext.Provider, { value: name, children });
6101
+ return /* @__PURE__ */ jsx12(GuidelyScreenContext.Provider, { value: name, children });
5044
6102
  }
5045
6103
  function guidelyScreenLayout({ route, children }) {
5046
- return /* @__PURE__ */ jsx11(GuidelyScreenScope, { name: route.name, children });
6104
+ return /* @__PURE__ */ jsx12(GuidelyScreenScope, { name: route.name, children });
5047
6105
  }
5048
- var TAP_SLOP = 10;
6106
+ var TAP_SLOP2 = 10;
5049
6107
  function useGuidelyTarget(id) {
5050
6108
  const client = useContext2(GuidelyContext);
5051
6109
  const screen = useContext2(GuidelyScreenContext);
5052
- const ref = useRef5(null);
5053
- const start = useRef5(null);
6110
+ const ref = useRef6(null);
6111
+ const start = useRef6(null);
5054
6112
  useEffect8(() => {
5055
6113
  if (!client || !id) return;
5056
6114
  return client.targets.register(id, {
@@ -5070,7 +6128,7 @@ function useGuidelyTarget(id) {
5070
6128
  (e) => {
5071
6129
  const s = start.current;
5072
6130
  start.current = null;
5073
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
6131
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
5074
6132
  },
5075
6133
  [client, id]
5076
6134
  );
@@ -5078,8 +6136,8 @@ function useGuidelyTarget(id) {
5078
6136
  }
5079
6137
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
5080
6138
  const t = useGuidelyTarget(id);
5081
- return /* @__PURE__ */ jsx11(
5082
- View10,
6139
+ return /* @__PURE__ */ jsx12(
6140
+ View11,
5083
6141
  {
5084
6142
  ...rest,
5085
6143
  ref: t.ref,
@@ -5103,7 +6161,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
5103
6161
 
5104
6162
  // src/index.tsx
5105
6163
  import { createFlagsClient as createFlagsClient3, InMemoryStickyBucketStore } from "@guidelytech/flags";
5106
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
6164
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
5107
6165
  function deviceLocale() {
5108
6166
  try {
5109
6167
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -5121,21 +6179,22 @@ function defaultBridge(appVersion) {
5121
6179
  appVersion
5122
6180
  },
5123
6181
  openExternal: (url) => void Linking.openURL(url).catch(() => {
5124
- })
6182
+ }),
6183
+ windowSize: () => Dimensions.get("window")
5125
6184
  };
5126
6185
  }
5127
6186
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
5128
6187
  const [client, setClient] = useState8(null);
5129
- const optionsRef = useRef6(options);
6188
+ const optionsRef = useRef7(options);
5130
6189
  optionsRef.current = options;
5131
- const userRef = useRef6(user);
6190
+ const userRef = useRef7(user);
5132
6191
  userRef.current = user;
5133
6192
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
5134
6193
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
5135
- const payloadRef = useRef6(payload);
6194
+ const payloadRef = useRef7(payload);
5136
6195
  payloadRef.current = payload;
5137
- const appAttributes = useMemo5(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify2(flagsOpts?.attributes ?? {}), stableStringify2(flagAttributes ?? {})]);
5138
- const appAttributesRef = useRef6(appAttributes);
6196
+ const appAttributes = useMemo6(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify3(flagsOpts?.attributes ?? {}), stableStringify3(flagAttributes ?? {})]);
6197
+ const appAttributesRef = useRef7(appAttributes);
5139
6198
  appAttributesRef.current = appAttributes;
5140
6199
  const { projectId, environment, apiUrl, storage } = options;
5141
6200
  useEffect9(() => {
@@ -5172,7 +6231,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5172
6231
  };
5173
6232
  }, [projectId, environment, apiUrl, storage]);
5174
6233
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
5175
- const hadUser = useRef6(false);
6234
+ const hadUser = useRef7(false);
5176
6235
  useEffect9(() => {
5177
6236
  if (!client) return;
5178
6237
  if (user && user.id) {
@@ -5186,13 +6245,18 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5186
6245
  useEffect9(() => {
5187
6246
  client?.flags.setAttributes(appAttributes);
5188
6247
  }, [client, appAttributes]);
6248
+ useEffect9(() => {
6249
+ client?.setLocale(options.locale ?? null);
6250
+ }, [client, options.locale]);
5189
6251
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
5190
- const flags = useMemo5(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
5191
- return /* @__PURE__ */ jsx12(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
5192
- children,
5193
- client ? /* @__PURE__ */ jsx12(GuidelyOverlay, { client, insets, launcherOffset }) : null
6252
+ const flags = useMemo6(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6253
+ const tapObserver = useTapObserver(client);
6254
+ return /* @__PURE__ */ jsx13(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
6255
+ options.autocapture === false ? children : /* @__PURE__ */ jsx13(View12, { style: styles10.root, ...tapObserver, children }),
6256
+ client ? /* @__PURE__ */ jsx13(GuidelyOverlay, { client, insets, launcherOffset }) : null
5194
6257
  ] }) });
5195
6258
  }
6259
+ var styles10 = StyleSheet10.create({ root: { flex: 1 } });
5196
6260
  var NOOP_RESULTS = {
5197
6261
  init: Promise.resolve(),
5198
6262
  ready: Promise.resolve(),
@@ -5264,7 +6328,7 @@ function useGuidelyIdentify(userId, traits, account) {
5264
6328
  }
5265
6329
  function useGuidelyEvent(event, handler) {
5266
6330
  const client = useContext3(GuidelyContext);
5267
- const ref = useRef6(handler);
6331
+ const ref = useRef7(handler);
5268
6332
  ref.current = handler;
5269
6333
  useEffect9(() => {
5270
6334
  if (!client) return;
@@ -5279,7 +6343,7 @@ function useGuidelyScreen(name, properties) {
5279
6343
  }
5280
6344
  function useGuidelyNavigation(navigationRef, options = {}) {
5281
6345
  const client = useContext3(GuidelyContext);
5282
- const nameRef = useRef6(options.screenName);
6346
+ const nameRef = useRef7(options.screenName);
5283
6347
  nameRef.current = options.screenName;
5284
6348
  const report = () => {
5285
6349
  const route = navigationRef.getCurrentRoute?.();
@@ -5290,7 +6354,7 @@ function useGuidelyNavigation(navigationRef, options = {}) {
5290
6354
  }
5291
6355
  function useUiSelector(select, fallback) {
5292
6356
  const client = useContext3(GuidelyContext);
5293
- const selectRef = useRef6(select);
6357
+ const selectRef = useRef7(select);
5294
6358
  selectRef.current = select;
5295
6359
  const [value, setValue] = useState8(() => client ? select(client) : fallback);
5296
6360
  useEffect9(() => {
@@ -5332,14 +6396,18 @@ export {
5332
6396
  FeatureValue,
5333
6397
  FlagsProvider,
5334
6398
  GuidelyClient,
6399
+ GuidelyMask,
5335
6400
  GuidelyOverlay,
5336
6401
  GuidelyProvider,
5337
6402
  GuidelyScreenScope,
5338
6403
  GuidelyTarget,
6404
+ HeatmapBuffer,
5339
6405
  InMemoryStickyBucketStore,
5340
6406
  Messenger,
6407
+ RageClickDetector,
5341
6408
  SDK_NAME,
5342
6409
  SDK_VERSION,
6410
+ TapTracker,
5343
6411
  TargetRegistry,
5344
6412
  createFlagsClient3 as createFlagsClient,
5345
6413
  createSseParser,