@wireai/activation 0.14.1 → 0.14.3

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/CHANGELOG.md CHANGED
@@ -3,6 +3,103 @@
3
3
  All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
4
4
  Historical entries below the rename keep the old package name on purpose.
5
5
 
6
+ ## [0.14.3] - 2026-08-18
7
+
8
+ **PATCH: no public surface gained, lost or renamed a member.** `SpotlightOverlay`, `GestureHint`
9
+ and `FeatureShowcase` keep their props and their exports; `react-native-reanimated` and
10
+ `@blazejkustra/react-native-onboarding` stay OPTIONAL peers. This finishes the class 0.14.2 started
11
+ with `expo-blur`: **no source under `src/` statically value-imports an optional peer any more.**
12
+
13
+ ### Fixed
14
+
15
+ - **The coachmarks subpath no longer force-imports its optional peer `react-native-reanimated`.**
16
+ `SpotlightOverlay` and `GestureHint` both did `import Animated, { … } from
17
+ "react-native-reanimated"` at module scope, and `coachmarks/index.ts` re-exports them with no
18
+ wildcard escape — so importing ANYTHING from `@wireai/activation/coachmarks`, even
19
+ `setCoachmarkStorage`, dragged the peer in, and `@wireai/activation/showcase` inherited the same
20
+ edge through `GestureHint`. A host that took "optional" at its word hit a Metro resolution
21
+ failure. The peer is now resolved through a guarded lazy require with a string-literal specifier
22
+ inside a try/catch (`src/coachmarks/reanimated.ts`, the same shape as `icons/expoIcons.ts` /
23
+ `coachmarks/expoBlur.ts`), so neither subpath carries a static top-level import of it.
24
+ - **Degradation, by design:** without the peer both surfaces render STATICALLY at their resting
25
+ frame. The spotlight keeps its ring, tooltip, copy, scrim/frost and gestures, with no fade-in
26
+ and no pulse; the gesture glyph keeps its capsules, ring, dot and motion track, at full opacity
27
+ and no travel. Nothing moves; everything renders. Reduce Motion is still honoured — the kit's
28
+ own `AccessibilityInfo`-backed hook stands in for the peer's `useReducedMotion`.
29
+ - The accessor re-exports the peer's own names (`useAnimatedStyle`, `useAnimatedProps`,
30
+ `withTiming`, …) and the call sites keep calling them unqualified, because reanimated's Babel
31
+ plugin decides what to workletize by the callee NAME. Hosts that HAVE the peer therefore get
32
+ byte-for-byte the same worklets, on the UI thread, as before.
33
+
34
+ - **The showcase subpath no longer force-imports its optional peer
35
+ `@blazejkustra/react-native-onboarding`.** `FeatureShowcase` imported the pager's default export
36
+ at module scope, so importing anything from `@wireai/activation/showcase` — even
37
+ `selectShowcaseSlides` — required a peer npm never installs. It is now resolved through a guarded
38
+ lazy require (`src/showcase/blazejOnboarding.ts`); only the TYPES are still imported statically,
39
+ which every toolchain erases.
40
+ - **Degradation, by design:** absent, the showcase renders null and calls `onDone` from an effect
41
+ — the SAME path the feature kill switch already takes — so the host's flow always advances
42
+ rather than stranding the user on a blank screen. The seen gate is deliberately NOT written, so
43
+ installing the peer later still plays the deck once.
44
+
45
+ - **A rule file prescribed the inverted optional-require contract, for the second time.**
46
+ `ai_rules/context_map.md` told the next author that the `expoIcons` callee "MUST be a local `req`,
47
+ never a literal `require`" — verbatim the `const req = require; req(name)` shape that
48
+ `icons/expoIcons.ts` autopsies as the 0.8.0 red-box bug (aliasing hides the call from Metro's
49
+ static collector, so the module never enters the bundle). 0.14.2 corrected this claim in
50
+ `frequent_rules.md`; this was its second copy, and it was still live. Rewritten to the one correct
51
+ contract, citing `src/icons/expoIcons.ts:98`.
52
+
53
+ ### Changed
54
+
55
+ - **`metro/index.js` `OPTIONAL_MODULES` now lists both modules**, and `tsup.config.ts` drops them
56
+ from its static-peer list (`external` is derived from the union, so they stay external, and the
57
+ CJS keep-literal-require hook now covers them). The helper's "a statically-imported module must NOT be
58
+ stubbed" note named exactly these two; it is rewritten, because the kit now has NO
59
+ statically-imported optional peer — everything it imports statically is a REQUIRED peer whose
60
+ absence must stay a hard build error.
61
+ - **Docs that promised the old behaviour are corrected in the same wave**, both directions: the
62
+ coachmarks and showcase barrel docs and the README said importing a subpath "pulls in" these
63
+ peers (no longer true), while the README/AGENTS peer tables already claimed every optional peer
64
+ "sits behind a guarded lazy require" (not true until now).
65
+
66
+ ### Added
67
+
68
+ - **A whole-class ratchet in `test/canary/metroResolution.test.tsx`:** every file under `src/` is
69
+ scanned for a static value-import of ANY module in `OPTIONAL_MODULES`, with a positive control
70
+ proving the scanner catches default, named, wrapped multi-line, bare and subpath imports while
71
+ ignoring `import type` and the guarded require itself. Each optional peer so far has arrived as
72
+ the same defect; a new one now inherits the check for free. Plus per-module collection tests
73
+ (Metro marks both new specifiers `isOptional`) and component-level canaries for both peers in
74
+ both directions, including the absent-reanimated/present-blur combination that pins the frost at
75
+ full intensity.
76
+
77
+ ## [0.14.2] - 2026-08-18
78
+
79
+ **PATCH: no public surface gained, lost or renamed a member.** `SpotlightOverlay` keeps its props
80
+ and its export; `expo-blur` stays an OPTIONAL peer.
81
+
82
+ ### Fixed
83
+
84
+ - **The coachmarks subpath no longer force-imports its optional peer `expo-blur`.**
85
+ `SpotlightOverlay` did `import { BlurView } from "expo-blur"` at module scope and built its
86
+ animated blur component at module load, and `coachmarks/index.ts` re-exports the overlay with no
87
+ wildcard escape — so importing ANYTHING from `@wireai/activation/coachmarks`, even
88
+ `setCoachmarkStorage`, dragged `expo-blur` in. A host that took the peer's "optional" at its word
89
+ and skipped installing it hit a Metro resolution failure the moment it touched coachmarks, which
90
+ is why the AI Mobile Launcher tiers (and any Expo Go build) could not adopt the capability. The
91
+ peer is now resolved through a guarded lazy require with a string-literal specifier inside a
92
+ try/catch (the same shape as `icons/expoIcons.ts` / `device/appVersion.ts`), and the animated
93
+ component is built once on first render, so the subpath carries no static top-level import of the
94
+ peer.
95
+ - **Degradation, by design:** when `expo-blur` is absent the spotlight drops the frost to an
96
+ equivalent dimmed scrim. The ring, tooltip and gestures behave identically — a missing
97
+ decorative frost never blanks a tour, never crashes, never renders empty.
98
+ - `metro/index.js` `OPTIONAL_MODULES` and `tsup.config.ts` now treat `expo-blur` as a guarded
99
+ optional peer (moved off the static-peer list), so the built `dist/` collects it as optional and
100
+ the metro helper's empty-module safety net covers it for hosts that disable Metro's
101
+ `allowOptionalDependencies`.
102
+
6
103
  ## [0.14.1] - 2026-08-18
7
104
 
8
105
  Two rulings from the 0.14.0 audit, applied. Both were reproduced there and left for a decision
package/README.md CHANGED
@@ -801,8 +801,12 @@ consuming app, since the kit has no local TS install).
801
801
 
802
802
  The kit ships a performance-first guided-tour engine on a **subpath** so the main
803
803
  barrel stays dependency-free — import it from `@wireai/activation/coachmarks`. It
804
- pulls in two **optional** peers only when you use it: `react-native-reanimated`
805
- (UI-thread gesture + ring animation) and `expo-blur` (the frosted spotlight).
804
+ can USE two **optional** peers, and needs neither: `react-native-reanimated`
805
+ (UI-thread gesture + ring animation) and `expo-blur` (the frosted spotlight). Both
806
+ sit behind a guarded lazy require, so importing the subpath never pulls them into
807
+ your bundle. Without reanimated the tour renders statically (the ring, tooltip,
808
+ copy and gestures are unchanged — nothing animates); without expo-blur the frost
809
+ becomes a plain dimmed scrim.
806
810
  Source consumers get it through the existing `@wireai/activation/*` tsconfig path
807
811
  mapping; installed apps resolve the subpath export.
808
812
 
@@ -922,6 +926,12 @@ theme colors, and gates once through the same storage as the tours
922
926
  (`wire_showcase_<id>_seen`; `isTestingCoachmark` bypasses it). If already seen it
923
927
  renders nothing and calls `onDone` from an effect.
924
928
 
929
+ The pager is a guarded lazy require, so importing `@wireai/activation/showcase`
930
+ does not pull it into your bundle. If you never install it the showcase renders
931
+ nothing and calls `onDone` — your flow advances exactly as if the deck had been
932
+ seen — and the seen gate is NOT written, so installing the peer later still plays
933
+ the deck once.
934
+
925
935
  **Recommended placement (v0.3.0+): AFTER `onComplete`, as a personalized value
926
936
  bridge.** The user just told you what they want in `result.answers`, so the
927
937
  terminal recap is the highest-value slot to show the 2-3 slides that answer it,
@@ -245,10 +245,18 @@ declare const selectTourSteps: <T extends {
245
245
  }>(catalog: T[], selection?: string[]) => T[];
246
246
 
247
247
  /**
248
- * Abstract gesture pictogram shown beside a coachmark tooltip. Pure reanimated
249
- * every animation runs on the UI thread (shared values + worklets), never
250
- * setState, so it costs nothing on the JS thread. Honors Reduce Motion by
251
- * dropping the motion loop and rendering the glyph static.
248
+ * Abstract gesture pictogram shown beside a coachmark tooltip. With the OPTIONAL
249
+ * `react-native-reanimated` peer installed every animation runs on the UI thread
250
+ * (shared values + worklets), never setState, so it costs nothing on the JS
251
+ * thread. Honors Reduce Motion by dropping the motion loop and rendering the
252
+ * glyph static.
253
+ *
254
+ * The peer is reached through the guarded lazy require in `reanimated.ts`, never
255
+ * a static import, so `@wireai/activation/coachmarks` (and `/showcase`, which
256
+ * reaches this component through FeatureShowcase) builds on a host that never
257
+ * installed it. Absent, the glyph renders STATICALLY at its resting frame — the
258
+ * same capsules, ring, dot and motion track, at full opacity and no travel, with
259
+ * one render instead of a UI-thread loop. The pictogram never disappears.
252
260
  *
253
261
  * NO drawn-hand anatomy. Swipes render the standard two-finger swipe glyph: two
254
262
  * rounded vertical capsules (an abstract index + middle finger, slightly
@@ -245,10 +245,18 @@ declare const selectTourSteps: <T extends {
245
245
  }>(catalog: T[], selection?: string[]) => T[];
246
246
 
247
247
  /**
248
- * Abstract gesture pictogram shown beside a coachmark tooltip. Pure reanimated
249
- * every animation runs on the UI thread (shared values + worklets), never
250
- * setState, so it costs nothing on the JS thread. Honors Reduce Motion by
251
- * dropping the motion loop and rendering the glyph static.
248
+ * Abstract gesture pictogram shown beside a coachmark tooltip. With the OPTIONAL
249
+ * `react-native-reanimated` peer installed every animation runs on the UI thread
250
+ * (shared values + worklets), never setState, so it costs nothing on the JS
251
+ * thread. Honors Reduce Motion by dropping the motion loop and rendering the
252
+ * glyph static.
253
+ *
254
+ * The peer is reached through the guarded lazy require in `reanimated.ts`, never
255
+ * a static import, so `@wireai/activation/coachmarks` (and `/showcase`, which
256
+ * reaches this component through FeatureShowcase) builds on a host that never
257
+ * installed it. Absent, the glyph renders STATICALLY at its resting frame — the
258
+ * same capsules, ring, dot and motion track, at full opacity and no travel, with
259
+ * one render instead of a UI-thread loop. The pictogram never disappears.
252
260
  *
253
261
  * NO drawn-hand anatomy. Swipes render the standard two-finger swipe glyph: two
254
262
  * rounded vertical capsules (an abstract index + middle finger, slightly
@@ -2,16 +2,18 @@
2
2
 
3
3
  var React3 = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
- var expoBlur = require('expo-blur');
6
5
  var reactNative = require('react-native');
7
- var Animated = require('react-native-reanimated');
8
6
 
9
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
8
 
11
9
  var React3__default = /*#__PURE__*/_interopDefault(React3);
12
- var Animated__default = /*#__PURE__*/_interopDefault(Animated);
13
10
 
14
- // src/coachmarks/CoachmarkProvider.tsx
11
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
+ }) : x)(function(x) {
14
+ if (typeof require !== "undefined") return require.apply(this, arguments);
15
+ throw Error('Dynamic require of "' + x + '" is not supported');
16
+ });
15
17
 
16
18
  // src/features/defaults.ts
17
19
  var defaultWireFeatures = Object.freeze({
@@ -90,8 +92,8 @@ var DEFAULT_TIMEOUT_MS = 4e3;
90
92
  var failOpen = async (storage, key) => {
91
93
  var _a;
92
94
  if (!storage) return defaultWireFeatures;
93
- const cached = await readCachedFeatures(storage, key);
94
- return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
95
+ const cached3 = await readCachedFeatures(storage, key);
96
+ return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
95
97
  };
96
98
  var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
97
99
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
@@ -329,6 +331,198 @@ var useOnboardingTheme = () => {
329
331
  const ctx = React3.useContext(OnboardingThemeContext);
330
332
  return ctx != null ? ctx : mergeTheme();
331
333
  };
334
+
335
+ // src/coachmarks/expoBlur.ts
336
+ var runtimeRequire = (moduleName) => {
337
+ if (moduleName !== "expo-blur") return void 0;
338
+ if (typeof __require !== "function") return void 0;
339
+ try {
340
+ return require("expo-blur");
341
+ } catch {
342
+ return void 0;
343
+ }
344
+ };
345
+ var interop = (mod) => {
346
+ if (!mod || typeof mod !== "object") return void 0;
347
+ const ns = mod;
348
+ if (ns.BlurView) return ns;
349
+ const def = mod.default;
350
+ if (def && typeof def === "object") return def;
351
+ return ns;
352
+ };
353
+ var asComponent = (value) => {
354
+ if (typeof value === "function") return value;
355
+ if (value && typeof value === "object" && "$$typeof" in value) {
356
+ return value;
357
+ }
358
+ return void 0;
359
+ };
360
+ var cached;
361
+ var resolveBlurView = (requireModule = runtimeRequire) => {
362
+ try {
363
+ if (cached === void 0 || requireModule !== runtimeRequire) {
364
+ const resolved = interop(requireModule("expo-blur"));
365
+ const component = resolved ? asComponent(resolved.BlurView) : void 0;
366
+ if (requireModule === runtimeRequire) cached = component != null ? component : null;
367
+ return component;
368
+ }
369
+ if (cached === null) return void 0;
370
+ return cached;
371
+ } catch {
372
+ return void 0;
373
+ }
374
+ };
375
+ var lastKnown = false;
376
+ var useReducedMotion = () => {
377
+ const [reduced, setReduced] = React3.useState(lastKnown);
378
+ React3.useEffect(() => {
379
+ let mounted = true;
380
+ reactNative.AccessibilityInfo.isReduceMotionEnabled().then((value) => {
381
+ lastKnown = value;
382
+ if (mounted) setReduced(value);
383
+ }).catch(() => {
384
+ });
385
+ const sub = reactNative.AccessibilityInfo.addEventListener("reduceMotionChanged", (value) => {
386
+ lastKnown = value;
387
+ setReduced(value);
388
+ });
389
+ return () => {
390
+ mounted = false;
391
+ sub == null ? void 0 : sub.remove();
392
+ };
393
+ }, []);
394
+ return reduced;
395
+ };
396
+
397
+ // src/coachmarks/reanimated.ts
398
+ var runtimeRequire2 = (moduleName) => {
399
+ if (moduleName !== "react-native-reanimated") return void 0;
400
+ if (typeof __require !== "function") return void 0;
401
+ try {
402
+ return require("react-native-reanimated");
403
+ } catch {
404
+ return void 0;
405
+ }
406
+ };
407
+ var useStaticSharedValue = (initial) => {
408
+ const [, bump] = React3.useReducer((n) => n + 1, 0);
409
+ const box = React3.useRef(null);
410
+ if (box.current === null) {
411
+ let current2 = initial;
412
+ box.current = {
413
+ get value() {
414
+ return current2;
415
+ },
416
+ set value(next) {
417
+ if (Object.is(next, current2)) return;
418
+ current2 = next;
419
+ bump();
420
+ }
421
+ };
422
+ }
423
+ return box.current;
424
+ };
425
+ var runWorklet = (factory, fallback) => {
426
+ var _a;
427
+ try {
428
+ return (_a = factory()) != null ? _a : fallback;
429
+ } catch {
430
+ return fallback;
431
+ }
432
+ };
433
+ var StaticAnimatedView = React3.forwardRef(function StaticAnimatedView2({ entering: _entering, exiting: _exiting, children, ...rest }, ref) {
434
+ return React3.createElement(reactNative.View, { ...rest, ref }, children);
435
+ });
436
+ var staticCreateAnimatedComponent = (component) => function StaticAnimatedComponent({ animatedProps, ...rest }) {
437
+ return React3.createElement(component, { ...rest, ...animatedProps });
438
+ };
439
+ var staticApi = {
440
+ present: false,
441
+ View: StaticAnimatedView,
442
+ createAnimatedComponent: staticCreateAnimatedComponent,
443
+ useSharedValue: useStaticSharedValue,
444
+ useAnimatedStyle: (factory) => runWorklet(factory, {}),
445
+ useAnimatedProps: (factory) => runWorklet(factory, {}),
446
+ useReducedMotion,
447
+ withTiming: (toValue) => toValue,
448
+ withRepeat: (animation) => animation,
449
+ withSequence: (...animations) => {
450
+ var _a;
451
+ return (_a = animations[animations.length - 1]) != null ? _a : 0;
452
+ },
453
+ withDelay: (_ms, animation) => animation,
454
+ fadeIn: () => void 0,
455
+ fadeOut: () => void 0
456
+ };
457
+ var isFunction = (value) => typeof value === "function";
458
+ var isComponent = (value) => typeof value === "function" || !!value && typeof value === "object" && "$$typeof" in value;
459
+ var fromModule = (mod) => {
460
+ var _a, _b;
461
+ if (!mod || typeof mod !== "object") return void 0;
462
+ const ns = mod;
463
+ const def = ns.default && typeof ns.default === "object" ? ns.default : void 0;
464
+ const pick = (name) => {
465
+ var _a2;
466
+ return (_a2 = ns[name]) != null ? _a2 : def == null ? void 0 : def[name];
467
+ };
468
+ const view = (_a = def == null ? void 0 : def.View) != null ? _a : ns.View;
469
+ const createAnimated = (_b = def == null ? void 0 : def.createAnimatedComponent) != null ? _b : ns.createAnimatedComponent;
470
+ const hooks = {
471
+ useSharedValue: pick("useSharedValue"),
472
+ useAnimatedStyle: pick("useAnimatedStyle"),
473
+ useAnimatedProps: pick("useAnimatedProps"),
474
+ useReducedMotion: pick("useReducedMotion"),
475
+ withTiming: pick("withTiming"),
476
+ withRepeat: pick("withRepeat"),
477
+ withSequence: pick("withSequence"),
478
+ withDelay: pick("withDelay")
479
+ };
480
+ const fadeIn = pick("FadeIn");
481
+ const fadeOut = pick("FadeOut");
482
+ if (!isComponent(view) || !isFunction(createAnimated)) return void 0;
483
+ if (Object.values(hooks).some((member) => !isFunction(member))) return void 0;
484
+ if (!fadeIn || !fadeOut) return void 0;
485
+ const api2 = {
486
+ present: true,
487
+ View: view,
488
+ createAnimatedComponent: createAnimated,
489
+ ...hooks,
490
+ fadeIn: (durationMs) => fadeIn.duration(durationMs),
491
+ fadeOut: (durationMs) => fadeOut.duration(durationMs)
492
+ };
493
+ return api2;
494
+ };
495
+ var cached2;
496
+ var resolveReanimated = (requireModule = runtimeRequire2) => {
497
+ var _a;
498
+ try {
499
+ if (cached2 === void 0 || requireModule !== runtimeRequire2) {
500
+ const resolved = (_a = fromModule(requireModule("react-native-reanimated"))) != null ? _a : staticApi;
501
+ if (requireModule === runtimeRequire2) cached2 = resolved;
502
+ return resolved;
503
+ }
504
+ return cached2;
505
+ } catch {
506
+ return staticApi;
507
+ }
508
+ };
509
+ var api = () => resolveReanimated();
510
+ var Animated = {
511
+ get View() {
512
+ return api().View;
513
+ },
514
+ createAnimatedComponent: (component) => api().createAnimatedComponent(component)
515
+ };
516
+ var FadeIn = { duration: (durationMs) => api().fadeIn(durationMs) };
517
+ var FadeOut = { duration: (durationMs) => api().fadeOut(durationMs) };
518
+ var useSharedValue = (initial) => api().useSharedValue(initial);
519
+ var useAnimatedStyle = (factory) => api().useAnimatedStyle(factory);
520
+ var useAnimatedProps = (factory) => api().useAnimatedProps(factory);
521
+ var useReducedMotion2 = () => api().useReducedMotion();
522
+ var withTiming = (toValue, config) => api().withTiming(toValue, config);
523
+ var withRepeat = (animation, count, reverse) => api().withRepeat(animation, count, reverse);
524
+ var withSequence = (...animations) => api().withSequence(...animations);
525
+ var withDelay = (ms, animation) => api().withDelay(ms, animation);
332
526
  var SWIPE_MS = 700;
333
527
  var TAP_MS = 320;
334
528
  var SPREAD_MS = 620;
@@ -386,29 +580,29 @@ var GestureHintComponent = ({
386
580
  renderHand
387
581
  }) => {
388
582
  var _a;
389
- const translate = Animated.useSharedValue(0);
390
- const opacity = Animated.useSharedValue(1);
391
- const pulse = Animated.useSharedValue(1);
392
- const spread = Animated.useSharedValue(0);
393
- const reduced = Animated.useReducedMotion();
583
+ const translate = useSharedValue(0);
584
+ const opacity = useSharedValue(1);
585
+ const pulse = useSharedValue(1);
586
+ const spread = useSharedValue(0);
587
+ const reduced = useReducedMotion2();
394
588
  const isSwipe = type === "swipe_up" || type === "swipe_down" || type === "swipe_left" || type === "swipe_right";
395
589
  const isSpread = type === "pinch" || type === "zoom";
396
590
  const isTap = type === "tap" || type === "double_tap";
397
591
  React3.useEffect(() => {
398
592
  if (type === "none" || reduced) return void 0;
399
593
  if (isTap) {
400
- pulse.value = Animated.withRepeat(
401
- Animated.withSequence(
402
- Animated.withTiming(0.82, { duration: TAP_MS }),
403
- Animated.withTiming(1, { duration: TAP_MS })
594
+ pulse.value = withRepeat(
595
+ withSequence(
596
+ withTiming(0.82, { duration: TAP_MS }),
597
+ withTiming(1, { duration: TAP_MS })
404
598
  ),
405
599
  -1,
406
600
  false
407
601
  );
408
- opacity.value = Animated.withRepeat(
409
- Animated.withSequence(
410
- Animated.withTiming(0.55, { duration: TAP_MS }),
411
- Animated.withTiming(1, { duration: TAP_MS })
602
+ opacity.value = withRepeat(
603
+ withSequence(
604
+ withTiming(0.55, { duration: TAP_MS }),
605
+ withTiming(1, { duration: TAP_MS })
412
606
  ),
413
607
  -1,
414
608
  false
@@ -416,48 +610,48 @@ var GestureHintComponent = ({
416
610
  return void 0;
417
611
  }
418
612
  if (isSpread) {
419
- spread.value = Animated.withRepeat(
420
- Animated.withSequence(
421
- Animated.withTiming(size * 0.26, { duration: SPREAD_MS }),
422
- Animated.withTiming(0, { duration: SPREAD_MS })
613
+ spread.value = withRepeat(
614
+ withSequence(
615
+ withTiming(size * 0.26, { duration: SPREAD_MS }),
616
+ withTiming(0, { duration: SPREAD_MS })
423
617
  ),
424
618
  -1,
425
619
  false
426
620
  );
427
621
  return void 0;
428
622
  }
429
- translate.value = Animated.withRepeat(
430
- Animated.withSequence(
431
- Animated.withTiming(0, { duration: 0 }),
432
- Animated.withTiming(-26, { duration: SWIPE_MS }),
433
- Animated.withDelay(140, Animated.withTiming(0, { duration: 0 }))
623
+ translate.value = withRepeat(
624
+ withSequence(
625
+ withTiming(0, { duration: 0 }),
626
+ withTiming(-26, { duration: SWIPE_MS }),
627
+ withDelay(140, withTiming(0, { duration: 0 }))
434
628
  ),
435
629
  -1,
436
630
  false
437
631
  );
438
- opacity.value = Animated.withRepeat(
439
- Animated.withSequence(
440
- Animated.withTiming(1, { duration: SWIPE_MS / 2 }),
441
- Animated.withTiming(0, { duration: SWIPE_MS / 2 }),
442
- Animated.withDelay(140, Animated.withTiming(1, { duration: 0 }))
632
+ opacity.value = withRepeat(
633
+ withSequence(
634
+ withTiming(1, { duration: SWIPE_MS / 2 }),
635
+ withTiming(0, { duration: SWIPE_MS / 2 }),
636
+ withDelay(140, withTiming(1, { duration: 0 }))
443
637
  ),
444
638
  -1,
445
639
  false
446
640
  );
447
641
  return void 0;
448
642
  }, [type, reduced, isTap, isSpread, translate, opacity, pulse, spread, size]);
449
- const swipeAnimatedStyle = Animated.useAnimatedStyle(() => ({
643
+ const swipeAnimatedStyle = useAnimatedStyle(() => ({
450
644
  opacity: opacity.value,
451
645
  transform: [{ translateY: translate.value }]
452
646
  }));
453
- const tapAnimatedStyle = Animated.useAnimatedStyle(() => ({
647
+ const tapAnimatedStyle = useAnimatedStyle(() => ({
454
648
  opacity: opacity.value,
455
649
  transform: [{ scale: pulse.value }]
456
650
  }));
457
- const spreadLeftStyle = Animated.useAnimatedStyle(() => ({
651
+ const spreadLeftStyle = useAnimatedStyle(() => ({
458
652
  transform: [{ translateX: -spread.value }]
459
653
  }));
460
- const spreadRightStyle = Animated.useAnimatedStyle(() => ({
654
+ const spreadRightStyle = useAnimatedStyle(() => ({
461
655
  transform: [{ translateX: spread.value }]
462
656
  }));
463
657
  if (type === "none") return null;
@@ -469,7 +663,7 @@ var GestureHintComponent = ({
469
663
  style: { transform: [{ rotate: (_a = GLYPH_ROTATION[type]) != null ? _a : "0deg" }] },
470
664
  pointerEvents: "none",
471
665
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.swipeStack, children: [
472
- /* @__PURE__ */ jsxRuntime.jsx(Animated__default.default.View, { style: swipeAnimatedStyle, pointerEvents: "none", children: customGlyph != null ? customGlyph : /* @__PURE__ */ jsxRuntime.jsx(FingerPair, { size, color }) }),
666
+ /* @__PURE__ */ jsxRuntime.jsx(Animated.View, { style: swipeAnimatedStyle, pointerEvents: "none", children: customGlyph != null ? customGlyph : /* @__PURE__ */ jsxRuntime.jsx(FingerPair, { size, color }) }),
473
667
  /* @__PURE__ */ jsxRuntime.jsx(MotionTrack, { size, color })
474
668
  ] })
475
669
  }
@@ -484,16 +678,16 @@ var GestureHintComponent = ({
484
678
  backgroundColor: color
485
679
  };
486
680
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.spreadRow, { gap: size * 0.12 }], pointerEvents: "none", children: [
487
- /* @__PURE__ */ jsxRuntime.jsx(Animated__default.default.View, { style: spreadLeftStyle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: capsule }) }),
488
- /* @__PURE__ */ jsxRuntime.jsx(Animated__default.default.View, { style: spreadRightStyle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: capsule }) })
681
+ /* @__PURE__ */ jsxRuntime.jsx(Animated.View, { style: spreadLeftStyle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: capsule }) }),
682
+ /* @__PURE__ */ jsxRuntime.jsx(Animated.View, { style: spreadRightStyle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: capsule }) })
489
683
  ] });
490
684
  }
491
685
  const ringSize = size * 0.86;
492
686
  const ringBorder = Math.max(2, size * 0.08);
493
687
  const dotSize = size * 0.32;
494
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.tapWrap, { width: size, height: size }], pointerEvents: "none", children: customGlyph ? /* @__PURE__ */ jsxRuntime.jsx(Animated__default.default.View, { style: tapAnimatedStyle, pointerEvents: "none", children: customGlyph }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
688
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.tapWrap, { width: size, height: size }], pointerEvents: "none", children: customGlyph ? /* @__PURE__ */ jsxRuntime.jsx(Animated.View, { style: tapAnimatedStyle, pointerEvents: "none", children: customGlyph }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
495
689
  /* @__PURE__ */ jsxRuntime.jsx(
496
- Animated__default.default.View,
690
+ Animated.View,
497
691
  {
498
692
  style: [
499
693
  styles.tapRing,
@@ -543,7 +737,6 @@ var styles = reactNative.StyleSheet.create({
543
737
  });
544
738
  var GestureHint = React3__default.default.memo(GestureHintComponent);
545
739
  GestureHint.displayName = "GestureHint";
546
- var AnimatedBlurView = Animated__default.default.createAnimatedComponent(expoBlur.BlurView);
547
740
  var BLUR_INTENSITY = 26;
548
741
  var FADE_MS = 280;
549
742
  var RING_PADDING = 10;
@@ -561,31 +754,35 @@ var SpotlightOverlayComponent = ({
561
754
  accentTextColor
562
755
  }) => {
563
756
  const { height: screenH } = reactNative.useWindowDimensions();
564
- const reduced = Animated.useReducedMotion();
757
+ const reduced = useReducedMotion2();
565
758
  const theme = useOnboardingTheme();
566
759
  const accent = accentColor != null ? accentColor : theme.colors.primary;
567
760
  const accentText = accentTextColor != null ? accentTextColor : theme.colors.onPrimary;
761
+ const AnimatedBlurView = React3.useMemo(() => {
762
+ const BlurView = resolveBlurView();
763
+ return BlurView ? Animated.createAnimatedComponent(BlurView) : null;
764
+ }, []);
568
765
  const showGesture = gesture !== "none" && gesture !== "tap" && gesture !== "double_tap";
569
- const intensity = Animated.useSharedValue(0);
570
- const ringScale = Animated.useSharedValue(1);
766
+ const intensity = useSharedValue(0);
767
+ const ringScale = useSharedValue(1);
571
768
  React3.useEffect(() => {
572
- intensity.value = Animated.withTiming(BLUR_INTENSITY, { duration: FADE_MS });
769
+ intensity.value = withTiming(BLUR_INTENSITY, { duration: FADE_MS });
573
770
  }, [intensity]);
574
771
  React3.useEffect(() => {
575
772
  if (reduced) return;
576
- ringScale.value = Animated.withRepeat(
577
- Animated.withSequence(
578
- Animated.withTiming(1.06, { duration: 700 }),
579
- Animated.withTiming(1, { duration: 700 })
773
+ ringScale.value = withRepeat(
774
+ withSequence(
775
+ withTiming(1.06, { duration: 700 }),
776
+ withTiming(1, { duration: 700 })
580
777
  ),
581
778
  -1,
582
779
  true
583
780
  );
584
781
  }, [reduced, ringScale]);
585
- const blurAnimatedProps = Animated.useAnimatedProps(() => ({
782
+ const blurAnimatedProps = useAnimatedProps(() => ({
586
783
  intensity: intensity.value
587
784
  }));
588
- const ringAnimatedStyle = Animated.useAnimatedStyle(() => ({
785
+ const ringAnimatedStyle = useAnimatedStyle(() => ({
589
786
  transform: [{ scale: ringScale.value }]
590
787
  }));
591
788
  const geometry = React3.useMemo(() => {
@@ -607,19 +804,29 @@ var SpotlightOverlayComponent = ({
607
804
  };
608
805
  }, [targetRect, placement, screenH]);
609
806
  return /* @__PURE__ */ jsxRuntime.jsxs(
610
- Animated__default.default.View,
807
+ Animated.View,
611
808
  {
612
809
  style: [reactNative.StyleSheet.absoluteFill, styles2.root],
613
- entering: Animated.FadeIn.duration(FADE_MS),
614
- exiting: Animated.FadeOut.duration(FADE_MS),
810
+ entering: FadeIn.duration(FADE_MS),
811
+ exiting: FadeOut.duration(FADE_MS),
615
812
  children: [
616
- /* @__PURE__ */ jsxRuntime.jsx(
813
+ AnimatedBlurView ? /* @__PURE__ */ jsxRuntime.jsx(
617
814
  AnimatedBlurView,
618
815
  {
619
816
  tint: "dark",
620
817
  animatedProps: blurAnimatedProps,
621
818
  style: reactNative.StyleSheet.absoluteFill
622
819
  }
820
+ ) : (
821
+ // `expo-blur` absent → a plain dimmed scrim stands in for the frost. Same dark backdrop
822
+ // the tour reads against, so the ring, tooltip and gestures behave identically.
823
+ /* @__PURE__ */ jsxRuntime.jsx(
824
+ Animated.View,
825
+ {
826
+ style: [reactNative.StyleSheet.absoluteFill, styles2.scrim],
827
+ pointerEvents: "none"
828
+ }
829
+ )
623
830
  ),
624
831
  /* @__PURE__ */ jsxRuntime.jsx(
625
832
  reactNative.Pressable,
@@ -631,7 +838,7 @@ var SpotlightOverlayComponent = ({
631
838
  }
632
839
  ),
633
840
  geometry && /* @__PURE__ */ jsxRuntime.jsx(
634
- Animated__default.default.View,
841
+ Animated.View,
635
842
  {
636
843
  style: [
637
844
  styles2.ring,
@@ -686,6 +893,11 @@ var styles2 = reactNative.StyleSheet.create({
686
893
  zIndex: 9999,
687
894
  elevation: 9999
688
895
  },
896
+ // Fallback backdrop when `expo-blur` is absent. A structural dim (not a brand token), tuned to
897
+ // read like the dark blur at BLUR_INTENSITY so the ring + tooltip keep the same contrast.
898
+ scrim: {
899
+ backgroundColor: "rgba(0, 0, 0, 0.55)"
900
+ },
689
901
  ring: {
690
902
  position: "absolute",
691
903
  borderWidth: 2.5,