@sigx/lynx-navigation 0.1.0 → 0.1.2

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 (139) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +355 -0
  3. package/dist/components/Drawer.d.ts +56 -0
  4. package/dist/components/Drawer.d.ts.map +1 -0
  5. package/dist/components/Drawer.js +74 -0
  6. package/dist/components/Drawer.js.map +1 -0
  7. package/dist/components/EdgeBackHandle.js +144 -0
  8. package/dist/components/EdgeBackHandle.js.map +1 -0
  9. package/dist/components/EntryScope.d.ts +26 -0
  10. package/dist/components/EntryScope.d.ts.map +1 -0
  11. package/dist/components/EntryScope.js +33 -0
  12. package/dist/components/EntryScope.js.map +1 -0
  13. package/dist/components/Header.d.ts +7 -0
  14. package/dist/components/Header.d.ts.map +1 -0
  15. package/dist/components/Header.js +103 -0
  16. package/dist/components/Header.js.map +1 -0
  17. package/dist/components/Link.js +1 -4
  18. package/dist/components/Link.js.map +1 -1
  19. package/dist/components/NavigationRoot.d.ts +1 -1
  20. package/dist/components/NavigationRoot.d.ts.map +1 -1
  21. package/dist/components/NavigationRoot.js +29 -3
  22. package/dist/components/NavigationRoot.js.map +1 -1
  23. package/dist/components/Screen.d.ts +98 -0
  24. package/dist/components/Screen.d.ts.map +1 -0
  25. package/dist/components/Screen.js +94 -0
  26. package/dist/components/Screen.js.map +1 -0
  27. package/dist/components/ScreenContainer.d.ts.map +1 -1
  28. package/dist/components/ScreenContainer.js +77 -0
  29. package/dist/components/ScreenContainer.js.map +1 -0
  30. package/dist/components/Stack.d.ts.map +1 -1
  31. package/dist/components/Stack.js +60 -24
  32. package/dist/components/Stack.js.map +1 -1
  33. package/dist/components/TabBar.d.ts +40 -0
  34. package/dist/components/TabBar.d.ts.map +1 -0
  35. package/dist/components/TabBar.js +63 -0
  36. package/dist/components/TabBar.js.map +1 -0
  37. package/dist/components/Tabs.d.ts +101 -0
  38. package/dist/components/Tabs.d.ts.map +1 -0
  39. package/dist/components/Tabs.js +140 -0
  40. package/dist/components/Tabs.js.map +1 -0
  41. package/dist/hooks/use-focus.d.ts +46 -0
  42. package/dist/hooks/use-focus.d.ts.map +1 -0
  43. package/dist/hooks/use-focus.js +81 -0
  44. package/dist/hooks/use-focus.js.map +1 -0
  45. package/dist/hooks/use-hardware-back.js +50 -0
  46. package/dist/hooks/use-hardware-back.js.map +1 -0
  47. package/dist/hooks/use-linking-nav.d.ts +92 -0
  48. package/dist/hooks/use-linking-nav.d.ts.map +1 -0
  49. package/dist/hooks/use-linking-nav.js +109 -0
  50. package/dist/hooks/use-linking-nav.js.map +1 -0
  51. package/dist/hooks/use-nav-internal.d.ts +38 -1
  52. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  53. package/dist/hooks/use-nav-internal.js +32 -0
  54. package/dist/hooks/use-nav-internal.js.map +1 -1
  55. package/dist/hooks/use-nav-serializer.d.ts +83 -0
  56. package/dist/hooks/use-nav-serializer.d.ts.map +1 -0
  57. package/dist/hooks/use-nav-serializer.js +181 -0
  58. package/dist/hooks/use-nav-serializer.js.map +1 -0
  59. package/dist/hooks/use-nav.js.map +1 -1
  60. package/dist/hooks/use-screen-options.d.ts +3 -0
  61. package/dist/hooks/use-screen-options.d.ts.map +1 -0
  62. package/dist/hooks/use-screen-options.js +43 -0
  63. package/dist/hooks/use-screen-options.js.map +1 -0
  64. package/dist/href.d.ts +16 -1
  65. package/dist/href.d.ts.map +1 -1
  66. package/dist/href.js +50 -7
  67. package/dist/href.js.map +1 -1
  68. package/dist/index.d.ts +18 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +15 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/internal/screen-registry.d.ts +49 -0
  73. package/dist/internal/screen-registry.d.ts.map +1 -0
  74. package/dist/internal/screen-registry.js +59 -0
  75. package/dist/internal/screen-registry.js.map +1 -0
  76. package/dist/internal/screen-width.js +30 -0
  77. package/dist/internal/screen-width.js.map +1 -0
  78. package/dist/navigator/core.d.ts +20 -1
  79. package/dist/navigator/core.d.ts.map +1 -1
  80. package/dist/navigator/core.js +231 -36
  81. package/dist/navigator/core.js.map +1 -1
  82. package/dist/types.d.ts +56 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/url/build.d.ts +16 -0
  85. package/dist/url/build.d.ts.map +1 -0
  86. package/dist/url/build.js +30 -0
  87. package/dist/url/build.js.map +1 -0
  88. package/dist/url/compile.d.ts +35 -0
  89. package/dist/url/compile.d.ts.map +1 -0
  90. package/dist/url/compile.js +83 -0
  91. package/dist/url/compile.js.map +1 -0
  92. package/dist/url/format.d.ts +29 -0
  93. package/dist/url/format.d.ts.map +1 -0
  94. package/dist/url/format.js +102 -0
  95. package/dist/url/format.js.map +1 -0
  96. package/dist/url/index.d.ts +13 -0
  97. package/dist/url/index.d.ts.map +1 -0
  98. package/dist/url/index.js +13 -0
  99. package/dist/url/index.js.map +1 -0
  100. package/dist/url/parse.d.ts +21 -0
  101. package/dist/url/parse.d.ts.map +1 -0
  102. package/dist/url/parse.js +94 -0
  103. package/dist/url/parse.js.map +1 -0
  104. package/dist/url/registry.d.ts +41 -0
  105. package/dist/url/registry.d.ts.map +1 -0
  106. package/dist/url/registry.js +56 -0
  107. package/dist/url/registry.js.map +1 -0
  108. package/dist/url/validate.d.ts +24 -0
  109. package/dist/url/validate.d.ts.map +1 -0
  110. package/dist/url/validate.js +37 -0
  111. package/dist/url/validate.js.map +1 -0
  112. package/package.json +44 -15
  113. package/src/components/Drawer.tsx +119 -0
  114. package/src/components/EdgeBackHandle.tsx +1 -1
  115. package/src/components/EntryScope.tsx +38 -0
  116. package/src/components/Header.tsx +129 -0
  117. package/src/components/NavigationRoot.tsx +9 -1
  118. package/src/components/Screen.tsx +116 -0
  119. package/src/components/ScreenContainer.tsx +14 -1
  120. package/src/components/Stack.tsx +21 -2
  121. package/src/components/TabBar.tsx +104 -0
  122. package/src/components/Tabs.tsx +216 -0
  123. package/src/hooks/use-focus.ts +88 -0
  124. package/src/hooks/use-linking-nav.ts +159 -0
  125. package/src/hooks/use-nav-internal.ts +48 -1
  126. package/src/hooks/use-nav-serializer.ts +239 -0
  127. package/src/hooks/use-screen-options.ts +48 -0
  128. package/src/href.ts +68 -11
  129. package/src/index.ts +29 -0
  130. package/src/internal/screen-registry.ts +89 -0
  131. package/src/navigator/core.ts +86 -4
  132. package/src/types.ts +56 -0
  133. package/src/url/build.ts +35 -0
  134. package/src/url/compile.ts +109 -0
  135. package/src/url/format.ts +95 -0
  136. package/src/url/index.ts +18 -0
  137. package/src/url/parse.ts +102 -0
  138. package/src/url/registry.ts +69 -0
  139. package/src/url/validate.ts +67 -0
@@ -0,0 +1,50 @@
1
+ import { onMounted } from '@sigx/lynx';
2
+ import { BackHandler } from '@sigx/lynx-linking';
3
+ import { useNav } from './use-nav.js';
4
+ /**
5
+ * Wire the Android hardware back button to the active navigator.
6
+ *
7
+ * Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
8
+ * `BackHandler` (which the native side dispatches from
9
+ * `MainActivity.onBackPressed`). On press:
10
+ *
11
+ * - If `nav.canGoBack` → `nav.pop()`.
12
+ * - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
13
+ * keeps the bundle warm; iOS: rejects, since iOS doesn't permit
14
+ * programmatic termination).
15
+ *
16
+ * Call this once in any component under `<NavigationRoot>` (typically a
17
+ * thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
18
+ * hook is a no-op there.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const BackHandlerWiring = component(() => {
23
+ * useHardwareBack();
24
+ * return () => null;
25
+ * });
26
+ *
27
+ * <NavigationRoot routes={routes}>
28
+ * <BackHandlerWiring />
29
+ * <Stack />
30
+ * </NavigationRoot>
31
+ * ```
32
+ */
33
+ export function useHardwareBack() {
34
+ const nav = useNav();
35
+ onMounted(() => {
36
+ const sub = BackHandler.addEventListener(() => {
37
+ if (nav.canGoBack) {
38
+ nav.pop();
39
+ return true;
40
+ }
41
+ // At the root — leave the app. Promise is fire-and-forget; we
42
+ // don't await because we want the back press to feel instant
43
+ // (Android starts the move-to-back transition immediately).
44
+ void BackHandler.exitApp();
45
+ return true;
46
+ });
47
+ return () => sub.remove();
48
+ });
49
+ }
50
+ //# sourceMappingURL=use-hardware-back.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-hardware-back.js","sourceRoot":"","sources":["../../src/hooks/use-hardware-back.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe;IAC3B,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,GAAG,GAAG,WAAW,CAAC,gBAAgB,CAAC,GAAG,EAAE;YAC1C,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;gBAChB,GAAG,CAAC,GAAG,EAAE,CAAC;gBACV,OAAO,IAAI,CAAC;YAChB,CAAC;YACD,8DAA8D;YAC9D,6DAA6D;YAC7D,4DAA4D;YAC5D,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { type Href } from '../href.js';
2
+ import { type Nav } from './use-nav.js';
3
+ import type { RouteMap } from '../types.js';
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;
92
+ //# sourceMappingURL=use-linking-nav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-linking-nav.d.ts","sourceRoot":"","sources":["../../src/hooks/use-linking-nav.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAU,KAAK,GAAG,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,oBAAoB;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,oBAAyB,GAAG,IAAI,CA0BnE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CASrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,QAAQ,EAChB,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,GAAG,SAAS,GACzB,IAAI,CAkBN"}
@@ -0,0 +1,109 @@
1
+ import { onMounted } from '@sigx/lynx';
2
+ import { Linking } from '@sigx/lynx-linking';
3
+ import { parseHref } from '../href.js';
4
+ import { useNav } from './use-nav.js';
5
+ import { useNavRoutes } from './use-nav-internal.js';
6
+ /**
7
+ * Bridge `@sigx/lynx-linking` URL events into a `@sigx/lynx-navigation`
8
+ * navigator. Call once inside a `<NavigationRoot>` subtree.
9
+ *
10
+ * Handles both delivery modes:
11
+ * - **cold start** — `Linking.getInitialURL()` is read on mount and, if
12
+ * present, dispatched (replacing the initial route by default).
13
+ * - **warm start** — `Linking.addEventListener('url', ...)` subscribes for
14
+ * URLs delivered while the app is already running; each one is pushed.
15
+ *
16
+ * URL → route dispatch goes through `parseHref`, which matches the URL's
17
+ * pathname against the route registry seeded by `<NavigationRoot>`. Routes
18
+ * without a `path` template are never matched by deep links — only typed
19
+ * `<Link>` / `nav.push` calls reach them.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import { useLinkingNav } from '@sigx/lynx-navigation';
24
+ *
25
+ * const DeepLinks = component(() => {
26
+ * useLinkingNav({
27
+ * prefixes: ['myapp://', 'https://myapp.com'],
28
+ * onUnmatched: (url) => console.warn('Unknown deep link:', url),
29
+ * });
30
+ * return () => null;
31
+ * });
32
+ *
33
+ * <NavigationRoot routes={routes}>
34
+ * <DeepLinks />
35
+ * <Stack />
36
+ * </NavigationRoot>
37
+ * ```
38
+ */
39
+ export function useLinkingNav(opts = {}) {
40
+ const nav = useNav();
41
+ const routes = useNavRoutes();
42
+ const dispatch = (url, kind) => {
43
+ if (opts.onURL) {
44
+ opts.onURL(url, nav);
45
+ return;
46
+ }
47
+ const stripped = _stripPrefix(url, opts.prefixes);
48
+ const href = parseHref(stripped);
49
+ if (!href) {
50
+ opts.onUnmatched?.(url);
51
+ return;
52
+ }
53
+ _navigateToHref(nav, routes, href, kind);
54
+ };
55
+ onMounted(() => {
56
+ const initial = Linking.getInitialURL();
57
+ if (initial) {
58
+ dispatch(initial, opts.replaceInitial === false ? 'push' : 'replace');
59
+ }
60
+ const sub = Linking.addEventListener('url', (e) => dispatch(e.url, 'push'));
61
+ return () => sub.remove();
62
+ });
63
+ }
64
+ /**
65
+ * Strip the first matching prefix from `url`, returning a pathname-like
66
+ * string. If no prefixes are provided, or none match, the original URL is
67
+ * returned unchanged so `parseHref` can still handle scheme-prefixed forms
68
+ * via `@sigx/lynx-linking`'s `parse`.
69
+ *
70
+ * Exported for unit testing — not part of the package public API.
71
+ */
72
+ export function _stripPrefix(url, prefixes) {
73
+ if (!prefixes || prefixes.length === 0)
74
+ return url;
75
+ for (const prefix of prefixes) {
76
+ if (url.startsWith(prefix)) {
77
+ const rest = url.slice(prefix.length);
78
+ return rest.startsWith('/') ? rest : `/${rest}`;
79
+ }
80
+ }
81
+ return url;
82
+ }
83
+ /**
84
+ * Call the right `nav.push` / `nav.replace` overload for `href`. The
85
+ * overloads differ in positional layout: routes with a params schema take
86
+ * `(name, params, search?, options?)`; routes without take `(name, search?,
87
+ * options?)`. Calling the wrong shape silently shifts `search` into the
88
+ * `options` slot, so we look the route up in the registry and branch.
89
+ *
90
+ * Exported for unit testing — not part of the package public API.
91
+ */
92
+ export function _navigateToHref(nav, routes, href, kind) {
93
+ const def = routes[href.route];
94
+ // Defensive: `parseHref` already validated against the registry, so this
95
+ // really shouldn't happen — but if the registry was cleared between
96
+ // parse and dispatch (multi-NavigationRoot scenarios), bail rather than
97
+ // throw.
98
+ if (!def)
99
+ return;
100
+ const hasParams = !!def.params;
101
+ const action = kind === 'replace' ? nav.replace : nav.push;
102
+ if (hasParams) {
103
+ action(href.route, href.params, href.search);
104
+ }
105
+ else {
106
+ action(href.route, href.search);
107
+ }
108
+ }
109
+ //# sourceMappingURL=use-linking-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-linking-nav.js","sourceRoot":"","sources":["../../src/hooks/use-linking-nav.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,18 @@
1
1
  import { type SharedValue } from '@sigx/lynx';
2
- import type { RouteMap } from '../types.js';
2
+ import type { ScreenRegistry } from '../internal/screen-registry.js';
3
+ import type { RouteMap, StackEntry } from '../types.js';
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>>;
3
16
  /**
4
17
  * Internal injectable: the route registry passed into `<NavigationRoot>`.
5
18
  * Components (Stack, Screen) read this to look up route definitions by name.
@@ -32,6 +45,30 @@ export interface NavInternals {
32
45
  cancelBackGesture(): void;
33
46
  /** Whether the user opted into the edge-swipe-back gesture. */
34
47
  readonly edgeSwipeEnabled: boolean;
48
+ /**
49
+ * Cross-entry screen registry controller. `<EntryScope>` calls
50
+ * `register` on mount and `unregister` on unmount. Persistent chrome
51
+ * (HeaderBar / TabBar — later slices) calls `get(entryKey)` to read
52
+ * the focused screen's options + slot fills without remounting itself.
53
+ */
54
+ readonly screens: {
55
+ register(registry: ScreenRegistry): void;
56
+ unregister(entryKey: string): void;
57
+ get(entryKey: string): ScreenRegistry | undefined;
58
+ };
35
59
  }
36
60
  export declare const useNavInternals: import("@sigx/runtime-core").InjectableFunction<NavInternals>;
61
+ /**
62
+ * Internal injectable: the calling screen's `ScreenRegistry`.
63
+ *
64
+ * Provided by `<EntryScope>` alongside `useCurrentEntry`. The `<Screen>`
65
+ * component and its slot-filling sub-components write options and slot
66
+ * fills here; the navigator's persistent chrome (HeaderBar, TabBar — later
67
+ * slices) reads from this registry via `getScreenRegistry(key)` on the
68
+ * navigator state, which keys into a cross-entry map.
69
+ *
70
+ * Throws when used outside an EntryScope so calling `<Screen>` at the app
71
+ * root surfaces a clear error rather than silently no-op'ing.
72
+ */
73
+ export declare const useScreenRegistry: import("@sigx/runtime-core").InjectableFunction<ScreenRegistry>;
37
74
  //# sourceMappingURL=use-nav-internal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-nav-internal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-nav-internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,2DAIvB,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IACzB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C;;;;OAIG;IACH,gBAAgB,IAAI,IAAI,CAAC;IACzB,iEAAiE;IACjE,iBAAiB,IAAI,IAAI,CAAC;IAC1B,iEAAiE;IACjE,iBAAiB,IAAI,IAAI,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACtC;AAED,eAAO,MAAM,eAAe,+DAI1B,CAAC"}
1
+ {"version":3,"file":"use-nav-internal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-nav-internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,uFAI1B,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,2DAIvB,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IACzB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C;;;;OAIG;IACH,gBAAgB,IAAI,IAAI,CAAC;IACzB,iEAAiE;IACjE,iBAAiB,IAAI,IAAI,CAAC;IAC1B,iEAAiE;IACjE,iBAAiB,IAAI,IAAI,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE;QACd,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;QACzC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;KACrD,CAAC;CACL;AAED,eAAO,MAAM,eAAe,+DAI1B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,iEAI5B,CAAC"}
@@ -1,4 +1,18 @@
1
1
  import { defineInjectable } from '@sigx/lynx';
2
+ /**
3
+ * Internal injectable: the `StackEntry` the calling screen was rendered for.
4
+ *
5
+ * Provided by `<EntryScope>` which `<Stack>` and `<ScreenContainer>` wrap
6
+ * around each screen component mount. Screens use this to derive their own
7
+ * focus state (`useIsFocused`, `useFocusEffect`) without having to track
8
+ * `entry.key` themselves.
9
+ *
10
+ * Default throws so calling `useIsFocused()` outside a screen mounted by a
11
+ * navigator surfaces a clear error rather than silently returning `false`.
12
+ */
13
+ export const useCurrentEntry = defineInjectable(() => {
14
+ throw new Error('[lynx-navigation] No screen entry in scope. `useIsFocused` / `useFocusEffect` must be called from a component rendered as a route by <Stack>.');
15
+ });
2
16
  /**
3
17
  * Internal injectable: the route registry passed into `<NavigationRoot>`.
4
18
  * Components (Stack, Screen) read this to look up route definitions by name.
@@ -9,4 +23,22 @@ import { defineInjectable } from '@sigx/lynx';
9
23
  export const useNavRoutes = defineInjectable(() => {
10
24
  throw new Error('[lynx-navigation] No <NavigationRoot> found in the component tree.');
11
25
  });
26
+ export const useNavInternals = defineInjectable(() => {
27
+ throw new Error('[lynx-navigation] No <NavigationRoot> found in the component tree.');
28
+ });
29
+ /**
30
+ * Internal injectable: the calling screen's `ScreenRegistry`.
31
+ *
32
+ * Provided by `<EntryScope>` alongside `useCurrentEntry`. The `<Screen>`
33
+ * component and its slot-filling sub-components write options and slot
34
+ * fills here; the navigator's persistent chrome (HeaderBar, TabBar — later
35
+ * slices) reads from this registry via `getScreenRegistry(key)` on the
36
+ * navigator state, which keys into a cross-entry map.
37
+ *
38
+ * Throws when used outside an EntryScope so calling `<Screen>` at the app
39
+ * root surfaces a clear error rather than silently no-op'ing.
40
+ */
41
+ export const useScreenRegistry = defineInjectable(() => {
42
+ throw new Error('[lynx-navigation] No screen registry in scope. `<Screen>` (and `<Screen.Header>`, etc.) must be used inside a route component rendered by `<Stack>`.');
43
+ });
12
44
  //# sourceMappingURL=use-nav-internal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-nav-internal.js","sourceRoot":"","sources":["../../src/hooks/use-nav-internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAW,GAAG,EAAE;IACxD,MAAM,IAAI,KAAK,CACX,oEAAoE,CACvE,CAAC;AACN,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"use-nav-internal.js","sourceRoot":"","sources":["../../src/hooks/use-nav-internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAoB,MAAM,YAAY,CAAC;AAIhE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,gBAAgB,CAAa,GAAG,EAAE;IAC7D,MAAM,IAAI,KAAK,CACX,+IAA+I,CAClJ,CAAC;AACN,CAAC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAW,GAAG,EAAE;IACxD,MAAM,IAAI,KAAK,CACX,oEAAoE,CACvE,CAAC;AACN,CAAC,CAAC,CAAC;AAuCH,MAAM,CAAC,MAAM,eAAe,GAAG,gBAAgB,CAAe,GAAG,EAAE;IAC/D,MAAM,IAAI,KAAK,CACX,oEAAoE,CACvE,CAAC;AACN,CAAC,CAAC,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,gBAAgB,CAAiB,GAAG,EAAE;IACnE,MAAM,IAAI,KAAK,CACX,sJAAsJ,CACzJ,CAAC;AACN,CAAC,CAAC,CAAC"}
@@ -0,0 +1,83 @@
1
+ import type { StackEntry } from '../types.js';
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;
83
+ //# sourceMappingURL=use-nav-serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-nav-serializer.d.ts","sourceRoot":"","sources":["../../src/hooks/use-nav-serializer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAC9B,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,WAAW,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,uBAAuB;IACpC,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7C;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,YAAY,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1G;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAsIvE"}
@@ -0,0 +1,181 @@
1
+ import { effect, onMounted, onUnmounted } from '@sigx/lynx';
2
+ import { useNav } from './use-nav.js';
3
+ import { useNavRoutes } from './use-nav-internal.js';
4
+ export const NAV_SNAPSHOT_VERSION = 1;
5
+ /**
6
+ * Wire a navigator's stack to a storage adapter.
7
+ *
8
+ * On mount:
9
+ * 1. Call `storage.load()`.
10
+ * 2. Validate the snapshot (version match, every entry's route still
11
+ * registered).
12
+ * 3. On success, `nav.reset({ stack })` to apply.
13
+ * 4. On any failure, leave the stack alone (initial route remains).
14
+ *
15
+ * Then subscribe to `nav.stack` and call `storage.save(snapshot)` debounced.
16
+ *
17
+ * Why we don't validate `params` / `search` against schemas here: schemas
18
+ * are part of the route definition, and re-running them across all entries
19
+ * on every launch costs more than it's worth. The contract is "entries were
20
+ * validated when they were pushed; if the schema has since changed in a
21
+ * breaking way, bump `version` to reject old snapshots wholesale." Callers
22
+ * who want a stricter check can run their own validation in
23
+ * `storage.load()` and return `null` on mismatch.
24
+ */
25
+ export function useNavSerializer(options) {
26
+ const nav = useNav();
27
+ const routes = useNavRoutes();
28
+ const debounceMs = options.debounceMs ?? 250;
29
+ const onRestored = options.onRestored;
30
+ const onErr = options.onRestoreError;
31
+ // Mutable mount/state flags. Plain closure vars (no signals) — we don't
32
+ // want any of this driving a render and we don't want it tracked by the
33
+ // save-effect below.
34
+ let mounted = true;
35
+ let restoreDone = false;
36
+ let pendingTimer = null;
37
+ let stopEffect = null;
38
+ onMounted(() => {
39
+ // Kick off the load synchronously — adapters that return a value
40
+ // immediately (sync stores, test doubles) hit the resolve branch on
41
+ // the same tick. Promise adapters resolve on the microtask queue;
42
+ // the `mounted` guard catches teardown races.
43
+ Promise.resolve()
44
+ .then(() => options.storage.load())
45
+ .then((snap) => {
46
+ if (!mounted)
47
+ return;
48
+ if (snap == null) {
49
+ restoreDone = true;
50
+ startSaveEffect();
51
+ return;
52
+ }
53
+ if (!isValidShape(snap)) {
54
+ onErr?.('shape');
55
+ restoreDone = true;
56
+ startSaveEffect();
57
+ return;
58
+ }
59
+ if (snap.version !== NAV_SNAPSHOT_VERSION) {
60
+ onErr?.('version');
61
+ restoreDone = true;
62
+ startSaveEffect();
63
+ return;
64
+ }
65
+ // Drop the snapshot if any entry references a route the app
66
+ // no longer knows about — partial restoration is worse than
67
+ // no restoration (could leave the user stranded on a screen
68
+ // whose params won't validate when read by `useParams`).
69
+ for (const entry of snap.stack) {
70
+ if (!routes[entry.route]) {
71
+ onErr?.('unknown-route');
72
+ restoreDone = true;
73
+ startSaveEffect();
74
+ return;
75
+ }
76
+ }
77
+ if (snap.stack.length === 0) {
78
+ onErr?.('shape');
79
+ restoreDone = true;
80
+ startSaveEffect();
81
+ return;
82
+ }
83
+ nav.reset({ stack: snap.stack });
84
+ onRestored?.(snap);
85
+ restoreDone = true;
86
+ startSaveEffect();
87
+ })
88
+ .catch((err) => {
89
+ if (!mounted)
90
+ return;
91
+ onErr?.('load-threw', err);
92
+ restoreDone = true;
93
+ startSaveEffect();
94
+ });
95
+ });
96
+ function startSaveEffect() {
97
+ if (!mounted || stopEffect)
98
+ return;
99
+ // The first effect run is just the initial subscription read — it
100
+ // happens immediately when `effect()` is called, before any user
101
+ // navigation, and represents the stack-as-restored (or the initial
102
+ // route when there was nothing to restore). Either way, we don't
103
+ // want to persist it: in the restore case it would race with the
104
+ // adapter that just supplied this state, and in the fresh case
105
+ // it's redundant.
106
+ let firstRun = true;
107
+ const runner = effect(() => {
108
+ const stack = nav.stack;
109
+ const snapshot = {
110
+ version: NAV_SNAPSHOT_VERSION,
111
+ stack: stack.map((e) => ({
112
+ key: e.key,
113
+ route: e.route,
114
+ params: e.params,
115
+ search: e.search,
116
+ state: e.state,
117
+ presentation: e.presentation,
118
+ })),
119
+ };
120
+ if (firstRun) {
121
+ firstRun = false;
122
+ return;
123
+ }
124
+ schedule(snapshot);
125
+ });
126
+ stopEffect = () => runner.stop();
127
+ }
128
+ function schedule(snapshot) {
129
+ if (pendingTimer != null)
130
+ clearTimeout(pendingTimer);
131
+ pendingTimer = setTimeout(() => {
132
+ pendingTimer = null;
133
+ try {
134
+ const r = options.storage.save(snapshot);
135
+ if (r && typeof r.catch === 'function') {
136
+ r.catch(() => {
137
+ // Save errors are intentionally swallowed — see the
138
+ // hook doc-comment. Hosts that need visibility can
139
+ // wrap their adapter.
140
+ });
141
+ }
142
+ }
143
+ catch {
144
+ // Same rationale.
145
+ }
146
+ }, debounceMs);
147
+ }
148
+ onUnmounted(() => {
149
+ mounted = false;
150
+ if (pendingTimer != null) {
151
+ clearTimeout(pendingTimer);
152
+ pendingTimer = null;
153
+ }
154
+ if (stopEffect) {
155
+ stopEffect();
156
+ stopEffect = null;
157
+ }
158
+ });
159
+ }
160
+ function isValidShape(s) {
161
+ if (!s || typeof s !== 'object')
162
+ return false;
163
+ const obj = s;
164
+ if (typeof obj.version !== 'number')
165
+ return false;
166
+ if (!Array.isArray(obj.stack))
167
+ return false;
168
+ for (const entry of obj.stack) {
169
+ if (!entry || typeof entry !== 'object')
170
+ return false;
171
+ const e = entry;
172
+ if (typeof e.key !== 'string')
173
+ return false;
174
+ if (typeof e.route !== 'string')
175
+ return false;
176
+ if (typeof e.presentation !== 'string')
177
+ return false;
178
+ }
179
+ return true;
180
+ }
181
+ //# sourceMappingURL=use-nav-serializer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-nav-serializer.js","sourceRoot":"","sources":["../../src/hooks/use-nav-serializer.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-nav.js","sourceRoot":"","sources":["../../src/hooks/use-nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA6F9C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,gBAAgB,CAAM,GAAG,EAAE;IAC7C,MAAM,IAAI,KAAK,CACX,6FAA6F,CAChG,CAAC;AACN,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"use-nav.js","sourceRoot":"","sources":["../../src/hooks/use-nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA2G9C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,gBAAgB,CAAM,GAAG,EAAE;IAC7C,MAAM,IAAI,KAAK,CACX,6FAA6F,CAChG,CAAC;AACN,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ScreenOptions } from '../types.js';
2
+ export declare function useScreenOptions(optionsOrFn: ScreenOptions | (() => ScreenOptions)): void;
3
+ //# sourceMappingURL=use-screen-options.d.ts.map