@guidelytech/react-native 0.4.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -54,7 +54,7 @@ module.exports = __toCommonJS(index_exports);
54
54
  var import_react8 = require("react");
55
55
  var import_react_native11 = require("react-native");
56
56
 
57
- // ../shared/dist/chunk-QUKMOTT4.js
57
+ // ../shared/dist/chunk-OLWGMDRX.js
58
58
  function fnv1a(input) {
59
59
  let h = 2166136261;
60
60
  for (let i = 0; i < input.length; i++) {
@@ -358,8 +358,194 @@ function sanitizeProperties(props) {
358
358
  return out;
359
359
  }
360
360
  var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
361
+ var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
362
+ var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
363
+ var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
364
+ var MEDIA_MAX_BYTES = Math.max(MEDIA_IMAGE_MAX_BYTES, MEDIA_VIDEO_MAX_BYTES, MEDIA_FILE_MAX_BYTES);
365
+ var FILE_LINK_CLASS = "gd-file";
366
+ var PDF_BLOCK_CLASS = "gd-pdf";
367
+ function isFileLink(className, href) {
368
+ return !!className && className.split(/\s+/).includes(FILE_LINK_CLASS) && isSafeMediaUrl(href);
369
+ }
370
+ var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
371
+ var VIMEO_ID = /^\d{1,12}$/;
372
+ var VIMEO_HASH = /^[0-9a-f]{6,20}$/i;
373
+ var LOOM_ID = /^[0-9a-f]{32}$/i;
374
+ var WISTIA_ID = /^[a-z0-9]{10}$/i;
375
+ function parseUrl(input) {
376
+ const s = input.trim();
377
+ if (!s) return null;
378
+ try {
379
+ const u = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : `https://${s}`);
380
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
381
+ if (u.username || u.password || u.port && u.port !== "443" && u.port !== "80") return null;
382
+ return u;
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ function parseStart(v) {
388
+ if (!v) return 0;
389
+ if (/^\d+$/.test(v)) return Math.min(Number(v), 86400);
390
+ const m = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(v);
391
+ if (!m || !m[0]) return 0;
392
+ return Math.min(Number(m[1] ?? 0) * 3600 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0), 86400);
393
+ }
394
+ function youtube(id, start) {
395
+ if (!YOUTUBE_ID.test(id)) return null;
396
+ return {
397
+ provider: "youtube",
398
+ src: `https://www.youtube-nocookie.com/embed/${id}${start ? `?start=${start}` : ""}`,
399
+ watchUrl: `https://www.youtube.com/watch?v=${id}${start ? `&t=${start}s` : ""}`,
400
+ title: "YouTube video"
401
+ };
402
+ }
403
+ function vimeo(id, hash) {
404
+ if (!VIMEO_ID.test(id) || hash !== null && !VIMEO_HASH.test(hash)) return null;
405
+ const h = hash ? hash.toLowerCase() : null;
406
+ return { provider: "vimeo", src: `https://player.vimeo.com/video/${id}${h ? `?h=${h}` : ""}`, watchUrl: `https://vimeo.com/${id}${h ? `/${h}` : ""}`, title: "Vimeo video" };
407
+ }
408
+ function toEmbedUrl(input) {
409
+ const u = parseUrl(input);
410
+ if (!u) return null;
411
+ const host = u.hostname.toLowerCase().replace(/^www\./, "");
412
+ const parts = u.pathname.split("/").filter(Boolean);
413
+ switch (host) {
414
+ case "youtube.com":
415
+ case "m.youtube.com":
416
+ case "youtube-nocookie.com": {
417
+ const start = parseStart(u.searchParams.get("start") ?? u.searchParams.get("t"));
418
+ if (parts[0] === "watch" && parts.length === 1) return youtube(u.searchParams.get("v") ?? "", start);
419
+ if ((parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "live") && parts.length === 2) return youtube(parts[1], start);
420
+ return null;
421
+ }
422
+ case "youtu.be":
423
+ return parts.length === 1 ? youtube(parts[0], parseStart(u.searchParams.get("t") ?? u.searchParams.get("start"))) : null;
424
+ case "vimeo.com":
425
+ if (parts.length === 1) return vimeo(parts[0], null);
426
+ if (parts.length === 2 && VIMEO_ID.test(parts[0])) return vimeo(parts[0], parts[1]);
427
+ return null;
428
+ case "player.vimeo.com":
429
+ return parts[0] === "video" && parts.length === 2 ? vimeo(parts[1], u.searchParams.get("h")) : null;
430
+ case "loom.com":
431
+ if ((parts[0] === "share" || parts[0] === "embed") && parts.length === 2 && LOOM_ID.test(parts[1])) {
432
+ const id = parts[1].toLowerCase();
433
+ return { provider: "loom", src: `https://www.loom.com/embed/${id}`, watchUrl: `https://www.loom.com/share/${id}`, title: "Loom video" };
434
+ }
435
+ return null;
436
+ default: {
437
+ let id;
438
+ if (host === "fast.wistia.net" || host === "fast.wistia.com") id = parts[0] === "embed" && parts[1] === "iframe" && parts.length === 3 ? parts[2] : void 0;
439
+ else if (host === "wi.st" || host === "wistia.com" || /^[a-z0-9-]+\.wistia\.com$/.test(host)) id = parts[0] === "medias" && parts.length === 2 ? parts[1] : void 0;
440
+ if (!id || !WISTIA_ID.test(id)) return null;
441
+ return { provider: "wistia", src: `https://fast.wistia.net/embed/iframe/${id}`, watchUrl: `https://fast.wistia.net/embed/iframe/${id}`, title: "Wistia video" };
442
+ }
443
+ }
444
+ }
445
+ function isAllowedEmbedSrc(src) {
446
+ if (!src) return false;
447
+ return toEmbedUrl(src)?.src === src;
448
+ }
449
+ function isSafeMediaUrl(url) {
450
+ if (!url) return false;
451
+ try {
452
+ const u = new URL(url);
453
+ return (u.protocol === "https:" || u.protocol === "http:") && !u.username && !u.password;
454
+ } catch {
455
+ return false;
456
+ }
457
+ }
458
+ var MAX_COLLECTION_DEPTH = 3;
459
+ function matchesAudiences(required, member) {
460
+ if (!required || required.length === 0) return true;
461
+ if (!member) return false;
462
+ const has2 = member instanceof Set ? (id) => member.has(id) : (id) => member.includes(id);
463
+ return required.some(has2);
464
+ }
465
+ function resourceCenterSections(collections, articles, member) {
466
+ const byId = new Map(collections.map((c) => [c.id, c]));
467
+ const children = /* @__PURE__ */ new Map();
468
+ for (const c of collections) {
469
+ const parent = c.parentId && c.parentId !== c.id && byId.has(c.parentId) ? c.parentId : null;
470
+ const list = children.get(parent) ?? [];
471
+ list.push(c);
472
+ children.set(parent, list);
473
+ }
474
+ for (const list of children.values()) list.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
475
+ const direct = /* @__PURE__ */ new Map();
476
+ const uncategorized = [];
477
+ for (const a of articles) {
478
+ if (!matchesAudiences(a.audienceIds, member)) continue;
479
+ if (a.collectionId && byId.has(a.collectionId)) {
480
+ const list = direct.get(a.collectionId) ?? [];
481
+ list.push(a);
482
+ direct.set(a.collectionId, list);
483
+ } else uncategorized.push(a);
484
+ }
485
+ const sections = [];
486
+ const seen = /* @__PURE__ */ new Set();
487
+ const visit = (c, path) => {
488
+ if (seen.has(c.id)) return;
489
+ seen.add(c.id);
490
+ if (!matchesAudiences(c.audienceIds, member)) return;
491
+ const here = [...path, c.name];
492
+ const list = direct.get(c.id);
493
+ if (list?.length) sections.push({ collection: c, path: here, depth: here.length - 1, articles: list });
494
+ for (const child of children.get(c.id) ?? []) visit(child, here);
495
+ };
496
+ for (const root of children.get(null) ?? []) visit(root, []);
497
+ if (uncategorized.length) sections.push({ collection: null, path: [], depth: 0, articles: uncategorized });
498
+ return sections;
499
+ }
500
+ function isArticleListed(collections, article, member) {
501
+ if (!matchesAudiences(article.audienceIds, member)) return false;
502
+ const byId = new Map(collections.map((c) => [c.id, c]));
503
+ let cur = article.collectionId ? byId.get(article.collectionId) : void 0;
504
+ for (let guard = 0; cur && guard <= MAX_COLLECTION_DEPTH * 4; guard++) {
505
+ if (!matchesAudiences(cur.audienceIds, member)) return false;
506
+ cur = cur.parentId ? byId.get(cur.parentId) : void 0;
507
+ }
508
+ return true;
509
+ }
510
+ var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
361
511
 
362
- // ../sdk-core/dist/chunk-GJT7CFAC.js
512
+ // ../sdk-core/dist/chunk-CYAG6ZYX.js
513
+ function readJson(store, key, fallback) {
514
+ const raw = store.get(key);
515
+ if (raw == null) return fallback;
516
+ try {
517
+ return JSON.parse(raw);
518
+ } catch {
519
+ return fallback;
520
+ }
521
+ }
522
+ function writeJson(store, key, value) {
523
+ try {
524
+ store.set(key, JSON.stringify(value));
525
+ } catch {
526
+ }
527
+ }
528
+ var KEYS = {
529
+ anon: "guidely:anon",
530
+ seen: "guidely:seen",
531
+ config: (key) => `guidely:config:${key}`,
532
+ etag: (key) => `guidely:etag:${key}`,
533
+ state: (key) => `guidely:state:${key}`,
534
+ user: (key) => `guidely:user:${key}`,
535
+ queue: (key) => `guidely:queue:${key}`,
536
+ session: "guidely:session",
537
+ active: "guidely:active",
538
+ authoring: "guidely:authoring",
539
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
540
+ capture: "guidely:capture",
541
+ capturePending: "guidely:capture:pending",
542
+ /** Messenger conversations this browser can read: [{ id, token }]. */
543
+ conversations: (key) => `guidely:conversations:${key}`,
544
+ /** Announcement ids opened in the messenger's News tab. */
545
+ newsSeen: (key) => `guidely:news:${key}`
546
+ };
547
+
548
+ // ../sdk-core/dist/chunk-AKNE7D5M.js
363
549
  var __defProp2 = Object.defineProperty;
364
550
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
365
551
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -381,7 +567,8 @@ var Api = class {
381
567
  const f = this.fetcher;
382
568
  if (!f) throw new Error("fetch is not available");
383
569
  const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
384
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
570
+ const form = typeof FormData !== "undefined" && options.body instanceof FormData;
571
+ if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
385
572
  if (options.etag) headers["If-None-Match"] = options.etag;
386
573
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
387
574
  const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
@@ -389,7 +576,7 @@ var Api = class {
389
576
  const res = await f(this.url(path, options.params), {
390
577
  method,
391
578
  headers,
392
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
579
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
393
580
  keepalive: options.keepalive,
394
581
  signal: ac?.signal,
395
582
  credentials: "omit",
@@ -414,6 +601,10 @@ var Api = class {
414
601
  post(path, body, options) {
415
602
  return this.request("POST", path, { body, ...options });
416
603
  }
604
+ /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
605
+ postForm(path, form, options) {
606
+ return this.request("POST", path, { body: form, ...options });
607
+ }
417
608
  /**
418
609
  * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
419
610
  * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
@@ -456,10 +647,10 @@ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
456
647
  function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
457
648
  return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
458
649
  }
459
- async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
650
+ async function withRetry(send, attempts = 3, baseMs = 500) {
460
651
  for (let i = 0; i < attempts; i++) {
461
652
  try {
462
- const res = await api.post(path, body);
653
+ const res = await send();
463
654
  if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
464
655
  } catch {
465
656
  }
@@ -467,6 +658,9 @@ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
467
658
  }
468
659
  return null;
469
660
  }
661
+ function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
662
+ return withRetry(() => api.post(path, body), attempts, baseMs);
663
+ }
470
664
  async function readEventStream(body, onEvent) {
471
665
  const reader = body.getReader();
472
666
  const decoder = new TextDecoder();
@@ -501,36 +695,7 @@ async function readEventStream(body, onEvent) {
501
695
  if (buffer.trim()) dispatch(buffer);
502
696
  }
503
697
 
504
- // ../sdk-core/dist/chunk-HLR3W63Z.js
505
- function readJson(store, key, fallback) {
506
- const raw = store.get(key);
507
- if (raw == null) return fallback;
508
- try {
509
- return JSON.parse(raw);
510
- } catch {
511
- return fallback;
512
- }
513
- }
514
- function writeJson(store, key, value) {
515
- try {
516
- store.set(key, JSON.stringify(value));
517
- } catch {
518
- }
519
- }
520
- var KEYS = {
521
- anon: "guidely:anon",
522
- seen: "guidely:seen",
523
- config: (key) => `guidely:config:${key}`,
524
- etag: (key) => `guidely:etag:${key}`,
525
- state: (key) => `guidely:state:${key}`,
526
- user: (key) => `guidely:user:${key}`,
527
- queue: (key) => `guidely:queue:${key}`,
528
- session: "guidely:session",
529
- active: "guidely:active",
530
- authoring: "guidely:authoring",
531
- /** Messenger conversations this browser can read: [{ id, token }]. */
532
- conversations: (key) => `guidely:conversations:${key}`
533
- };
698
+ // ../sdk-core/dist/chunk-GDF6RCHQ.js
534
699
  var EventQueue = class {
535
700
  constructor(opts) {
536
701
  __publicField2(this, "opts", opts);
@@ -1612,7 +1777,7 @@ var PersistentStore = class {
1612
1777
 
1613
1778
  // src/core/client.ts
1614
1779
  var SDK_NAME = "@guidelytech/react-native";
1615
- var SDK_VERSION = "0.4.1";
1780
+ var SDK_VERSION = "0.6.1";
1616
1781
  var DEFAULT_API_URL = "https://guidelytech.com";
1617
1782
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1618
1783
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -2477,6 +2642,10 @@ var GuidelyClient = class {
2477
2642
  if (this.activeSurvey?.guide.id !== guideId) return;
2478
2643
  this.finishSurvey(this.surveyView?.submitted ? "submitted" : "dismissed");
2479
2644
  }
2645
+ /** The visitor's audiences (from the last identify), for audience-targeted help articles. */
2646
+ getAudienceIds() {
2647
+ return this.state.audienceIds;
2648
+ }
2480
2649
  getResourceCenter() {
2481
2650
  const rc = this.config?.resourceCenter;
2482
2651
  return rc && rc.enabled ? rc : null;
@@ -2814,7 +2983,8 @@ function decodeEntities(s) {
2814
2983
  });
2815
2984
  }
2816
2985
  var BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div", "section", "article", "header", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", "ul", "ol", "table", "tr", "pre"]);
2817
- var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "head", "title", "svg", "template"]);
2986
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "head", "title", "svg", "template", "iframe", "video"]);
2987
+ var PROVIDER_LABEL = { youtube: "Watch on YouTube", vimeo: "Watch on Vimeo", loom: "Watch on Loom", wistia: "Watch video" };
2818
2988
  function attr(attrs, name) {
2819
2989
  const m = attrs.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
2820
2990
  return m ? decodeEntities(m[2] ?? m[3] ?? m[4] ?? "") : void 0;
@@ -2826,6 +2996,12 @@ function parseHtml(html) {
2826
2996
  const lists = [];
2827
2997
  let current = null;
2828
2998
  let skipping = null;
2999
+ let pdf = null;
3000
+ let fileLink = null;
3001
+ const media = (block) => {
3002
+ flush();
3003
+ blocks.push({ level: 1, runs: [], ...block });
3004
+ };
2829
3005
  const flush = () => {
2830
3006
  if (current) {
2831
3007
  const runs = current.runs;
@@ -2856,12 +3032,30 @@ function parseHtml(html) {
2856
3032
  if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
2857
3033
  else current.runs.push(run);
2858
3034
  };
3035
+ const size = (attrs, name) => {
3036
+ const n = Number(attr(attrs, name));
3037
+ return n > 0 ? n : void 0;
3038
+ };
3039
+ const endPdf = (p) => {
3040
+ const name = p.name.replace(/\s+/g, " ").trim();
3041
+ if (p.href && p.pages.length) media({ kind: "pdf", href: p.href, alt: name || "PDF", pages: p.pages });
3042
+ else if (p.href) media({ kind: "file", href: p.href, alt: name || "Open PDF" });
3043
+ else for (const page of p.pages) media({ kind: "image", src: page.src, alt: page.alt, ...page.width && page.height ? { width: page.width, height: page.height } : {} });
3044
+ };
2859
3045
  const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
2860
3046
  let m;
2861
3047
  while (m = re.exec(html || "")) {
2862
3048
  const [token, rawTag, attrs = "", text] = m;
2863
3049
  if (text !== void 0) {
2864
3050
  if (skipping) continue;
3051
+ if (pdf) {
3052
+ if (pdf.inLink) pdf.name += decodeEntities(text);
3053
+ continue;
3054
+ }
3055
+ if (fileLink) {
3056
+ fileLink.name += decodeEntities(text);
3057
+ continue;
3058
+ }
2865
3059
  push(decodeEntities(text.replace(/\s+/g, " ")));
2866
3060
  continue;
2867
3061
  }
@@ -2873,7 +3067,54 @@ function parseHtml(html) {
2873
3067
  continue;
2874
3068
  }
2875
3069
  if (SKIP_TAGS.has(tag)) {
2876
- if (!closing && !token.endsWith("/>")) skipping = tag;
3070
+ if (closing) continue;
3071
+ if (tag === "iframe") {
3072
+ const src = attr(attrs, "src");
3073
+ const embed = isAllowedEmbedSrc(src) ? toEmbedUrl(src) : null;
3074
+ if (embed) media({ kind: "video", href: embed.watchUrl, alt: PROVIDER_LABEL[embed.provider] });
3075
+ } else if (tag === "video") {
3076
+ const src = attr(attrs, "src");
3077
+ if (isSafeMediaUrl(src)) media({ kind: "video", href: src, alt: "Play video" });
3078
+ }
3079
+ if (!token.endsWith("/>")) skipping = tag;
3080
+ continue;
3081
+ }
3082
+ if (pdf) {
3083
+ if (tag === "div") {
3084
+ pdf.depth += closing ? -1 : 1;
3085
+ if (pdf.depth === 0) {
3086
+ endPdf(pdf);
3087
+ pdf = null;
3088
+ }
3089
+ } else if (tag === "a") {
3090
+ if (closing) pdf.inLink = false;
3091
+ else if (!pdf.href && isFileLink(attr(attrs, "class"), attr(attrs, "href"))) {
3092
+ pdf.href = attr(attrs, "href");
3093
+ pdf.inLink = true;
3094
+ }
3095
+ } else if (tag === "img" && !closing) {
3096
+ const src = attr(attrs, "src");
3097
+ const w = size(attrs, "width");
3098
+ const h = size(attrs, "height");
3099
+ if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
3100
+ }
3101
+ continue;
3102
+ }
3103
+ if (fileLink) {
3104
+ if (closing && tag === "a") {
3105
+ media({ kind: "file", href: fileLink.href, alt: fileLink.name.replace(/\s+/g, " ").trim() || "Open PDF" });
3106
+ fileLink = null;
3107
+ }
3108
+ continue;
3109
+ }
3110
+ if (!closing && tag === "div" && (attr(attrs, "class") ?? "").split(/\s+/).includes(PDF_BLOCK_CLASS)) {
3111
+ flush();
3112
+ pdf = { depth: 1, name: "", inLink: false, pages: [] };
3113
+ continue;
3114
+ }
3115
+ if (!closing && tag === "a" && isFileLink(attr(attrs, "class"), attr(attrs, "href"))) {
3116
+ flush();
3117
+ fileLink = { href: attr(attrs, "href"), name: "" };
2877
3118
  continue;
2878
3119
  }
2879
3120
  if (closing) {
@@ -2934,16 +3175,23 @@ function parseHtml(html) {
2934
3175
  default:
2935
3176
  if (BLOCK_TAGS.has(tag)) open("paragraph");
2936
3177
  else if (tag === "img") {
2937
- const alt = attr(attrs, "alt");
2938
- if (alt) push(alt);
3178
+ const src = attr(attrs, "src") ?? "";
3179
+ const alt = attr(attrs, "alt") ?? "";
3180
+ if (isSafeMediaUrl(src) || /^data:image\//i.test(src)) {
3181
+ const w = Number(attr(attrs, "width"));
3182
+ const h = Number(attr(attrs, "height"));
3183
+ media({ kind: "image", src, alt, ...w > 0 && h > 0 ? { width: w, height: h } : {} });
3184
+ } else if (alt) push(alt);
2939
3185
  }
2940
3186
  }
2941
3187
  }
3188
+ if (pdf) endPdf(pdf);
3189
+ if (fileLink) media({ kind: "file", href: fileLink.href, alt: fileLink.name.replace(/\s+/g, " ").trim() || "Open PDF" });
2942
3190
  flush();
2943
3191
  return blocks;
2944
3192
  }
2945
3193
  function htmlToText(html) {
2946
- return parseHtml(html).map((b) => (b.marker ? `${b.marker} ` : "") + b.runs.map((r) => r.text).join("")).join("\n");
3194
+ return parseHtml(html).map((b) => b.kind === "image" || b.kind === "video" || b.kind === "pdf" || b.kind === "file" ? b.alt ?? "" : (b.marker ? `${b.marker} ` : "") + b.runs.map((r) => r.text).join("")).filter(Boolean).join("\n");
2947
3195
  }
2948
3196
 
2949
3197
  // src/ui/RichText.tsx
@@ -2960,11 +3208,76 @@ function runStyle(run, p) {
2960
3208
  color: run.href ? p.primary : void 0
2961
3209
  };
2962
3210
  }
3211
+ function BodyImage({ block, palette: p }) {
3212
+ const [ratio, setRatio] = (0, import_react.useState)(block.width && block.height ? block.width / block.height : 16 / 9);
3213
+ (0, import_react.useEffect)(() => {
3214
+ if (block.width && block.height) return;
3215
+ let live = true;
3216
+ import_react_native2.Image.getSize(block.src, (w, h) => live && w > 0 && h > 0 && setRatio(w / h), () => {
3217
+ });
3218
+ return () => {
3219
+ live = false;
3220
+ };
3221
+ }, [block.src, block.width, block.height]);
3222
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Image, { source: { uri: block.src }, accessible: true, accessibilityRole: "image", accessibilityLabel: block.alt || void 0, resizeMode: "contain", style: [styles2.image, { aspectRatio: ratio, backgroundColor: p.surface }] });
3223
+ }
3224
+ function BodyVideo({ block, palette: p, onLink }) {
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.Pressable, { accessibilityRole: "link", accessibilityLabel: block.alt, onPress: () => onLink(block.href), style: ({ pressed }) => [styles2.video, { borderColor: p.border, backgroundColor: p.surface, opacity: pressed ? 0.7 : 1 }], children: [
3226
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: [styles2.play, { color: p.primary }], children: "\u25B6" }),
3227
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: { color: p.primary, fontFamily: p.fontFamily, fontWeight: "600" }, children: block.alt })
3228
+ ] });
3229
+ }
3230
+ var PAGE_RATIO = 1 / 1.414;
3231
+ var pageRatio = (p) => p.width && p.height ? p.width / p.height : PAGE_RATIO;
3232
+ function PdfBadge() {
3233
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles2.badge, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: styles2.badgeText, children: "PDF" }) });
3234
+ }
3235
+ function BodyFile({ block, palette: p, onLink }) {
3236
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.Pressable, { accessibilityRole: "link", accessibilityLabel: `${block.alt}, PDF`, onPress: () => onLink(block.href), style: ({ pressed }) => [styles2.video, { borderColor: p.border, backgroundColor: p.surface, opacity: pressed ? 0.7 : 1 }], children: [
3237
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PdfBadge, {}),
3238
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { numberOfLines: 2, ellipsizeMode: "middle", style: [styles2.flex, { color: p.text, fontFamily: p.fontFamily, fontWeight: "600" }], children: block.alt })
3239
+ ] });
3240
+ }
3241
+ function BodyPdf({ block, palette: p, onLink }) {
3242
+ const pages = block.pages ?? [];
3243
+ const [width, setWidth] = (0, import_react.useState)(0);
3244
+ const [index, setIndex] = (0, import_react.useState)(0);
3245
+ const [reading, setReading] = (0, import_react.useState)(false);
3246
+ const height = width ? Math.max(...pages.map((pg) => width / pageRatio(pg))) : 0;
3247
+ const onScroll = (e) => width && setIndex(Math.round(e.nativeEvent.contentOffset.x / width));
3248
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: [styles2.pdf, { borderColor: p.border, backgroundColor: p.surface }], children: [
3249
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.Pressable, { accessibilityRole: "button", accessibilityLabel: `Read ${block.alt} full screen`, onPress: () => setReading(true), style: ({ pressed }) => [styles2.pdfHeader, { opacity: pressed ? 0.7 : 1 }], children: [
3250
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PdfBadge, {}),
3251
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { numberOfLines: 1, ellipsizeMode: "middle", style: [styles2.flex, { color: p.text, fontFamily: p.fontFamily, fontWeight: "600" }], children: block.alt }),
3252
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: { color: p.muted, fontSize: 16 }, children: "\u2922" })
3253
+ ] }),
3254
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles2.pdfPages, onLayout: (e) => setWidth(e.nativeEvent.layout.width), children: [
3255
+ width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.ScrollView, { horizontal: true, pagingEnabled: true, showsHorizontalScrollIndicator: false, onScroll, scrollEventThrottle: 32, accessibilityLabel: `${block.alt}, page ${index + 1} of ${pages.length}`, children: pages.map((pg, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Pressable, { onPress: () => setReading(true), accessibilityRole: "imagebutton", accessibilityLabel: pg.alt || `Page ${i + 1}`, accessibilityHint: "Opens the document full screen", style: { width, height, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Image, { source: { uri: pg.src }, resizeMode: "contain", style: [styles2.page, { width: Math.min(width, height * pageRatio(pg)), aspectRatio: pageRatio(pg) }] }) }, i)) }),
3256
+ pages.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { pointerEvents: "none", style: styles2.count, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.Text, { style: styles2.countText, children: [
3257
+ index + 1,
3258
+ " / ",
3259
+ pages.length
3260
+ ] }) })
3261
+ ] }),
3262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Modal, { visible: reading, animationType: "slide", presentationStyle: "fullScreen", onRequestClose: () => setReading(false), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles2.reader, children: [
3263
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles2.readerBar, children: [
3264
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { numberOfLines: 1, ellipsizeMode: "middle", style: [styles2.flex, styles2.readerTitle], accessibilityRole: "header", children: block.alt }),
3265
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Pressable, { accessibilityRole: "link", onPress: () => onLink(block.href), style: styles2.readerBtn, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: styles2.readerBtnText, children: "Open PDF \u2197" }) }),
3266
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", onPress: () => setReading(false), style: styles2.readerBtn, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: styles2.readerBtnText, children: "Done" }) })
3267
+ ] }),
3268
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.ScrollView, { contentContainerStyle: styles2.readerPages, children: pages.map((pg, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Image, { source: { uri: pg.src }, accessible: true, accessibilityLabel: pg.alt || `Page ${i + 1}`, resizeMode: "contain", style: [styles2.readerPage, { aspectRatio: pageRatio(pg) }] }, i)) })
3269
+ ] }) })
3270
+ ] });
3271
+ }
2963
3272
  function RichText({ html, palette: p, onLink, size = 15 }) {
2964
3273
  const blocks = (0, import_react.useMemo)(() => parseHtml(html), [html]);
2965
3274
  if (!blocks.length) return null;
2966
3275
  const base = { color: p.text, fontSize: size, lineHeight: Math.round(size * 1.42), fontFamily: p.fontFamily };
2967
3276
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles2.wrap, children: blocks.map((b, i) => {
3277
+ if (b.kind === "image") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BodyImage, { block: b, palette: p }, i);
3278
+ if (b.kind === "video") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BodyVideo, { block: b, palette: p, onLink }, i);
3279
+ if (b.kind === "pdf") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BodyPdf, { block: b, palette: p, onLink }, i);
3280
+ if (b.kind === "file") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BodyFile, { block: b, palette: p, onLink }, i);
2968
3281
  const runs = b.runs.map((r, j) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: runStyle(r, p), onPress: r.href ? () => onLink(r.href) : void 0, accessibilityRole: r.href ? "link" : void 0, children: r.text }, j));
2969
3282
  if (b.kind === "list-item") {
2970
3283
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: [styles2.li, { paddingLeft: (b.level - 1) * 14 }], children: [
@@ -2983,7 +3296,25 @@ var styles2 = import_react_native2.StyleSheet.create({
2983
3296
  wrap: { gap: 6 },
2984
3297
  li: { flexDirection: "row", gap: 6 },
2985
3298
  marker: { minWidth: 14 },
2986
- flex: { flex: 1 }
3299
+ flex: { flex: 1 },
3300
+ image: { width: "100%", borderRadius: 6 },
3301
+ video: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 12, paddingHorizontal: 14, borderWidth: 1, borderRadius: 8 },
3302
+ play: { fontSize: 14 },
3303
+ badge: { backgroundColor: "#e5484d", borderRadius: 4, paddingHorizontal: 5, paddingVertical: 3 },
3304
+ badgeText: { color: "#fff", fontSize: 9, fontWeight: "700", letterSpacing: 0.4 },
3305
+ pdf: { borderWidth: 1, borderRadius: 10, overflow: "hidden" },
3306
+ pdfHeader: { flexDirection: "row", alignItems: "center", gap: 8, minHeight: 44, paddingHorizontal: 12 },
3307
+ pdfPages: { marginHorizontal: 10, marginBottom: 10 },
3308
+ page: { backgroundColor: "#fff", borderRadius: 3 },
3309
+ count: { position: "absolute", right: 8, bottom: 8, backgroundColor: "rgba(17,17,17,0.72)", borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2 },
3310
+ countText: { color: "#fff", fontSize: 11, fontWeight: "500", fontVariant: ["tabular-nums"] },
3311
+ reader: { flex: 1, backgroundColor: "#0b0b0d" },
3312
+ readerBar: { flexDirection: "row", alignItems: "center", gap: 8, paddingTop: import_react_native2.Platform.OS === "ios" ? 52 : 12, paddingBottom: 10, paddingHorizontal: 16 },
3313
+ readerTitle: { color: "#fff", fontSize: 16, fontWeight: "600" },
3314
+ readerBtn: { minHeight: 36, justifyContent: "center", paddingHorizontal: 12, borderRadius: 8, backgroundColor: "rgba(255,255,255,0.12)" },
3315
+ readerBtnText: { color: "#fff", fontWeight: "600" },
3316
+ readerPages: { gap: 12, paddingHorizontal: 12, paddingBottom: 32 },
3317
+ readerPage: { width: "100%", backgroundColor: "#fff", borderRadius: 2 }
2987
3318
  });
2988
3319
 
2989
3320
  // src/ui/Checklist.tsx
@@ -3333,18 +3664,15 @@ function Help({ store, client, palette: p }) {
3333
3664
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { accessibilityRole: "header", style: [styles4.sectionTitle, { color: p.muted }], children: title.toUpperCase() }),
3334
3665
  children
3335
3666
  ] }, title);
3336
- const collections = [...articles?.collections ?? []].sort((a, b) => a.order - b.order);
3337
- const loose = (articles?.articles ?? []).filter((a) => !a.collectionId || !collections.some((c) => c.id === a.collectionId));
3667
+ const audiences = client.getAudienceIds();
3668
+ const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
3669
+ const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
3338
3670
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.ScrollView, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3339
3671
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.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) }] }),
3340
- results ? section(`Results for \u201C${q.trim()}\u201D`, results.length ? results.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
3672
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
3341
3673
  recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
3342
3674
  !articles ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.ActivityIndicator, { color: p.muted }) : null,
3343
- collections.map((c) => {
3344
- const items = (articles?.articles ?? []).filter((a) => a.collectionId === c.id);
3345
- return items.length ? section(c.name, items.map((a) => row(a.id, a.title, a.summary, () => open(a)))) : null;
3346
- }),
3347
- loose.length ? section("Articles", loose.map((a) => row(a.id, a.title, a.summary, () => open(a)))) : null,
3675
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
3348
3676
  rc?.faqs.length ? section(
3349
3677
  "FAQ",
3350
3678
  rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native4.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
@@ -3475,8 +3803,9 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3475
3803
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { accessibilityRole: "header", style: [styles5.sectionTitle, { color: p.muted }], children: title.toUpperCase() }),
3476
3804
  children
3477
3805
  ] }, title);
3478
- const collections = [...rc.collections].sort((a, b) => a.order - b.order);
3479
- const loose = rc.articles.filter((a) => !a.collectionId || !rc.collections.some((c) => c.id === a.collectionId));
3806
+ const audiences = client.getAudienceIds();
3807
+ const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
3808
+ const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
3480
3809
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Modal, { visible: open, animationType: "slide", presentationStyle: import_react_native5.Platform.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: import_react_native5.Platform.OS === "ios" ? 8 : topInset }], children: [
3481
3810
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles5.header, { borderColor: p.border }], children: [
3482
3811
  article ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -3500,17 +3829,13 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3500
3829
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
3501
3830
  }
3502
3831
  ),
3503
- results ? section(
3832
+ listed ? section(
3504
3833
  `Results for \u201C${query.trim()}\u201D`,
3505
- results.length ? results.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
3834
+ listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
3506
3835
  ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
3507
3836
  recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
3508
3837
  checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
3509
- collections.map((c) => {
3510
- const items = rc.articles.filter((a) => a.collectionId === c.id);
3511
- return items.length ? section(c.name, items.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))) : null;
3512
- }),
3513
- loose.length ? section("Articles", loose.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))) : null,
3838
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
3514
3839
  rc.faqs.length ? section(
3515
3840
  "FAQ",
3516
3841
  rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [