@guidelytech/react-native 0.5.0 → 0.6.2

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.d.cts CHANGED
@@ -319,7 +319,7 @@ interface UiState {
319
319
  }
320
320
 
321
321
  declare const SDK_NAME = "@guidelytech/react-native";
322
- declare const SDK_VERSION = "0.5.0";
322
+ declare const SDK_VERSION = "0.6.2";
323
323
  declare const DEFAULT_API_URL = "https://guidelytech.com";
324
324
  /** Host-platform services the engine needs; the provider supplies React Native implementations. */
325
325
  interface PlatformBridge {
@@ -637,9 +637,16 @@ declare function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }:
637
637
  * Tiny, tolerant HTML → rich-text converter for guide bodies (already sanitized server-side).
638
638
  * Supports p, div, br, strong/b, em/i, u, a, code, ul/ol/li, h1–h6 and blockquote; every other tag
639
639
  * is reduced to its text. `img` becomes an image block, and `video` / canonical embed `iframe`s
640
- * become video blocks (shown as a link that opens the video). Output is plain data so it can be
641
- * unit-tested without React Native.
640
+ * become video blocks (shown as a link that opens the video). A PDF (`div.gd-pdf`: its link and its
641
+ * pages as images) becomes one pdf block, and a lone file link (`a.gd-file`) a file block. Output is
642
+ * plain data so it can be unit-tested without React Native.
642
643
  */
644
+ interface PdfPage {
645
+ src: string;
646
+ alt: string;
647
+ width?: number;
648
+ height?: number;
649
+ }
643
650
  interface TextRun {
644
651
  text: string;
645
652
  bold?: boolean;
@@ -649,7 +656,7 @@ interface TextRun {
649
656
  href?: string;
650
657
  }
651
658
  interface TextBlock {
652
- kind: "paragraph" | "heading" | "list-item" | "quote" | "image" | "video";
659
+ kind: "paragraph" | "heading" | "list-item" | "quote" | "image" | "video" | "pdf" | "file";
653
660
  /** Heading level 1–3, list nesting depth from 1. */
654
661
  level: number;
655
662
  /** For list items: the bullet ("•") or number ("1.") to show. */
@@ -657,10 +664,12 @@ interface TextBlock {
657
664
  runs: TextRun[];
658
665
  /** Image blocks: http(s) or data:image source. */
659
666
  src?: string;
660
- /** Image alt text, or a video's label. */
667
+ /** Image alt text, a video's label, or a PDF's file name. */
661
668
  alt?: string;
662
- /** Video blocks: the page or file to open. */
669
+ /** Video, pdf and file blocks: the page or file to open. */
663
670
  href?: string;
671
+ /** Pdf blocks: the rendered pages, in order. */
672
+ pages?: PdfPage[];
664
673
  /** Image intrinsic size from `width`/`height`, when present. */
665
674
  width?: number;
666
675
  height?: number;
package/dist/index.d.ts CHANGED
@@ -319,7 +319,7 @@ interface UiState {
319
319
  }
320
320
 
321
321
  declare const SDK_NAME = "@guidelytech/react-native";
322
- declare const SDK_VERSION = "0.5.0";
322
+ declare const SDK_VERSION = "0.6.2";
323
323
  declare const DEFAULT_API_URL = "https://guidelytech.com";
324
324
  /** Host-platform services the engine needs; the provider supplies React Native implementations. */
325
325
  interface PlatformBridge {
@@ -637,9 +637,16 @@ declare function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }:
637
637
  * Tiny, tolerant HTML → rich-text converter for guide bodies (already sanitized server-side).
638
638
  * Supports p, div, br, strong/b, em/i, u, a, code, ul/ol/li, h1–h6 and blockquote; every other tag
639
639
  * is reduced to its text. `img` becomes an image block, and `video` / canonical embed `iframe`s
640
- * become video blocks (shown as a link that opens the video). Output is plain data so it can be
641
- * unit-tested without React Native.
640
+ * become video blocks (shown as a link that opens the video). A PDF (`div.gd-pdf`: its link and its
641
+ * pages as images) becomes one pdf block, and a lone file link (`a.gd-file`) a file block. Output is
642
+ * plain data so it can be unit-tested without React Native.
642
643
  */
644
+ interface PdfPage {
645
+ src: string;
646
+ alt: string;
647
+ width?: number;
648
+ height?: number;
649
+ }
643
650
  interface TextRun {
644
651
  text: string;
645
652
  bold?: boolean;
@@ -649,7 +656,7 @@ interface TextRun {
649
656
  href?: string;
650
657
  }
651
658
  interface TextBlock {
652
- kind: "paragraph" | "heading" | "list-item" | "quote" | "image" | "video";
659
+ kind: "paragraph" | "heading" | "list-item" | "quote" | "image" | "video" | "pdf" | "file";
653
660
  /** Heading level 1–3, list nesting depth from 1. */
654
661
  level: number;
655
662
  /** For list items: the bullet ("•") or number ("1.") to show. */
@@ -657,10 +664,12 @@ interface TextBlock {
657
664
  runs: TextRun[];
658
665
  /** Image blocks: http(s) or data:image source. */
659
666
  src?: string;
660
- /** Image alt text, or a video's label. */
667
+ /** Image alt text, a video's label, or a PDF's file name. */
661
668
  alt?: string;
662
- /** Video blocks: the page or file to open. */
669
+ /** Video, pdf and file blocks: the page or file to open. */
663
670
  href?: string;
671
+ /** Pdf blocks: the rendered pages, in order. */
672
+ pages?: PdfPage[];
664
673
  /** Image intrinsic size from `width`/`height`, when present. */
665
674
  width?: number;
666
675
  height?: number;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
6
6
  import { useContext as useContext2, useEffect as useEffect8, useRef as useRef5, useState as useState7 } from "react";
7
7
  import { AppState, Dimensions, Linking, Platform as Platform6 } from "react-native";
8
8
 
9
- // ../shared/dist/chunk-5IQVBZO6.js
9
+ // ../shared/dist/chunk-OLWGMDRX.js
10
10
  function fnv1a(input) {
11
11
  let h = 2166136261;
12
12
  for (let i = 0; i < input.length; i++) {
@@ -312,6 +312,13 @@ function sanitizeProperties(props) {
312
312
  var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
313
313
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
314
314
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
315
+ var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
316
+ var MEDIA_MAX_BYTES = Math.max(MEDIA_IMAGE_MAX_BYTES, MEDIA_VIDEO_MAX_BYTES, MEDIA_FILE_MAX_BYTES);
317
+ var FILE_LINK_CLASS = "gd-file";
318
+ var PDF_BLOCK_CLASS = "gd-pdf";
319
+ function isFileLink(className, href) {
320
+ return !!className && className.split(/\s+/).includes(FILE_LINK_CLASS) && isSafeMediaUrl(href);
321
+ }
315
322
  var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
316
323
  var VIMEO_ID = /^\d{1,12}$/;
317
324
  var VIMEO_HASH = /^[0-9a-f]{6,20}$/i;
@@ -452,8 +459,45 @@ function isArticleListed(collections, article, member) {
452
459
  }
453
460
  return true;
454
461
  }
462
+ var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
455
463
 
456
- // ../sdk-core/dist/chunk-GJT7CFAC.js
464
+ // ../sdk-core/dist/chunk-CYAG6ZYX.js
465
+ function readJson(store, key, fallback) {
466
+ const raw = store.get(key);
467
+ if (raw == null) return fallback;
468
+ try {
469
+ return JSON.parse(raw);
470
+ } catch {
471
+ return fallback;
472
+ }
473
+ }
474
+ function writeJson(store, key, value) {
475
+ try {
476
+ store.set(key, JSON.stringify(value));
477
+ } catch {
478
+ }
479
+ }
480
+ var KEYS = {
481
+ anon: "guidely:anon",
482
+ seen: "guidely:seen",
483
+ config: (key) => `guidely:config:${key}`,
484
+ etag: (key) => `guidely:etag:${key}`,
485
+ state: (key) => `guidely:state:${key}`,
486
+ user: (key) => `guidely:user:${key}`,
487
+ queue: (key) => `guidely:queue:${key}`,
488
+ session: "guidely:session",
489
+ active: "guidely:active",
490
+ authoring: "guidely:authoring",
491
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
492
+ capture: "guidely:capture",
493
+ capturePending: "guidely:capture:pending",
494
+ /** Messenger conversations this browser can read: [{ id, token }]. */
495
+ conversations: (key) => `guidely:conversations:${key}`,
496
+ /** Announcement ids opened in the messenger's News tab. */
497
+ newsSeen: (key) => `guidely:news:${key}`
498
+ };
499
+
500
+ // ../sdk-core/dist/chunk-AKNE7D5M.js
457
501
  var __defProp2 = Object.defineProperty;
458
502
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
459
503
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -475,7 +519,8 @@ var Api = class {
475
519
  const f = this.fetcher;
476
520
  if (!f) throw new Error("fetch is not available");
477
521
  const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
478
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
522
+ const form = typeof FormData !== "undefined" && options.body instanceof FormData;
523
+ if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
479
524
  if (options.etag) headers["If-None-Match"] = options.etag;
480
525
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
481
526
  const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
@@ -483,7 +528,7 @@ var Api = class {
483
528
  const res = await f(this.url(path, options.params), {
484
529
  method,
485
530
  headers,
486
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
531
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
487
532
  keepalive: options.keepalive,
488
533
  signal: ac?.signal,
489
534
  credentials: "omit",
@@ -508,6 +553,10 @@ var Api = class {
508
553
  post(path, body, options) {
509
554
  return this.request("POST", path, { body, ...options });
510
555
  }
556
+ /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
557
+ postForm(path, form, options) {
558
+ return this.request("POST", path, { body: form, ...options });
559
+ }
511
560
  /**
512
561
  * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
513
562
  * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
@@ -550,10 +599,10 @@ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
550
599
  function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
551
600
  return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
552
601
  }
553
- async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
602
+ async function withRetry(send, attempts = 3, baseMs = 500) {
554
603
  for (let i = 0; i < attempts; i++) {
555
604
  try {
556
- const res = await api.post(path, body);
605
+ const res = await send();
557
606
  if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
558
607
  } catch {
559
608
  }
@@ -561,6 +610,9 @@ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
561
610
  }
562
611
  return null;
563
612
  }
613
+ function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
614
+ return withRetry(() => api.post(path, body), attempts, baseMs);
615
+ }
564
616
  async function readEventStream(body, onEvent) {
565
617
  const reader = body.getReader();
566
618
  const decoder = new TextDecoder();
@@ -595,36 +647,7 @@ async function readEventStream(body, onEvent) {
595
647
  if (buffer.trim()) dispatch(buffer);
596
648
  }
597
649
 
598
- // ../sdk-core/dist/chunk-HLR3W63Z.js
599
- function readJson(store, key, fallback) {
600
- const raw = store.get(key);
601
- if (raw == null) return fallback;
602
- try {
603
- return JSON.parse(raw);
604
- } catch {
605
- return fallback;
606
- }
607
- }
608
- function writeJson(store, key, value) {
609
- try {
610
- store.set(key, JSON.stringify(value));
611
- } catch {
612
- }
613
- }
614
- var KEYS = {
615
- anon: "guidely:anon",
616
- seen: "guidely:seen",
617
- config: (key) => `guidely:config:${key}`,
618
- etag: (key) => `guidely:etag:${key}`,
619
- state: (key) => `guidely:state:${key}`,
620
- user: (key) => `guidely:user:${key}`,
621
- queue: (key) => `guidely:queue:${key}`,
622
- session: "guidely:session",
623
- active: "guidely:active",
624
- authoring: "guidely:authoring",
625
- /** Messenger conversations this browser can read: [{ id, token }]. */
626
- conversations: (key) => `guidely:conversations:${key}`
627
- };
650
+ // ../sdk-core/dist/chunk-GDF6RCHQ.js
628
651
  var EventQueue = class {
629
652
  constructor(opts) {
630
653
  __publicField2(this, "opts", opts);
@@ -1706,7 +1729,7 @@ var PersistentStore = class {
1706
1729
 
1707
1730
  // src/core/client.ts
1708
1731
  var SDK_NAME = "@guidelytech/react-native";
1709
- var SDK_VERSION = "0.5.0";
1732
+ var SDK_VERSION = "0.6.2";
1710
1733
  var DEFAULT_API_URL = "https://guidelytech.com";
1711
1734
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1712
1735
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -2843,7 +2866,7 @@ import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef3
2843
2866
  import { Platform as Platform5, Pressable as Pressable8, StatusBar, StyleSheet as StyleSheet9, Text as Text8, useColorScheme, useWindowDimensions, View as View9 } from "react-native";
2844
2867
 
2845
2868
  // src/ui/Checklist.tsx
2846
- import { Pressable as Pressable3, ScrollView, StyleSheet as StyleSheet3, Text as Text3, View as View3 } from "react-native";
2869
+ import { Pressable as Pressable3, ScrollView as ScrollView2, StyleSheet as StyleSheet3, Text as Text3, View as View3 } from "react-native";
2847
2870
 
2848
2871
  // src/ui/primitives.tsx
2849
2872
  import { Pressable, StyleSheet, Text, View } from "react-native";
@@ -2898,7 +2921,7 @@ var styles = StyleSheet.create({
2898
2921
 
2899
2922
  // src/ui/RichText.tsx
2900
2923
  import { useEffect, useMemo, useState } from "react";
2901
- import { Image, Platform, Pressable as Pressable2, StyleSheet as StyleSheet2, Text as Text2, View as View2 } from "react-native";
2924
+ import { Image, Modal, Platform, Pressable as Pressable2, ScrollView, StyleSheet as StyleSheet2, Text as Text2, View as View2 } from "react-native";
2902
2925
 
2903
2926
  // src/core/html.ts
2904
2927
  var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0", mdash: "\u2014", ndash: "\u2013", hellip: "\u2026", rsquo: "\u2019", lsquo: "\u2018", rdquo: "\u201D", ldquo: "\u201C", copy: "\xA9", reg: "\xAE", trade: "\u2122", times: "\xD7", rarr: "\u2192", larr: "\u2190", bull: "\u2022" };
@@ -2925,6 +2948,8 @@ function parseHtml(html) {
2925
2948
  const lists = [];
2926
2949
  let current = null;
2927
2950
  let skipping = null;
2951
+ let pdf = null;
2952
+ let fileLink = null;
2928
2953
  const media = (block) => {
2929
2954
  flush();
2930
2955
  blocks.push({ level: 1, runs: [], ...block });
@@ -2959,12 +2984,30 @@ function parseHtml(html) {
2959
2984
  if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
2960
2985
  else current.runs.push(run);
2961
2986
  };
2987
+ const size = (attrs, name) => {
2988
+ const n = Number(attr(attrs, name));
2989
+ return n > 0 ? n : void 0;
2990
+ };
2991
+ const endPdf = (p) => {
2992
+ const name = p.name.replace(/\s+/g, " ").trim();
2993
+ if (p.href && p.pages.length) media({ kind: "pdf", href: p.href, alt: name || "PDF", pages: p.pages });
2994
+ else if (p.href) media({ kind: "file", href: p.href, alt: name || "Open PDF" });
2995
+ 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 } : {} });
2996
+ };
2962
2997
  const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
2963
2998
  let m;
2964
2999
  while (m = re.exec(html || "")) {
2965
3000
  const [token, rawTag, attrs = "", text] = m;
2966
3001
  if (text !== void 0) {
2967
3002
  if (skipping) continue;
3003
+ if (pdf) {
3004
+ if (pdf.inLink) pdf.name += decodeEntities(text);
3005
+ continue;
3006
+ }
3007
+ if (fileLink) {
3008
+ fileLink.name += decodeEntities(text);
3009
+ continue;
3010
+ }
2968
3011
  push(decodeEntities(text.replace(/\s+/g, " ")));
2969
3012
  continue;
2970
3013
  }
@@ -2988,6 +3031,44 @@ function parseHtml(html) {
2988
3031
  if (!token.endsWith("/>")) skipping = tag;
2989
3032
  continue;
2990
3033
  }
3034
+ if (pdf) {
3035
+ if (tag === "div") {
3036
+ pdf.depth += closing ? -1 : 1;
3037
+ if (pdf.depth === 0) {
3038
+ endPdf(pdf);
3039
+ pdf = null;
3040
+ }
3041
+ } else if (tag === "a") {
3042
+ if (closing) pdf.inLink = false;
3043
+ else if (!pdf.href && isFileLink(attr(attrs, "class"), attr(attrs, "href"))) {
3044
+ pdf.href = attr(attrs, "href");
3045
+ pdf.inLink = true;
3046
+ }
3047
+ } else if (tag === "img" && !closing) {
3048
+ const src = attr(attrs, "src");
3049
+ const w = size(attrs, "width");
3050
+ const h = size(attrs, "height");
3051
+ if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
3052
+ }
3053
+ continue;
3054
+ }
3055
+ if (fileLink) {
3056
+ if (closing && tag === "a") {
3057
+ media({ kind: "file", href: fileLink.href, alt: fileLink.name.replace(/\s+/g, " ").trim() || "Open PDF" });
3058
+ fileLink = null;
3059
+ }
3060
+ continue;
3061
+ }
3062
+ if (!closing && tag === "div" && (attr(attrs, "class") ?? "").split(/\s+/).includes(PDF_BLOCK_CLASS)) {
3063
+ flush();
3064
+ pdf = { depth: 1, name: "", inLink: false, pages: [] };
3065
+ continue;
3066
+ }
3067
+ if (!closing && tag === "a" && isFileLink(attr(attrs, "class"), attr(attrs, "href"))) {
3068
+ flush();
3069
+ fileLink = { href: attr(attrs, "href"), name: "" };
3070
+ continue;
3071
+ }
2991
3072
  if (closing) {
2992
3073
  if (tag === "b" || tag === "strong") style.bold = Math.max(0, style.bold - 1);
2993
3074
  else if (tag === "i" || tag === "em") style.italic = Math.max(0, style.italic - 1);
@@ -3056,11 +3137,13 @@ function parseHtml(html) {
3056
3137
  }
3057
3138
  }
3058
3139
  }
3140
+ if (pdf) endPdf(pdf);
3141
+ if (fileLink) media({ kind: "file", href: fileLink.href, alt: fileLink.name.replace(/\s+/g, " ").trim() || "Open PDF" });
3059
3142
  flush();
3060
3143
  return blocks;
3061
3144
  }
3062
3145
  function htmlToText(html) {
3063
- return parseHtml(html).map((b) => b.kind === "image" || b.kind === "video" ? b.alt ?? "" : (b.marker ? `${b.marker} ` : "") + b.runs.map((r) => r.text).join("")).filter(Boolean).join("\n");
3146
+ 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");
3064
3147
  }
3065
3148
 
3066
3149
  // src/ui/RichText.tsx
@@ -3096,6 +3179,48 @@ function BodyVideo({ block, palette: p, onLink }) {
3096
3179
  /* @__PURE__ */ jsx2(Text2, { style: { color: p.primary, fontFamily: p.fontFamily, fontWeight: "600" }, children: block.alt })
3097
3180
  ] });
3098
3181
  }
3182
+ var PAGE_RATIO = 1 / 1.414;
3183
+ var pageRatio = (p) => p.width && p.height ? p.width / p.height : PAGE_RATIO;
3184
+ function PdfBadge() {
3185
+ return /* @__PURE__ */ jsx2(View2, { style: styles2.badge, children: /* @__PURE__ */ jsx2(Text2, { style: styles2.badgeText, children: "PDF" }) });
3186
+ }
3187
+ function BodyFile({ block, palette: p, onLink }) {
3188
+ return /* @__PURE__ */ jsxs(Pressable2, { 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: [
3189
+ /* @__PURE__ */ jsx2(PdfBadge, {}),
3190
+ /* @__PURE__ */ jsx2(Text2, { numberOfLines: 2, ellipsizeMode: "middle", style: [styles2.flex, { color: p.text, fontFamily: p.fontFamily, fontWeight: "600" }], children: block.alt })
3191
+ ] });
3192
+ }
3193
+ function BodyPdf({ block, palette: p, onLink }) {
3194
+ const pages = block.pages ?? [];
3195
+ const [width, setWidth] = useState(0);
3196
+ const [index, setIndex] = useState(0);
3197
+ const [reading, setReading] = useState(false);
3198
+ const height = width ? Math.max(...pages.map((pg) => width / pageRatio(pg))) : 0;
3199
+ const onScroll = (e) => width && setIndex(Math.round(e.nativeEvent.contentOffset.x / width));
3200
+ return /* @__PURE__ */ jsxs(View2, { style: [styles2.pdf, { borderColor: p.border, backgroundColor: p.surface }], children: [
3201
+ /* @__PURE__ */ jsxs(Pressable2, { accessibilityRole: "button", accessibilityLabel: `Read ${block.alt} full screen`, onPress: () => setReading(true), style: ({ pressed }) => [styles2.pdfHeader, { opacity: pressed ? 0.7 : 1 }], children: [
3202
+ /* @__PURE__ */ jsx2(PdfBadge, {}),
3203
+ /* @__PURE__ */ jsx2(Text2, { numberOfLines: 1, ellipsizeMode: "middle", style: [styles2.flex, { color: p.text, fontFamily: p.fontFamily, fontWeight: "600" }], children: block.alt }),
3204
+ /* @__PURE__ */ jsx2(Text2, { style: { color: p.muted, fontSize: 16 }, children: "\u2922" })
3205
+ ] }),
3206
+ /* @__PURE__ */ jsxs(View2, { style: styles2.pdfPages, onLayout: (e) => setWidth(e.nativeEvent.layout.width), children: [
3207
+ width > 0 && /* @__PURE__ */ jsx2(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__ */ jsx2(Pressable2, { 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__ */ jsx2(Image, { source: { uri: pg.src }, resizeMode: "contain", style: [styles2.page, { width: Math.min(width, height * pageRatio(pg)), aspectRatio: pageRatio(pg) }] }) }, i)) }),
3208
+ pages.length > 1 && /* @__PURE__ */ jsx2(View2, { pointerEvents: "none", style: styles2.count, children: /* @__PURE__ */ jsxs(Text2, { style: styles2.countText, children: [
3209
+ index + 1,
3210
+ " / ",
3211
+ pages.length
3212
+ ] }) })
3213
+ ] }),
3214
+ /* @__PURE__ */ jsx2(Modal, { visible: reading, animationType: "slide", presentationStyle: "fullScreen", onRequestClose: () => setReading(false), children: /* @__PURE__ */ jsxs(View2, { style: styles2.reader, children: [
3215
+ /* @__PURE__ */ jsxs(View2, { style: styles2.readerBar, children: [
3216
+ /* @__PURE__ */ jsx2(Text2, { numberOfLines: 1, ellipsizeMode: "middle", style: [styles2.flex, styles2.readerTitle], accessibilityRole: "header", children: block.alt }),
3217
+ /* @__PURE__ */ jsx2(Pressable2, { accessibilityRole: "link", onPress: () => onLink(block.href), style: styles2.readerBtn, children: /* @__PURE__ */ jsx2(Text2, { style: styles2.readerBtnText, children: "Open PDF \u2197" }) }),
3218
+ /* @__PURE__ */ jsx2(Pressable2, { accessibilityRole: "button", accessibilityLabel: "Close", onPress: () => setReading(false), style: styles2.readerBtn, children: /* @__PURE__ */ jsx2(Text2, { style: styles2.readerBtnText, children: "Done" }) })
3219
+ ] }),
3220
+ /* @__PURE__ */ jsx2(ScrollView, { contentContainerStyle: styles2.readerPages, children: pages.map((pg, i) => /* @__PURE__ */ jsx2(Image, { source: { uri: pg.src }, accessible: true, accessibilityLabel: pg.alt || `Page ${i + 1}`, resizeMode: "contain", style: [styles2.readerPage, { aspectRatio: pageRatio(pg) }] }, i)) })
3221
+ ] }) })
3222
+ ] });
3223
+ }
3099
3224
  function RichText({ html, palette: p, onLink, size = 15 }) {
3100
3225
  const blocks = useMemo(() => parseHtml(html), [html]);
3101
3226
  if (!blocks.length) return null;
@@ -3103,6 +3228,8 @@ function RichText({ html, palette: p, onLink, size = 15 }) {
3103
3228
  return /* @__PURE__ */ jsx2(View2, { style: styles2.wrap, children: blocks.map((b, i) => {
3104
3229
  if (b.kind === "image") return /* @__PURE__ */ jsx2(BodyImage, { block: b, palette: p }, i);
3105
3230
  if (b.kind === "video") return /* @__PURE__ */ jsx2(BodyVideo, { block: b, palette: p, onLink }, i);
3231
+ if (b.kind === "pdf") return /* @__PURE__ */ jsx2(BodyPdf, { block: b, palette: p, onLink }, i);
3232
+ if (b.kind === "file") return /* @__PURE__ */ jsx2(BodyFile, { block: b, palette: p, onLink }, i);
3106
3233
  const runs = b.runs.map((r, j) => /* @__PURE__ */ jsx2(Text2, { style: runStyle(r, p), onPress: r.href ? () => onLink(r.href) : void 0, accessibilityRole: r.href ? "link" : void 0, children: r.text }, j));
3107
3234
  if (b.kind === "list-item") {
3108
3235
  return /* @__PURE__ */ jsxs(View2, { style: [styles2.li, { paddingLeft: (b.level - 1) * 14 }], children: [
@@ -3124,7 +3251,22 @@ var styles2 = StyleSheet2.create({
3124
3251
  flex: { flex: 1 },
3125
3252
  image: { width: "100%", borderRadius: 6 },
3126
3253
  video: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 12, paddingHorizontal: 14, borderWidth: 1, borderRadius: 8 },
3127
- play: { fontSize: 14 }
3254
+ play: { fontSize: 14 },
3255
+ badge: { backgroundColor: "#e5484d", borderRadius: 4, paddingHorizontal: 5, paddingVertical: 3 },
3256
+ badgeText: { color: "#fff", fontSize: 9, fontWeight: "700", letterSpacing: 0.4 },
3257
+ pdf: { borderWidth: 1, borderRadius: 10, overflow: "hidden" },
3258
+ pdfHeader: { flexDirection: "row", alignItems: "center", gap: 8, minHeight: 44, paddingHorizontal: 12 },
3259
+ pdfPages: { marginHorizontal: 10, marginBottom: 10 },
3260
+ page: { backgroundColor: "#fff", borderRadius: 3 },
3261
+ count: { position: "absolute", right: 8, bottom: 8, backgroundColor: "rgba(17,17,17,0.72)", borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2 },
3262
+ countText: { color: "#fff", fontSize: 11, fontWeight: "500", fontVariant: ["tabular-nums"] },
3263
+ reader: { flex: 1, backgroundColor: "#0b0b0d" },
3264
+ readerBar: { flexDirection: "row", alignItems: "center", gap: 8, paddingTop: Platform.OS === "ios" ? 52 : 12, paddingBottom: 10, paddingHorizontal: 16 },
3265
+ readerTitle: { color: "#fff", fontSize: 16, fontWeight: "600" },
3266
+ readerBtn: { minHeight: 36, justifyContent: "center", paddingHorizontal: 12, borderRadius: 8, backgroundColor: "rgba(255,255,255,0.12)" },
3267
+ readerBtnText: { color: "#fff", fontWeight: "600" },
3268
+ readerPages: { gap: 12, paddingHorizontal: 12, paddingBottom: 32 },
3269
+ readerPage: { width: "100%", backgroundColor: "#fff", borderRadius: 2 }
3128
3270
  });
3129
3271
 
3130
3272
  // src/ui/Checklist.tsx
@@ -3158,7 +3300,7 @@ function Checklist({ view, client, palette: p, geometry, showLauncher, launcherB
3158
3300
  ] }),
3159
3301
  /* @__PURE__ */ jsx3(View3, { style: [styles3.bar, { backgroundColor: p.surface }], accessibilityRole: "progressbar", accessibilityValue: { min: 0, max: total, now: completed }, children: /* @__PURE__ */ jsx3(View3, { style: [styles3.fill, { backgroundColor: p.primary, width: `${total ? completed / total * 100 : 0}%` }] }) }),
3160
3302
  finished && config.completionBody ? /* @__PURE__ */ jsx3(RichText, { html: config.completionBody, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
3161
- /* @__PURE__ */ jsx3(ScrollView, { style: styles3.tasks, contentContainerStyle: styles3.tasksContent, children: config.tasks.map((task) => {
3303
+ /* @__PURE__ */ jsx3(ScrollView2, { style: styles3.tasks, contentContainerStyle: styles3.tasksContent, children: config.tasks.map((task) => {
3162
3304
  const isDone = done.has(task.id);
3163
3305
  const locked = !!nextOpen && !isDone && task.id !== nextOpen;
3164
3306
  return /* @__PURE__ */ jsxs2(
@@ -3241,7 +3383,7 @@ var styles3 = StyleSheet3.create({
3241
3383
 
3242
3384
  // src/ui/Messenger.tsx
3243
3385
  import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2, useSyncExternalStore } from "react";
3244
- import { ActivityIndicator, Keyboard, Modal, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView2, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
3386
+ 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";
3245
3387
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3246
3388
  function useMessenger(store) {
3247
3389
  return useSyncExternalStore(
@@ -3284,7 +3426,7 @@ function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
3284
3426
  const keyboard = useKeyboardHeight();
3285
3427
  const v = snap.view;
3286
3428
  const title = v.kind === "thread" ? snap.thread?.conversation?.subject || "New conversation" : v.kind === "article" ? "" : v.tab === "home" ? snap.config.name : v.tab === "messages" ? "Messages" : "Help";
3287
- return /* @__PURE__ */ jsx4(Modal, { visible: snap.open, animationType: "slide", presentationStyle: Platform2.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => v.kind === "tab" ? store.close() : store.back(), children: /* @__PURE__ */ jsxs3(View4, { style: [styles4.sheet, { backgroundColor: p.background, paddingTop: Platform2.OS === "ios" ? 8 : topInset, paddingBottom: keyboard }], children: [
3429
+ return /* @__PURE__ */ jsx4(Modal2, { visible: snap.open, animationType: "slide", presentationStyle: Platform2.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => v.kind === "tab" ? store.close() : store.back(), children: /* @__PURE__ */ jsxs3(View4, { style: [styles4.sheet, { backgroundColor: p.background, paddingTop: Platform2.OS === "ios" ? 8 : topInset, paddingBottom: keyboard }], children: [
3288
3430
  /* @__PURE__ */ jsxs3(View4, { style: [styles4.header, { borderColor: p.border }], children: [
3289
3431
  v.kind !== "tab" ? /* @__PURE__ */ jsx4(Pressable4, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => store.back(), hitSlop: 10, children: /* @__PURE__ */ jsx4(Text4, { style: [styles4.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
3290
3432
  /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "header", numberOfLines: 1, style: [styles4.title, { color: p.text, fontFamily: p.fontFamily }], children: title }),
@@ -3314,7 +3456,7 @@ function Home({ store, palette: p }) {
3314
3456
  const snap = useMessenger(store);
3315
3457
  const recent = snap.conversations[0];
3316
3458
  const [q, setQ] = useState2("");
3317
- return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3459
+ return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3318
3460
  /* @__PURE__ */ jsxs3(View4, { style: [styles4.hero, { backgroundColor: p.primary, borderRadius: p.radius }], children: [
3319
3461
  /* @__PURE__ */ jsx4(Text4, { style: [styles4.heroTitle, { color: p.onPrimary }], children: snap.config.greeting || `Hi! I'm ${snap.config.name}.` }),
3320
3462
  /* @__PURE__ */ jsx4(Text4, { style: [styles4.heroSub, { color: p.onPrimary }], children: snap.config.aiAvailable ? `${snap.config.name} answers right away${snap.config.handoffEnabled ? ", and the team is here when you need a person" : ""}.` : "The team usually replies within a few hours." })
@@ -3360,7 +3502,7 @@ function ConversationRow({ c, palette: p, onPress }) {
3360
3502
  }
3361
3503
  function Conversations({ store, palette: p }) {
3362
3504
  const snap = useMessenger(store);
3363
- return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, children: [
3505
+ return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, children: [
3364
3506
  snap.conversations.length ? snap.conversations.map((c) => /* @__PURE__ */ jsx4(ConversationRow, { c, palette: p, onPress: () => void store.openConversation(c.id) }, c.id)) : /* @__PURE__ */ jsx4(Text4, { style: [styles4.empty, { color: p.muted }], children: "No conversations yet." }),
3365
3507
  /* @__PURE__ */ jsx4(GButton, { label: "New conversation", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth })
3366
3508
  ] });
@@ -3405,7 +3547,7 @@ function Thread({ store, palette: p, bottomInset }) {
3405
3547
  };
3406
3548
  const canHandoff = snap.config.handoffEnabled && snap.config.aiAvailable && convo?.status === "bot" && !t.sending;
3407
3549
  return /* @__PURE__ */ jsxs3(View4, { style: styles4.flex, children: [
3408
- /* @__PURE__ */ jsxs3(ScrollView2, { ref: scroll, contentContainerStyle: styles4.thread, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [
3550
+ /* @__PURE__ */ jsxs3(ScrollView3, { ref: scroll, contentContainerStyle: styles4.thread, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [
3409
3551
  !t.messages.length && !t.sending ? /* @__PURE__ */ jsx4(Text4, { style: [styles4.empty, { color: p.muted }], children: snap.config.greeting || "Ask us anything." }) : null,
3410
3552
  t.messages.map((m) => /* @__PURE__ */ jsx4(Bubble, { m, text: t.streaming[m.id] ?? m.body, name: snap.config.name, palette: p, onArticle: (id) => void store.openArticle(id), onGuide: (k) => store.startGuide(k) }, m.id)),
3411
3553
  streamingIds.map((id) => /* @__PURE__ */ jsx4(Bubble, { m: { id, author: "scout", body: "", citations: [], suggestedGuides: [], status: "pending", createdAt: (/* @__PURE__ */ new Date()).toISOString() }, text: t.streaming[id] ?? "", name: snap.config.name, palette: p, onArticle: () => {
@@ -3477,7 +3619,7 @@ function Help({ store, client, palette: p }) {
3477
3619
  const audiences = client.getAudienceIds();
3478
3620
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
3479
3621
  const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
3480
- return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3622
+ return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3481
3623
  /* @__PURE__ */ jsx4(TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
3482
3624
  listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx4(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsxs3(Fragment, { children: [
3483
3625
  recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
@@ -3499,7 +3641,7 @@ function Help({ store, client, palette: p }) {
3499
3641
  }
3500
3642
  function Article({ article, palette: p, client }) {
3501
3643
  if (!article) return /* @__PURE__ */ jsx4(ActivityIndicator, { style: styles4.loading, color: p.muted });
3502
- return /* @__PURE__ */ jsxs3(ScrollView2, { contentContainerStyle: styles4.content, children: [
3644
+ return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, children: [
3503
3645
  /* @__PURE__ */ jsx4(Text4, { accessibilityRole: "header", style: [styles4.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
3504
3646
  /* @__PURE__ */ jsx4(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
3505
3647
  ] });
@@ -3562,7 +3704,7 @@ var styles4 = StyleSheet4.create({
3562
3704
 
3563
3705
  // src/ui/ResourceCenter.tsx
3564
3706
  import { useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
3565
- import { Modal as Modal2, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView3, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
3707
+ 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";
3566
3708
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3567
3709
  function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3568
3710
  const rc = client.getResourceCenter();
@@ -3616,16 +3758,16 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3616
3758
  const audiences = client.getAudienceIds();
3617
3759
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
3618
3760
  const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
3619
- return /* @__PURE__ */ jsx5(Modal2, { visible: open, animationType: "slide", presentationStyle: Platform3.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ jsxs4(View5, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: Platform3.OS === "ios" ? 8 : topInset }], children: [
3761
+ return /* @__PURE__ */ jsx5(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: [
3620
3762
  /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { borderColor: p.border }], children: [
3621
3763
  article ? /* @__PURE__ */ jsx5(Pressable5, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ jsx5(Text5, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
3622
3764
  /* @__PURE__ */ jsx5(Text5, { accessibilityRole: "header", style: [styles5.title, { color: p.text, fontFamily: p.fontFamily }], numberOfLines: 1, children: article ? "" : rc.title }),
3623
3765
  /* @__PURE__ */ jsx5(CloseButton, { palette: p, onPress: close, label: "Close help" })
3624
3766
  ] }),
3625
- article ? /* @__PURE__ */ jsxs4(ScrollView3, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], children: [
3767
+ article ? /* @__PURE__ */ jsxs4(ScrollView4, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], children: [
3626
3768
  /* @__PURE__ */ jsx5(Text5, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
3627
3769
  /* @__PURE__ */ jsx5(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
3628
- ] }) : /* @__PURE__ */ jsxs4(ScrollView3, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
3770
+ ] }) : /* @__PURE__ */ jsxs4(ScrollView4, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
3629
3771
  /* @__PURE__ */ jsx5(
3630
3772
  TextInput2,
3631
3773
  {
@@ -3930,7 +4072,7 @@ import {
3930
4072
  KeyboardAvoidingView,
3931
4073
  Platform as Platform4,
3932
4074
  Pressable as Pressable7,
3933
- ScrollView as ScrollView4,
4075
+ ScrollView as ScrollView5,
3934
4076
  StyleSheet as StyleSheet8,
3935
4077
  Text as Text7,
3936
4078
  TextInput as TextInput3,
@@ -4026,7 +4168,7 @@ function Survey({
4026
4168
  }
4027
4169
  ) : null,
4028
4170
  /* @__PURE__ */ jsx8(GButton, { label: "Done", palette: p, onPress: close, style: styles8.end })
4029
- ] }) : /* @__PURE__ */ jsxs7(ScrollView4, { contentContainerStyle: styles8.inner, keyboardShouldPersistTaps: "handled", children: [
4171
+ ] }) : /* @__PURE__ */ jsxs7(ScrollView5, { contentContainerStyle: styles8.inner, keyboardShouldPersistTaps: "handled", children: [
4030
4172
  config.questions.filter((q) => isQuestionVisible(q, answers)).map((q, i) => /* @__PURE__ */ jsxs7(View8, { style: styles8.question, children: [
4031
4173
  /* @__PURE__ */ jsxs7(View8, { style: styles8.head, children: [
4032
4174
  /* @__PURE__ */ jsxs7(Text7, { style: [styles8.qTitle, { color: p.text, fontFamily: p.fontFamily }], children: [