@sigx/lynx-navigation 0.4.4 → 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.
@@ -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
- * Wire the Android hardware back button to the active navigator.
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 to the
10
- * deepest currently-focused navigator (per-tab `<Stack>`s register with
11
- * their parent), then walks back up the `parent` chain looking for the
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
- * The traversal means you only need to call this once at the root — a
20
- * back press from inside a tab pops that tab's nested stack first, only
21
- * exiting the app once every level is at its base entry.
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 this once in any component under `<NavigationRoot>` (typically a
24
- * thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
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
- const sub = BackHandler.addEventListener(() => {
44
- // Walk down to the deepest focused nav. Per-tab `<Stack>`s
45
- // register themselves via `parent._children.add(nav)`; only one
46
- // child per level is `isLocallyFocused` at a time, so the
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;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAY,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,eAAe;IAC3B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,GAAG,GAAG,WAAW,CAAC,gBAAgB,CAAC,GAAG,EAAE;YAC1C,2DAA2D;YAC3D,gEAAgE;YAChE,0DAA0D;YAC1D,8DAA8D;YAC9D,iCAAiC;YACjC,IAAI,MAAM,GAAQ,GAAG,CAAC;YACtB,iEAAiE;YACjE,yCAAyC;YACzC,KAAK,EAAE,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACtC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;oBACnC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;wBACzB,MAAM,GAAG,KAAK,CAAC;wBACf,SAAS,KAAK,CAAC;oBACnB,CAAC;gBACL,CAAC;gBACD,kDAAkD;gBAClD,MAAM;YACV,CAAC;YACD,4DAA4D;YAC5D,4DAA4D;YAC5D,gEAAgE;YAChE,uCAAuC;YACvC,IAAI,GAAG,GAAe,MAAM,CAAC;YAC7B,OAAO,GAAG,EAAE,CAAC;gBACT,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;oBAChB,GAAG,CAAC,GAAG,EAAE,CAAC;oBACV,OAAO,IAAI,CAAC;gBAChB,CAAC;gBACD,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;YACrB,CAAC;YACD,8DAA8D;YAC9D,2DAA2D;YAC3D,yDAAyD;YACzD,2BAA2B;YAC3B,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,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
- * - **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.
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
- * - **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.
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
- * - **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).
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
- * `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.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EACR,YAAY,EACZ,UAAU,EAEV,eAAe,EAClB,MAAM,aAAa,CAAC;AAIrB,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;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,CAMzE;AAwCD;;;GAGG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,EAClC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,GACrC,KAAK,EAAE,CAwDT"}
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
- * Suffix used in a layer's render key. Stable for the layer's
8
- * lifetime (same entry, same animation kind) and changes when the
9
- * animation transitions on/off so the Layer remounts and rebinds.
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 function animationVariant(animation) {
12
- if (!animation)
13
- return 'static';
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
- let baseIdx = stack.length - 1;
56
- while (baseIdx > 0 && isOverlayPresentation(stack[baseIdx].presentation)) {
57
- baseIdx -= 1;
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
- // A transition is in flight. `progress` may still be null when
62
- // animations are disabled produce static layers in that case
63
- // (the animation never plays; the transition timer just ticks).
64
- const isOverlay = isOverlayPresentation(transition.topEntry.presentation);
65
- if (!isOverlay) {
66
- // Card transition: just the two participating entries, both
67
- // animated (parallax underneath + slide top).
68
- return [
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
- // Overlay transition: render the full idle layer stack up through
80
- // the underneath entry (all static they don't animate) plus the
81
- // animated top.
82
- const underneathIdx = stack.findIndex((e) => e.key === transition.underneathEntry.key);
83
- // If the underneath isn't in the stack (e.g. mid-pop where the
84
- // stack mutation already removed an entry), fall back to the
85
- // current top of the stack.
86
- const lastStaticIdx = underneathIdx >= 0 ? underneathIdx : stack.length - 1;
87
- let baseIdx = lastStaticIdx;
88
- while (baseIdx > 0 && isOverlayPresentation(stack[baseIdx].presentation)) {
89
- baseIdx -= 1;
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
- const staticLayers = stack
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":"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"}
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,EAKH,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,CA0TjF"}
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"}
@@ -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
- setStack(cur.slice(0, cur.length - 1));
212
- setTransition(null);
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
- setStack(cur.slice(0, cur.length - 1));
218
- setTransition(null);
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
- setStack([...state.stack]);
247
- setTransition(null);
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
- if (cur.length >= 2) {
286
- setStack(cur.slice(0, cur.length - 1));
287
- }
288
- setTransition(null);
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);