@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.
- package/README.md +128 -8
- package/dist/components/EntryScope.d.ts.map +1 -1
- package/dist/components/EntryScope.js +8 -2
- package/dist/components/EntryScope.js.map +1 -1
- package/dist/components/Layer.d.ts +34 -0
- package/dist/components/Layer.d.ts.map +1 -0
- package/dist/components/Layer.js +66 -0
- package/dist/components/Layer.js.map +1 -0
- package/dist/components/Screen.d.ts +6 -6
- package/dist/components/Screen.d.ts.map +1 -1
- package/dist/components/Screen.js +13 -9
- package/dist/components/Screen.js.map +1 -1
- package/dist/components/Stack.d.ts +41 -16
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Stack.js +90 -54
- package/dist/components/Stack.js.map +1 -1
- package/dist/components/TabBar.d.ts +18 -19
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/TabBar.js +21 -21
- package/dist/components/TabBar.js.map +1 -1
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs.js +15 -1
- package/dist/components/Tabs.js.map +1 -1
- package/dist/hooks/use-linking-nav.js.map +1 -1
- package/dist/hooks/use-nav-internal.d.ts +19 -1
- package/dist/hooks/use-nav-internal.d.ts.map +1 -1
- package/dist/hooks/use-nav-internal.js +11 -0
- package/dist/hooks/use-nav-internal.js.map +1 -1
- package/dist/hooks/use-screen-chrome.d.ts +19 -0
- package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
- package/dist/hooks/use-screen-chrome.js +102 -0
- package/dist/hooks/use-screen-chrome.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/layer-plan.d.ts +69 -0
- package/dist/internal/layer-plan.d.ts.map +1 -0
- package/dist/internal/layer-plan.js +102 -0
- package/dist/internal/layer-plan.js.map +1 -0
- package/dist/internal/screen-width.d.ts +9 -7
- package/dist/internal/screen-width.d.ts.map +1 -1
- package/dist/internal/screen-width.js +15 -13
- package/dist/internal/screen-width.js.map +1 -1
- package/dist/navigator/core.d.ts +2 -1
- package/dist/navigator/core.d.ts.map +1 -1
- package/dist/navigator/core.js +13 -2
- package/dist/navigator/core.js.map +1 -1
- package/dist/url/format.js.map +1 -1
- package/package.json +13 -11
- package/src/components/EntryScope.tsx +13 -2
- package/src/components/Layer.tsx +96 -0
- package/src/components/Screen.tsx +11 -9
- package/src/components/Stack.tsx +136 -92
- package/src/components/TabBar.tsx +21 -21
- package/src/components/Tabs.tsx +15 -1
- package/src/hooks/use-nav-internal.ts +22 -1
- package/src/hooks/use-screen-chrome.ts +122 -0
- package/src/index.ts +2 -0
- package/src/internal/layer-plan.ts +187 -0
- package/src/internal/screen-width.ts +22 -14
- package/src/navigator/core.ts +14 -3
- package/dist/components/ScreenContainer.d.ts +0 -18
- package/dist/components/ScreenContainer.d.ts.map +0 -1
- package/dist/components/ScreenContainer.js +0 -77
- package/dist/components/ScreenContainer.js.map +0 -1
- 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
|
|
3
|
-
* Falls back to
|
|
4
|
-
* load happens BG-side after the bundle initializes, by which time
|
|
5
|
-
* `lynx.SystemInfo` is populated, so the fallback only fires in tests /
|
|
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
|
|
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
|
|
14
|
+
* geometry won't line up. Single shared module avoids drift.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
declare const lynx:
|
|
17
|
-
| {
|
|
18
|
+
| {
|
|
19
|
+
SystemInfo?: {
|
|
20
|
+
pixelWidth?: number;
|
|
21
|
+
pixelHeight?: number;
|
|
22
|
+
pixelRatio?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
18
25
|
| undefined;
|
|
19
26
|
|
|
20
|
-
function
|
|
27
|
+
function readDp(prop: 'pixelWidth' | 'pixelHeight', fallback: number): number {
|
|
21
28
|
try {
|
|
22
29
|
const info = typeof lynx !== 'undefined' ? lynx?.SystemInfo : undefined;
|
|
23
|
-
const
|
|
30
|
+
const px = info?.[prop];
|
|
24
31
|
const pr = info?.pixelRatio || 1;
|
|
25
|
-
if (typeof
|
|
26
|
-
return Math.round(
|
|
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
|
|
38
|
+
return fallback;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
export const SCREEN_WIDTH =
|
|
41
|
+
export const SCREEN_WIDTH = readDp('pixelWidth', 400);
|
|
42
|
+
export const SCREEN_HEIGHT = readDp('pixelHeight', 800);
|
package/src/navigator/core.ts
CHANGED
|
@@ -61,7 +61,8 @@ export interface NavigatorState {
|
|
|
61
61
|
*/
|
|
62
62
|
readonly _screens: {
|
|
63
63
|
register(registry: ScreenRegistry): void;
|
|
64
|
-
|
|
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
|
|
524
|
-
|
|
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
|
-
});
|