@guidelytech/react-native 0.11.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { SelectorStrategy, AutocaptureSettings, HeatmapPoint, MessengerConfig, MessengerMessage, MessengerConversation, SdkArticlesResponse, ResourceCenterArticle, PublishedGuide, ProjectTheme, GuideStep, SdkConfigResponse, ChecklistTask, SdkEvent, TargetingContext, StepButton, ResourceCenterConfig, RuleGroup, ResourceCenterAnnouncement, Placement } from '@guidelytech/shared';
3
+ import { SelectorStrategy, Placement, AutocaptureSettings, HeatmapPoint, MessengerConfig, MessengerMessage, MessengerConversation, SdkArticlesResponse, ResourceCenterArticle, PublishedGuide, ProjectTheme, GuideStep, SdkConfigResponse, ChecklistTask, SdkEvent, TargetingContext, StepButton, ResourceCenterConfig, RuleGroup, ResourceCenterAnnouncement } from '@guidelytech/shared';
4
4
  export { AutocaptureSettings, GuideStep, HeatmapPoint, PublishedGuide, SdkConfigResponse } from '@guidelytech/shared';
5
5
  import { FlagsClientOptions, FlagsClient, JsonValue, FlagValue, FlagDetails, FlagsBootstrap, Attributes } from '@guidelytech/flags';
6
6
  export { Attributes, EvaluationResult, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsPayload, InMemoryStickyBucketStore, JsonValue, createFlagsClient } from '@guidelytech/flags';
@@ -77,6 +77,41 @@ declare class TargetRegistry {
77
77
 
78
78
  type GuideStatus = "idle" | "running" | "waiting_url" | "waiting_target" | "ended";
79
79
 
80
+ type Side = "top" | "bottom" | "left" | "right";
81
+ interface Insets {
82
+ top: number;
83
+ bottom: number;
84
+ left: number;
85
+ right: number;
86
+ }
87
+ interface CardPosition {
88
+ x: number;
89
+ y: number;
90
+ side: Side;
91
+ /** Arrow centre along the card edge facing the target (px from the card's left/top). */
92
+ arrow: number;
93
+ }
94
+ /**
95
+ * Places a card of `size` next to `target` inside `viewport` minus `insets`, honouring the step's
96
+ * placement when it fits and flipping (then falling back to below/above) when it does not.
97
+ */
98
+ declare function positionCard(target: Rect, size: {
99
+ width: number;
100
+ height: number;
101
+ }, viewport: {
102
+ width: number;
103
+ height: number;
104
+ }, insets: Insets, placement: Placement, offset: number): CardPosition;
105
+
106
+ /**
107
+ * Where the visitor dragged the Scout bubble on this device: the side edge it snapped to and how
108
+ * high it sits, as a fraction of the allowed range (0 = the default bottom position, 1 = highest).
109
+ */
110
+ interface LauncherPlacement {
111
+ side: "left" | "right";
112
+ fraction: number;
113
+ }
114
+
80
115
  /** `autocapture` option: `false` turns every capability off, an object turns single ones off. */
81
116
  type AutocaptureOption = boolean | Partial<AutocaptureSettings>;
82
117
  /** The element a tap resolved to (or the view it hit), as `click` properties need it. */
@@ -443,7 +478,7 @@ interface UiState {
443
478
  }
444
479
 
445
480
  declare const SDK_NAME = "@guidelytech/react-native";
446
- declare const SDK_VERSION = "0.11.0";
481
+ declare const SDK_VERSION = "0.12.0";
447
482
  declare const DEFAULT_API_URL = "https://guidelytech.com";
448
483
  /** Host-platform services the engine needs; the provider supplies React Native implementations. */
449
484
  interface PlatformBridge {
@@ -729,6 +764,10 @@ declare class GuidelyClient {
729
764
  private syncMessenger;
730
765
  /** The Scout messenger store when Scout is on for this platform (rendered by the overlay). */
731
766
  getMessenger(): Messenger | null;
767
+ /** Where the visitor dragged the messenger bubble on this device, or null to use the configured spot. */
768
+ getMessengerLauncherPlacement(): LauncherPlacement | null;
769
+ /** Remembers the bubble's spot on this device (in memory only when no `storage` was passed). */
770
+ setMessengerLauncherPlacement(placement: LauncherPlacement): void;
732
771
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
733
772
  openMessenger(tab?: MessengerTab): void;
734
773
  closeMessenger(): void;
@@ -742,6 +781,8 @@ declare class GuidelyClient {
742
781
  getFeatureDetails<T extends JsonValue>(key: string, fallback: T): FlagDetails<FlagValue<T>>;
743
782
  /** Calls `cb` whenever flag values may have changed. Returns an unsubscribe function. */
744
783
  onFlagsChange(cb: () => void): () => void;
784
+ /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
785
+ private deviceProperties;
745
786
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
746
787
  private flagContext;
747
788
  private syncFlagAttributes;
@@ -792,32 +833,6 @@ declare class GuidelyClient {
792
833
  private log;
793
834
  }
794
835
 
795
- type Side = "top" | "bottom" | "left" | "right";
796
- interface Insets {
797
- top: number;
798
- bottom: number;
799
- left: number;
800
- right: number;
801
- }
802
- interface CardPosition {
803
- x: number;
804
- y: number;
805
- side: Side;
806
- /** Arrow centre along the card edge facing the target (px from the card's left/top). */
807
- arrow: number;
808
- }
809
- /**
810
- * Places a card of `size` next to `target` inside `viewport` minus `insets`, honouring the step's
811
- * placement when it fits and flipping (then falling back to below/above) when it does not.
812
- */
813
- declare function positionCard(target: Rect, size: {
814
- width: number;
815
- height: number;
816
- }, viewport: {
817
- width: number;
818
- height: number;
819
- }, insets: Insets, placement: Placement, offset: number): CardPosition;
820
-
821
836
  /**
822
837
  * Declares which screen its children are on, so targets on screens that are mounted but not
823
838
  * visible (inactive tabs, screens under a stack) are never used. `name` must match what you pass
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { SelectorStrategy, AutocaptureSettings, HeatmapPoint, MessengerConfig, MessengerMessage, MessengerConversation, SdkArticlesResponse, ResourceCenterArticle, PublishedGuide, ProjectTheme, GuideStep, SdkConfigResponse, ChecklistTask, SdkEvent, TargetingContext, StepButton, ResourceCenterConfig, RuleGroup, ResourceCenterAnnouncement, Placement } from '@guidelytech/shared';
3
+ import { SelectorStrategy, Placement, AutocaptureSettings, HeatmapPoint, MessengerConfig, MessengerMessage, MessengerConversation, SdkArticlesResponse, ResourceCenterArticle, PublishedGuide, ProjectTheme, GuideStep, SdkConfigResponse, ChecklistTask, SdkEvent, TargetingContext, StepButton, ResourceCenterConfig, RuleGroup, ResourceCenterAnnouncement } from '@guidelytech/shared';
4
4
  export { AutocaptureSettings, GuideStep, HeatmapPoint, PublishedGuide, SdkConfigResponse } from '@guidelytech/shared';
5
5
  import { FlagsClientOptions, FlagsClient, JsonValue, FlagValue, FlagDetails, FlagsBootstrap, Attributes } from '@guidelytech/flags';
6
6
  export { Attributes, EvaluationResult, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsPayload, InMemoryStickyBucketStore, JsonValue, createFlagsClient } from '@guidelytech/flags';
@@ -77,6 +77,41 @@ declare class TargetRegistry {
77
77
 
78
78
  type GuideStatus = "idle" | "running" | "waiting_url" | "waiting_target" | "ended";
79
79
 
80
+ type Side = "top" | "bottom" | "left" | "right";
81
+ interface Insets {
82
+ top: number;
83
+ bottom: number;
84
+ left: number;
85
+ right: number;
86
+ }
87
+ interface CardPosition {
88
+ x: number;
89
+ y: number;
90
+ side: Side;
91
+ /** Arrow centre along the card edge facing the target (px from the card's left/top). */
92
+ arrow: number;
93
+ }
94
+ /**
95
+ * Places a card of `size` next to `target` inside `viewport` minus `insets`, honouring the step's
96
+ * placement when it fits and flipping (then falling back to below/above) when it does not.
97
+ */
98
+ declare function positionCard(target: Rect, size: {
99
+ width: number;
100
+ height: number;
101
+ }, viewport: {
102
+ width: number;
103
+ height: number;
104
+ }, insets: Insets, placement: Placement, offset: number): CardPosition;
105
+
106
+ /**
107
+ * Where the visitor dragged the Scout bubble on this device: the side edge it snapped to and how
108
+ * high it sits, as a fraction of the allowed range (0 = the default bottom position, 1 = highest).
109
+ */
110
+ interface LauncherPlacement {
111
+ side: "left" | "right";
112
+ fraction: number;
113
+ }
114
+
80
115
  /** `autocapture` option: `false` turns every capability off, an object turns single ones off. */
81
116
  type AutocaptureOption = boolean | Partial<AutocaptureSettings>;
82
117
  /** The element a tap resolved to (or the view it hit), as `click` properties need it. */
@@ -443,7 +478,7 @@ interface UiState {
443
478
  }
444
479
 
445
480
  declare const SDK_NAME = "@guidelytech/react-native";
446
- declare const SDK_VERSION = "0.11.0";
481
+ declare const SDK_VERSION = "0.12.0";
447
482
  declare const DEFAULT_API_URL = "https://guidelytech.com";
448
483
  /** Host-platform services the engine needs; the provider supplies React Native implementations. */
449
484
  interface PlatformBridge {
@@ -729,6 +764,10 @@ declare class GuidelyClient {
729
764
  private syncMessenger;
730
765
  /** The Scout messenger store when Scout is on for this platform (rendered by the overlay). */
731
766
  getMessenger(): Messenger | null;
767
+ /** Where the visitor dragged the messenger bubble on this device, or null to use the configured spot. */
768
+ getMessengerLauncherPlacement(): LauncherPlacement | null;
769
+ /** Remembers the bubble's spot on this device (in memory only when no `storage` was passed). */
770
+ setMessengerLauncherPlacement(placement: LauncherPlacement): void;
732
771
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
733
772
  openMessenger(tab?: MessengerTab): void;
734
773
  closeMessenger(): void;
@@ -742,6 +781,8 @@ declare class GuidelyClient {
742
781
  getFeatureDetails<T extends JsonValue>(key: string, fallback: T): FlagDetails<FlagValue<T>>;
743
782
  /** Calls `cb` whenever flag values may have changed. Returns an unsubscribe function. */
744
783
  onFlagsChange(cb: () => void): () => void;
784
+ /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
785
+ private deviceProperties;
745
786
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
746
787
  private flagContext;
747
788
  private syncFlagAttributes;
@@ -792,32 +833,6 @@ declare class GuidelyClient {
792
833
  private log;
793
834
  }
794
835
 
795
- type Side = "top" | "bottom" | "left" | "right";
796
- interface Insets {
797
- top: number;
798
- bottom: number;
799
- left: number;
800
- right: number;
801
- }
802
- interface CardPosition {
803
- x: number;
804
- y: number;
805
- side: Side;
806
- /** Arrow centre along the card edge facing the target (px from the card's left/top). */
807
- arrow: number;
808
- }
809
- /**
810
- * Places a card of `size` next to `target` inside `viewport` minus `insets`, honouring the step's
811
- * placement when it fits and flipping (then falling back to below/above) when it does not.
812
- */
813
- declare function positionCard(target: Rect, size: {
814
- width: number;
815
- height: number;
816
- }, viewport: {
817
- width: number;
818
- height: number;
819
- }, insets: Insets, placement: Placement, offset: number): CardPosition;
820
-
821
836
  /**
822
837
  * Declares which screen its children are on, so targets on screens that are mounted but not
823
838
  * visible (inactive tabs, screens under a stack) are never used. `name` must match what you pass
package/dist/index.js CHANGED
@@ -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-TGIUEYXM.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++) {
@@ -406,7 +406,7 @@ function sanitizeProperties(props) {
406
406
  }
407
407
  return out;
408
408
  }
409
- var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
409
+ var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
410
410
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
411
411
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
412
412
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
@@ -701,7 +701,7 @@ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
701
701
  };
702
702
  }
703
703
 
704
- // ../sdk-core/dist/chunk-JUSM7J5N.js
704
+ // ../sdk-core/dist/chunk-XML74K2P.js
705
705
  var __defProp2 = Object.defineProperty;
706
706
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
707
707
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1010,10 +1010,12 @@ var KEYS = {
1010
1010
  /** Messenger conversations this browser can read: [{ id, token }]. */
1011
1011
  conversations: (key) => `guidely:conversations:${key}`,
1012
1012
  /** Announcement ids opened in the messenger's News tab. */
1013
- 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}`
1014
1016
  };
1015
1017
 
1016
- // ../sdk-core/dist/chunk-ZAF2KRJA.js
1018
+ // ../sdk-core/dist/chunk-2E2YUQAG.js
1017
1019
  var EventQueue = class {
1018
1020
  constructor(opts) {
1019
1021
  __publicField2(this, "opts", opts);
@@ -1323,6 +1325,49 @@ function dismissedAfterMerge(local, server) {
1323
1325
  // src/core/client.ts
1324
1326
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
1325
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
+
1326
1371
  // src/core/autocapture.ts
1327
1372
  var TAP_SLOP = 10;
1328
1373
  var TAP_MAX_MS = 1e3;
@@ -2436,7 +2481,7 @@ var PersistentStore = class {
2436
2481
 
2437
2482
  // src/core/client.ts
2438
2483
  var SDK_NAME = "@guidelytech/react-native";
2439
- var SDK_VERSION = "0.11.0";
2484
+ var SDK_VERSION = "0.12.0";
2440
2485
  var DEFAULT_API_URL = "https://guidelytech.com";
2441
2486
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2442
2487
  var CACHED_START_MS = 1500;
@@ -2602,7 +2647,7 @@ var GuidelyClient = class {
2602
2647
  async bootstrap() {
2603
2648
  const key = this.options.projectId;
2604
2649
  try {
2605
- 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)]);
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]);
2606
2651
  if (this.destroyed) return;
2607
2652
  this.anonymousId = this.store.get(KEYS.anon) || "";
2608
2653
  this.hadIdentity = !!this.anonymousId;
@@ -2963,7 +3008,7 @@ var GuidelyClient = class {
2963
3008
  if (changed) {
2964
3009
  this.screenStartedAt = Date.now();
2965
3010
  this.screenLeft = this.backgroundedAt !== null;
2966
- if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
3011
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, ...this.deviceProperties() } });
2967
3012
  }
2968
3013
  this.completeChecklistTasksBy("url_visited", url);
2969
3014
  this.active?.controller.resume();
@@ -3128,7 +3173,7 @@ var GuidelyClient = class {
3128
3173
  if (this.screenName && this.screenLeft) {
3129
3174
  this.screenStartedAt = Date.now();
3130
3175
  this.screenLeft = false;
3131
- if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3176
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, ...this.deviceProperties(), resumed: true } });
3132
3177
  }
3133
3178
  } catch (err) {
3134
3179
  this.reportError(err);
@@ -3890,6 +3935,14 @@ var GuidelyClient = class {
3890
3935
  getMessenger() {
3891
3936
  return this.messenger;
3892
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
+ }
3893
3946
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
3894
3947
  openMessenger(tab) {
3895
3948
  this.whenHydrated(() => this.messenger?.open(tab));
@@ -3920,6 +3973,11 @@ var GuidelyClient = class {
3920
3973
  onFlagsChange(cb) {
3921
3974
  return this.flags.subscribe(cb);
3922
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
+ }
3923
3981
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
3924
3982
  flagContext() {
3925
3983
  const d = this.bridge.device;
@@ -4718,8 +4776,8 @@ var styles3 = StyleSheet3.create({
4718
4776
  });
4719
4777
 
4720
4778
  // src/ui/Messenger.tsx
4721
- import { Fragment as Fragment2, useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
4722
- 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";
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";
4723
4781
  import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
4724
4782
  function useMessenger(store) {
4725
4783
  return useSyncExternalStore2(
@@ -4748,14 +4806,89 @@ function ago(iso) {
4748
4806
  if (s < 86400) return `${Math.floor(s / 3600)}h`;
4749
4807
  return `${Math.floor(s / 86400)}d`;
4750
4808
  }
4751
- 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 }) {
4752
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]);
4753
4886
  if (snap.open) return null;
4754
4887
  const label = snap.unread ? `Open ${snap.config.name}, ${snap.unread} unread` : `Open ${snap.config.name}`;
4755
- 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: [
4756
4889
  /* @__PURE__ */ jsx5(Text4, { style: [styles4.launcherIcon, { color: p.onPrimary }], children: "\u{1F4AC}" }),
4757
4890
  snap.unread ? /* @__PURE__ */ jsx5(View4, { style: styles4.badge, children: /* @__PURE__ */ jsx5(Text4, { style: styles4.badgeText, children: snap.unread }) }) : null
4758
- ] });
4891
+ ] }) });
4759
4892
  }
4760
4893
  function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
4761
4894
  const snap = useMessenger(store);
@@ -5000,7 +5133,8 @@ function Article({ article, palette: p, client }) {
5000
5133
  }
5001
5134
  var styles4 = StyleSheet4.create({
5002
5135
  flex: { flex: 1 },
5003
- 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 } },
5004
5138
  launcherIcon: { fontSize: 24 },
5005
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" },
5006
5140
  badgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" },
@@ -5210,7 +5344,7 @@ var styles5 = StyleSheet5.create({
5210
5344
 
5211
5345
  // src/ui/StepLayer.tsx
5212
5346
  import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
5213
- 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";
5214
5348
 
5215
5349
  // src/core/position.ts
5216
5350
  var MARGIN = 12;
@@ -5233,20 +5367,20 @@ function positionCard(target, size, viewport, insets, placement, offset) {
5233
5367
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
5234
5368
  const space = { top: target.y - insets.top, bottom: viewport.height - insets.bottom - (target.y + target.height) };
5235
5369
  const side = order.find(fits) ?? (space.bottom >= space.top ? "bottom" : "top");
5236
- 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));
5237
5371
  if (side === "top" || side === "bottom") {
5238
5372
  const cx = target.x + target.width / 2;
5239
5373
  let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
5240
- 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));
5241
5375
  let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
5242
- y2 = clamp2(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5243
- 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) };
5244
5378
  }
5245
5379
  const cy = target.y + target.height / 2;
5246
5380
  let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
5247
- 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));
5248
5382
  const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
5249
- 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) };
5250
5384
  }
5251
5385
 
5252
5386
  // src/ui/StepCard.tsx
@@ -5342,10 +5476,10 @@ function useTargetRect(client, targetId, origin) {
5342
5476
  return rect;
5343
5477
  }
5344
5478
  function useFadeIn(key) {
5345
- const opacity = useRef3(new Animated.Value(0)).current;
5479
+ const opacity = useRef3(new Animated2.Value(0)).current;
5346
5480
  useEffect5(() => {
5347
5481
  opacity.setValue(0);
5348
- 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();
5349
5483
  }, [key, opacity]);
5350
5484
  return opacity;
5351
5485
  }
@@ -5357,22 +5491,22 @@ function StepLayer({ view, client, palette: p, geometry }) {
5357
5491
  const cardWidth = Math.min(viewport.width - 32, step.width ?? 340);
5358
5492
  useEffect5(() => {
5359
5493
  if (step.type === "hotspot" && !view.expanded) return;
5360
- AccessibilityInfo.announceForAccessibility?.(announcement(view));
5494
+ AccessibilityInfo2.announceForAccessibility?.(announcement(view));
5361
5495
  }, [view.key, view.expanded]);
5362
5496
  const backdrop = (children) => /* @__PURE__ */ jsx8(Pressable6, { accessible: false, style: [StyleSheet7.absoluteFill, { backgroundColor: p.overlay }], onPress: () => client.backdropPressed(view.guide.id), children });
5363
5497
  const anchored = !view.asModal && ["tooltip", "popover", "spotlight", "hotspot"].includes(step.type);
5364
5498
  if (!anchored && step.type === "banner") {
5365
5499
  const top = step.position.startsWith("top");
5366
- 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 }) });
5367
5501
  }
5368
5502
  if (!anchored && step.type === "slideout") {
5369
5503
  const wide = viewport.width >= 600;
5370
5504
  const right = !step.position.endsWith("left");
5371
5505
  const width = wide ? Math.min(380, cardWidth) : viewport.width - insets.left - insets.right - 24;
5372
- 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 }) });
5373
5507
  }
5374
5508
  if (!anchored) {
5375
- 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: [
5376
5510
  step.showOverlay ? backdrop() : null,
5377
5511
  /* @__PURE__ */ jsx8(StepCard, { view, client, palette: p, width: Math.min(viewport.width - 40, step.width ?? 360) })
5378
5512
  ] });
@@ -5397,7 +5531,7 @@ function StepLayer({ view, client, palette: p, geometry }) {
5397
5531
  [hole.x, hole.y + hole.height - r, -r, -2 * r],
5398
5532
  [hole.x + hole.width - r, hole.y + hole.height - r, -2 * r, -2 * r]
5399
5533
  ];
5400
- 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: [
5401
5535
  /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: 0, height: Math.max(0, hole.y) }] }),
5402
5536
  /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: hole.y + hole.height, bottom: 0 }] }),
5403
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 }] }),
@@ -5407,7 +5541,7 @@ function StepLayer({ view, client, palette: p, geometry }) {
5407
5541
  /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect: hole, width: cardWidth })
5408
5542
  ] });
5409
5543
  }
5410
- 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: [
5411
5545
  step.showOverlay ? backdrop() : null,
5412
5546
  /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth })
5413
5547
  ] });
@@ -5429,11 +5563,11 @@ function Arrow({ side, at, color, border }) {
5429
5563
  return /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
5430
5564
  }
5431
5565
  function Hotspot({ x, y, size, pulse, color, onPress, label }) {
5432
- const scale = useRef3(new Animated.Value(0)).current;
5566
+ const scale = useRef3(new Animated2.Value(0)).current;
5433
5567
  const [reduceMotion, setReduceMotion] = useState5(false);
5434
5568
  useEffect5(() => {
5435
5569
  let alive = true;
5436
- AccessibilityInfo.isReduceMotionEnabled?.().then((v) => alive && setReduceMotion(v), () => {
5570
+ AccessibilityInfo2.isReduceMotionEnabled?.().then((v) => alive && setReduceMotion(v), () => {
5437
5571
  });
5438
5572
  return () => {
5439
5573
  alive = false;
@@ -5441,13 +5575,13 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
5441
5575
  }, []);
5442
5576
  useEffect5(() => {
5443
5577
  if (!pulse || reduceMotion) return;
5444
- 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 }));
5445
5579
  loop.start();
5446
5580
  return () => loop.stop();
5447
5581
  }, [pulse, reduceMotion, scale]);
5448
5582
  return /* @__PURE__ */ jsxs6(Pressable6, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
5449
5583
  pulse && !reduceMotion ? /* @__PURE__ */ jsx8(
5450
- Animated.View,
5584
+ Animated2.View,
5451
5585
  {
5452
5586
  pointerEvents: "none",
5453
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] }) }] }]
@@ -5466,7 +5600,7 @@ var styles7 = StyleSheet7.create({
5466
5600
  // src/ui/Survey.tsx
5467
5601
  import { useEffect as useEffect6, useState as useState6 } from "react";
5468
5602
  import {
5469
- AccessibilityInfo as AccessibilityInfo2,
5603
+ AccessibilityInfo as AccessibilityInfo3,
5470
5604
  KeyboardAvoidingView,
5471
5605
  Platform as Platform4,
5472
5606
  Pressable as Pressable7,
@@ -5489,7 +5623,7 @@ function Survey({
5489
5623
  useEffect6(() => {
5490
5624
  setAnswers({});
5491
5625
  setError("");
5492
- AccessibilityInfo2.announceForAccessibility?.(
5626
+ AccessibilityInfo3.announceForAccessibility?.(
5493
5627
  `Survey: ${config?.questions[0]?.title ?? view.guide.name}`
5494
5628
  );
5495
5629
  }, [view.key]);
@@ -5872,6 +6006,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5872
6006
  const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5873
6007
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5874
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);
5875
6010
  return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
5876
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)),
5877
6012
  showRcLauncher ? /* @__PURE__ */ jsxs8(
@@ -5887,7 +6022,17 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5887
6022
  ]
5888
6023
  }
5889
6024
  ) : null,
5890
- 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,
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"
6034
+ }
6035
+ ) : null,
5891
6036
  ui.survey ? /* @__PURE__ */ jsx10(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
5892
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)),
5893
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 })