@sigx/lynx-navigation 0.2.0 → 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 (193) hide show
  1. package/README.md +128 -8
  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 +41 -16
  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.map +1 -1
  17. package/dist/define-routes.d.ts +1 -1
  18. package/dist/define-routes.d.ts.map +1 -1
  19. package/dist/hooks/use-linking-nav.d.ts +3 -3
  20. package/dist/hooks/use-linking-nav.d.ts.map +1 -1
  21. package/dist/hooks/use-nav-internal.d.ts +21 -3
  22. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  23. package/dist/hooks/use-nav-serializer.d.ts +1 -1
  24. package/dist/hooks/use-nav-serializer.d.ts.map +1 -1
  25. package/dist/hooks/use-nav.d.ts +2 -2
  26. package/dist/hooks/use-nav.d.ts.map +1 -1
  27. package/dist/hooks/use-params.d.ts +1 -1
  28. package/dist/hooks/use-params.d.ts.map +1 -1
  29. package/dist/hooks/use-screen-chrome.d.ts +19 -0
  30. package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
  31. package/dist/hooks/use-screen-options.d.ts +1 -1
  32. package/dist/hooks/use-screen-options.d.ts.map +1 -1
  33. package/dist/hooks/use-search.d.ts +1 -1
  34. package/dist/hooks/use-search.d.ts.map +1 -1
  35. package/dist/href.d.ts +2 -2
  36. package/dist/href.d.ts.map +1 -1
  37. package/dist/index.d.ts +33 -31
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1160 -29
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/layer-plan.d.ts +69 -0
  42. package/dist/internal/layer-plan.d.ts.map +1 -0
  43. package/dist/internal/screen-registry.d.ts +1 -1
  44. package/dist/internal/screen-registry.d.ts.map +1 -1
  45. package/dist/internal/screen-width.d.ts +9 -7
  46. package/dist/internal/screen-width.d.ts.map +1 -1
  47. package/dist/navigator/core.d.ts +5 -4
  48. package/dist/navigator/core.d.ts.map +1 -1
  49. package/dist/register.d.ts +1 -1
  50. package/dist/register.d.ts.map +1 -1
  51. package/dist/url/index.d.ts +6 -6
  52. package/dist/url/index.d.ts.map +1 -1
  53. package/dist/url/parse.d.ts +1 -1
  54. package/dist/url/parse.d.ts.map +1 -1
  55. package/dist/url/registry.d.ts +2 -2
  56. package/dist/url/registry.d.ts.map +1 -1
  57. package/dist/url/validate.d.ts +1 -1
  58. package/dist/url/validate.d.ts.map +1 -1
  59. package/package.json +11 -10
  60. package/src/components/Drawer.d.ts +55 -0
  61. package/src/components/EdgeBackHandle.d.ts +1 -0
  62. package/src/components/EdgeBackHandle.tsx +2 -2
  63. package/{dist/components/EntryScope.js → src/components/EntryScope.d.ts} +7 -15
  64. package/src/components/EntryScope.tsx +15 -4
  65. package/src/components/Header.d.ts +6 -0
  66. package/src/components/Header.tsx +3 -3
  67. package/src/components/Layer.d.ts +33 -0
  68. package/src/components/Layer.tsx +96 -0
  69. package/src/components/Link.d.ts +60 -0
  70. package/src/components/Link.tsx +4 -4
  71. package/src/components/NavigationRoot.d.ts +36 -0
  72. package/src/components/NavigationRoot.tsx +6 -6
  73. package/src/components/Screen.d.ts +97 -0
  74. package/src/components/Screen.tsx +13 -11
  75. package/src/components/Stack.d.ts +90 -0
  76. package/src/components/Stack.tsx +142 -98
  77. package/src/components/TabBar.d.ts +38 -0
  78. package/src/components/TabBar.tsx +22 -22
  79. package/src/components/Tabs.d.ts +109 -0
  80. package/src/components/Tabs.tsx +15 -1
  81. package/{dist/define-routes.js → src/define-routes.d.ts} +2 -4
  82. package/src/define-routes.ts +1 -1
  83. package/src/hooks/use-focus.d.ts +45 -0
  84. package/src/hooks/use-focus.ts +2 -2
  85. package/src/hooks/use-hardware-back.d.ts +37 -0
  86. package/src/hooks/use-hardware-back.ts +1 -1
  87. package/src/hooks/use-linking-nav.d.ts +91 -0
  88. package/src/hooks/use-linking-nav.ts +4 -4
  89. package/src/hooks/use-nav-internal.d.ts +91 -0
  90. package/src/hooks/use-nav-internal.ts +24 -3
  91. package/src/hooks/use-nav-serializer.d.ts +82 -0
  92. package/src/hooks/use-nav-serializer.ts +3 -3
  93. package/src/hooks/use-nav.d.ts +111 -0
  94. package/src/hooks/use-nav.ts +2 -2
  95. package/{dist/hooks/use-params.js → src/hooks/use-params.d.ts} +2 -6
  96. package/src/hooks/use-params.ts +2 -2
  97. package/src/hooks/use-screen-chrome.d.ts +18 -0
  98. package/src/hooks/use-screen-chrome.ts +122 -0
  99. package/src/hooks/use-screen-options.d.ts +2 -0
  100. package/src/hooks/use-screen-options.ts +3 -3
  101. package/{dist/hooks/use-search.js → src/hooks/use-search.d.ts} +2 -6
  102. package/src/hooks/use-search.ts +2 -2
  103. package/src/href.d.ts +54 -0
  104. package/src/href.ts +6 -6
  105. package/src/index.d.ts +39 -0
  106. package/src/index.ts +33 -31
  107. package/src/internal/layer-plan.d.ts +68 -0
  108. package/src/internal/layer-plan.ts +187 -0
  109. package/{dist/internal/screen-registry.js → src/internal/screen-registry.d.ts} +21 -32
  110. package/src/internal/screen-registry.ts +1 -1
  111. package/src/internal/screen-width.d.ts +17 -0
  112. package/src/internal/screen-width.ts +22 -14
  113. package/src/navigator/core.d.ts +96 -0
  114. package/src/navigator/core.ts +17 -6
  115. package/src/register.d.ts +37 -0
  116. package/src/register.ts +1 -1
  117. package/src/types.d.ts +217 -0
  118. package/src/url/build.d.ts +15 -0
  119. package/src/url/build.ts +2 -2
  120. package/src/url/compile.d.ts +34 -0
  121. package/src/url/format.d.ts +28 -0
  122. package/src/url/index.ts +6 -6
  123. package/src/url/parse.d.ts +20 -0
  124. package/src/url/parse.ts +6 -6
  125. package/{dist/url/registry.js → src/url/registry.d.ts} +12 -28
  126. package/src/url/registry.ts +3 -3
  127. package/src/url/validate.d.ts +23 -0
  128. package/src/url/validate.ts +1 -1
  129. package/dist/components/Drawer.js +0 -74
  130. package/dist/components/Drawer.js.map +0 -1
  131. package/dist/components/EdgeBackHandle.js +0 -144
  132. package/dist/components/EdgeBackHandle.js.map +0 -1
  133. package/dist/components/EntryScope.js.map +0 -1
  134. package/dist/components/Header.js +0 -103
  135. package/dist/components/Header.js.map +0 -1
  136. package/dist/components/Link.js +0 -51
  137. package/dist/components/Link.js.map +0 -1
  138. package/dist/components/NavigationRoot.js +0 -67
  139. package/dist/components/NavigationRoot.js.map +0 -1
  140. package/dist/components/Screen.js +0 -94
  141. package/dist/components/Screen.js.map +0 -1
  142. package/dist/components/ScreenContainer.d.ts +0 -18
  143. package/dist/components/ScreenContainer.d.ts.map +0 -1
  144. package/dist/components/ScreenContainer.js +0 -77
  145. package/dist/components/ScreenContainer.js.map +0 -1
  146. package/dist/components/Stack.js +0 -221
  147. package/dist/components/Stack.js.map +0 -1
  148. package/dist/components/TabBar.js +0 -63
  149. package/dist/components/TabBar.js.map +0 -1
  150. package/dist/components/Tabs.js +0 -154
  151. package/dist/components/Tabs.js.map +0 -1
  152. package/dist/define-routes.js.map +0 -1
  153. package/dist/hooks/use-focus.js +0 -87
  154. package/dist/hooks/use-focus.js.map +0 -1
  155. package/dist/hooks/use-hardware-back.js +0 -84
  156. package/dist/hooks/use-hardware-back.js.map +0 -1
  157. package/dist/hooks/use-linking-nav.js +0 -109
  158. package/dist/hooks/use-linking-nav.js.map +0 -1
  159. package/dist/hooks/use-nav-internal.js +0 -44
  160. package/dist/hooks/use-nav-internal.js.map +0 -1
  161. package/dist/hooks/use-nav-serializer.js +0 -181
  162. package/dist/hooks/use-nav-serializer.js.map +0 -1
  163. package/dist/hooks/use-nav.js +0 -11
  164. package/dist/hooks/use-nav.js.map +0 -1
  165. package/dist/hooks/use-params.js.map +0 -1
  166. package/dist/hooks/use-screen-options.js +0 -43
  167. package/dist/hooks/use-screen-options.js.map +0 -1
  168. package/dist/hooks/use-search.js.map +0 -1
  169. package/dist/href.js +0 -57
  170. package/dist/href.js.map +0 -1
  171. package/dist/internal/screen-registry.js.map +0 -1
  172. package/dist/internal/screen-width.js +0 -30
  173. package/dist/internal/screen-width.js.map +0 -1
  174. package/dist/navigator/core.js +0 -383
  175. package/dist/navigator/core.js.map +0 -1
  176. package/dist/register.js +0 -2
  177. package/dist/register.js.map +0 -1
  178. package/dist/types.js +0 -9
  179. package/dist/types.js.map +0 -1
  180. package/dist/url/build.js +0 -30
  181. package/dist/url/build.js.map +0 -1
  182. package/dist/url/compile.js +0 -83
  183. package/dist/url/compile.js.map +0 -1
  184. package/dist/url/format.js +0 -102
  185. package/dist/url/format.js.map +0 -1
  186. package/dist/url/index.js +0 -13
  187. package/dist/url/index.js.map +0 -1
  188. package/dist/url/parse.js +0 -94
  189. package/dist/url/parse.js.map +0 -1
  190. package/dist/url/registry.js.map +0 -1
  191. package/dist/url/validate.js +0 -37
  192. package/dist/url/validate.js.map +0 -1
  193. package/src/components/ScreenContainer.tsx +0 -114
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `<Screen>` — declarative per-screen options + slot fills.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```tsx
7
+ * const ProfileScreen = component(() => () => (
8
+ * <Screen title="Profile" headerShown gestureEnabled>
9
+ * <Screen.HeaderRight>
10
+ * <text bindtap={onEdit}>Edit</text>
11
+ * </Screen.HeaderRight>
12
+ * <view>body…</view>
13
+ * </Screen>
14
+ * ));
15
+ * ```
16
+ *
17
+ * `<Screen>` itself renders its `default` slot inline — so the body lives
18
+ * where you'd expect with no extra layout wrapper. The sub-components
19
+ * (`Screen.Header`, `Screen.HeaderLeft`, `Screen.HeaderRight`,
20
+ * `Screen.TabBarItem`) render `null` and write into the entry's
21
+ * `ScreenRegistry`. The navigator's persistent chrome reads from there.
22
+ *
23
+ * Note: `<Screen.TabBarItem>` registers a scoped slot fill on the entry's
24
+ * `ScreenRegistry`, but the built-in `<TabBar>` doesn't read it yet — the
25
+ * fill is exposed for custom tab-bar renderers (pass `renderTab` and look
26
+ * up the active entry's registry yourself).
27
+ *
28
+ * Sub-component placement inside `<Screen>` is conventional — sigx scopes
29
+ * are by component tree, so they work anywhere under the same EntryScope.
30
+ * Placing them as direct children of `<Screen>` keeps the call site
31
+ * declarative and grep-friendly.
32
+ */
33
+ import { type Define } from '@sigx/lynx';
34
+ type ScreenProps = Define.Prop<'title', string | (() => string)> & Define.Prop<'headerShown', boolean> & Define.Prop<'gestureEnabled', boolean> & Define.Slot<'default'>;
35
+ type SimpleSlotProps = Define.Slot<'default'>;
36
+ /**
37
+ * `<Screen.TabBarItem>` — scoped slot. The default slot is a function that
38
+ * receives `{ active }`; whatever it returns is the tab-bar item content.
39
+ *
40
+ * Sigx's `Define.Slot<'default', { active: boolean }>` would express this
41
+ * directly on the component, but since `<Screen.TabBarItem>`'s parent
42
+ * (the user's tree, not the navigator) doesn't actually pass `active`, we
43
+ * accept a plain default slot whose body is itself a function. The
44
+ * navigator's TabBar invokes that function with the active flag.
45
+ */
46
+ type TabBarItemProps = Define.Slot<'default'>;
47
+ /**
48
+ * Compound export. `Screen` is callable as a JSX element and exposes the
49
+ * sub-components as properties (`Screen.Header`, etc.) for the declarative
50
+ * call site shown in the file header.
51
+ */
52
+ export declare const Screen: ((props: {
53
+ gestureEnabled?: boolean | undefined;
54
+ headerShown?: boolean | undefined;
55
+ title?: string | (() => string) | undefined;
56
+ } & {} & {
57
+ slots?: Partial<{
58
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
59
+ }> | undefined;
60
+ } & {} & JSX.IntrinsicAttributes & import("@sigx/runtime-core").ComponentAttributeExtensions & {
61
+ ref?: import("@sigx/runtime-core").Ref<void> | undefined;
62
+ children?: any;
63
+ }) => import("@sigx/runtime-core").JSXElement) & {
64
+ __setup: import("@sigx/runtime-core").SetupFn<{
65
+ gestureEnabled?: boolean | undefined;
66
+ headerShown?: boolean | undefined;
67
+ title?: string | (() => string) | undefined;
68
+ }, ScreenProps, void, {
69
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
70
+ }>;
71
+ __name?: string;
72
+ __islandId?: string;
73
+ __props: {
74
+ gestureEnabled?: boolean | undefined;
75
+ headerShown?: boolean | undefined;
76
+ title?: string | (() => string) | undefined;
77
+ };
78
+ __events: ScreenProps;
79
+ __ref: void;
80
+ __slots: {
81
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
82
+ };
83
+ } & {
84
+ Header: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
85
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
86
+ }>;
87
+ HeaderLeft: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
88
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
89
+ }>;
90
+ HeaderRight: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
91
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
92
+ }>;
93
+ TabBarItem: import("@sigx/runtime-core").ComponentFactory<TabBarItemProps, void, {
94
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
95
+ }>;
96
+ };
97
+ export {};
@@ -31,8 +31,9 @@
31
31
  * declarative and grep-friendly.
32
32
  */
33
33
  import { component, onUnmounted, type Define } from '@sigx/lynx';
34
- import { useScreenRegistry } from '../hooks/use-nav-internal.js';
35
- import { mergeOptions, setSlot } from '../internal/screen-registry.js';
34
+ import { useScreenRegistry } from '../hooks/use-nav-internal';
35
+ import { mergeOptions, setSlot } from '../internal/screen-registry';
36
+ import type { ScreenOptions } from '../types';
36
37
 
37
38
  type ScreenProps =
38
39
  & Define.Prop<'title', string | (() => string)>
@@ -42,15 +43,16 @@ type ScreenProps =
42
43
 
43
44
  const ScreenRoot = component<ScreenProps>(({ props, slots }) => {
44
45
  const registry = useScreenRegistry();
45
- // Apply options whenever the component sets up. Options are reactive
46
- // through the registry's `options` signal chrome consumers re-render
47
- // on the next merge. We don't bother diffing here: the patch is small
48
- // and writes only happen during setup + explicit prop changes upstream.
49
- mergeOptions(registry, {
50
- title: props.title,
51
- headerShown: props.headerShown,
52
- gestureEnabled: props.gestureEnabled,
53
- });
46
+ // Apply options whenever the component sets up. Only set keys that
47
+ // were actually passed `mergeOptions` treats `undefined` as "clear
48
+ // this key", so building the patch from raw `props.X` would wipe
49
+ // every option a previous `useScreenOptions(...)` (or another `<Screen>`)
50
+ // had set on this same entry.
51
+ const patch: ScreenOptions = {};
52
+ if (props.title !== undefined) patch.title = props.title;
53
+ if (props.headerShown !== undefined) patch.headerShown = props.headerShown;
54
+ if (props.gestureEnabled !== undefined) patch.gestureEnabled = props.gestureEnabled;
55
+ mergeOptions(registry, patch);
54
56
  return () => slots.default?.();
55
57
  });
56
58
 
@@ -0,0 +1,90 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ type StackProps =
3
+ /**
4
+ * Mint a nested navigator with this route at its base. When set, the
5
+ * `<Stack>` becomes the owner of a new `NavigatorState` and provides
6
+ * `useNav` / `useNavInternals` / `useNavRoutes` to its subtree, so
7
+ * `nav.push('card-route', …)` from inside the stack stays *inside* it
8
+ * (e.g. for per-tab stacks). Routes presented as `modal` / `fullScreen` /
9
+ * `transparent-modal` automatically escalate to the parent navigator
10
+ * via `nav.parent`, walking up until they reach the root — so modals
11
+ * still overlay the whole app.
12
+ *
13
+ * Omit to render the *enclosing* navigator's stack (the default — this
14
+ * is how `<NavigationRoot> → <Stack />` works).
15
+ */
16
+ Define.Prop<'initialRoute', string>
17
+ /** Initial params for the nested-stack base entry. */
18
+ & Define.Prop<'initialParams', Record<string, unknown>>
19
+ /** Initial search for the nested-stack base entry. */
20
+ & Define.Prop<'initialSearch', Record<string, unknown>>
21
+ /**
22
+ * Optional chrome rendered *above* the active screen, **inside this
23
+ * Stack's nav scope**. The intended use is `<Header />`, which needs
24
+ * to resolve `useNav()` to the per-stack nav (not the enclosing one)
25
+ * so it can react to pushes inside this stack — e.g. show a back
26
+ * button when a card is pushed onto a per-tab stack.
27
+ *
28
+ * Without this, a `<Header />` placed as a sibling of `<Stack>`
29
+ * would see the enclosing nav and never update when pushes happen
30
+ * inside the nested stack.
31
+ */
32
+ & Define.Slot<'default'>;
33
+ /**
34
+ * Stack navigator — renders the topmost stack entry's component at rest, or
35
+ * the top + underneath entries during a transition.
36
+ *
37
+ * Two modes:
38
+ *
39
+ * **Bound** (no `initialRoute`): renders the enclosing navigator's stack.
40
+ * This is the shape used directly under `<NavigationRoot>` and is what
41
+ * single-stack apps want.
42
+ *
43
+ * **Nested-owner** (`initialRoute="…"`): mints a fresh `NavigatorState` with
44
+ * its own progress `SharedValue` and edge-back gesture, and provides
45
+ * `useNav` / `useNavInternals` / `useNavRoutes` to its subtree. `useNav()`
46
+ * inside this stack returns the nested nav; `nav.parent` points to the
47
+ * enclosing one. Per-tab stacks are the canonical use case:
48
+ *
49
+ * ```tsx
50
+ * <Tabs initialTab="trips">
51
+ * <Tabs.Screen name="trips"><Stack initialRoute="tripsHome" /></Tabs.Screen>
52
+ * <Tabs.Screen name="map"><Stack initialRoute="mapHome" /></Tabs.Screen>
53
+ * </Tabs>
54
+ * ```
55
+ *
56
+ * Modal/fullScreen pushes escalate up the parent chain automatically — so
57
+ * `nav.push('newTrip')` from inside Trips (where `newTrip` is `modal`)
58
+ * walks to root and overlays the whole UI. `replace` stays strictly local
59
+ * (asymmetric with `push`) so a modal `replace` never wipes the root stack.
60
+ *
61
+ * **Render strategy.** Stack always emits the same JSX shape — a
62
+ * relative wrapper containing one `<Layer>` per entry returned by
63
+ * `computeLayers(stack, transition, progress)`. Each Layer is an
64
+ * absolutely-positioned host view with optional MT-bound translate
65
+ * animation. The pure layer-plan function decides:
66
+ *
67
+ * - **Idle.** Topmost non-overlay base + any overlays above it. All
68
+ * static (no transform). Overlays (`modal` / `fullScreen` /
69
+ * `transparent-modal`) keep their underneath mounted; cards
70
+ * replace their underneath in the base layer.
71
+ * - **Card transition.** Both top and underneath animate (slide-in
72
+ * + parallax). After settle, idle rules apply — the underneath
73
+ * unmounts because the new top is the sole base.
74
+ * - **Overlay transition.** The full idle layer stack up through
75
+ * the underneath stays static; only the animated top has a
76
+ * transform. After settle, the overlay either joins the static
77
+ * idle stack (push) or unmounts (pop).
78
+ *
79
+ * Layer keys are `layer-${entry.key}-${animationVariant}`. The variant
80
+ * suffix forces a remount when an entry transitions from animated to
81
+ * static (or vice versa) — `useAnimatedStyle` binds once at setup and
82
+ * can't switch its mapper at runtime. Modal underneath layers never
83
+ * animate, so their key is stable across the modal lifecycle and the
84
+ * subtree's state (per-tab Stack navigators, scroll positions,
85
+ * in-flight inputs) survives.
86
+ */
87
+ export declare const Stack: import("@sigx/runtime-core").ComponentFactory<StackProps, void, {
88
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
89
+ }>;
90
+ export {};
@@ -5,24 +5,21 @@ import {
5
5
  onUnmounted,
6
6
  untrack,
7
7
  useSharedValue,
8
- type ComponentFactory,
9
8
  type Define,
10
- type SharedValue,
11
9
  } from '@sigx/lynx';
12
- import { Suspense, isLazyComponent } from '@sigx/lynx';
13
- import { createNavigatorState } from '../navigator/core.js';
14
- import { useNav, type Nav } from '../hooks/use-nav.js';
10
+ import { createNavigatorState } from '../navigator/core';
11
+ import { useNav, type Nav } from '../hooks/use-nav';
15
12
  import {
16
13
  useCurrentEntry,
17
14
  useNavInternals,
18
15
  useNavRoutes,
19
16
  type NavInternals,
20
- } from '../hooks/use-nav-internal.js';
21
- import type { Presentation, StackEntry } from '../types.js';
22
- import { ScreenContainer } from './ScreenContainer.js';
23
- import { EdgeBackHandle } from './EdgeBackHandle.js';
24
- import { EntryScope } from './EntryScope.js';
25
- import { useTabScreenName, useTabs } from './Tabs.js';
17
+ } from '../hooks/use-nav-internal';
18
+ import type { Presentation, StackEntry } from '../types';
19
+ import { animationVariant, computeLayers, isOverlayPresentation } from '../internal/layer-plan';
20
+ import { EdgeBackHandle } from './EdgeBackHandle';
21
+ import { Layer } from './Layer';
22
+ import { useTabScreenName, useTabs } from './Tabs';
26
23
 
27
24
  type StackProps =
28
25
  /**
@@ -42,7 +39,19 @@ type StackProps =
42
39
  /** Initial params for the nested-stack base entry. */
43
40
  & Define.Prop<'initialParams', Record<string, unknown>>
44
41
  /** Initial search for the nested-stack base entry. */
45
- & Define.Prop<'initialSearch', Record<string, unknown>>;
42
+ & Define.Prop<'initialSearch', Record<string, unknown>>
43
+ /**
44
+ * Optional chrome rendered *above* the active screen, **inside this
45
+ * Stack's nav scope**. The intended use is `<Header />`, which needs
46
+ * to resolve `useNav()` to the per-stack nav (not the enclosing one)
47
+ * so it can react to pushes inside this stack — e.g. show a back
48
+ * button when a card is pushed onto a per-tab stack.
49
+ *
50
+ * Without this, a `<Header />` placed as a sibling of `<Stack>`
51
+ * would see the enclosing nav and never update when pushes happen
52
+ * inside the nested stack.
53
+ */
54
+ & Define.Slot<'default'>;
46
55
 
47
56
  let _nestedKeyCounter = 0;
48
57
 
@@ -74,22 +83,33 @@ let _nestedKeyCounter = 0;
74
83
  * walks to root and overlays the whole UI. `replace` stays strictly local
75
84
  * (asymmetric with `push`) so a modal `replace` never wipes the root stack.
76
85
  *
77
- * **Render strategy** (same in both modes):
78
- * - **Idle**: just the top entry, full-bleed, no transform. The screen
79
- * component mounts directly so it can use its own layout (no extra
80
- * absolute positioning that would break percentage heights).
81
- * - **Transitioning**: two `<ScreenContainer>` instances stacked
82
- * absolutely, each with an MT-driven `translateX` that reads from the
83
- * navigator's progress `SharedValue`. The host's BG thread doesn't tick
84
- * per frame — `useAnimatedStyle` runs the interpolation entirely on MT.
86
+ * **Render strategy.** Stack always emits the same JSX shape — a
87
+ * relative wrapper containing one `<Layer>` per entry returned by
88
+ * `computeLayers(stack, transition, progress)`. Each Layer is an
89
+ * absolutely-positioned host view with optional MT-bound translate
90
+ * animation. The pure layer-plan function decides:
85
91
  *
86
- * `key={top.key}` keeps the idle render's component instance stable across
87
- * unrelated re-renders. During transitions, composite keys
88
- * (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair
89
- * so the `useAnimatedStyle` binding is set with the right input/output
90
- * ranges.
92
+ * - **Idle.** Topmost non-overlay base + any overlays above it. All
93
+ * static (no transform). Overlays (`modal` / `fullScreen` /
94
+ * `transparent-modal`) keep their underneath mounted; cards
95
+ * replace their underneath in the base layer.
96
+ * - **Card transition.** Both top and underneath animate (slide-in
97
+ * + parallax). After settle, idle rules apply — the underneath
98
+ * unmounts because the new top is the sole base.
99
+ * - **Overlay transition.** The full idle layer stack up through
100
+ * the underneath stays static; only the animated top has a
101
+ * transform. After settle, the overlay either joins the static
102
+ * idle stack (push) or unmounts (pop).
103
+ *
104
+ * Layer keys are `layer-${entry.key}-${animationVariant}`. The variant
105
+ * suffix forces a remount when an entry transitions from animated to
106
+ * static (or vice versa) — `useAnimatedStyle` binds once at setup and
107
+ * can't switch its mapper at runtime. Modal underneath layers never
108
+ * animate, so their key is stable across the modal lifecycle and the
109
+ * subtree's state (per-tab Stack navigators, scroll positions,
110
+ * in-flight inputs) survives.
91
111
  */
92
- export const Stack = component<StackProps>(({ props }) => {
112
+ export const Stack = component<StackProps>(({ props, slots }) => {
93
113
  // Capture enclosing scope's nav + routes + internals BEFORE any of the
94
114
  // defineProvide calls below override them for descendants. These are
95
115
  // always the "outer" values regardless of whether this Stack is bound
@@ -226,89 +246,113 @@ export const Stack = component<StackProps>(({ props }) => {
226
246
  internals = parentInternals;
227
247
  }
228
248
 
249
+ // Per-stack chrome (slots.default) renders *inside* this Stack's
250
+ // nav scope so a `<Header />` placed there resolves `useNav()` to
251
+ // the per-stack nav. Wrapping the active body in a flex-column
252
+ // with the slot above does that without disturbing layer-fill
253
+ // semantics — the slot takes natural height, the body keeps
254
+ // flex-fill.
255
+ const flexColumnFill = {
256
+ flexGrow: 1,
257
+ flexShrink: 1,
258
+ flexBasis: 0,
259
+ minHeight: 0,
260
+ display: 'flex',
261
+ flexDirection: 'column',
262
+ } as const;
263
+
229
264
  return () => {
230
- const transition = nav.transition;
231
- const top = nav.current;
265
+ const chrome = slots.default?.();
266
+ const layers = computeLayers(nav.stack, nav.transition, internals.progress);
232
267
 
233
- if (!transition) {
234
- const route = routes[top.route];
235
- if (!route) return null;
236
- const Comp = route.component as unknown as ComponentFactory<
237
- Record<string, unknown>,
238
- unknown,
239
- unknown
240
- >;
241
- if (typeof Comp !== 'function') return null;
242
- const params = top.params as Record<string, unknown>;
243
- // Wrap lazy routes that declare a `fallback` in <Suspense> so the
244
- // chunk-load shows the user-provided spinner instead of throwing
245
- // up to the nearest outer boundary (which may be wrong layer or
246
- // missing entirely).
247
- const body = isLazyComponent(Comp) && route.fallback
248
- ? (
249
- <Suspense fallback={route.fallback as never}>
250
- <Comp {...params} />
251
- </Suspense>
252
- )
253
- : <Comp {...params} />;
254
- // When canGoBack and edge-swipe is enabled, overlay the gesture
255
- // handle so the user can pan from the left edge to start a back
256
- // transition. `position: absolute` doesn't disturb the screen's
257
- // own layout — the handle only intercepts touches in the leftmost
258
- // 20px, and only when they pan rightward past `MIN_DISTANCE`.
259
- if (nav.canGoBack && internals.edgeSwipeEnabled) {
260
- return (
261
- <view
262
- style={{
263
- position: 'relative',
264
- width: '100%',
265
- height: '100%',
266
- }}
267
- >
268
- <EntryScope key={top.key} entry={top}>
269
- {body}
270
- </EntryScope>
271
- <EdgeBackHandle key="edge-back" />
272
- </view>
273
- );
274
- }
275
- return (
276
- <EntryScope key={top.key} entry={top}>
277
- {body}
278
- </EntryScope>
279
- );
280
- }
268
+ const renderLayerNode = (layer: typeof layers[number] | undefined) =>
269
+ layer ? (
270
+ <Layer
271
+ key={`layer-${layer.entry.key}-${animationVariant(layer.animation)}`}
272
+ entry={layer.entry}
273
+ routes={routes}
274
+ animation={layer.animation}
275
+ />
276
+ ) : null;
277
+ // sigx's reconciler treats a single array-valued JSX child as
278
+ // one "slot": when the array's *length* changes between
279
+ // renders, keyed children inside can be remounted even if
280
+ // their keys are stable. To make stacked-overlay state
281
+ // preservation work (modal A still mounted after modal B
282
+ // pushes on top), each layer is emitted as its own separate
283
+ // JSX child slot rather than as an array. The slots are
284
+ // position-stable across renders — the only thing that
285
+ // changes is a slot turning from `null` to a Layer (mount) or
286
+ // vice versa (unmount). MAX_LAYERS caps the supported stack
287
+ // depth; in practice apps rarely stack more than 2-3 overlays.
288
+ // If you hit the cap, increase the constant — the unrolled
289
+ // shape is just verbose, not algorithmically limited.
281
290
 
282
- // Cast progress: TransitionState carries it as `unknown` to avoid
283
- // pinning the contract to `@sigx/lynx`'s SharedValue at the type
284
- // level; here at the runtime boundary we know it's a SharedValue<number>.
285
- const progress = transition.progress as SharedValue<number>;
291
+ // Edge-swipe handle on top, gated on:
292
+ // - `internals.edgeSwipeEnabled` opt-out flag (also off
293
+ // when the navigator has no progress SharedValue, i.e.
294
+ // animations disabled no in-flight gesture to animate).
295
+ // - `nav.canGoBack` — something to pop back to.
296
+ // - `!nav.transition` — no animation already running.
297
+ // - The current top is a card (not an overlay). Edge-swipe
298
+ // is the iOS-style horizontal pop gesture for card stacks;
299
+ // using it to dismiss a modal would be the wrong axis +
300
+ // the wrong dismissal semantic.
301
+ //
302
+ // The handle only intercepts touches in the leftmost 20px and
303
+ // ignores small drags, so placing it last (highest z) doesn't
304
+ // disturb screen touches.
305
+ const top = nav.current;
306
+ const edgeHandle = (
307
+ internals.edgeSwipeEnabled
308
+ && nav.canGoBack
309
+ && !nav.transition
310
+ && !isOverlayPresentation(top.presentation)
311
+ )
312
+ ? <EdgeBackHandle key="edge-back" />
313
+ : null;
286
314
 
287
- return (
315
+ const body = (
288
316
  <view
289
317
  style={{
290
318
  position: 'relative',
291
319
  width: '100%',
292
- height: '100%',
320
+ // Flex-fill so the layer container has a real
321
+ // height — `<Layer>`s anchor via `position:
322
+ // absolute; top/right/bottom/left: 0`, which
323
+ // needs a sized relative parent.
324
+ ...flexColumnFill,
325
+ // Clip any animated layer that translates off-
326
+ // screen so the slide doesn't bleed past the
327
+ // Stack's bounds.
293
328
  overflow: 'hidden',
294
329
  }}
295
330
  >
296
- <ScreenContainer
297
- key={`${transition.underneathEntry.key}-underneath-${transition.kind}`}
298
- entry={transition.underneathEntry}
299
- routes={routes}
300
- role="underneath"
301
- kind={transition.kind}
302
- progress={progress}
303
- />
304
- <ScreenContainer
305
- key={`${transition.topEntry.key}-top-${transition.kind}`}
306
- entry={transition.topEntry}
307
- routes={routes}
308
- role="top"
309
- kind={transition.kind}
310
- progress={progress}
311
- />
331
+ {renderLayerNode(layers[0])}
332
+ {renderLayerNode(layers[1])}
333
+ {renderLayerNode(layers[2])}
334
+ {renderLayerNode(layers[3])}
335
+ {renderLayerNode(layers[4])}
336
+ {renderLayerNode(layers[5])}
337
+ {renderLayerNode(layers[6])}
338
+ {renderLayerNode(layers[7])}
339
+ {renderLayerNode(layers[8])}
340
+ {renderLayerNode(layers[9])}
341
+ {renderLayerNode(layers[10])}
342
+ {renderLayerNode(layers[11])}
343
+ {renderLayerNode(layers[12])}
344
+ {renderLayerNode(layers[13])}
345
+ {renderLayerNode(layers[14])}
346
+ {renderLayerNode(layers[15])}
347
+ {edgeHandle}
348
+ </view>
349
+ );
350
+
351
+ if (chrome == null) return body as never;
352
+ return (
353
+ <view style={flexColumnFill}>
354
+ {chrome}
355
+ <view style={flexColumnFill}>{body}</view>
312
356
  </view>
313
357
  );
314
358
  };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `<TabBar>` — headless default chrome for `<Tabs>`.
3
+ *
4
+ * Renders the active-tab buttons reading from the enclosing `useTabs()`
5
+ * navigator. Intentionally **unstyled** — this lives in the (theme-less)
6
+ * navigation package, so it ships pure structure + accessibility wiring.
7
+ * Themed chrome belongs in a UI-kit package: see `<NavTabBar />` in
8
+ * `@sigx/lynx-daisyui` for the daisy-themed equivalent.
9
+ *
10
+ * Use this directly only if you want to handle styling yourself via the
11
+ * `renderTab` prop. For a "looks like a tab bar out of the box" component,
12
+ * pull `<NavTabBar />` from `@sigx/lynx-daisyui` (or your own UI kit).
13
+ *
14
+ * Customization:
15
+ * - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
16
+ * default button rendering for each tab. `ctx.active` tells the
17
+ * consumer whether this tab is currently focused; `ctx.onPress`
18
+ * activates the tab. **Recommended** for any visual treatment.
19
+ *
20
+ * Accessibility (baked into the default button — the one structural
21
+ * concern this component keeps):
22
+ * - `accessibility-label` from `info.accessibilityLabel ?? info.label ?? info.name`.
23
+ * - `accessibility-element="true"` so screen readers see the whole pill.
24
+ * - `accessibility-trait="button"` and a `selected` flag on the active
25
+ * one so VoiceOver/TalkBack announces focus state on tab switch.
26
+ */
27
+ import { type JSXElement } from '@sigx/lynx';
28
+ import { type TabInfo } from './Tabs';
29
+ /** Rendering context passed to a `renderTab` consumer. */
30
+ export interface TabRenderContext {
31
+ /** True when this tab is currently active. Reactive — re-runs render on change. */
32
+ readonly active: boolean;
33
+ /** Activates this tab. Use as a `bindtap` handler on the rendered node. */
34
+ onPress(): void;
35
+ }
36
+ export declare const TabBar: import("@sigx/runtime-core").ComponentFactory<{
37
+ renderTab?: ((info: TabInfo, ctx: TabRenderContext) => JSXElement) | undefined;
38
+ }, void, {}>;
@@ -1,36 +1,35 @@
1
1
  /**
2
- * `<TabBar>` — default chrome for `<Tabs>`.
2
+ * `<TabBar>` — headless default chrome for `<Tabs>`.
3
3
  *
4
- * Renders a row of tab buttons reading from the enclosing `useTabs()`
5
- * navigator. Active tab is highlighted via the `active` prop on each
6
- * default button (consumers can re-style via `renderTab`).
4
+ * Renders the active-tab buttons reading from the enclosing `useTabs()`
5
+ * navigator. Intentionally **unstyled** this lives in the (theme-less)
6
+ * navigation package, so it ships pure structure + accessibility wiring.
7
+ * Themed chrome belongs in a UI-kit package: see `<NavTabBar />` in
8
+ * `@sigx/lynx-daisyui` for the daisy-themed equivalent.
7
9
  *
8
- * Customization knobs:
10
+ * Use this directly only if you want to handle styling yourself via the
11
+ * `renderTab` prop. For a "looks like a tab bar out of the box" component,
12
+ * pull `<NavTabBar />` from `@sigx/lynx-daisyui` (or your own UI kit).
13
+ *
14
+ * Customization:
9
15
  * - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
10
16
  * default button rendering for each tab. `ctx.active` tells the
11
17
  * consumer whether this tab is currently focused; `ctx.onPress`
12
- * activates the tab.
13
- *
14
- * Accessibility:
15
- * - Each default button gets `accessibility-label` from
16
- * `info.accessibilityLabel ?? info.label ?? info.name`.
17
- * - Each default button gets `accessibility-element="true"` so screen
18
- * readers see the whole pill, not just the inner `<text>`.
19
- * - Each default button gets `accessibility-trait="button"` and a
20
- * `selected` flag on the active one so VoiceOver/TalkBack announces
21
- * focus state on tab switch.
18
+ * activates the tab. **Recommended** for any visual treatment.
22
19
  *
23
- * Placement: mount inside `<Tabs>` alongside the `<Tabs.Screen>`s. Order
24
- * matters visually (place above or below the screen bodies depending on
25
- * the layout), and `<Tabs.Screen>` bodies all stack with `display:flex` so
26
- * the TabBar should be at a deterministic position in the JSX.
20
+ * Accessibility (baked into the default button — the one structural
21
+ * concern this component keeps):
22
+ * - `accessibility-label` from `info.accessibilityLabel ?? info.label ?? info.name`.
23
+ * - `accessibility-element="true"` so screen readers see the whole pill.
24
+ * - `accessibility-trait="button"` and a `selected` flag on the active
25
+ * one so VoiceOver/TalkBack announces focus state on tab switch.
27
26
  */
28
27
  import {
29
28
  component,
30
29
  type Define,
31
30
  type JSXElement,
32
31
  } from '@sigx/lynx';
33
- import { useTabs, type TabInfo } from './Tabs.js';
32
+ import { useTabs, type TabInfo } from './Tabs';
34
33
 
35
34
  /** Rendering context passed to a `renderTab` consumer. */
36
35
  export interface TabRenderContext {
@@ -46,8 +45,9 @@ type TabBarProps =
46
45
  /**
47
46
  * Default per-tab button. Plain `<view>` with a `<text>` inside, an
48
47
  * `accessibility-*` cluster for screen readers, and a tap handler. No
49
- * styling beyond a minimal active-state marker — consumers that want
50
- * branded chrome pass `renderTab`.
48
+ * styling beyond a minimal active-state opacity hint — consumers that
49
+ * want branded chrome pass `renderTab` or use a UI-kit-provided tab bar
50
+ * (e.g. `<NavTabBar />` from `@sigx/lynx-daisyui`).
51
51
  */
52
52
  const DefaultTabButton = component<
53
53
  & Define.Prop<'info', TabInfo, true>