@sigx/lynx-navigation 0.4.3 → 0.4.5
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/components/Layer.d.ts +18 -11
- package/dist/components/Layer.d.ts.map +1 -1
- package/dist/components/Layer.js +33 -21
- package/dist/components/Layer.js.map +1 -1
- package/dist/components/NavigationRoot.d.ts +9 -1
- package/dist/components/NavigationRoot.d.ts.map +1 -1
- package/dist/components/NavigationRoot.js +12 -1
- package/dist/components/NavigationRoot.js.map +1 -1
- package/dist/components/Stack.d.ts +19 -7
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Stack.js +28 -15
- package/dist/components/Stack.js.map +1 -1
- package/dist/hooks/use-hardware-back.d.ts +30 -12
- package/dist/hooks/use-hardware-back.d.ts.map +1 -1
- package/dist/hooks/use-hardware-back.js +99 -54
- package/dist/hooks/use-hardware-back.js.map +1 -1
- package/dist/internal/layer-plan.d.ts +56 -30
- package/dist/internal/layer-plan.d.ts.map +1 -1
- package/dist/internal/layer-plan.js +84 -48
- package/dist/internal/layer-plan.js.map +1 -1
- package/dist/navigator/core.d.ts.map +1 -1
- package/dist/navigator/core.js +41 -21
- package/dist/navigator/core.js.map +1 -1
- package/package.json +10 -10
- package/src/components/Layer.tsx +41 -22
- package/src/components/NavigationRoot.tsx +21 -1
- package/src/components/Stack.tsx +58 -14
- package/src/hooks/use-hardware-back.ts +102 -54
- package/src/internal/layer-plan.ts +128 -75
- package/src/navigator/core.ts +42 -21
|
@@ -1,28 +1,109 @@
|
|
|
1
|
-
import { onMounted } from '@sigx/lynx';
|
|
1
|
+
import { onMounted, onUnmounted } from '@sigx/lynx';
|
|
2
2
|
import { BackHandler } from '@sigx/lynx-linking';
|
|
3
3
|
import { useNav } from './use-nav.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Navigator trees with an active hardware-back registration, keyed by
|
|
6
|
+
* their root nav. Used to keep wiring idempotent per tree: `<NavigationRoot>`
|
|
7
|
+
* auto-wires by default, and an app that *also* calls `useHardwareBack()`
|
|
8
|
+
* (or migrates from manual wiring) must not end up with two listeners that
|
|
9
|
+
* both pop on a single back press. The first registration for a root wins;
|
|
10
|
+
* later ones are no-ops until it unsubscribes.
|
|
11
|
+
*/
|
|
12
|
+
const wiredRoots = new WeakSet();
|
|
13
|
+
/** Walk up the `parent` chain to the top-most navigator. */
|
|
14
|
+
function rootOf(nav) {
|
|
15
|
+
let cur = nav;
|
|
16
|
+
while (cur.parent)
|
|
17
|
+
cur = cur.parent;
|
|
18
|
+
return cur;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe the Android hardware back button/gesture to a navigator tree.
|
|
6
22
|
*
|
|
7
23
|
* Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
|
|
8
24
|
* `BackHandler` (which the native side dispatches from
|
|
9
|
-
* `MainActivity.onBackPressed`). On press the handler walks
|
|
10
|
-
* deepest currently-focused navigator (per-tab `<Stack>`s
|
|
11
|
-
* their parent), then walks back up the `parent` chain looking
|
|
12
|
-
* first nav that `canGoBack`:
|
|
25
|
+
* `MainActivity.onBackPressed`). On press the handler walks from the tree's
|
|
26
|
+
* root to the deepest currently-focused navigator (per-tab `<Stack>`s
|
|
27
|
+
* register with their parent), then walks back up the `parent` chain looking
|
|
28
|
+
* for the first nav that `canGoBack`:
|
|
13
29
|
*
|
|
14
30
|
* - If any nav in the chain can go back → `nav.pop()` on that nav.
|
|
15
31
|
* - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
|
|
16
32
|
* keeps the bundle warm; iOS: rejects, since iOS doesn't permit
|
|
17
33
|
* programmatic termination).
|
|
18
34
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
35
|
+
* **Idempotent per tree.** Only the first registration for a given root
|
|
36
|
+
* actually subscribes; later calls return a no-op disposer. So `<NavigationRoot>`
|
|
37
|
+
* auto-wiring this and an app calling `useHardwareBack()` coexist without
|
|
38
|
+
* double-popping.
|
|
39
|
+
*
|
|
40
|
+
* Returns a disposer that unsubscribes (and frees the root for re-wiring).
|
|
41
|
+
* No-op on iOS (event never fires) and in non-native environments (no
|
|
42
|
+
* `GlobalEventEmitter`, e.g. web/SSR/tests) — `addEventListener` returns a
|
|
43
|
+
* no-op subscription there.
|
|
44
|
+
*
|
|
45
|
+
* @internal Apps should rely on `<NavigationRoot>`'s default or call
|
|
46
|
+
* `useHardwareBack()`; this raw form exists so the root can wire from setup.
|
|
47
|
+
*/
|
|
48
|
+
export function wireHardwareBack(nav) {
|
|
49
|
+
const root = rootOf(nav);
|
|
50
|
+
// A registration for this tree already exists — don't add a second
|
|
51
|
+
// listener that would pop twice on one press.
|
|
52
|
+
if (wiredRoots.has(root))
|
|
53
|
+
return () => { };
|
|
54
|
+
wiredRoots.add(root);
|
|
55
|
+
const sub = BackHandler.addEventListener(() => {
|
|
56
|
+
// Walk down to the deepest focused nav. Per-tab `<Stack>`s register
|
|
57
|
+
// themselves via `parent._children.add(nav)`; only one child per
|
|
58
|
+
// level is `isLocallyFocused` at a time, so the traversal is
|
|
59
|
+
// unambiguous. Falls back to the root if no nested stacks are wired.
|
|
60
|
+
let active = root;
|
|
61
|
+
// Loop instead of recursion so a deeply-nested tree doesn't blow the
|
|
62
|
+
// stack on a synchronous back press.
|
|
63
|
+
outer: while (active._children.size > 0) {
|
|
64
|
+
for (const child of active._children) {
|
|
65
|
+
if (child.isLocallyFocused) {
|
|
66
|
+
active = child;
|
|
67
|
+
continue outer;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// No focused child at this level — stop drilling.
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
// Walk back up the chain looking for the first nav that has something
|
|
74
|
+
// to pop. This is what makes "back press in trips tab with empty
|
|
75
|
+
// inner stack" fall through to root (which might have a modal on top)
|
|
76
|
+
// before exiting.
|
|
77
|
+
let cur = active;
|
|
78
|
+
while (cur) {
|
|
79
|
+
if (cur.canGoBack) {
|
|
80
|
+
cur.pop();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
cur = cur.parent;
|
|
84
|
+
}
|
|
85
|
+
// At the root with nothing to pop — leave the app. Promise is
|
|
86
|
+
// fire-and-forget; we don't await because we want the back press to
|
|
87
|
+
// feel instant (Android starts the move-to-back transition
|
|
88
|
+
// immediately).
|
|
89
|
+
void BackHandler.exitApp();
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
return () => {
|
|
93
|
+
wiredRoots.delete(root);
|
|
94
|
+
sub.remove();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wire the Android hardware back button to the active navigator.
|
|
99
|
+
*
|
|
100
|
+
* `<NavigationRoot>` already does this by default (see its `hardwareBack`
|
|
101
|
+
* prop). Use this hook only when you've opted out (`hardwareBack={false}`)
|
|
102
|
+
* and want to wire it yourself — it's safe to call regardless thanks to the
|
|
103
|
+
* idempotency in {@link wireHardwareBack}.
|
|
22
104
|
*
|
|
23
|
-
* Call
|
|
24
|
-
*
|
|
25
|
-
* hook is a no-op there.
|
|
105
|
+
* Call it once in any component under `<NavigationRoot>`. iOS doesn't fire
|
|
106
|
+
* the event so the hook is a no-op there.
|
|
26
107
|
*
|
|
27
108
|
* @example
|
|
28
109
|
* ```tsx
|
|
@@ -31,7 +112,7 @@ import { useNav } from './use-nav.js';
|
|
|
31
112
|
* return () => null;
|
|
32
113
|
* });
|
|
33
114
|
*
|
|
34
|
-
* <NavigationRoot routes={routes}>
|
|
115
|
+
* <NavigationRoot routes={routes} hardwareBack={false}>
|
|
35
116
|
* <BackHandlerWiring />
|
|
36
117
|
* <Stack />
|
|
37
118
|
* </NavigationRoot>
|
|
@@ -39,46 +120,10 @@ import { useNav } from './use-nav.js';
|
|
|
39
120
|
*/
|
|
40
121
|
export function useHardwareBack() {
|
|
41
122
|
const nav = useNav();
|
|
42
|
-
onMounted
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// traversal is unambiguous. Falls back to the starting nav if
|
|
48
|
-
// no nested stacks are wired up.
|
|
49
|
-
let active = nav;
|
|
50
|
-
// Loop instead of recursion so a deeply-nested tree doesn't blow
|
|
51
|
-
// the stack on a synchronous back press.
|
|
52
|
-
outer: while (active._children.size > 0) {
|
|
53
|
-
for (const child of active._children) {
|
|
54
|
-
if (child.isLocallyFocused) {
|
|
55
|
-
active = child;
|
|
56
|
-
continue outer;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// No focused child at this level — stop drilling.
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
// Walk back up the chain looking for the first nav that has
|
|
63
|
-
// something to pop. This is what makes "back press in trips
|
|
64
|
-
// tab with empty inner stack" fall through to root (which might
|
|
65
|
-
// have a modal on top) before exiting.
|
|
66
|
-
let cur = active;
|
|
67
|
-
while (cur) {
|
|
68
|
-
if (cur.canGoBack) {
|
|
69
|
-
cur.pop();
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
cur = cur.parent;
|
|
73
|
-
}
|
|
74
|
-
// At the root with nothing to pop — leave the app. Promise is
|
|
75
|
-
// fire-and-forget; we don't await because we want the back
|
|
76
|
-
// press to feel instant (Android starts the move-to-back
|
|
77
|
-
// transition immediately).
|
|
78
|
-
void BackHandler.exitApp();
|
|
79
|
-
return true;
|
|
80
|
-
});
|
|
81
|
-
return () => sub.remove();
|
|
82
|
-
});
|
|
123
|
+
// `onMounted`'s return value isn't a cleanup hook in sigx — register the
|
|
124
|
+
// disposer with `onUnmounted` explicitly so the listener is released.
|
|
125
|
+
let dispose = () => { };
|
|
126
|
+
onMounted(() => { dispose = wireHardwareBack(nav); });
|
|
127
|
+
onUnmounted(() => { dispose(); });
|
|
83
128
|
}
|
|
84
129
|
//# sourceMappingURL=use-hardware-back.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-hardware-back.js","sourceRoot":"","sources":["../../src/hooks/use-hardware-back.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"use-hardware-back.js","sourceRoot":"","sources":["../../src/hooks/use-hardware-back.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAY,MAAM,cAAc,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,GAAG,IAAI,OAAO,EAAO,CAAC;AAEtC,4DAA4D;AAC5D,SAAS,MAAM,CAAC,GAAQ;IACpB,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,OAAO,GAAG,CAAC,MAAM;QAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;IACpC,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAQ;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,mEAAmE;IACnE,8CAA8C;IAC9C,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAC1C,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAErB,MAAM,GAAG,GAAG,WAAW,CAAC,gBAAgB,CAAC,GAAG,EAAE;QAC1C,oEAAoE;QACpE,iEAAiE;QACjE,6DAA6D;QAC7D,qEAAqE;QACrE,IAAI,MAAM,GAAQ,IAAI,CAAC;QACvB,qEAAqE;QACrE,qCAAqC;QACrC,KAAK,EAAE,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACtC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACnC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;oBACzB,MAAM,GAAG,KAAK,CAAC;oBACf,SAAS,KAAK,CAAC;gBACnB,CAAC;YACL,CAAC;YACD,kDAAkD;YAClD,MAAM;QACV,CAAC;QACD,sEAAsE;QACtE,iEAAiE;QACjE,sEAAsE;QACtE,kBAAkB;QAClB,IAAI,GAAG,GAAe,MAAM,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC;YACT,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;gBAChB,GAAG,CAAC,GAAG,EAAE,CAAC;gBACV,OAAO,IAAI,CAAC;YAChB,CAAC;YACD,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;QACrB,CAAC;QACD,8DAA8D;QAC9D,oEAAoE;QACpE,2DAA2D;QAC3D,gBAAgB;QAChB,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE;QACR,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxB,GAAG,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,eAAe;IAC3B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,sEAAsE;IACtE,IAAI,OAAO,GAAe,GAAG,EAAE,GAAE,CAAC,CAAC;IACnC,SAAS,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -13,35 +13,51 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Rules:
|
|
15
15
|
*
|
|
16
|
-
* - **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* - **Visible region.** Each branch first computes its *visible*
|
|
17
|
+
* region — the layers the user can actually see:
|
|
18
|
+
* - **Idle (no transition).** The topmost non-overlay entry as
|
|
19
|
+
* the base, plus every overlay entry above it.
|
|
20
|
+
* - **Card transition.** The underneath entry (parallax) + the
|
|
21
|
+
* top entry (slide), both animated.
|
|
22
|
+
* - **Overlay transition.** The static base..underneath run plus
|
|
23
|
+
* the animated overlay top.
|
|
21
24
|
*
|
|
22
|
-
* - **
|
|
23
|
-
*
|
|
24
|
-
* (
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* - **Retention (card-stack screen retention).** Every entry *below*
|
|
26
|
+
* the visible region's base is kept mounted as a `hidden: true`
|
|
27
|
+
* static layer (`display: none`), instead of unmounting. So a card
|
|
28
|
+
* push leaves the covered card mounted, and a pop reveals the
|
|
29
|
+
* already-mounted underneath — no rebuild, no lost scroll/UI state
|
|
30
|
+
* (matching native stacks). Overlays already preserved their
|
|
31
|
+
* underneath; this extends the same retain-below-base behaviour to
|
|
32
|
+
* cards, and to entries deeper than the immediate underneath during
|
|
33
|
+
* a deep-stack transition.
|
|
27
34
|
*
|
|
28
|
-
* - **
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* (pop).
|
|
35
|
+
* - **Bounds.** `maxRetained` (the `<Stack maxRetainedScreens>` prop)
|
|
36
|
+
* caps how many covered cards stay mounted; `MAX_LAYERS` is the hard
|
|
37
|
+
* renderer-slot cap. Both trim the *deepest* (front) layers, so the
|
|
38
|
+
* visible region at the end of the list is never truncated.
|
|
33
39
|
*
|
|
34
|
-
* The Layer.key for the Stack render is
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* The Layer.key for the Stack render is `layer-${entry.key}` — stable
|
|
41
|
+
* across animation phases. `<Layer>` rebinds its transform reactively
|
|
42
|
+
* (via the reactive form of `useAnimatedStyle`) as `animation` flips
|
|
43
|
+
* between a spec and `null`, so the layer never remounts just to change
|
|
44
|
+
* its animation state and screen subtrees survive the transition. The
|
|
45
|
+
* reactive binding dedupes its own register/unregister by signature
|
|
46
|
+
* internally (see `useAnimatedStyle`), so no per-layer variant key is
|
|
47
|
+
* computed here.
|
|
42
48
|
*/
|
|
43
49
|
import type { SharedValue } from '@sigx/lynx';
|
|
44
50
|
import type { Presentation, StackEntry, TransitionState } from '../types.js';
|
|
51
|
+
/**
|
|
52
|
+
* Hard cap on how many layers `<Stack>` renders at once. The Stack body
|
|
53
|
+
* emits exactly this many position-stable slots (unrolled), so
|
|
54
|
+
* `computeLayers` must never return more — excess deepest (hidden,
|
|
55
|
+
* retained) layers are trimmed off the front. The slots are mechanical
|
|
56
|
+
* (just verbose), so this can be raised if an app legitimately stacks
|
|
57
|
+
* more; 24 is high enough that normal card apps never hit the trim
|
|
58
|
+
* boundary, while still bounding retained-screen memory.
|
|
59
|
+
*/
|
|
60
|
+
export declare const MAX_LAYERS = 24;
|
|
45
61
|
export type LayerAnimation = {
|
|
46
62
|
axis: 'translateX' | 'translateY';
|
|
47
63
|
inputRange: readonly [number, number];
|
|
@@ -53,17 +69,27 @@ export interface Layer {
|
|
|
53
69
|
readonly entry: StackEntry;
|
|
54
70
|
/** When non-null, the layer's host view binds a `useAnimatedStyle` mapper. */
|
|
55
71
|
readonly animation: LayerAnimation | null;
|
|
72
|
+
/**
|
|
73
|
+
* Retained-but-covered layer: mounted (so its screen subtree, signals,
|
|
74
|
+
* and scroll offset survive), rendered with `display: none` so it costs
|
|
75
|
+
* no paint/layout while a higher opaque card covers it. Always paired
|
|
76
|
+
* with `animation: null` — a hidden layer never animates. Falsy/omitted
|
|
77
|
+
* = visible.
|
|
78
|
+
*/
|
|
79
|
+
readonly hidden?: boolean;
|
|
56
80
|
}
|
|
57
81
|
export declare function isOverlayPresentation(p: Presentation): boolean;
|
|
58
|
-
/**
|
|
59
|
-
* Suffix used in a layer's render key. Stable for the layer's
|
|
60
|
-
* lifetime (same entry, same animation kind) and changes when the
|
|
61
|
-
* animation transitions on/off so the Layer remounts and rebinds.
|
|
62
|
-
*/
|
|
63
|
-
export declare function animationVariant(animation: LayerAnimation | null): string;
|
|
64
82
|
/**
|
|
65
83
|
* Compute the visible-layer list for one render of `<Stack>`. Pure —
|
|
66
84
|
* unit-testable independently of the renderer.
|
|
85
|
+
*
|
|
86
|
+
* Each branch produces `{ visBaseIdx, visible }`: `visible` is the
|
|
87
|
+
* animated/painted region, and `visBaseIdx` is the stack index of that
|
|
88
|
+
* region's lowest layer. Everything in `stack` below `visBaseIdx` is
|
|
89
|
+
* then retained as hidden static layers (see the module docstring).
|
|
90
|
+
*
|
|
91
|
+
* `maxRetained` caps the retained (hidden) layers; `undefined` means
|
|
92
|
+
* "retain all, bounded only by `MAX_LAYERS`".
|
|
67
93
|
*/
|
|
68
|
-
export declare function computeLayers(stack: readonly StackEntry[], transition: TransitionState | null, progress: SharedValue<number> | null): Layer[];
|
|
94
|
+
export declare function computeLayers(stack: readonly StackEntry[], transition: TransitionState | null, progress: SharedValue<number> | null, maxRetained?: number): Layer[];
|
|
69
95
|
//# sourceMappingURL=layer-plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layer-plan.d.ts","sourceRoot":"","sources":["../../src/internal/layer-plan.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"layer-plan.d.ts","sourceRoot":"","sources":["../../src/internal/layer-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EACR,YAAY,EACZ,UAAU,EAEV,eAAe,EAClB,MAAM,aAAa,CAAC;AAIrB;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,KAAK,CAAC;AAE7B,MAAM,MAAM,cAAc,GAAG;IACzB,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;IAClC,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAEF,MAAM,WAAW,KAAK;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAE9D;AAiDD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,EAClC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,WAAW,CAAC,EAAE,MAAM,GACrB,KAAK,EAAE,CA+ET"}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { SCREEN_HEIGHT, SCREEN_WIDTH } from './screen-width.js';
|
|
2
2
|
const PARALLAX_FACTOR = 0.3;
|
|
3
|
-
export function isOverlayPresentation(p) {
|
|
4
|
-
return p === 'modal' || p === 'fullScreen' || p === 'transparent-modal';
|
|
5
|
-
}
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Hard cap on how many layers `<Stack>` renders at once. The Stack body
|
|
5
|
+
* emits exactly this many position-stable slots (unrolled), so
|
|
6
|
+
* `computeLayers` must never return more — excess deepest (hidden,
|
|
7
|
+
* retained) layers are trimmed off the front. The slots are mechanical
|
|
8
|
+
* (just verbose), so this can be raised if an app legitimately stacks
|
|
9
|
+
* more; 24 is high enough that normal card apps never hit the trim
|
|
10
|
+
* boundary, while still bounding retained-screen memory.
|
|
10
11
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Output range alone identifies the transition shape — different
|
|
15
|
-
// animations (card-top vs card-underneath vs overlay-top, push vs
|
|
16
|
-
// pop) all land on different range tuples.
|
|
17
|
-
return `${animation.axis}:${animation.outputRange[0]}->${animation.outputRange[1]}`;
|
|
12
|
+
export const MAX_LAYERS = 24;
|
|
13
|
+
export function isOverlayPresentation(p) {
|
|
14
|
+
return p === 'modal' || p === 'fullScreen' || p === 'transparent-modal';
|
|
18
15
|
}
|
|
19
16
|
/**
|
|
20
17
|
* Card-presentation transition transforms. `role='top'` is the entry
|
|
@@ -45,58 +42,97 @@ function overlayTopAnimation(kind, progress) {
|
|
|
45
42
|
}
|
|
46
43
|
return { axis: 'translateY', inputRange: [0, 1], outputRange: [0, SCREEN_HEIGHT], progress };
|
|
47
44
|
}
|
|
45
|
+
/** Walk back from `from` past overlay entries to the topmost non-overlay. */
|
|
46
|
+
function nonOverlayBaseIdx(stack, from) {
|
|
47
|
+
let baseIdx = from;
|
|
48
|
+
while (baseIdx > 0 && isOverlayPresentation(stack[baseIdx].presentation)) {
|
|
49
|
+
baseIdx -= 1;
|
|
50
|
+
}
|
|
51
|
+
return baseIdx;
|
|
52
|
+
}
|
|
48
53
|
/**
|
|
49
54
|
* Compute the visible-layer list for one render of `<Stack>`. Pure —
|
|
50
55
|
* unit-testable independently of the renderer.
|
|
56
|
+
*
|
|
57
|
+
* Each branch produces `{ visBaseIdx, visible }`: `visible` is the
|
|
58
|
+
* animated/painted region, and `visBaseIdx` is the stack index of that
|
|
59
|
+
* region's lowest layer. Everything in `stack` below `visBaseIdx` is
|
|
60
|
+
* then retained as hidden static layers (see the module docstring).
|
|
61
|
+
*
|
|
62
|
+
* `maxRetained` caps the retained (hidden) layers; `undefined` means
|
|
63
|
+
* "retain all, bounded only by `MAX_LAYERS`".
|
|
51
64
|
*/
|
|
52
|
-
export function computeLayers(stack, transition, progress) {
|
|
65
|
+
export function computeLayers(stack, transition, progress, maxRetained) {
|
|
66
|
+
let visBaseIdx;
|
|
67
|
+
let visible;
|
|
53
68
|
if (!transition) {
|
|
54
69
|
// Idle: topmost non-overlay base + any overlays above it.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return stack.slice(baseIdx).map((entry) => ({ entry, animation: null }));
|
|
70
|
+
visBaseIdx = nonOverlayBaseIdx(stack, stack.length - 1);
|
|
71
|
+
visible = stack
|
|
72
|
+
.slice(visBaseIdx)
|
|
73
|
+
.map((entry) => ({ entry, animation: null, hidden: false }));
|
|
60
74
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
75
|
+
else if (!isOverlayPresentation(transition.topEntry.presentation)) {
|
|
76
|
+
// Card transition: the two participating entries, both animated
|
|
77
|
+
// (parallax underneath + slide top). `progress` may be null when
|
|
78
|
+
// animations are disabled — produce static layers in that case.
|
|
79
|
+
const underneathIdx = stack.findIndex((e) => e.key === transition.underneathEntry.key);
|
|
80
|
+
// Underneath is the visible base. If it isn't on the stack (e.g.
|
|
81
|
+
// mid-pop where the mutation already ran), retain nothing rather
|
|
82
|
+
// than slicing with a negative index.
|
|
83
|
+
visBaseIdx = underneathIdx >= 0 ? underneathIdx : 0;
|
|
84
|
+
visible = [
|
|
69
85
|
{
|
|
70
86
|
entry: transition.underneathEntry,
|
|
71
87
|
animation: progress ? cardAnimation('underneath', transition.kind, progress) : null,
|
|
88
|
+
hidden: false,
|
|
72
89
|
},
|
|
73
90
|
{
|
|
74
91
|
entry: transition.topEntry,
|
|
75
92
|
animation: progress ? cardAnimation('top', transition.kind, progress) : null,
|
|
93
|
+
hidden: false,
|
|
76
94
|
},
|
|
77
95
|
];
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
else {
|
|
98
|
+
// Overlay transition: the full layer stack up through the
|
|
99
|
+
// underneath entry stays static (no transform) plus the animated
|
|
100
|
+
// top.
|
|
101
|
+
const underneathIdx = stack.findIndex((e) => e.key === transition.underneathEntry.key);
|
|
102
|
+
// If the underneath isn't in the stack (e.g. mid-pop where the
|
|
103
|
+
// stack mutation already removed an entry), fall back to the
|
|
104
|
+
// current top of the stack.
|
|
105
|
+
const lastStaticIdx = underneathIdx >= 0 ? underneathIdx : stack.length - 1;
|
|
106
|
+
visBaseIdx = nonOverlayBaseIdx(stack, lastStaticIdx);
|
|
107
|
+
const staticLayers = stack
|
|
108
|
+
.slice(visBaseIdx, lastStaticIdx + 1)
|
|
109
|
+
.map((entry) => ({ entry, animation: null, hidden: false }));
|
|
110
|
+
visible = [
|
|
111
|
+
...staticLayers,
|
|
112
|
+
{
|
|
113
|
+
entry: transition.topEntry,
|
|
114
|
+
animation: progress ? overlayTopAnimation(transition.kind, progress) : null,
|
|
115
|
+
hidden: false,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
// Retention: every entry below the visible base stays mounted as a
|
|
120
|
+
// hidden static layer. Ordered deepest-first so each surviving entry
|
|
121
|
+
// keeps a stable slot index across pushes/pops (the Stack renderer's
|
|
122
|
+
// fixed slots remount keyed children whose slot index shifts).
|
|
123
|
+
const retained = stack
|
|
124
|
+
.slice(0, visBaseIdx)
|
|
125
|
+
.map((entry) => ({ entry, animation: null, hidden: true }));
|
|
126
|
+
let result = [...retained, ...visible];
|
|
127
|
+
// Apply the user's retention window, then the hard renderer cap.
|
|
128
|
+
// Both trim the deepest (front) layers, so the visible region at the
|
|
129
|
+
// tail is never truncated.
|
|
130
|
+
if (maxRetained != null && retained.length > maxRetained) {
|
|
131
|
+
result = result.slice(retained.length - maxRetained);
|
|
132
|
+
}
|
|
133
|
+
if (result.length > MAX_LAYERS) {
|
|
134
|
+
result = result.slice(result.length - MAX_LAYERS);
|
|
90
135
|
}
|
|
91
|
-
|
|
92
|
-
.slice(baseIdx, lastStaticIdx + 1)
|
|
93
|
-
.map((entry) => ({ entry, animation: null }));
|
|
94
|
-
return [
|
|
95
|
-
...staticLayers,
|
|
96
|
-
{
|
|
97
|
-
entry: transition.topEntry,
|
|
98
|
-
animation: progress ? overlayTopAnimation(transition.kind, progress) : null,
|
|
99
|
-
},
|
|
100
|
-
];
|
|
136
|
+
return result;
|
|
101
137
|
}
|
|
102
138
|
//# sourceMappingURL=layer-plan.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layer-plan.js","sourceRoot":"","sources":["../../src/internal/layer-plan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"layer-plan.js","sourceRoot":"","sources":["../../src/internal/layer-plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/navigator/core.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/navigator/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,KAAK,WAAW,EACnB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAIR,QAAQ,EACR,UAAU,EAEb,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE;QACf,gBAAgB,IAAI,IAAI,CAAC;QACzB,iBAAiB,IAAI,IAAI,CAAC;QAC1B,iBAAiB,IAAI,IAAI,CAAC;KAC7B,CAAC;IACF;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,EAAE;QACf,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;QACzC,wEAAwE;QACxE,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;QAC3C,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;KACrD,CAAC;IACF;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3D;AAuED,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,QAAQ,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IACpB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,GAAG,cAAc,CA8UjF"}
|
package/dist/navigator/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runOnMainThread, signal, untrack, } from '@sigx/lynx';
|
|
1
|
+
import { batch, runOnMainThread, signal, untrack, } from '@sigx/lynx';
|
|
2
2
|
import { isLazyComponent } from '@sigx/lynx';
|
|
3
3
|
import { withTiming } from '@sigx/lynx-motion';
|
|
4
4
|
/**
|
|
@@ -155,19 +155,28 @@ export function createNavigatorState(opts) {
|
|
|
155
155
|
const newEntry = makeEntry(name, params, search, options, routes);
|
|
156
156
|
const cur = getStack();
|
|
157
157
|
const prevTop = cur[cur.length - 1];
|
|
158
|
-
// Append eagerly — UX-wise the user just initiated a forward nav, so
|
|
159
|
-
// the new entry should be queryable immediately (`nav.current` =
|
|
160
|
-
// newEntry). The slide animation overlays the visual transition.
|
|
161
|
-
setStack([...cur, newEntry]);
|
|
162
158
|
const animated = options?.animated !== false && !!progress;
|
|
159
|
+
// Commit the stack append and the transition in a single batch so the
|
|
160
|
+
// Stack renders once with both screens already present. Without the
|
|
161
|
+
// batch, `@sigx/reactivity` flushes the stack write eagerly, producing
|
|
162
|
+
// an intermediate render where only the new top is on the stack and
|
|
163
|
+
// no transition is in flight — `computeLayers` would drop the
|
|
164
|
+
// underneath and the Stack would remount it on the next render.
|
|
165
|
+
// Append eagerly so the new entry is queryable immediately
|
|
166
|
+
// (`nav.current` = newEntry); the slide animation overlays the visual.
|
|
167
|
+
batch(() => {
|
|
168
|
+
setStack([...cur, newEntry]);
|
|
169
|
+
if (animated) {
|
|
170
|
+
setTransition({
|
|
171
|
+
kind: 'push',
|
|
172
|
+
topEntry: newEntry,
|
|
173
|
+
underneathEntry: prevTop,
|
|
174
|
+
progress,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
163
178
|
if (!animated)
|
|
164
179
|
return;
|
|
165
|
-
setTransition({
|
|
166
|
-
kind: 'push',
|
|
167
|
-
topEntry: newEntry,
|
|
168
|
-
underneathEntry: prevTop,
|
|
169
|
-
progress,
|
|
170
|
-
});
|
|
171
180
|
animateProgress(1, TRANSITION_DURATION_SEC).then(() => setTransition(null), () => setTransition(null));
|
|
172
181
|
});
|
|
173
182
|
const replace = ((name, ...args) => {
|
|
@@ -208,14 +217,21 @@ export function createNavigatorState(opts) {
|
|
|
208
217
|
progress,
|
|
209
218
|
});
|
|
210
219
|
animateProgress(1, TRANSITION_DURATION_SEC).then(() => {
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
// Batch so the commit (drop the popped entry) and clearing the
|
|
221
|
+
// transition land in one render — no intermediate frame where
|
|
222
|
+
// the stack has mutated but the transition is still in flight.
|
|
223
|
+
batch(() => {
|
|
224
|
+
setStack(cur.slice(0, cur.length - 1));
|
|
225
|
+
setTransition(null);
|
|
226
|
+
});
|
|
213
227
|
}, () => {
|
|
214
228
|
// On animation failure, snap to the destination state anyway —
|
|
215
229
|
// leaving the popped entry rendered would be more confusing
|
|
216
230
|
// than skipping the animation.
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
batch(() => {
|
|
232
|
+
setStack(cur.slice(0, cur.length - 1));
|
|
233
|
+
setTransition(null);
|
|
234
|
+
});
|
|
219
235
|
});
|
|
220
236
|
}
|
|
221
237
|
function popTo(name) {
|
|
@@ -243,8 +259,10 @@ export function createNavigatorState(opts) {
|
|
|
243
259
|
if (state.stack.length === 0) {
|
|
244
260
|
throw new Error('[lynx-navigation] reset() called with empty stack.');
|
|
245
261
|
}
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
batch(() => {
|
|
263
|
+
setStack([...state.stack]);
|
|
264
|
+
setTransition(null);
|
|
265
|
+
});
|
|
248
266
|
}
|
|
249
267
|
function dismiss() {
|
|
250
268
|
if (isTransitioning())
|
|
@@ -282,10 +300,12 @@ export function createNavigatorState(opts) {
|
|
|
282
300
|
}
|
|
283
301
|
function commitBackGesture() {
|
|
284
302
|
const cur = getStack();
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
303
|
+
batch(() => {
|
|
304
|
+
if (cur.length >= 2) {
|
|
305
|
+
setStack(cur.slice(0, cur.length - 1));
|
|
306
|
+
}
|
|
307
|
+
setTransition(null);
|
|
308
|
+
});
|
|
289
309
|
}
|
|
290
310
|
function cancelBackGesture() {
|
|
291
311
|
setTransition(null);
|