@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,109 @@
1
+ /**
2
+ * `<Tabs>` — Lynx tab navigator.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```tsx
7
+ * <NavigationRoot routes={routes} initialRoute="root">
8
+ * <Stack />
9
+ * </NavigationRoot>
10
+ *
11
+ * // The route "root" component renders:
12
+ * <Tabs initialTab="feed">
13
+ * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
14
+ * <Stack initialRoute="feedHome" />
15
+ * </Tabs.Screen>
16
+ * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
17
+ * <Stack initialRoute="profileHome" />
18
+ * </Tabs.Screen>
19
+ * <TabBar />
20
+ * </Tabs>
21
+ * ```
22
+ *
23
+ * Tab bodies stay mounted across switches (the inactive ones render with
24
+ * `display: 'none'`), so each tab's nested `<Stack>` keeps its history when
25
+ * the user flips back to it. The active tab is reactive via `useTabs()`.
26
+ *
27
+ * Per-tab stacks: each `<Tabs.Screen>` can host a `<Stack initialRoute="…">`
28
+ * which mints its own navigator. `useNav()` inside that subtree resolves to
29
+ * the tab's stack, so `nav.push('card-route', …)` stays inside the tab.
30
+ * Routes presented as `modal` / `fullScreen` / `transparent-modal` escalate
31
+ * up `nav.parent` to the root navigator automatically — they overlay the
32
+ * tabs UI (TabBar included) and dismiss back into the originating tab.
33
+ */
34
+ import { type Define, type JSXElement } from '@sigx/lynx';
35
+ /** Metadata about a registered `<Tabs.Screen>`. */
36
+ export interface TabInfo {
37
+ /** Stable tab id, used by `setActive`. */
38
+ readonly name: string;
39
+ /** Optional icon node — passed through to the default tab bar. */
40
+ readonly icon?: JSXElement;
41
+ /** Optional human-readable label. Defaults to `name`. */
42
+ readonly label?: string;
43
+ /**
44
+ * Accessibility label announced by screen readers. Falls back to
45
+ * `label`, then `name`. Surfaced as `accessibility-label` on the
46
+ * default `<TabBar>` button.
47
+ */
48
+ readonly accessibilityLabel?: string;
49
+ }
50
+ /** Reactive controller exposed by `useTabs()`. */
51
+ export interface TabsNav {
52
+ /** Currently-active tab name. Reactive — accessing inside render/effect tracks. */
53
+ readonly active: string;
54
+ /** Switch the active tab. Triggers reactive updates in any consumer. */
55
+ setActive(name: string): void;
56
+ /** Snapshot of registered tabs in registration order. Reactive. */
57
+ readonly tabs: ReadonlyArray<TabInfo>;
58
+ }
59
+ /**
60
+ * Access the enclosing Tabs navigator. Throws when called outside `<Tabs>`.
61
+ */
62
+ export declare const useTabs: import("@sigx/runtime-core").InjectableFunction<TabsNav>;
63
+ /**
64
+ * @internal
65
+ * Provided by each `<Tabs.Screen>` so a nested `<Stack initialRoute>` can
66
+ * discover *which* tab it's hosted by, and gate its focus state on that
67
+ * tab being active. Throws when called outside a `<Tabs.Screen>` body so
68
+ * the gate degrades to "always active" via the caller's try/catch.
69
+ */
70
+ export declare const useTabScreenName: import("@sigx/runtime-core").InjectableFunction<string>;
71
+ type TabsProps = Define.Prop<'initialTab', string> & Define.Slot<'default'>;
72
+ type TabsScreenProps = Define.Prop<'name', string, true> & Define.Prop<'icon', JSXElement> & Define.Prop<'label', string> & Define.Prop<'accessibilityLabel', string> & Define.Slot<'default'>;
73
+ /**
74
+ * Compound export. `Tabs` is the parent component; `Tabs.Screen` registers
75
+ * an individual tab. Matches the `Screen` / `Screen.Header` shape used
76
+ * elsewhere in this package and the daisyui `Modal` / `Modal.Header`
77
+ * convention.
78
+ */
79
+ export declare const Tabs: ((props: {
80
+ initialTab?: string | undefined;
81
+ } & {} & {
82
+ slots?: Partial<{
83
+ default: () => JSXElement | JSXElement[] | null;
84
+ }> | undefined;
85
+ } & {} & JSX.IntrinsicAttributes & import("@sigx/runtime-core").ComponentAttributeExtensions & {
86
+ ref?: import("@sigx/runtime-core").Ref<void> | undefined;
87
+ children?: any;
88
+ }) => JSXElement) & {
89
+ __setup: import("@sigx/runtime-core").SetupFn<{
90
+ initialTab?: string | undefined;
91
+ }, TabsProps, void, {
92
+ default: () => JSXElement | JSXElement[] | null;
93
+ }>;
94
+ __name?: string;
95
+ __islandId?: string;
96
+ __props: {
97
+ initialTab?: string | undefined;
98
+ };
99
+ __events: TabsProps;
100
+ __ref: void;
101
+ __slots: {
102
+ default: () => JSXElement | JSXElement[] | null;
103
+ };
104
+ } & {
105
+ Screen: import("@sigx/runtime-core").ComponentFactory<TabsScreenProps, void, {
106
+ default: () => JSXElement | JSXElement[] | null;
107
+ }>;
108
+ };
109
+ export {};
@@ -208,13 +208,27 @@ const TabsScreen = component<TabsScreenProps>(({ props, slots }) => {
208
208
  // `display: none` keeps the body mounted so per-tab state survives
209
209
  // tab switches. Read activeSignal here so re-activating triggers a
210
210
  // re-render with display restored.
211
+ //
212
+ // Flex-fill long-form (`flex-grow/shrink/basis`) instead of
213
+ // `height: '100%'`. The percentage form only resolves against an
214
+ // explicit parent height, which means consumers had to wrap us
215
+ // in a `flexFill + height: '100%'` view to make us visible — and
216
+ // every Lynx app got that wrong (myself included) until we hit
217
+ // it on the showcase. With flex-fill we just take whatever space
218
+ // our parent flex container gives us; the parent only needs to
219
+ // be a flex column with a known height (e.g. SafeAreaView, which
220
+ // now defaults to that).
211
221
  const active = registrar.activeSignal.value === name;
212
222
  return (
213
223
  <view
214
224
  style={{
215
225
  display: active ? 'flex' : 'none',
226
+ flexDirection: 'column',
216
227
  width: '100%',
217
- height: '100%',
228
+ flexGrow: 1,
229
+ flexShrink: 1,
230
+ flexBasis: 0,
231
+ minHeight: 0,
218
232
  }}
219
233
  >
220
234
  {slots.default?.()}
@@ -1,3 +1,4 @@
1
+ import type { RouteMap } from './types';
1
2
  /**
2
3
  * Define a typed route registry.
3
4
  *
@@ -26,7 +27,4 @@
26
27
  * }
27
28
  * ```
28
29
  */
29
- export function defineRoutes(routes) {
30
- return routes;
31
- }
32
- //# sourceMappingURL=define-routes.js.map
30
+ export declare function defineRoutes<const T extends RouteMap>(routes: T): T;
@@ -1,4 +1,4 @@
1
- import type { RouteMap } from './types.js';
1
+ import type { RouteMap } from './types';
2
2
 
3
3
  /**
4
4
  * Define a typed route registry.
@@ -0,0 +1,45 @@
1
+ import { type Computed } from '@sigx/lynx';
2
+ /**
3
+ * Reactive "is this screen the focused entry?" signal.
4
+ *
5
+ * Must be called from inside a component rendered as a route by `<Stack>` (or
6
+ * any other navigator that uses `<EntryScope>`); throws otherwise. The
7
+ * returned `Computed` reads `nav.current.key` and compares it to the entry
8
+ * the calling screen was mounted for, so any nav mutation that changes the
9
+ * top entry flips the value.
10
+ *
11
+ * Note: screens stay mounted when something is pushed on top of them — they
12
+ * just lose focus. Pop the new top off and they regain focus.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const Profile = component(() => {
17
+ * const isFocused = useIsFocused();
18
+ * return () => <text>{isFocused.value ? 'visible' : 'hidden'}</text>;
19
+ * });
20
+ * ```
21
+ */
22
+ export declare function useIsFocused(): Computed<boolean>;
23
+ /**
24
+ * Run `cb` whenever this screen gains focus; run the returned cleanup when it
25
+ * loses focus or unmounts. Mirrors React Navigation's `useFocusEffect`.
26
+ *
27
+ * Lifecycle:
28
+ * - cb runs immediately if the screen is already focused at mount.
29
+ * - When the screen loses focus (something pushed on top), cleanup runs.
30
+ * - When focus returns (the cover is popped), `cb` runs again — yielding a
31
+ * fresh cleanup for the next blur.
32
+ * - On unmount, cleanup runs once if still focused.
33
+ *
34
+ * Common uses: subscribe to a data source while visible, track an analytics
35
+ * "screen view" event, start/stop a polling loop.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * useFocusEffect(() => {
40
+ * const id = setInterval(refresh, 5000);
41
+ * return () => clearInterval(id);
42
+ * });
43
+ * ```
44
+ */
45
+ export declare function useFocusEffect(cb: () => void | (() => void)): void;
@@ -5,8 +5,8 @@ import {
5
5
  untrack,
6
6
  type Computed,
7
7
  } from '@sigx/lynx';
8
- import { useNav } from './use-nav.js';
9
- import { useCurrentEntry } from './use-nav-internal.js';
8
+ import { useNav } from './use-nav';
9
+ import { useCurrentEntry } from './use-nav-internal';
10
10
 
11
11
  /**
12
12
  * Reactive "is this screen the focused entry?" signal.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Wire the Android hardware back button to the active navigator.
3
+ *
4
+ * Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
5
+ * `BackHandler` (which the native side dispatches from
6
+ * `MainActivity.onBackPressed`). On press the handler walks to the
7
+ * deepest currently-focused navigator (per-tab `<Stack>`s register with
8
+ * their parent), then walks back up the `parent` chain looking for the
9
+ * first nav that `canGoBack`:
10
+ *
11
+ * - If any nav in the chain can go back → `nav.pop()` on that nav.
12
+ * - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
13
+ * keeps the bundle warm; iOS: rejects, since iOS doesn't permit
14
+ * programmatic termination).
15
+ *
16
+ * The traversal means you only need to call this once at the root — a
17
+ * back press from inside a tab pops that tab's nested stack first, only
18
+ * exiting the app once every level is at its base entry.
19
+ *
20
+ * Call this once in any component under `<NavigationRoot>` (typically a
21
+ * thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
22
+ * hook is a no-op there.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const BackHandlerWiring = component(() => {
27
+ * useHardwareBack();
28
+ * return () => null;
29
+ * });
30
+ *
31
+ * <NavigationRoot routes={routes}>
32
+ * <BackHandlerWiring />
33
+ * <Stack />
34
+ * </NavigationRoot>
35
+ * ```
36
+ */
37
+ export declare function useHardwareBack(): void;
@@ -1,6 +1,6 @@
1
1
  import { onMounted } from '@sigx/lynx';
2
2
  import { BackHandler } from '@sigx/lynx-linking';
3
- import { useNav, type Nav } from './use-nav.js';
3
+ import { useNav, type Nav } from './use-nav';
4
4
 
5
5
  /**
6
6
  * Wire the Android hardware back button to the active navigator.
@@ -0,0 +1,91 @@
1
+ import { type Href } from '../href';
2
+ import { type Nav } from './use-nav';
3
+ import type { RouteMap } from '../types';
4
+ export interface UseLinkingNavOptions {
5
+ /**
6
+ * Schemes/prefixes to strip before parsing. Matched in order; the first
7
+ * match wins. Example: `['myapp://', 'https://myapp.com']` lets
8
+ * `https://myapp.com/users/42` parse against the same routes as
9
+ * `/users/42`.
10
+ *
11
+ * After stripping, a leading `/` is added if missing so the result is a
12
+ * valid pathname.
13
+ */
14
+ prefixes?: string[];
15
+ /**
16
+ * Custom handler invoked instead of the default dispatch. Use this when
17
+ * you need to intercept (e.g. for auth callbacks, analytics) before
18
+ * routing. If you call `nav.push` / `nav.replace` from here, the default
19
+ * dispatch is skipped — return `void`.
20
+ */
21
+ onURL?: (url: string, nav: Nav) => void;
22
+ /**
23
+ * Called when an incoming URL doesn't match any registered route's `path`
24
+ * template (or fails schema validation). Defaults to a no-op so unknown
25
+ * URLs are dropped silently. Use this to surface "page not found" UX or
26
+ * to forward to a catch-all route.
27
+ */
28
+ onUnmatched?: (url: string) => void;
29
+ /**
30
+ * Whether to use `nav.replace` instead of `nav.push` for the cold-start
31
+ * initial URL. Defaults to `true` — restoring an app into a deep link
32
+ * shouldn't leave a stray "initial route" entry beneath it that the back
33
+ * button can return to.
34
+ *
35
+ * Runtime URLs (from `addEventListener`) always `push`.
36
+ */
37
+ replaceInitial?: boolean;
38
+ }
39
+ /**
40
+ * Bridge `@sigx/lynx-linking` URL events into a `@sigx/lynx-navigation`
41
+ * navigator. Call once inside a `<NavigationRoot>` subtree.
42
+ *
43
+ * Handles both delivery modes:
44
+ * - **cold start** — `Linking.getInitialURL()` is read on mount and, if
45
+ * present, dispatched (replacing the initial route by default).
46
+ * - **warm start** — `Linking.addEventListener('url', ...)` subscribes for
47
+ * URLs delivered while the app is already running; each one is pushed.
48
+ *
49
+ * URL → route dispatch goes through `parseHref`, which matches the URL's
50
+ * pathname against the route registry seeded by `<NavigationRoot>`. Routes
51
+ * without a `path` template are never matched by deep links — only typed
52
+ * `<Link>` / `nav.push` calls reach them.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * import { useLinkingNav } from '@sigx/lynx-navigation';
57
+ *
58
+ * const DeepLinks = component(() => {
59
+ * useLinkingNav({
60
+ * prefixes: ['myapp://', 'https://myapp.com'],
61
+ * onUnmatched: (url) => console.warn('Unknown deep link:', url),
62
+ * });
63
+ * return () => null;
64
+ * });
65
+ *
66
+ * <NavigationRoot routes={routes}>
67
+ * <DeepLinks />
68
+ * <Stack />
69
+ * </NavigationRoot>
70
+ * ```
71
+ */
72
+ export declare function useLinkingNav(opts?: UseLinkingNavOptions): void;
73
+ /**
74
+ * Strip the first matching prefix from `url`, returning a pathname-like
75
+ * string. If no prefixes are provided, or none match, the original URL is
76
+ * returned unchanged so `parseHref` can still handle scheme-prefixed forms
77
+ * via `@sigx/lynx-linking`'s `parse`.
78
+ *
79
+ * Exported for unit testing — not part of the package public API.
80
+ */
81
+ export declare function _stripPrefix(url: string, prefixes?: string[]): string;
82
+ /**
83
+ * Call the right `nav.push` / `nav.replace` overload for `href`. The
84
+ * overloads differ in positional layout: routes with a params schema take
85
+ * `(name, params, search?, options?)`; routes without take `(name, search?,
86
+ * options?)`. Calling the wrong shape silently shifts `search` into the
87
+ * `options` slot, so we look the route up in the registry and branch.
88
+ *
89
+ * Exported for unit testing — not part of the package public API.
90
+ */
91
+ export declare function _navigateToHref(nav: Nav, routes: RouteMap, href: Href, kind: 'push' | 'replace'): void;
@@ -1,9 +1,9 @@
1
1
  import { onMounted } from '@sigx/lynx';
2
2
  import { Linking } from '@sigx/lynx-linking';
3
- import { parseHref, type Href } from '../href.js';
4
- import { useNav, type Nav } from './use-nav.js';
5
- import { useNavRoutes } from './use-nav-internal.js';
6
- import type { RouteMap } from '../types.js';
3
+ import { parseHref, type Href } from '../href';
4
+ import { useNav, type Nav } from './use-nav';
5
+ import { useNavRoutes } from './use-nav-internal';
6
+ import type { RouteMap } from '../types';
7
7
 
8
8
  export interface UseLinkingNavOptions {
9
9
  /**
@@ -0,0 +1,91 @@
1
+ import { type SharedValue } from '@sigx/lynx';
2
+ import type { ScreenRegistry } from '../internal/screen-registry';
3
+ import type { RouteMap, StackEntry } from '../types';
4
+ /**
5
+ * Internal injectable: the `StackEntry` the calling screen was rendered for.
6
+ *
7
+ * Provided by `<EntryScope>` which `<Stack>` and `<ScreenContainer>` wrap
8
+ * around each screen component mount. Screens use this to derive their own
9
+ * focus state (`useIsFocused`, `useFocusEffect`) without having to track
10
+ * `entry.key` themselves.
11
+ *
12
+ * Default throws so calling `useIsFocused()` outside a screen mounted by a
13
+ * navigator surfaces a clear error rather than silently returning `false`.
14
+ */
15
+ export declare const useCurrentEntry: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown>>;
16
+ /**
17
+ * Soft companion to {@link useCurrentEntry} — returns the current scope's
18
+ * entry if any, `null` when called outside an `<EntryScope>` instead of
19
+ * throwing. Provided alongside the strict version by `<EntryScope>`.
20
+ *
21
+ * Used by chrome consumers (`useScreenChrome`) where "no scoped entry"
22
+ * is a legitimate state (a Stack chrome slot lives outside the screen's
23
+ * EntryScope) and the caller wants to soft-fallback to the navigator's
24
+ * destination entry rather than crash.
25
+ */
26
+ export declare const useCurrentEntryOptional: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown> | null>;
27
+ /**
28
+ * Internal injectable: the route registry passed into `<NavigationRoot>`.
29
+ * Components (Stack, Screen) read this to look up route definitions by name.
30
+ *
31
+ * Not exported from the package barrel — use `useNav()` for navigation, and
32
+ * the registry is implicit from `<NavigationRoot routes={...}>`.
33
+ */
34
+ export declare const useNavRoutes: import("@sigx/runtime-core").InjectableFunction<RouteMap>;
35
+ /**
36
+ * Internal injectable: low-level navigator handles used by the edge-back
37
+ * gesture. Holds the progress SharedValue (so gesture worklets can write it
38
+ * directly on MT) plus BG-side begin/commit/cancel functions invoked via
39
+ * `runOnBackground` from gesture worklets.
40
+ *
41
+ * `progress` is `null` when the navigator was created with `animated={false}`
42
+ * (e.g. tests). `beginBackGesture` is also a no-op in that case.
43
+ */
44
+ export interface NavInternals {
45
+ /** MT-driven transition progress; null when animations are disabled. */
46
+ readonly progress: SharedValue<number> | null;
47
+ /**
48
+ * Set transition state for a gesture-driven pop. Does not start any
49
+ * automatic animation — the gesture worklet writes `progress` directly
50
+ * per frame, then animates to the commit/cancel endpoint on release.
51
+ */
52
+ beginBackGesture(): void;
53
+ /** Commit the back gesture: pop top entry + clear transition. */
54
+ commitBackGesture(): void;
55
+ /** Cancel the back gesture: clear transition without popping. */
56
+ cancelBackGesture(): void;
57
+ /** Whether the user opted into the edge-swipe-back gesture. */
58
+ readonly edgeSwipeEnabled: boolean;
59
+ /**
60
+ * Cross-entry screen registry controller. `<EntryScope>` calls
61
+ * `register` on mount and `unregister` on unmount. Persistent chrome
62
+ * (HeaderBar / TabBar — later slices) calls `get(entryKey)` to read
63
+ * the focused screen's options + slot fills without remounting itself.
64
+ */
65
+ readonly screens: {
66
+ register(registry: ScreenRegistry): void;
67
+ /**
68
+ * Identity-checked: only removes the entry if `registry` is the
69
+ * one currently registered under its `entry.key`. A no-op when
70
+ * a newer registry has already taken that slot (which happens
71
+ * at the transition→idle handoff, where a fresh `<EntryScope>`
72
+ * for the same entry mounts before the old one's unmount fires).
73
+ */
74
+ unregister(registry: ScreenRegistry): void;
75
+ get(entryKey: string): ScreenRegistry | undefined;
76
+ };
77
+ }
78
+ export declare const useNavInternals: import("@sigx/runtime-core").InjectableFunction<NavInternals>;
79
+ /**
80
+ * Internal injectable: the calling screen's `ScreenRegistry`.
81
+ *
82
+ * Provided by `<EntryScope>` alongside `useCurrentEntry`. The `<Screen>`
83
+ * component and its slot-filling sub-components write options and slot
84
+ * fills here; the navigator's persistent chrome (HeaderBar, TabBar — later
85
+ * slices) reads from this registry via `getScreenRegistry(key)` on the
86
+ * navigator state, which keys into a cross-entry map.
87
+ *
88
+ * Throws when used outside an EntryScope so calling `<Screen>` at the app
89
+ * root surfaces a clear error rather than silently no-op'ing.
90
+ */
91
+ export declare const useScreenRegistry: import("@sigx/runtime-core").InjectableFunction<ScreenRegistry>;
@@ -1,6 +1,6 @@
1
1
  import { defineInjectable, type SharedValue } from '@sigx/lynx';
2
- import type { ScreenRegistry } from '../internal/screen-registry.js';
3
- import type { RouteMap, StackEntry } from '../types.js';
2
+ import type { ScreenRegistry } from '../internal/screen-registry';
3
+ import type { RouteMap, StackEntry } from '../types';
4
4
 
5
5
  /**
6
6
  * Internal injectable: the `StackEntry` the calling screen was rendered for.
@@ -19,6 +19,20 @@ export const useCurrentEntry = defineInjectable<StackEntry>(() => {
19
19
  );
20
20
  });
21
21
 
22
+ /**
23
+ * Soft companion to {@link useCurrentEntry} — returns the current scope's
24
+ * entry if any, `null` when called outside an `<EntryScope>` instead of
25
+ * throwing. Provided alongside the strict version by `<EntryScope>`.
26
+ *
27
+ * Used by chrome consumers (`useScreenChrome`) where "no scoped entry"
28
+ * is a legitimate state (a Stack chrome slot lives outside the screen's
29
+ * EntryScope) and the caller wants to soft-fallback to the navigator's
30
+ * destination entry rather than crash.
31
+ */
32
+ export const useCurrentEntryOptional = defineInjectable<StackEntry | null>(
33
+ () => null,
34
+ );
35
+
22
36
  /**
23
37
  * Internal injectable: the route registry passed into `<NavigationRoot>`.
24
38
  * Components (Stack, Screen) read this to look up route definitions by name.
@@ -64,7 +78,14 @@ export interface NavInternals {
64
78
  */
65
79
  readonly screens: {
66
80
  register(registry: ScreenRegistry): void;
67
- unregister(entryKey: string): void;
81
+ /**
82
+ * Identity-checked: only removes the entry if `registry` is the
83
+ * one currently registered under its `entry.key`. A no-op when
84
+ * a newer registry has already taken that slot (which happens
85
+ * at the transition→idle handoff, where a fresh `<EntryScope>`
86
+ * for the same entry mounts before the old one's unmount fires).
87
+ */
88
+ unregister(registry: ScreenRegistry): void;
68
89
  get(entryKey: string): ScreenRegistry | undefined;
69
90
  };
70
91
  }
@@ -0,0 +1,82 @@
1
+ import type { StackEntry } from '../types';
2
+ /**
3
+ * Plain JSON snapshot of a navigator. The whole point of holding navigation
4
+ * state in signals is that this is a one-liner — `JSON.stringify(nav.stack)`.
5
+ *
6
+ * Shape is deliberately minimal:
7
+ *
8
+ * {
9
+ * version: 1,
10
+ * stack: [ { key, route, params, search, state, presentation }, ... ],
11
+ * }
12
+ *
13
+ * `version` lets future schema migrations (or hard breakage) reject old
14
+ * snapshots cleanly rather than restoring incompatible state.
15
+ *
16
+ * Per spec resolved-decisions: only the root navigator is persisted in v1.
17
+ * Per-tab / nested-navigator stacks are deferred until the nested-navigators
18
+ * follow-up slice lands.
19
+ */
20
+ export interface NavSnapshot {
21
+ version: number;
22
+ stack: StackEntry[];
23
+ }
24
+ export declare const NAV_SNAPSHOT_VERSION = 1;
25
+ /**
26
+ * Adapter contract for `useNavSerializer`. Implementations bridge to whatever
27
+ * storage backend the host app uses — `@sigx/lynx-storage`, `localStorage`,
28
+ * an MMKV bridge, etc. Both methods may be async; the hook awaits load before
29
+ * applying anything to the stack and fires save in a debounced manner.
30
+ *
31
+ * - `load()` returns `null` (or rejects) when no snapshot exists, when the
32
+ * stored payload is malformed, or when the host opts not to restore on
33
+ * this launch.
34
+ * - `save(snapshot)` persists the latest stack. The hook drops save errors
35
+ * on the floor — losing a write is preferable to crashing the navigator.
36
+ */
37
+ export interface NavStorageAdapter {
38
+ load(): Promise<NavSnapshot | null> | NavSnapshot | null;
39
+ save(snapshot: NavSnapshot): Promise<void> | void;
40
+ }
41
+ export interface UseNavSerializerOptions {
42
+ storage: NavStorageAdapter;
43
+ /**
44
+ * Trailing-edge debounce in ms before pushing a stack change to storage.
45
+ * Defaults to 250ms — quick enough that a force-quit one tick after a
46
+ * push is recoverable, slow enough that rapid `pop/push` flurries
47
+ * coalesce into one write.
48
+ */
49
+ debounceMs?: number;
50
+ /**
51
+ * Optional callback after a successful restore — lets the host run
52
+ * post-restore wiring (analytics, focus shifts, etc.) only when we
53
+ * actually applied state, not on every mount.
54
+ */
55
+ onRestored?: (snapshot: NavSnapshot) => void;
56
+ /**
57
+ * Optional callback when a snapshot is rejected (validation failed or
58
+ * load threw). Defaults to silent. Useful for logging during migration.
59
+ */
60
+ onRestoreError?: (reason: 'version' | 'shape' | 'unknown-route' | 'load-threw', err?: unknown) => void;
61
+ }
62
+ /**
63
+ * Wire a navigator's stack to a storage adapter.
64
+ *
65
+ * On mount:
66
+ * 1. Call `storage.load()`.
67
+ * 2. Validate the snapshot (version match, every entry's route still
68
+ * registered).
69
+ * 3. On success, `nav.reset({ stack })` to apply.
70
+ * 4. On any failure, leave the stack alone (initial route remains).
71
+ *
72
+ * Then subscribe to `nav.stack` and call `storage.save(snapshot)` debounced.
73
+ *
74
+ * Why we don't validate `params` / `search` against schemas here: schemas
75
+ * are part of the route definition, and re-running them across all entries
76
+ * on every launch costs more than it's worth. The contract is "entries were
77
+ * validated when they were pushed; if the schema has since changed in a
78
+ * breaking way, bump `version` to reject old snapshots wholesale." Callers
79
+ * who want a stricter check can run their own validation in
80
+ * `storage.load()` and return `null` on mismatch.
81
+ */
82
+ export declare function useNavSerializer(options: UseNavSerializerOptions): void;
@@ -1,7 +1,7 @@
1
1
  import { effect, onMounted, onUnmounted } from '@sigx/lynx';
2
- import { useNav } from './use-nav.js';
3
- import { useNavRoutes } from './use-nav-internal.js';
4
- import type { StackEntry } from '../types.js';
2
+ import { useNav } from './use-nav';
3
+ import { useNavRoutes } from './use-nav-internal';
4
+ import type { StackEntry } from '../types';
5
5
 
6
6
  /**
7
7
  * Plain JSON snapshot of a navigator. The whole point of holding navigation