@sigx/lynx-navigation 0.1.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +189 -7
  2. package/dist/components/EntryScope.d.ts +1 -1
  3. package/dist/components/EntryScope.d.ts.map +1 -1
  4. package/dist/components/Layer.d.ts +34 -0
  5. package/dist/components/Layer.d.ts.map +1 -0
  6. package/dist/components/Link.d.ts +2 -2
  7. package/dist/components/Link.d.ts.map +1 -1
  8. package/dist/components/NavigationRoot.d.ts +2 -2
  9. package/dist/components/NavigationRoot.d.ts.map +1 -1
  10. package/dist/components/Screen.d.ts +6 -6
  11. package/dist/components/Screen.d.ts.map +1 -1
  12. package/dist/components/Stack.d.ts +83 -13
  13. package/dist/components/Stack.d.ts.map +1 -1
  14. package/dist/components/TabBar.d.ts +19 -20
  15. package/dist/components/TabBar.d.ts.map +1 -1
  16. package/dist/components/Tabs.d.ts +30 -21
  17. package/dist/components/Tabs.d.ts.map +1 -1
  18. package/dist/define-routes.d.ts +1 -1
  19. package/dist/define-routes.d.ts.map +1 -1
  20. package/dist/hooks/use-focus.d.ts.map +1 -1
  21. package/dist/hooks/use-hardware-back.d.ts +9 -2
  22. package/dist/hooks/use-hardware-back.d.ts.map +1 -1
  23. package/dist/hooks/use-linking-nav.d.ts +3 -3
  24. package/dist/hooks/use-linking-nav.d.ts.map +1 -1
  25. package/dist/hooks/use-nav-internal.d.ts +21 -3
  26. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  27. package/dist/hooks/use-nav-serializer.d.ts +1 -1
  28. package/dist/hooks/use-nav-serializer.d.ts.map +1 -1
  29. package/dist/hooks/use-nav.d.ts +38 -3
  30. package/dist/hooks/use-nav.d.ts.map +1 -1
  31. package/dist/hooks/use-params.d.ts +1 -1
  32. package/dist/hooks/use-params.d.ts.map +1 -1
  33. package/dist/hooks/use-screen-chrome.d.ts +19 -0
  34. package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
  35. package/dist/hooks/use-screen-options.d.ts +1 -1
  36. package/dist/hooks/use-screen-options.d.ts.map +1 -1
  37. package/dist/hooks/use-search.d.ts +1 -1
  38. package/dist/hooks/use-search.d.ts.map +1 -1
  39. package/dist/href.d.ts +2 -2
  40. package/dist/href.d.ts.map +1 -1
  41. package/dist/index.d.ts +33 -31
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1160 -29
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/layer-plan.d.ts +69 -0
  46. package/dist/internal/layer-plan.d.ts.map +1 -0
  47. package/dist/internal/screen-registry.d.ts +1 -1
  48. package/dist/internal/screen-registry.d.ts.map +1 -1
  49. package/dist/internal/screen-width.d.ts +9 -7
  50. package/dist/internal/screen-width.d.ts.map +1 -1
  51. package/dist/navigator/core.d.ts +31 -4
  52. package/dist/navigator/core.d.ts.map +1 -1
  53. package/dist/register.d.ts +1 -1
  54. package/dist/register.d.ts.map +1 -1
  55. package/dist/url/index.d.ts +6 -6
  56. package/dist/url/index.d.ts.map +1 -1
  57. package/dist/url/parse.d.ts +1 -1
  58. package/dist/url/parse.d.ts.map +1 -1
  59. package/dist/url/registry.d.ts +2 -2
  60. package/dist/url/registry.d.ts.map +1 -1
  61. package/dist/url/validate.d.ts +1 -1
  62. package/dist/url/validate.d.ts.map +1 -1
  63. package/package.json +11 -10
  64. package/src/components/Drawer.d.ts +55 -0
  65. package/src/components/EdgeBackHandle.d.ts +1 -0
  66. package/src/components/EdgeBackHandle.tsx +2 -2
  67. package/{dist/components/EntryScope.js → src/components/EntryScope.d.ts} +7 -15
  68. package/src/components/EntryScope.tsx +15 -4
  69. package/src/components/Header.d.ts +6 -0
  70. package/src/components/Header.tsx +3 -3
  71. package/src/components/Layer.d.ts +33 -0
  72. package/src/components/Layer.tsx +96 -0
  73. package/src/components/Link.d.ts +60 -0
  74. package/src/components/Link.tsx +4 -4
  75. package/src/components/NavigationRoot.d.ts +36 -0
  76. package/src/components/NavigationRoot.tsx +6 -6
  77. package/src/components/Screen.d.ts +97 -0
  78. package/src/components/Screen.tsx +13 -11
  79. package/src/components/Stack.d.ts +90 -0
  80. package/src/components/Stack.tsx +333 -92
  81. package/src/components/TabBar.d.ts +38 -0
  82. package/src/components/TabBar.tsx +22 -22
  83. package/src/components/Tabs.d.ts +109 -0
  84. package/src/components/Tabs.tsx +54 -22
  85. package/{dist/define-routes.js → src/define-routes.d.ts} +2 -4
  86. package/src/define-routes.ts +1 -1
  87. package/{dist/hooks/use-focus.js → src/hooks/use-focus.d.ts} +3 -39
  88. package/src/hooks/use-focus.ts +9 -3
  89. package/src/hooks/use-hardware-back.d.ts +37 -0
  90. package/src/hooks/use-hardware-back.ts +43 -9
  91. package/src/hooks/use-linking-nav.d.ts +91 -0
  92. package/src/hooks/use-linking-nav.ts +4 -4
  93. package/src/hooks/use-nav-internal.d.ts +91 -0
  94. package/src/hooks/use-nav-internal.ts +24 -3
  95. package/src/hooks/use-nav-serializer.d.ts +82 -0
  96. package/src/hooks/use-nav-serializer.ts +3 -3
  97. package/src/hooks/use-nav.d.ts +111 -0
  98. package/src/hooks/use-nav.ts +40 -3
  99. package/{dist/hooks/use-params.js → src/hooks/use-params.d.ts} +2 -6
  100. package/src/hooks/use-params.ts +2 -2
  101. package/src/hooks/use-screen-chrome.d.ts +18 -0
  102. package/src/hooks/use-screen-chrome.ts +122 -0
  103. package/src/hooks/use-screen-options.d.ts +2 -0
  104. package/src/hooks/use-screen-options.ts +3 -3
  105. package/{dist/hooks/use-search.js → src/hooks/use-search.d.ts} +2 -6
  106. package/src/hooks/use-search.ts +2 -2
  107. package/src/href.d.ts +54 -0
  108. package/src/href.ts +6 -6
  109. package/src/index.d.ts +39 -0
  110. package/src/index.ts +33 -31
  111. package/src/internal/layer-plan.d.ts +68 -0
  112. package/src/internal/layer-plan.ts +187 -0
  113. package/{dist/internal/screen-registry.js → src/internal/screen-registry.d.ts} +21 -32
  114. package/src/internal/screen-registry.ts +1 -1
  115. package/src/internal/screen-width.d.ts +17 -0
  116. package/src/internal/screen-width.ts +22 -14
  117. package/src/navigator/core.d.ts +96 -0
  118. package/src/navigator/core.ts +90 -10
  119. package/src/register.d.ts +37 -0
  120. package/src/register.ts +1 -1
  121. package/src/types.d.ts +217 -0
  122. package/src/url/build.d.ts +15 -0
  123. package/src/url/build.ts +2 -2
  124. package/src/url/compile.d.ts +34 -0
  125. package/src/url/format.d.ts +28 -0
  126. package/src/url/index.ts +6 -6
  127. package/src/url/parse.d.ts +20 -0
  128. package/src/url/parse.ts +6 -6
  129. package/{dist/url/registry.js → src/url/registry.d.ts} +12 -28
  130. package/src/url/registry.ts +3 -3
  131. package/src/url/validate.d.ts +23 -0
  132. package/src/url/validate.ts +1 -1
  133. package/dist/components/Drawer.js +0 -74
  134. package/dist/components/Drawer.js.map +0 -1
  135. package/dist/components/EdgeBackHandle.js +0 -144
  136. package/dist/components/EdgeBackHandle.js.map +0 -1
  137. package/dist/components/EntryScope.js.map +0 -1
  138. package/dist/components/Header.js +0 -103
  139. package/dist/components/Header.js.map +0 -1
  140. package/dist/components/Link.js +0 -51
  141. package/dist/components/Link.js.map +0 -1
  142. package/dist/components/NavigationRoot.js +0 -67
  143. package/dist/components/NavigationRoot.js.map +0 -1
  144. package/dist/components/Screen.js +0 -94
  145. package/dist/components/Screen.js.map +0 -1
  146. package/dist/components/ScreenContainer.d.ts +0 -18
  147. package/dist/components/ScreenContainer.d.ts.map +0 -1
  148. package/dist/components/ScreenContainer.js +0 -77
  149. package/dist/components/ScreenContainer.js.map +0 -1
  150. package/dist/components/Stack.js +0 -75
  151. package/dist/components/Stack.js.map +0 -1
  152. package/dist/components/TabBar.js +0 -63
  153. package/dist/components/TabBar.js.map +0 -1
  154. package/dist/components/Tabs.js +0 -140
  155. package/dist/components/Tabs.js.map +0 -1
  156. package/dist/define-routes.js.map +0 -1
  157. package/dist/hooks/use-focus.js.map +0 -1
  158. package/dist/hooks/use-hardware-back.js +0 -50
  159. package/dist/hooks/use-hardware-back.js.map +0 -1
  160. package/dist/hooks/use-linking-nav.js +0 -109
  161. package/dist/hooks/use-linking-nav.js.map +0 -1
  162. package/dist/hooks/use-nav-internal.js +0 -44
  163. package/dist/hooks/use-nav-internal.js.map +0 -1
  164. package/dist/hooks/use-nav-serializer.js +0 -181
  165. package/dist/hooks/use-nav-serializer.js.map +0 -1
  166. package/dist/hooks/use-nav.js +0 -11
  167. package/dist/hooks/use-nav.js.map +0 -1
  168. package/dist/hooks/use-params.js.map +0 -1
  169. package/dist/hooks/use-screen-options.js +0 -43
  170. package/dist/hooks/use-screen-options.js.map +0 -1
  171. package/dist/hooks/use-search.js.map +0 -1
  172. package/dist/href.js +0 -57
  173. package/dist/href.js.map +0 -1
  174. package/dist/internal/screen-registry.js.map +0 -1
  175. package/dist/internal/screen-width.js +0 -30
  176. package/dist/internal/screen-width.js.map +0 -1
  177. package/dist/navigator/core.js +0 -344
  178. package/dist/navigator/core.js.map +0 -1
  179. package/dist/register.js +0 -2
  180. package/dist/register.js.map +0 -1
  181. package/dist/types.js +0 -9
  182. package/dist/types.js.map +0 -1
  183. package/dist/url/build.js +0 -30
  184. package/dist/url/build.js.map +0 -1
  185. package/dist/url/compile.js +0 -83
  186. package/dist/url/compile.js.map +0 -1
  187. package/dist/url/format.js +0 -102
  188. package/dist/url/format.js.map +0 -1
  189. package/dist/url/index.js +0 -13
  190. package/dist/url/index.js.map +0 -1
  191. package/dist/url/parse.js +0 -94
  192. package/dist/url/parse.js.map +0 -1
  193. package/dist/url/registry.js.map +0 -1
  194. package/dist/url/validate.js +0 -37
  195. package/dist/url/validate.js.map +0 -1
  196. package/src/components/ScreenContainer.tsx +0 -114
package/src/index.ts CHANGED
@@ -5,46 +5,48 @@
5
5
  * Coming next: Screen with slot-based header API, MTS transitions, Tabs.
6
6
  */
7
7
 
8
- export { defineRoutes } from './define-routes.js';
9
- export type { Register, RegisteredRoutes, RouteId, RouteParams, RouteSearch } from './register.js';
10
- export { useNav } from './hooks/use-nav.js';
11
- export type { Nav, RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav.js';
12
- export { useParams } from './hooks/use-params.js';
13
- export { useSearch } from './hooks/use-search.js';
14
- export { useHardwareBack } from './hooks/use-hardware-back.js';
15
- export { useLinkingNav } from './hooks/use-linking-nav.js';
16
- export type { UseLinkingNavOptions } from './hooks/use-linking-nav.js';
17
- export { useIsFocused, useFocusEffect } from './hooks/use-focus.js';
18
- export { useScreenOptions } from './hooks/use-screen-options.js';
8
+ export { defineRoutes } from './define-routes';
9
+ export type { Register, RegisteredRoutes, RouteId, RouteParams, RouteSearch } from './register';
10
+ export { useNav } from './hooks/use-nav';
11
+ export type { Nav, RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav';
12
+ export { useParams } from './hooks/use-params';
13
+ export { useSearch } from './hooks/use-search';
14
+ export { useHardwareBack } from './hooks/use-hardware-back';
15
+ export { useLinkingNav } from './hooks/use-linking-nav';
16
+ export type { UseLinkingNavOptions } from './hooks/use-linking-nav';
17
+ export { useIsFocused, useFocusEffect } from './hooks/use-focus';
18
+ export { useScreenOptions } from './hooks/use-screen-options';
19
+ export { useScreenChrome } from './hooks/use-screen-chrome';
20
+ export type { ScreenChrome } from './hooks/use-screen-chrome';
19
21
  export {
20
22
  useNavSerializer,
21
23
  NAV_SNAPSHOT_VERSION,
22
- } from './hooks/use-nav-serializer.js';
24
+ } from './hooks/use-nav-serializer';
23
25
  export type {
24
26
  NavSnapshot,
25
27
  NavStorageAdapter,
26
28
  UseNavSerializerOptions,
27
- } from './hooks/use-nav-serializer.js';
28
- export { hrefFor, parseHref } from './href.js';
29
- export type { Href } from './href.js';
29
+ } from './hooks/use-nav-serializer';
30
+ export { hrefFor, parseHref } from './href';
31
+ export type { Href } from './href';
30
32
  // URL bridge internals: `_setRouteRegistry` is a leading-underscore export —
31
33
  // intended for tests, deep-link bootstrap before a NavigationRoot mounts, and
32
34
  // any other integration that needs to seed the registry imperatively.
33
- export { _setRouteRegistry, _clearRouteRegistry } from './url/registry.js';
34
- export { compilePath } from './url/compile.js';
35
- export type { CompiledPath } from './url/compile.js';
36
- export { NavigationRoot } from './components/NavigationRoot.js';
37
- export { Stack } from './components/Stack.js';
38
- export { Screen } from './components/Screen.js';
39
- export { Header } from './components/Header.js';
40
- export { Tabs, useTabs } from './components/Tabs.js';
41
- export type { TabInfo, TabsNav } from './components/Tabs.js';
42
- export { TabBar } from './components/TabBar.js';
43
- export type { TabRenderContext } from './components/TabBar.js';
44
- export { Drawer, useDrawer } from './components/Drawer.js';
45
- export type { DrawerNav } from './components/Drawer.js';
46
- export { Link } from './components/Link.js';
47
- export type { LinkProps } from './components/Link.js';
35
+ export { _setRouteRegistry, _clearRouteRegistry } from './url/registry';
36
+ export { compilePath } from './url/compile';
37
+ export type { CompiledPath } from './url/compile';
38
+ export { NavigationRoot } from './components/NavigationRoot';
39
+ export { Stack } from './components/Stack';
40
+ export { Screen } from './components/Screen';
41
+ export { Header } from './components/Header';
42
+ export { Tabs, useTabs } from './components/Tabs';
43
+ export type { TabInfo, TabsNav } from './components/Tabs';
44
+ export { TabBar } from './components/TabBar';
45
+ export type { TabRenderContext } from './components/TabBar';
46
+ export { Drawer, useDrawer } from './components/Drawer';
47
+ export type { DrawerNav } from './components/Drawer';
48
+ export { Link } from './components/Link';
49
+ export type { LinkProps } from './components/Link';
48
50
  export type {
49
51
  ComponentLike,
50
52
  EmptyParams,
@@ -64,4 +66,4 @@ export type {
64
66
  TransitionKind,
65
67
  TransitionRole,
66
68
  TransitionState,
67
- } from './types.js';
69
+ } from './types';
@@ -0,0 +1,68 @@
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 type { Presentation, StackEntry, TransitionState } from '../types';
45
+ export type LayerAnimation = {
46
+ axis: 'translateX' | 'translateY';
47
+ inputRange: readonly [number, number];
48
+ outputRange: readonly [number, number];
49
+ progress: SharedValue<number>;
50
+ };
51
+ export interface Layer {
52
+ /** The entry whose component renders inside this layer. */
53
+ readonly entry: StackEntry;
54
+ /** When non-null, the layer's host view binds a `useAnimatedStyle` mapper. */
55
+ readonly animation: LayerAnimation | null;
56
+ }
57
+ 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
+ /**
65
+ * Compute the visible-layer list for one render of `<Stack>`. Pure —
66
+ * unit-testable independently of the renderer.
67
+ */
68
+ export declare function computeLayers(stack: readonly StackEntry[], transition: TransitionState | null, progress: SharedValue<number> | null): Layer[];
@@ -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';
45
+ import type {
46
+ Presentation,
47
+ StackEntry,
48
+ TransitionKind,
49
+ TransitionState,
50
+ } from '../types';
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
+ }
@@ -15,45 +15,34 @@
15
15
  * so a persistent HeaderBar can read slots from the currently-focused entry
16
16
  * without needing to be itself remounted on each navigation.
17
17
  */
18
- import { signal } from '@sigx/lynx';
19
- /** Create a fresh registry for an entry. Options and slots start empty. */
20
- export function createScreenRegistry(entry) {
21
- return {
22
- entry,
23
- options: signal({}),
24
- slots: signal({}),
25
- };
18
+ import { type Signal } from '@sigx/lynx';
19
+ import type { ScreenOptions, ScreenSlotFills, StackEntry } from '../types';
20
+ /**
21
+ * Reactive container for one screen's options and slot fills.
22
+ *
23
+ * `options` and `slots` are deeply-reactive object signals (sigx's `signal()`
24
+ * of an object returns a Proxy that tracks per-key reads and notifies
25
+ * per-key writes). Writers assign individual keys; readers subscribe to the
26
+ * keys they actually use — no whole-object reads, no read/write cycles in
27
+ * setup.
28
+ */
29
+ export interface ScreenRegistry {
30
+ readonly entry: StackEntry;
31
+ /** Reactive ScreenOptions — written per-key by `<Screen>`. */
32
+ readonly options: Signal<ScreenOptions>;
33
+ /** Reactive ScreenSlotFills — written per-key by `<Screen.Header>` et al. */
34
+ readonly slots: Signal<ScreenSlotFills>;
26
35
  }
36
+ /** Create a fresh registry for an entry. Options and slots start empty. */
37
+ export declare function createScreenRegistry(entry: StackEntry): ScreenRegistry;
27
38
  /**
28
39
  * Set a single slot fill on a registry. Pass `undefined` to clear.
29
40
  * Per-key write on the proxy — does not read other keys, so it can't loop
30
41
  * with effects that read different slot keys.
31
42
  */
32
- export function setSlot(registry, name, fill) {
33
- if (fill === undefined) {
34
- // Assigning undefined keeps the key around in the proxy; explicit
35
- // delete is what consumers checking `name in slots` expect.
36
- delete registry.slots[name];
37
- return;
38
- }
39
- registry.slots[name] = fill;
40
- }
43
+ export declare function setSlot<K extends keyof ScreenSlotFills>(registry: ScreenRegistry, name: K, fill: ScreenSlotFills[K] | undefined): void;
41
44
  /**
42
45
  * Merge partial options into a registry. Each option key is written
43
46
  * independently on the proxy — `undefined` keys clear that option.
44
47
  */
45
- export function mergeOptions(registry, patch) {
46
- for (const key of Object.keys(patch)) {
47
- const v = patch[key];
48
- if (v === undefined) {
49
- delete registry.options[key];
50
- }
51
- else {
52
- // Property-level assignment on a deeply-reactive proxy: notifies
53
- // only subscribers of this specific key, never reads the whole
54
- // options object, so it can't trigger the setup that wrote it.
55
- registry.options[key] = v;
56
- }
57
- }
58
- }
59
- //# sourceMappingURL=screen-registry.js.map
48
+ export declare function mergeOptions(registry: ScreenRegistry, patch: ScreenOptions): void;
@@ -20,7 +20,7 @@ import type {
20
20
  ScreenOptions,
21
21
  ScreenSlotFills,
22
22
  StackEntry,
23
- } from '../types.js';
23
+ } from '../types';
24
24
 
25
25
  /**
26
26
  * Reactive container for one screen's options and slot fills.
@@ -0,0 +1,17 @@
1
+ /**
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
+ *
8
+ * Used by:
9
+ * - `<ScreenContainer>` for the slide-from-right (translateX) and
10
+ * slide-from-bottom (translateY, modal) transform output ranges.
11
+ * - `<EdgeBackHandle>` for the gesture commit threshold (`dx / width`).
12
+ *
13
+ * Both must agree, otherwise the commit threshold and the animation
14
+ * geometry won't line up. Single shared module avoids drift.
15
+ */
16
+ export declare const SCREEN_WIDTH: number;
17
+ export declare const SCREEN_HEIGHT: number;
@@ -1,34 +1,42 @@
1
1
  /**
2
- * Logical screen width (in dp) read from `lynx.SystemInfo` at module load.
3
- * Falls back to 400 (typical phone) if SystemInfo isn't available — module
4
- * load happens BG-side after the bundle initializes, by which time
5
- * `lynx.SystemInfo` is populated, so the fallback only fires in tests / SSR /
6
- * non-Lynx hosts.
2
+ * Logical screen dimensions (in dp) read from `lynx.SystemInfo` at module
3
+ * load. Falls back to typical phone values if SystemInfo isn't available —
4
+ * module load happens BG-side after the bundle initializes, by which time
5
+ * `lynx.SystemInfo` is populated, so the fallback only fires in tests /
6
+ * SSR / non-Lynx hosts.
7
7
  *
8
8
  * Used by:
9
- * - `<ScreenContainer>` for the slide-from-right transform output range.
9
+ * - `<ScreenContainer>` for the slide-from-right (translateX) and
10
+ * slide-from-bottom (translateY, modal) transform output ranges.
10
11
  * - `<EdgeBackHandle>` for the gesture commit threshold (`dx / width`).
11
12
  *
12
13
  * Both must agree, otherwise the commit threshold and the animation
13
- * geometry won't line up. Single shared constant avoids drift.
14
+ * geometry won't line up. Single shared module avoids drift.
14
15
  */
15
16
 
16
17
  declare const lynx:
17
- | { SystemInfo?: { pixelWidth?: number; pixelRatio?: number } }
18
+ | {
19
+ SystemInfo?: {
20
+ pixelWidth?: number;
21
+ pixelHeight?: number;
22
+ pixelRatio?: number;
23
+ };
24
+ }
18
25
  | undefined;
19
26
 
20
- function readScreenWidth(): number {
27
+ function readDp(prop: 'pixelWidth' | 'pixelHeight', fallback: number): number {
21
28
  try {
22
29
  const info = typeof lynx !== 'undefined' ? lynx?.SystemInfo : undefined;
23
- const pw = info?.pixelWidth;
30
+ const px = info?.[prop];
24
31
  const pr = info?.pixelRatio || 1;
25
- if (typeof pw === 'number' && pw > 0) {
26
- return Math.round(pw / pr);
32
+ if (typeof px === 'number' && px > 0) {
33
+ return Math.round(px / pr);
27
34
  }
28
35
  } catch {
29
36
  // Lynx globals not present (test env / SSR) — use fallback.
30
37
  }
31
- return 400;
38
+ return fallback;
32
39
  }
33
40
 
34
- export const SCREEN_WIDTH = readScreenWidth();
41
+ export const SCREEN_WIDTH = readDp('pixelWidth', 400);
42
+ export const SCREEN_HEIGHT = readDp('pixelHeight', 800);
@@ -0,0 +1,96 @@
1
+ import { type SharedValue } from '@sigx/lynx';
2
+ import type { Nav } from '../hooks/use-nav';
3
+ import type { ScreenRegistry } from '../internal/screen-registry';
4
+ import type { RouteMap, StackEntry } from '../types';
5
+ /**
6
+ * The reactive backing state for one navigator instance.
7
+ *
8
+ * Two reactive signals drive the public surface:
9
+ * - `stack` is the entry array (read via `nav.stack` / `nav.current`).
10
+ * - `transition` is non-null only while a push/pop animation is in flight;
11
+ * `<Stack>` reads it to decide whether to render one screen or two.
12
+ *
13
+ * Pop is committed *after* its slide animation completes — `nav.canGoBack`
14
+ * stays true during the slide, then flips when the entry actually leaves the
15
+ * stack. Push commits its stack mutation immediately and animates the new
16
+ * entry in.
17
+ */
18
+ export interface NavigatorState {
19
+ readonly nav: Nav;
20
+ readonly routes: RouteMap;
21
+ /**
22
+ * Internal: BG-side gesture-back controller used by `<EdgeBackHandle>`.
23
+ * The `progress` SharedValue is wired here so a gesture worklet can write
24
+ * it directly on MT; the begin/commit/cancel methods set the transition
25
+ * state appropriately without driving their own auto-animation (the
26
+ * gesture worklet is in charge of that).
27
+ */
28
+ readonly _gesture: {
29
+ beginBackGesture(): void;
30
+ commitBackGesture(): void;
31
+ cancelBackGesture(): void;
32
+ };
33
+ /**
34
+ * Internal: cross-entry `<Screen>` registry lookup.
35
+ *
36
+ * Each `<EntryScope>` registers its `ScreenRegistry` here on mount and
37
+ * removes it on unmount. The navigator's persistent chrome (HeaderBar /
38
+ * TabBar, shipped in later slices) calls `getScreenRegistry(entry.key)`
39
+ * to read the currently-focused screen's options/slot fills without
40
+ * being itself remounted on each navigation.
41
+ *
42
+ * Returns `undefined` when no screen for that key has mounted yet (or
43
+ * after it has unmounted) — consumers must tolerate this and render
44
+ * defaults.
45
+ */
46
+ readonly _screens: {
47
+ register(registry: ScreenRegistry): void;
48
+ /** Identity-checked: no-op when a newer registry has taken the slot. */
49
+ unregister(registry: ScreenRegistry): void;
50
+ get(entryKey: string): ScreenRegistry | undefined;
51
+ };
52
+ /**
53
+ * Internal: set `nav.isLocallyFocused` from outside.
54
+ *
55
+ * `<Stack>` calls this when its host entry's locally-focused state
56
+ * changes (top of parent + parent focused + enclosing tab active). For
57
+ * the root nav this stays `true` for the lifetime of the navigator.
58
+ */
59
+ readonly _setLocallyFocused: (focused: boolean) => void;
60
+ }
61
+ export interface CreateNavigatorOptions {
62
+ routes: RouteMap;
63
+ initial: StackEntry;
64
+ /**
65
+ * SharedValue driving push/pop transition progress. Created in
66
+ * `<NavigationRoot>` setup via `useSharedValue(0)` so the bridge
67
+ * plumbing is wired (SharedValue is an MT-bridged ref). When undefined,
68
+ * navigations are instant — used by tests against `@sigx/lynx-testing`
69
+ * that don't have an MT runtime.
70
+ */
71
+ progress?: SharedValue<number>;
72
+ /**
73
+ * Parent navigator. Set when this navigator is nested under another
74
+ * (e.g. a per-tab `<Stack initialRoute>` under root). Drives the
75
+ * `nav.parent` getter and the modal-escalation behaviour of `push`:
76
+ * a push of a route whose resolved presentation is not `'card'`
77
+ * recurses via `parent.push(...)`, walking up the chain until it
78
+ * lands on a navigator with no parent (the root).
79
+ *
80
+ * Leave undefined for the root navigator.
81
+ */
82
+ parent?: Nav | null;
83
+ /**
84
+ * Whether this navigator is considered "locally focused" at creation
85
+ * time. Defaults to true for the root nav; nested stacks pass `false`
86
+ * here and then flip the flag via `_setLocallyFocused` once their
87
+ * host-entry/tab-active state is computed.
88
+ */
89
+ initialLocallyFocused?: boolean;
90
+ }
91
+ /**
92
+ * Create a navigator. Returns the public `nav` handle plus the routes map.
93
+ * The transition signal lives on `nav` (via `nav.transition`) so `<Stack>`
94
+ * can subscribe to it.
95
+ */
96
+ export declare function createNavigatorState(opts: CreateNavigatorOptions): NavigatorState;