@sigx/lynx-navigation 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +128 -8
  2. package/dist/components/EntryScope.d.ts.map +1 -1
  3. package/dist/components/EntryScope.js +8 -2
  4. package/dist/components/EntryScope.js.map +1 -1
  5. package/dist/components/Layer.d.ts +34 -0
  6. package/dist/components/Layer.d.ts.map +1 -0
  7. package/dist/components/Layer.js +66 -0
  8. package/dist/components/Layer.js.map +1 -0
  9. package/dist/components/Screen.d.ts +6 -6
  10. package/dist/components/Screen.d.ts.map +1 -1
  11. package/dist/components/Screen.js +13 -9
  12. package/dist/components/Screen.js.map +1 -1
  13. package/dist/components/Stack.d.ts +41 -16
  14. package/dist/components/Stack.d.ts.map +1 -1
  15. package/dist/components/Stack.js +90 -54
  16. package/dist/components/Stack.js.map +1 -1
  17. package/dist/components/TabBar.d.ts +18 -19
  18. package/dist/components/TabBar.d.ts.map +1 -1
  19. package/dist/components/TabBar.js +21 -21
  20. package/dist/components/TabBar.js.map +1 -1
  21. package/dist/components/Tabs.d.ts.map +1 -1
  22. package/dist/components/Tabs.js +15 -1
  23. package/dist/components/Tabs.js.map +1 -1
  24. package/dist/hooks/use-linking-nav.js.map +1 -1
  25. package/dist/hooks/use-nav-internal.d.ts +19 -1
  26. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  27. package/dist/hooks/use-nav-internal.js +11 -0
  28. package/dist/hooks/use-nav-internal.js.map +1 -1
  29. package/dist/hooks/use-screen-chrome.d.ts +19 -0
  30. package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
  31. package/dist/hooks/use-screen-chrome.js +102 -0
  32. package/dist/hooks/use-screen-chrome.js.map +1 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal/layer-plan.d.ts +69 -0
  38. package/dist/internal/layer-plan.d.ts.map +1 -0
  39. package/dist/internal/layer-plan.js +102 -0
  40. package/dist/internal/layer-plan.js.map +1 -0
  41. package/dist/internal/screen-width.d.ts +9 -7
  42. package/dist/internal/screen-width.d.ts.map +1 -1
  43. package/dist/internal/screen-width.js +15 -13
  44. package/dist/internal/screen-width.js.map +1 -1
  45. package/dist/navigator/core.d.ts +2 -1
  46. package/dist/navigator/core.d.ts.map +1 -1
  47. package/dist/navigator/core.js +13 -2
  48. package/dist/navigator/core.js.map +1 -1
  49. package/dist/url/format.js.map +1 -1
  50. package/package.json +13 -11
  51. package/src/components/EntryScope.tsx +13 -2
  52. package/src/components/Layer.tsx +96 -0
  53. package/src/components/Screen.tsx +11 -9
  54. package/src/components/Stack.tsx +136 -92
  55. package/src/components/TabBar.tsx +21 -21
  56. package/src/components/Tabs.tsx +15 -1
  57. package/src/hooks/use-nav-internal.ts +22 -1
  58. package/src/hooks/use-screen-chrome.ts +122 -0
  59. package/src/index.ts +2 -0
  60. package/src/internal/layer-plan.ts +187 -0
  61. package/src/internal/screen-width.ts +22 -14
  62. package/src/navigator/core.ts +14 -3
  63. package/dist/components/ScreenContainer.d.ts +0 -18
  64. package/dist/components/ScreenContainer.d.ts.map +0 -1
  65. package/dist/components/ScreenContainer.js +0 -77
  66. package/dist/components/ScreenContainer.js.map +0 -1
  67. package/src/components/ScreenContainer.tsx +0 -114
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Pure layer-plan computation for `<Stack>`'s render.
3
+ *
4
+ * Given (stack, transition, progress), produces an ordered list of
5
+ * `Layer`s — each is an entry to render plus an optional transform
6
+ * spec for animation. The Stack render emits one absolutely-positioned
7
+ * `<view>` per layer, stacked bottom-to-top in document order.
8
+ *
9
+ * Why this is its own module: the layer-selection logic is the only
10
+ * non-obvious part of the navigator's render path, and the rules are
11
+ * easier to read (and unit-test) as a pure function over the
12
+ * navigator's state than as inline render branches.
13
+ *
14
+ * Rules:
15
+ *
16
+ * - **Idle (no transition).** Render the topmost non-overlay entry
17
+ * as the base, plus every overlay entry above it. Overlays
18
+ * (`modal` / `fullScreen` / `transparent-modal`) keep their
19
+ * underneath mounted; cards replace their underneath in the base
20
+ * layer.
21
+ *
22
+ * - **Card transition.** Two layers: the underneath entry (animated
23
+ * with the parallax-card-underneath spec) and the top entry
24
+ * (animated with the slide-in-from-right spec). After the
25
+ * transition completes, the idle rule kicks in — the underneath
26
+ * unmounts because the new top becomes the sole base.
27
+ *
28
+ * - **Overlay transition.** The full idle layer stack up through the
29
+ * underneath entry stays static (no transform). The animated top
30
+ * is the only layer with a transform. After the transition, the
31
+ * overlay either joins the static idle stack (push) or unmounts
32
+ * (pop).
33
+ *
34
+ * The Layer.key for the Stack render is
35
+ * `layer-${entry.key}-${animVariant(layer.animation)}`. The variant
36
+ * suffix forces a remount when an entry transitions from animated to
37
+ * static (or vice versa) — `useAnimatedStyle` can't re-bind mid-life,
38
+ * so we get a fresh `useAnimatedStyle` call per animation state.
39
+ * Modal underneath layers never animate, so they stay statically
40
+ * keyed across the modal lifecycle and their state (per-tab Stack,
41
+ * scroll, in-flight inputs) survives.
42
+ */
43
+ import type { SharedValue } from '@sigx/lynx';
44
+ import { SCREEN_HEIGHT, SCREEN_WIDTH } from './screen-width.js';
45
+ import type {
46
+ Presentation,
47
+ StackEntry,
48
+ TransitionKind,
49
+ TransitionState,
50
+ } from '../types.js';
51
+
52
+ const PARALLAX_FACTOR = 0.3;
53
+
54
+ export type LayerAnimation = {
55
+ axis: 'translateX' | 'translateY';
56
+ inputRange: readonly [number, number];
57
+ outputRange: readonly [number, number];
58
+ progress: SharedValue<number>;
59
+ };
60
+
61
+ export interface Layer {
62
+ /** The entry whose component renders inside this layer. */
63
+ readonly entry: StackEntry;
64
+ /** When non-null, the layer's host view binds a `useAnimatedStyle` mapper. */
65
+ readonly animation: LayerAnimation | null;
66
+ }
67
+
68
+ export function isOverlayPresentation(p: Presentation): boolean {
69
+ return p === 'modal' || p === 'fullScreen' || p === 'transparent-modal';
70
+ }
71
+
72
+ /**
73
+ * Suffix used in a layer's render key. Stable for the layer's
74
+ * lifetime (same entry, same animation kind) and changes when the
75
+ * animation transitions on/off so the Layer remounts and rebinds.
76
+ */
77
+ export function animationVariant(animation: LayerAnimation | null): string {
78
+ if (!animation) return 'static';
79
+ // Output range alone identifies the transition shape — different
80
+ // animations (card-top vs card-underneath vs overlay-top, push vs
81
+ // pop) all land on different range tuples.
82
+ return `${animation.axis}:${animation.outputRange[0]}->${animation.outputRange[1]}`;
83
+ }
84
+
85
+ /**
86
+ * Card-presentation transition transforms. `role='top'` is the entry
87
+ * being pushed/popped; `role='underneath'` is the one parallaxing.
88
+ */
89
+ function cardAnimation(
90
+ role: 'top' | 'underneath',
91
+ kind: TransitionKind,
92
+ progress: SharedValue<number>,
93
+ ): LayerAnimation {
94
+ if (kind === 'push') {
95
+ if (role === 'top') {
96
+ return { axis: 'translateX', inputRange: [0, 1], outputRange: [SCREEN_WIDTH, 0], progress };
97
+ }
98
+ return { axis: 'translateX', inputRange: [0, 1], outputRange: [0, -PARALLAX_FACTOR * SCREEN_WIDTH], progress };
99
+ }
100
+ // pop
101
+ if (role === 'top') {
102
+ return { axis: 'translateX', inputRange: [0, 1], outputRange: [0, SCREEN_WIDTH], progress };
103
+ }
104
+ return { axis: 'translateX', inputRange: [0, 1], outputRange: [-PARALLAX_FACTOR * SCREEN_WIDTH, 0], progress };
105
+ }
106
+
107
+ /**
108
+ * Overlay-presentation transition transform for the animated top.
109
+ * The underneath of an overlay transition does not animate (modal
110
+ * doesn't reposition its background); we render it as a static layer
111
+ * instead, so this function only produces the top's transform.
112
+ */
113
+ function overlayTopAnimation(
114
+ kind: TransitionKind,
115
+ progress: SharedValue<number>,
116
+ ): LayerAnimation {
117
+ if (kind === 'push') {
118
+ return { axis: 'translateY', inputRange: [0, 1], outputRange: [SCREEN_HEIGHT, 0], progress };
119
+ }
120
+ return { axis: 'translateY', inputRange: [0, 1], outputRange: [0, SCREEN_HEIGHT], progress };
121
+ }
122
+
123
+ /**
124
+ * Compute the visible-layer list for one render of `<Stack>`. Pure —
125
+ * unit-testable independently of the renderer.
126
+ */
127
+ export function computeLayers(
128
+ stack: readonly StackEntry[],
129
+ transition: TransitionState | null,
130
+ progress: SharedValue<number> | null,
131
+ ): Layer[] {
132
+ if (!transition) {
133
+ // Idle: topmost non-overlay base + any overlays above it.
134
+ let baseIdx = stack.length - 1;
135
+ while (baseIdx > 0 && isOverlayPresentation(stack[baseIdx].presentation)) {
136
+ baseIdx -= 1;
137
+ }
138
+ return stack.slice(baseIdx).map((entry) => ({ entry, animation: null }));
139
+ }
140
+
141
+ // A transition is in flight. `progress` may still be null when
142
+ // animations are disabled — produce static layers in that case
143
+ // (the animation never plays; the transition timer just ticks).
144
+ const isOverlay = isOverlayPresentation(transition.topEntry.presentation);
145
+ if (!isOverlay) {
146
+ // Card transition: just the two participating entries, both
147
+ // animated (parallax underneath + slide top).
148
+ return [
149
+ {
150
+ entry: transition.underneathEntry,
151
+ animation: progress ? cardAnimation('underneath', transition.kind, progress) : null,
152
+ },
153
+ {
154
+ entry: transition.topEntry,
155
+ animation: progress ? cardAnimation('top', transition.kind, progress) : null,
156
+ },
157
+ ];
158
+ }
159
+
160
+ // Overlay transition: render the full idle layer stack up through
161
+ // the underneath entry (all static — they don't animate) plus the
162
+ // animated top.
163
+ const underneathIdx = stack.findIndex(
164
+ (e) => e.key === transition.underneathEntry.key,
165
+ );
166
+ // If the underneath isn't in the stack (e.g. mid-pop where the
167
+ // stack mutation already removed an entry), fall back to the
168
+ // current top of the stack.
169
+ const lastStaticIdx = underneathIdx >= 0 ? underneathIdx : stack.length - 1;
170
+
171
+ let baseIdx = lastStaticIdx;
172
+ while (baseIdx > 0 && isOverlayPresentation(stack[baseIdx].presentation)) {
173
+ baseIdx -= 1;
174
+ }
175
+
176
+ const staticLayers: Layer[] = stack
177
+ .slice(baseIdx, lastStaticIdx + 1)
178
+ .map((entry) => ({ entry, animation: null }));
179
+
180
+ return [
181
+ ...staticLayers,
182
+ {
183
+ entry: transition.topEntry,
184
+ animation: progress ? overlayTopAnimation(transition.kind, progress) : null,
185
+ },
186
+ ];
187
+ }
@@ -1,34 +1,42 @@
1
1
  /**
2
- * Logical screen width (in dp) read from `lynx.SystemInfo` at module load.
3
- * Falls back to 400 (typical phone) if SystemInfo isn't available — module
4
- * load happens BG-side after the bundle initializes, by which time
5
- * `lynx.SystemInfo` is populated, so the fallback only fires in tests / SSR /
6
- * non-Lynx hosts.
2
+ * Logical screen dimensions (in dp) read from `lynx.SystemInfo` at module
3
+ * load. Falls back to typical phone values if SystemInfo isn't available —
4
+ * module load happens BG-side after the bundle initializes, by which time
5
+ * `lynx.SystemInfo` is populated, so the fallback only fires in tests /
6
+ * SSR / non-Lynx hosts.
7
7
  *
8
8
  * Used by:
9
- * - `<ScreenContainer>` for the slide-from-right transform output range.
9
+ * - `<ScreenContainer>` for the slide-from-right (translateX) and
10
+ * slide-from-bottom (translateY, modal) transform output ranges.
10
11
  * - `<EdgeBackHandle>` for the gesture commit threshold (`dx / width`).
11
12
  *
12
13
  * Both must agree, otherwise the commit threshold and the animation
13
- * geometry won't line up. Single shared constant avoids drift.
14
+ * geometry won't line up. Single shared module avoids drift.
14
15
  */
15
16
 
16
17
  declare const lynx:
17
- | { SystemInfo?: { pixelWidth?: number; pixelRatio?: number } }
18
+ | {
19
+ SystemInfo?: {
20
+ pixelWidth?: number;
21
+ pixelHeight?: number;
22
+ pixelRatio?: number;
23
+ };
24
+ }
18
25
  | undefined;
19
26
 
20
- function readScreenWidth(): number {
27
+ function readDp(prop: 'pixelWidth' | 'pixelHeight', fallback: number): number {
21
28
  try {
22
29
  const info = typeof lynx !== 'undefined' ? lynx?.SystemInfo : undefined;
23
- const pw = info?.pixelWidth;
30
+ const px = info?.[prop];
24
31
  const pr = info?.pixelRatio || 1;
25
- if (typeof pw === 'number' && pw > 0) {
26
- return Math.round(pw / pr);
32
+ if (typeof px === 'number' && px > 0) {
33
+ return Math.round(px / pr);
27
34
  }
28
35
  } catch {
29
36
  // Lynx globals not present (test env / SSR) — use fallback.
30
37
  }
31
- return 400;
38
+ return fallback;
32
39
  }
33
40
 
34
- export const SCREEN_WIDTH = readScreenWidth();
41
+ export const SCREEN_WIDTH = readDp('pixelWidth', 400);
42
+ export const SCREEN_HEIGHT = readDp('pixelHeight', 800);
@@ -61,7 +61,8 @@ export interface NavigatorState {
61
61
  */
62
62
  readonly _screens: {
63
63
  register(registry: ScreenRegistry): void;
64
- unregister(entryKey: string): void;
64
+ /** Identity-checked: no-op when a newer registry has taken the slot. */
65
+ unregister(registry: ScreenRegistry): void;
65
66
  get(entryKey: string): ScreenRegistry | undefined;
66
67
  };
67
68
  /**
@@ -520,8 +521,18 @@ function createScreenRegistries(): NavigatorState['_screens'] {
520
521
  // would self-loop, so we untrack the bump.
521
522
  untrack(() => { version.v = version.v + 1; });
522
523
  },
523
- unregister(key: string) {
524
- byKey.delete(key);
524
+ // Identity-checked unregister: deletes the entry only if the
525
+ // currently-registered registry is the *same instance* the caller
526
+ // holds. Without this, the transition→idle handoff (which can
527
+ // mount a new `<EntryScope>` for the same entry-key before the
528
+ // old one unmounts) would let the old scope's `onUnmounted` wipe
529
+ // out the fresh registry — leaving `screens.get(key)` returning
530
+ // undefined and chrome consumers (NavHeader) falling back to the
531
+ // route-name as title with all slot fills gone.
532
+ unregister(reg: ScreenRegistry) {
533
+ const cur = byKey.get(reg.entry.key);
534
+ if (cur !== reg) return;
535
+ byKey.delete(reg.entry.key);
525
536
  untrack(() => { version.v = version.v + 1; });
526
537
  },
527
538
  get(key: string) {
@@ -1,18 +0,0 @@
1
- import { type ComponentFactory, type Define, type SharedValue } from '@sigx/lynx';
2
- import type { RouteMap, StackEntry, TransitionKind, TransitionRole } from '../types.js';
3
- type ScreenContainerProps = Define.Prop<'entry', StackEntry, true> & Define.Prop<'routes', RouteMap, true> & Define.Prop<'role', TransitionRole, true> & Define.Prop<'kind', TransitionKind, true> & Define.Prop<'progress', SharedValue<number>, true>;
4
- /**
5
- * Animated screen slot — absolutely positioned, MT-bound translateX driven by
6
- * the navigator's progress SharedValue. Used during transitions to render the
7
- * top + underneath entries together.
8
- *
9
- * Each instance is keyed by `${entry.key}-${role}-${kind}` in the parent so a
10
- * role/kind change forces a fresh mount with a fresh `useAnimatedStyle`
11
- * binding (the binding is set at setup and can't be re-keyed mid-life). State
12
- * loss across transition boundaries is accepted in v0.2; persistent screen
13
- * state (scroll position, input fields surviving navigations) is a polish
14
- * item for Phase 0.5+.
15
- */
16
- export declare const ScreenContainer: ComponentFactory<ScreenContainerProps, void, {}>;
17
- export {};
18
- //# sourceMappingURL=ScreenContainer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ScreenContainer.d.ts","sourceRoot":"","sources":["../../src/components/ScreenContainer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,gBAAgB,EACrB,KAAK,MAAM,EAEX,KAAK,WAAW,EACnB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAwCxF,KAAK,oBAAoB,GACnB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,GACtC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,GACrC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,CAAC,GACzC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,CAAC,GACzC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,kDA0C1B,CAAC"}
@@ -1,77 +0,0 @@
1
- import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
- import { component, useMainThreadRef, useAnimatedStyle, } from '@sigx/lynx';
3
- import { Suspense, isLazyComponent } from '@sigx/lynx';
4
- import { SCREEN_WIDTH } from '../internal/screen-width.js';
5
- import { EntryScope } from './EntryScope.js';
6
- /**
7
- * Slide-from-right transition geometry. `SCREEN_WIDTH` is read from
8
- * `lynx.SystemInfo` at module load so the animation lands the screen at
9
- * exactly translateX=0 (centered) at progress=1, rather than overshooting
10
- * into the parent's clip region. `<EdgeBackHandle>` reads the same
11
- * constant — they have to agree, otherwise the gesture commit threshold
12
- * and the animation geometry don't line up.
13
- */
14
- const PARALLAX_FACTOR = 0.3;
15
- /**
16
- * Compute the `translateX` range for a given (role, kind) pair. Progress
17
- * always runs 0 → 1; the role and kind decide what visual state each end of
18
- * the progress represents.
19
- *
20
- * Slide-from-right semantics:
21
- * - PUSH: new top slides in from the right; old top parallaxes left.
22
- * - POP: current top slides out to the right; underneath returns from the
23
- * parallax-left position.
24
- */
25
- function getRangeParams(role, kind) {
26
- if (kind === 'push') {
27
- if (role === 'top') {
28
- return { inputRange: [0, 1], outputRange: [SCREEN_WIDTH, 0] };
29
- }
30
- return { inputRange: [0, 1], outputRange: [0, -PARALLAX_FACTOR * SCREEN_WIDTH] };
31
- }
32
- // pop
33
- if (role === 'top') {
34
- return { inputRange: [0, 1], outputRange: [0, SCREEN_WIDTH] };
35
- }
36
- return { inputRange: [0, 1], outputRange: [-PARALLAX_FACTOR * SCREEN_WIDTH, 0] };
37
- }
38
- /**
39
- * Animated screen slot — absolutely positioned, MT-bound translateX driven by
40
- * the navigator's progress SharedValue. Used during transitions to render the
41
- * top + underneath entries together.
42
- *
43
- * Each instance is keyed by `${entry.key}-${role}-${kind}` in the parent so a
44
- * role/kind change forces a fresh mount with a fresh `useAnimatedStyle`
45
- * binding (the binding is set at setup and can't be re-keyed mid-life). State
46
- * loss across transition boundaries is accepted in v0.2; persistent screen
47
- * state (scroll position, input fields surviving navigations) is a polish
48
- * item for Phase 0.5+.
49
- */
50
- export const ScreenContainer = component(({ props }) => {
51
- const ref = useMainThreadRef(null);
52
- const params = getRangeParams(props.role, props.kind);
53
- useAnimatedStyle(ref, props.progress, 'translateX', params);
54
- return () => {
55
- const route = props.routes[props.entry.route];
56
- if (!route)
57
- return null;
58
- const Comp = route.component;
59
- if (typeof Comp !== 'function')
60
- return null;
61
- const entryParams = props.entry.params;
62
- // Wrap lazy screens that declare a fallback in Suspense — see Stack.tsx
63
- // for rationale.
64
- const body = isLazyComponent(Comp) && route.fallback
65
- ? (_jsx(Suspense, { fallback: route.fallback, children: _jsx(Comp, { ...entryParams }) }))
66
- : _jsx(Comp, { ...entryParams });
67
- return (_jsx("view", { "main-thread:ref": ref, style: {
68
- position: 'absolute',
69
- top: '0',
70
- left: '0',
71
- right: '0',
72
- bottom: '0',
73
- backgroundColor: '#0f172a',
74
- }, children: _jsx(EntryScope, { entry: props.entry, children: body }, props.entry.key) }));
75
- };
76
- });
77
- //# sourceMappingURL=ScreenContainer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ScreenContainer.js","sourceRoot":"","sources":["../../src/components/ScreenContainer.tsx"],"names":[],"mappings":"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"}
@@ -1,114 +0,0 @@
1
- import {
2
- component,
3
- useMainThreadRef,
4
- useAnimatedStyle,
5
- type ComponentFactory,
6
- type Define,
7
- type MainThread,
8
- type SharedValue,
9
- } from '@sigx/lynx';
10
- import { Suspense, isLazyComponent } from '@sigx/lynx';
11
- import type { MapperParams } from '@sigx/lynx';
12
- import { SCREEN_WIDTH } from '../internal/screen-width.js';
13
- import type { RouteMap, StackEntry, TransitionKind, TransitionRole } from '../types.js';
14
- import { EntryScope } from './EntryScope.js';
15
-
16
- /**
17
- * Slide-from-right transition geometry. `SCREEN_WIDTH` is read from
18
- * `lynx.SystemInfo` at module load so the animation lands the screen at
19
- * exactly translateX=0 (centered) at progress=1, rather than overshooting
20
- * into the parent's clip region. `<EdgeBackHandle>` reads the same
21
- * constant — they have to agree, otherwise the gesture commit threshold
22
- * and the animation geometry don't line up.
23
- */
24
- const PARALLAX_FACTOR = 0.3;
25
-
26
- /**
27
- * Compute the `translateX` range for a given (role, kind) pair. Progress
28
- * always runs 0 → 1; the role and kind decide what visual state each end of
29
- * the progress represents.
30
- *
31
- * Slide-from-right semantics:
32
- * - PUSH: new top slides in from the right; old top parallaxes left.
33
- * - POP: current top slides out to the right; underneath returns from the
34
- * parallax-left position.
35
- */
36
- function getRangeParams(
37
- role: TransitionRole,
38
- kind: TransitionKind,
39
- ): MapperParams['translateX'] {
40
- if (kind === 'push') {
41
- if (role === 'top') {
42
- return { inputRange: [0, 1], outputRange: [SCREEN_WIDTH, 0] };
43
- }
44
- return { inputRange: [0, 1], outputRange: [0, -PARALLAX_FACTOR * SCREEN_WIDTH] };
45
- }
46
- // pop
47
- if (role === 'top') {
48
- return { inputRange: [0, 1], outputRange: [0, SCREEN_WIDTH] };
49
- }
50
- return { inputRange: [0, 1], outputRange: [-PARALLAX_FACTOR * SCREEN_WIDTH, 0] };
51
- }
52
-
53
- type ScreenContainerProps =
54
- & Define.Prop<'entry', StackEntry, true>
55
- & Define.Prop<'routes', RouteMap, true>
56
- & Define.Prop<'role', TransitionRole, true>
57
- & Define.Prop<'kind', TransitionKind, true>
58
- & Define.Prop<'progress', SharedValue<number>, true>;
59
-
60
- /**
61
- * Animated screen slot — absolutely positioned, MT-bound translateX driven by
62
- * the navigator's progress SharedValue. Used during transitions to render the
63
- * top + underneath entries together.
64
- *
65
- * Each instance is keyed by `${entry.key}-${role}-${kind}` in the parent so a
66
- * role/kind change forces a fresh mount with a fresh `useAnimatedStyle`
67
- * binding (the binding is set at setup and can't be re-keyed mid-life). State
68
- * loss across transition boundaries is accepted in v0.2; persistent screen
69
- * state (scroll position, input fields surviving navigations) is a polish
70
- * item for Phase 0.5+.
71
- */
72
- export const ScreenContainer = component<ScreenContainerProps>(({ props }) => {
73
- const ref = useMainThreadRef<MainThread.Element | null>(null);
74
- const params = getRangeParams(props.role, props.kind);
75
- useAnimatedStyle(ref, props.progress, 'translateX', params);
76
-
77
- return () => {
78
- const route = props.routes[props.entry.route];
79
- if (!route) return null;
80
- const Comp = route.component as unknown as ComponentFactory<
81
- Record<string, unknown>,
82
- unknown,
83
- unknown
84
- >;
85
- if (typeof Comp !== 'function') return null;
86
- const entryParams = props.entry.params as Record<string, unknown>;
87
- // Wrap lazy screens that declare a fallback in Suspense — see Stack.tsx
88
- // for rationale.
89
- const body = isLazyComponent(Comp) && route.fallback
90
- ? (
91
- <Suspense fallback={route.fallback as never}>
92
- <Comp {...entryParams} />
93
- </Suspense>
94
- )
95
- : <Comp {...entryParams} />;
96
- return (
97
- <view
98
- main-thread:ref={ref}
99
- style={{
100
- position: 'absolute',
101
- top: '0',
102
- left: '0',
103
- right: '0',
104
- bottom: '0',
105
- backgroundColor: '#0f172a',
106
- }}
107
- >
108
- <EntryScope key={props.entry.key} entry={props.entry}>
109
- {body}
110
- </EntryScope>
111
- </view>
112
- );
113
- };
114
- });