@sigx/lynx-navigation 0.1.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +189 -7
  2. package/dist/components/EntryScope.d.ts +1 -1
  3. package/dist/components/EntryScope.d.ts.map +1 -1
  4. package/dist/components/Layer.d.ts +34 -0
  5. package/dist/components/Layer.d.ts.map +1 -0
  6. package/dist/components/Link.d.ts +2 -2
  7. package/dist/components/Link.d.ts.map +1 -1
  8. package/dist/components/NavigationRoot.d.ts +2 -2
  9. package/dist/components/NavigationRoot.d.ts.map +1 -1
  10. package/dist/components/Screen.d.ts +6 -6
  11. package/dist/components/Screen.d.ts.map +1 -1
  12. package/dist/components/Stack.d.ts +83 -13
  13. package/dist/components/Stack.d.ts.map +1 -1
  14. package/dist/components/TabBar.d.ts +19 -20
  15. package/dist/components/TabBar.d.ts.map +1 -1
  16. package/dist/components/Tabs.d.ts +30 -21
  17. package/dist/components/Tabs.d.ts.map +1 -1
  18. package/dist/define-routes.d.ts +1 -1
  19. package/dist/define-routes.d.ts.map +1 -1
  20. package/dist/hooks/use-focus.d.ts.map +1 -1
  21. package/dist/hooks/use-hardware-back.d.ts +9 -2
  22. package/dist/hooks/use-hardware-back.d.ts.map +1 -1
  23. package/dist/hooks/use-linking-nav.d.ts +3 -3
  24. package/dist/hooks/use-linking-nav.d.ts.map +1 -1
  25. package/dist/hooks/use-nav-internal.d.ts +21 -3
  26. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  27. package/dist/hooks/use-nav-serializer.d.ts +1 -1
  28. package/dist/hooks/use-nav-serializer.d.ts.map +1 -1
  29. package/dist/hooks/use-nav.d.ts +38 -3
  30. package/dist/hooks/use-nav.d.ts.map +1 -1
  31. package/dist/hooks/use-params.d.ts +1 -1
  32. package/dist/hooks/use-params.d.ts.map +1 -1
  33. package/dist/hooks/use-screen-chrome.d.ts +19 -0
  34. package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
  35. package/dist/hooks/use-screen-options.d.ts +1 -1
  36. package/dist/hooks/use-screen-options.d.ts.map +1 -1
  37. package/dist/hooks/use-search.d.ts +1 -1
  38. package/dist/hooks/use-search.d.ts.map +1 -1
  39. package/dist/href.d.ts +2 -2
  40. package/dist/href.d.ts.map +1 -1
  41. package/dist/index.d.ts +33 -31
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1160 -29
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/layer-plan.d.ts +69 -0
  46. package/dist/internal/layer-plan.d.ts.map +1 -0
  47. package/dist/internal/screen-registry.d.ts +1 -1
  48. package/dist/internal/screen-registry.d.ts.map +1 -1
  49. package/dist/internal/screen-width.d.ts +9 -7
  50. package/dist/internal/screen-width.d.ts.map +1 -1
  51. package/dist/navigator/core.d.ts +31 -4
  52. package/dist/navigator/core.d.ts.map +1 -1
  53. package/dist/register.d.ts +1 -1
  54. package/dist/register.d.ts.map +1 -1
  55. package/dist/url/index.d.ts +6 -6
  56. package/dist/url/index.d.ts.map +1 -1
  57. package/dist/url/parse.d.ts +1 -1
  58. package/dist/url/parse.d.ts.map +1 -1
  59. package/dist/url/registry.d.ts +2 -2
  60. package/dist/url/registry.d.ts.map +1 -1
  61. package/dist/url/validate.d.ts +1 -1
  62. package/dist/url/validate.d.ts.map +1 -1
  63. package/package.json +11 -10
  64. package/src/components/Drawer.d.ts +55 -0
  65. package/src/components/EdgeBackHandle.d.ts +1 -0
  66. package/src/components/EdgeBackHandle.tsx +2 -2
  67. package/{dist/components/EntryScope.js → src/components/EntryScope.d.ts} +7 -15
  68. package/src/components/EntryScope.tsx +15 -4
  69. package/src/components/Header.d.ts +6 -0
  70. package/src/components/Header.tsx +3 -3
  71. package/src/components/Layer.d.ts +33 -0
  72. package/src/components/Layer.tsx +96 -0
  73. package/src/components/Link.d.ts +60 -0
  74. package/src/components/Link.tsx +4 -4
  75. package/src/components/NavigationRoot.d.ts +36 -0
  76. package/src/components/NavigationRoot.tsx +6 -6
  77. package/src/components/Screen.d.ts +97 -0
  78. package/src/components/Screen.tsx +13 -11
  79. package/src/components/Stack.d.ts +90 -0
  80. package/src/components/Stack.tsx +333 -92
  81. package/src/components/TabBar.d.ts +38 -0
  82. package/src/components/TabBar.tsx +22 -22
  83. package/src/components/Tabs.d.ts +109 -0
  84. package/src/components/Tabs.tsx +54 -22
  85. package/{dist/define-routes.js → src/define-routes.d.ts} +2 -4
  86. package/src/define-routes.ts +1 -1
  87. package/{dist/hooks/use-focus.js → src/hooks/use-focus.d.ts} +3 -39
  88. package/src/hooks/use-focus.ts +9 -3
  89. package/src/hooks/use-hardware-back.d.ts +37 -0
  90. package/src/hooks/use-hardware-back.ts +43 -9
  91. package/src/hooks/use-linking-nav.d.ts +91 -0
  92. package/src/hooks/use-linking-nav.ts +4 -4
  93. package/src/hooks/use-nav-internal.d.ts +91 -0
  94. package/src/hooks/use-nav-internal.ts +24 -3
  95. package/src/hooks/use-nav-serializer.d.ts +82 -0
  96. package/src/hooks/use-nav-serializer.ts +3 -3
  97. package/src/hooks/use-nav.d.ts +111 -0
  98. package/src/hooks/use-nav.ts +40 -3
  99. package/{dist/hooks/use-params.js → src/hooks/use-params.d.ts} +2 -6
  100. package/src/hooks/use-params.ts +2 -2
  101. package/src/hooks/use-screen-chrome.d.ts +18 -0
  102. package/src/hooks/use-screen-chrome.ts +122 -0
  103. package/src/hooks/use-screen-options.d.ts +2 -0
  104. package/src/hooks/use-screen-options.ts +3 -3
  105. package/{dist/hooks/use-search.js → src/hooks/use-search.d.ts} +2 -6
  106. package/src/hooks/use-search.ts +2 -2
  107. package/src/href.d.ts +54 -0
  108. package/src/href.ts +6 -6
  109. package/src/index.d.ts +39 -0
  110. package/src/index.ts +33 -31
  111. package/src/internal/layer-plan.d.ts +68 -0
  112. package/src/internal/layer-plan.ts +187 -0
  113. package/{dist/internal/screen-registry.js → src/internal/screen-registry.d.ts} +21 -32
  114. package/src/internal/screen-registry.ts +1 -1
  115. package/src/internal/screen-width.d.ts +17 -0
  116. package/src/internal/screen-width.ts +22 -14
  117. package/src/navigator/core.d.ts +96 -0
  118. package/src/navigator/core.ts +90 -10
  119. package/src/register.d.ts +37 -0
  120. package/src/register.ts +1 -1
  121. package/src/types.d.ts +217 -0
  122. package/src/url/build.d.ts +15 -0
  123. package/src/url/build.ts +2 -2
  124. package/src/url/compile.d.ts +34 -0
  125. package/src/url/format.d.ts +28 -0
  126. package/src/url/index.ts +6 -6
  127. package/src/url/parse.d.ts +20 -0
  128. package/src/url/parse.ts +6 -6
  129. package/{dist/url/registry.js → src/url/registry.d.ts} +12 -28
  130. package/src/url/registry.ts +3 -3
  131. package/src/url/validate.d.ts +23 -0
  132. package/src/url/validate.ts +1 -1
  133. package/dist/components/Drawer.js +0 -74
  134. package/dist/components/Drawer.js.map +0 -1
  135. package/dist/components/EdgeBackHandle.js +0 -144
  136. package/dist/components/EdgeBackHandle.js.map +0 -1
  137. package/dist/components/EntryScope.js.map +0 -1
  138. package/dist/components/Header.js +0 -103
  139. package/dist/components/Header.js.map +0 -1
  140. package/dist/components/Link.js +0 -51
  141. package/dist/components/Link.js.map +0 -1
  142. package/dist/components/NavigationRoot.js +0 -67
  143. package/dist/components/NavigationRoot.js.map +0 -1
  144. package/dist/components/Screen.js +0 -94
  145. package/dist/components/Screen.js.map +0 -1
  146. package/dist/components/ScreenContainer.d.ts +0 -18
  147. package/dist/components/ScreenContainer.d.ts.map +0 -1
  148. package/dist/components/ScreenContainer.js +0 -77
  149. package/dist/components/ScreenContainer.js.map +0 -1
  150. package/dist/components/Stack.js +0 -75
  151. package/dist/components/Stack.js.map +0 -1
  152. package/dist/components/TabBar.js +0 -63
  153. package/dist/components/TabBar.js.map +0 -1
  154. package/dist/components/Tabs.js +0 -140
  155. package/dist/components/Tabs.js.map +0 -1
  156. package/dist/define-routes.js.map +0 -1
  157. package/dist/hooks/use-focus.js.map +0 -1
  158. package/dist/hooks/use-hardware-back.js +0 -50
  159. package/dist/hooks/use-hardware-back.js.map +0 -1
  160. package/dist/hooks/use-linking-nav.js +0 -109
  161. package/dist/hooks/use-linking-nav.js.map +0 -1
  162. package/dist/hooks/use-nav-internal.js +0 -44
  163. package/dist/hooks/use-nav-internal.js.map +0 -1
  164. package/dist/hooks/use-nav-serializer.js +0 -181
  165. package/dist/hooks/use-nav-serializer.js.map +0 -1
  166. package/dist/hooks/use-nav.js +0 -11
  167. package/dist/hooks/use-nav.js.map +0 -1
  168. package/dist/hooks/use-params.js.map +0 -1
  169. package/dist/hooks/use-screen-options.js +0 -43
  170. package/dist/hooks/use-screen-options.js.map +0 -1
  171. package/dist/hooks/use-search.js.map +0 -1
  172. package/dist/href.js +0 -57
  173. package/dist/href.js.map +0 -1
  174. package/dist/internal/screen-registry.js.map +0 -1
  175. package/dist/internal/screen-width.js +0 -30
  176. package/dist/internal/screen-width.js.map +0 -1
  177. package/dist/navigator/core.js +0 -344
  178. package/dist/navigator/core.js.map +0 -1
  179. package/dist/register.js +0 -2
  180. package/dist/register.js.map +0 -1
  181. package/dist/types.js +0 -9
  182. package/dist/types.js.map +0 -1
  183. package/dist/url/build.js +0 -30
  184. package/dist/url/build.js.map +0 -1
  185. package/dist/url/compile.js +0 -83
  186. package/dist/url/compile.js.map +0 -1
  187. package/dist/url/format.js +0 -102
  188. package/dist/url/format.js.map +0 -1
  189. package/dist/url/index.js +0 -13
  190. package/dist/url/index.js.map +0 -1
  191. package/dist/url/parse.js +0 -94
  192. package/dist/url/parse.js.map +0 -1
  193. package/dist/url/registry.js.map +0 -1
  194. package/dist/url/validate.js +0 -37
  195. package/dist/url/validate.js.map +0 -1
  196. package/src/components/ScreenContainer.tsx +0 -114
@@ -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
@@ -0,0 +1,111 @@
1
+ import type { RegisteredRoutes, RouteId, RouteParams, RouteSearch } from '../register';
2
+ import type { PopOptions, PushOptions, RouteRequiresParams, StackEntry, TransitionState } from '../types';
3
+ /**
4
+ * Subset of registered route names that declare a `params` schema (and so
5
+ * require a `params` argument when navigating).
6
+ *
7
+ * Computed via mapped-type filtering rather than a conditional inside the
8
+ * method signature: when a conditional like `RouteRequiresParams<R[K]>` is
9
+ * embedded inside a generic method parameter, TS evaluates it at definition
10
+ * time with K bound to the *whole* union of route ids — which distributes
11
+ * `RouteRequiresParams` over every route and collapses the result to
12
+ * `boolean`, breaking the conditional. Filtering once at the type level avoids
13
+ * the issue and produces clean overload candidates.
14
+ */
15
+ export type RoutesWithParams = {
16
+ [K in RouteId]: RouteRequiresParams<RegisteredRoutes[K]> extends true ? K : never;
17
+ }[RouteId];
18
+ /** Routes that don't declare a `params` schema. */
19
+ export type RoutesWithoutParams = Exclude<RouteId, RoutesWithParams>;
20
+ /**
21
+ * The navigator handle returned by `useNav()`.
22
+ *
23
+ * Read access (`current`, `stack`, `canGoBack`) is reactive — these properties
24
+ * are getters that read from the underlying stack signal, so accessing them
25
+ * inside a component's render function (or inside `effect` / `computed`) takes
26
+ * a reactive dependency. Mutating methods (`push`, `pop`, etc.) trigger the
27
+ * dependents to update.
28
+ *
29
+ * `push` and `replace` are split into two overloads — one for routes without a
30
+ * params schema (no params arg) and one for routes with a params schema
31
+ * (params required). See `RoutesWithParams` above for why this isn't a single
32
+ * conditional return type.
33
+ */
34
+ export interface Nav {
35
+ /** Push a route that has no params schema. */
36
+ push<K extends RoutesWithoutParams>(name: K, search?: RouteSearch<K>, options?: PushOptions): void;
37
+ /** Push a route that requires params. */
38
+ push<K extends RoutesWithParams>(name: K, params: RouteParams<K>, search?: RouteSearch<K>, options?: PushOptions): void;
39
+ /** Replace the top entry — same overload pattern as push. */
40
+ replace<K extends RoutesWithoutParams>(name: K, search?: RouteSearch<K>, options?: PushOptions): void;
41
+ replace<K extends RoutesWithParams>(name: K, params: RouteParams<K>, search?: RouteSearch<K>, options?: PushOptions): void;
42
+ /** Pop one or more entries off the top of the stack. */
43
+ pop(count?: number, options?: PopOptions): void;
44
+ /** Pop entries until the named route is at the top. */
45
+ popTo<K extends RouteId>(name: K): void;
46
+ /** Pop all the way to the root entry. */
47
+ popToRoot(): void;
48
+ /** Wholesale-replace the stack. */
49
+ reset(state: {
50
+ stack: ReadonlyArray<StackEntry>;
51
+ }): void;
52
+ /** Dismiss the topmost modal stack (no-op if none active). */
53
+ dismiss(): void;
54
+ /** Currently-focused entry. Reactive via property access. */
55
+ readonly current: StackEntry;
56
+ /** Full stack, top last. Reactive. */
57
+ readonly stack: ReadonlyArray<StackEntry>;
58
+ /** Whether the user can go back from the current entry. Reactive. */
59
+ readonly canGoBack: boolean;
60
+ /**
61
+ * Parent navigator (e.g. the root nav above a per-tab `<Stack>`), or null
62
+ * at the root. Set when a `<Stack>` mints its own navigator via
63
+ * `<Stack initialRoute="…">` — that stack's `useNav()` returns a nav
64
+ * whose `parent` is the enclosing nav.
65
+ *
66
+ * `push` calls for routes whose resolved presentation is non-`card`
67
+ * (`modal` / `fullScreen` / `transparent-modal`) escalate up the
68
+ * `parent` chain automatically — you don't normally need to reach
69
+ * through `parent` to present modals. `parent` is exposed as an escape
70
+ * hatch for power users (e.g. imperative `parent.pop()` from a child
71
+ * stack). Avoid pushing card routes onto `parent` directly — that
72
+ * defeats per-tab stack isolation.
73
+ */
74
+ readonly parent: Nav | null;
75
+ /**
76
+ * Whether this navigator is part of the currently-focused chain. True
77
+ * for the root nav at all times; for a nested nav (e.g. a per-tab
78
+ * stack), true only when its host entry is the top of `parent`, the
79
+ * parent itself is locally focused, and any extra gate (e.g. the
80
+ * enclosing tab is active) reports active.
81
+ *
82
+ * Reactive. `useIsFocused()` ANDs `nav.current.key === myKey` with
83
+ * `nav.isLocallyFocused`.
84
+ */
85
+ readonly isLocallyFocused: boolean;
86
+ /**
87
+ * @internal
88
+ * Set of child navigators (per-tab `<Stack>` instances) that have
89
+ * registered themselves under this nav. Used by `useHardwareBack` to
90
+ * find the deepest currently-focused nav and route the back press
91
+ * there before falling back up the chain.
92
+ *
93
+ * Not part of the public API — leading-underscore marks it as
94
+ * implementation detail.
95
+ */
96
+ readonly _children: Set<Nav>;
97
+ /**
98
+ * In-flight transition, or null when navigation is at rest. Reactive —
99
+ * `<Stack>` reads this to decide whether to render one screen or two
100
+ * (during a slide transition both the outgoing and incoming screens
101
+ * stay mounted with `useAnimatedStyle`-driven transforms).
102
+ */
103
+ readonly transition: TransitionState | null;
104
+ }
105
+ /**
106
+ * Access the innermost navigator. Provided by `<NavigationRoot>` via
107
+ * `defineProvide`. Throws when called outside a NavigationRoot subtree.
108
+ *
109
+ * Mirrors `@sigx/router`'s `useRouter` pattern (`packages/router/src/router.ts:30`).
110
+ */
111
+ export declare const useNav: import("@sigx/runtime-core").InjectableFunction<Nav>;
@@ -1,12 +1,12 @@
1
1
  import { defineInjectable } from '@sigx/lynx';
2
- import type { RegisteredRoutes, RouteId, RouteParams, RouteSearch } from '../register.js';
2
+ import type { RegisteredRoutes, RouteId, RouteParams, RouteSearch } from '../register';
3
3
  import type {
4
4
  PopOptions,
5
5
  PushOptions,
6
6
  RouteRequiresParams,
7
7
  StackEntry,
8
8
  TransitionState,
9
- } from '../types.js';
9
+ } from '../types';
10
10
 
11
11
  /**
12
12
  * Subset of registered route names that declare a `params` schema (and so
@@ -93,9 +93,46 @@ export interface Nav {
93
93
  /** Whether the user can go back from the current entry. Reactive. */
94
94
  readonly canGoBack: boolean;
95
95
 
96
- /** Parent navigator (e.g. the Tabs above this Stack), or null at the root. */
96
+ /**
97
+ * Parent navigator (e.g. the root nav above a per-tab `<Stack>`), or null
98
+ * at the root. Set when a `<Stack>` mints its own navigator via
99
+ * `<Stack initialRoute="…">` — that stack's `useNav()` returns a nav
100
+ * whose `parent` is the enclosing nav.
101
+ *
102
+ * `push` calls for routes whose resolved presentation is non-`card`
103
+ * (`modal` / `fullScreen` / `transparent-modal`) escalate up the
104
+ * `parent` chain automatically — you don't normally need to reach
105
+ * through `parent` to present modals. `parent` is exposed as an escape
106
+ * hatch for power users (e.g. imperative `parent.pop()` from a child
107
+ * stack). Avoid pushing card routes onto `parent` directly — that
108
+ * defeats per-tab stack isolation.
109
+ */
97
110
  readonly parent: Nav | null;
98
111
 
112
+ /**
113
+ * Whether this navigator is part of the currently-focused chain. True
114
+ * for the root nav at all times; for a nested nav (e.g. a per-tab
115
+ * stack), true only when its host entry is the top of `parent`, the
116
+ * parent itself is locally focused, and any extra gate (e.g. the
117
+ * enclosing tab is active) reports active.
118
+ *
119
+ * Reactive. `useIsFocused()` ANDs `nav.current.key === myKey` with
120
+ * `nav.isLocallyFocused`.
121
+ */
122
+ readonly isLocallyFocused: boolean;
123
+
124
+ /**
125
+ * @internal
126
+ * Set of child navigators (per-tab `<Stack>` instances) that have
127
+ * registered themselves under this nav. Used by `useHardwareBack` to
128
+ * find the deepest currently-focused nav and route the back press
129
+ * there before falling back up the chain.
130
+ *
131
+ * Not part of the public API — leading-underscore marks it as
132
+ * implementation detail.
133
+ */
134
+ readonly _children: Set<Nav>;
135
+
99
136
  /**
100
137
  * In-flight transition, or null when navigation is at rest. Reactive —
101
138
  * `<Stack>` reads this to decide whether to render one screen or two
@@ -1,4 +1,4 @@
1
- import { useNav } from './use-nav.js';
1
+ import type { RouteId, RouteParams } from '../register';
2
2
  /**
3
3
  * Read the typed params for the current screen, asserted against the named
4
4
  * route from the registry.
@@ -15,8 +15,4 @@ import { useNav } from './use-nav.js';
15
15
  * update for the same mounted screen" path in v0.1, so a snapshot at setup
16
16
  * time is correct.
17
17
  */
18
- export function useParams(_name) {
19
- const nav = useNav();
20
- return nav.current.params;
21
- }
22
- //# sourceMappingURL=use-params.js.map
18
+ export declare function useParams<K extends RouteId>(_name: K): RouteParams<K>;
@@ -1,5 +1,5 @@
1
- import type { RouteId, RouteParams } from '../register.js';
2
- import { useNav } from './use-nav.js';
1
+ import type { RouteId, RouteParams } from '../register';
2
+ import { useNav } from './use-nav';
3
3
 
4
4
  /**
5
5
  * Read the typed params for the current screen, asserted against the named
@@ -0,0 +1,18 @@
1
+ import type { ScreenSlotFills } from '../types';
2
+ export interface ScreenChrome {
3
+ /** Resolved screen title — `options.title` (string or getter) or the route name as fallback. Reactive. */
4
+ readonly title: string;
5
+ /** Whether the header should render. Defaults to true unless the screen set `headerShown: false`. Reactive. */
6
+ readonly headerShown: boolean;
7
+ /** True when the current stack has more than one entry — i.e. there's something to pop back to. Reactive. */
8
+ readonly canGoBack: boolean;
9
+ /** Pop the top entry. No-op when `!canGoBack`. */
10
+ pop(): void;
11
+ /** Full header override slot, if `<Screen.Header>` was set. Render its return value in place of the default layout. */
12
+ readonly header: ScreenSlotFills['header'] | undefined;
13
+ /** Left-aligned slot (typically a back button). Reactive. */
14
+ readonly headerLeft: ScreenSlotFills['headerLeft'] | undefined;
15
+ /** Right-aligned slot (typically actions). Reactive. */
16
+ readonly headerRight: ScreenSlotFills['headerRight'] | undefined;
17
+ }
18
+ export declare function useScreenChrome(): ScreenChrome;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `useScreenChrome` — reactive read of the currently-focused screen's
3
+ * options + slot fills, plus navigation helpers a header would need
4
+ * (canGoBack, pop).
5
+ *
6
+ * The built-in `<Header />` reads this same data via internal hooks.
7
+ * `useScreenChrome` exposes it as a public API so theme packages
8
+ * (`@sigx/lynx-daisyui`, custom designs) can build their own header
9
+ * components without depending on internal modules.
10
+ *
11
+ * Resolution rules:
12
+ *
13
+ * - **Inside a screen body** (i.e. inside an EntryScope whose entry is
14
+ * on the nearest `useNav()`'s stack), bind to **this entry's**
15
+ * registry. Useful for modal screens that render their own
16
+ * NavHeader inside — the chrome slides with the sheet.
17
+ * - **Outside any matching EntryScope** (slot of `<Stack>`, persistent
18
+ * root-level Header, etc.), bind to the *destination* entry of the
19
+ * current nav state — what the navigator is settling on once the
20
+ * in-flight transition completes. Push: the new top (already at
21
+ * nav.current). Pop: the entry being revealed
22
+ * (`transition.underneathEntry`), *not* the one being animated off.
23
+ * Using the destination means the bar reflects what the user is
24
+ * navigating *to*, immediately, with no end-of-animation snap.
25
+ *
26
+ * Every property is a getter — reading inside a render / `computed`
27
+ * subscribes to the underlying signal, so consumers re-render when
28
+ * title / slots change.
29
+ */
30
+ import { useNav } from './use-nav';
31
+ import { useCurrentEntryOptional, useNavInternals } from './use-nav-internal';
32
+ import type { ScreenSlotFills, StackEntry } from '../types';
33
+
34
+ export interface ScreenChrome {
35
+ /** Resolved screen title — `options.title` (string or getter) or the route name as fallback. Reactive. */
36
+ readonly title: string;
37
+ /** Whether the header should render. Defaults to true unless the screen set `headerShown: false`. Reactive. */
38
+ readonly headerShown: boolean;
39
+ /** True when the current stack has more than one entry — i.e. there's something to pop back to. Reactive. */
40
+ readonly canGoBack: boolean;
41
+ /** Pop the top entry. No-op when `!canGoBack`. */
42
+ pop(): void;
43
+ /** Full header override slot, if `<Screen.Header>` was set. Render its return value in place of the default layout. */
44
+ readonly header: ScreenSlotFills['header'] | undefined;
45
+ /** Left-aligned slot (typically a back button). Reactive. */
46
+ readonly headerLeft: ScreenSlotFills['headerLeft'] | undefined;
47
+ /** Right-aligned slot (typically actions). Reactive. */
48
+ readonly headerRight: ScreenSlotFills['headerRight'] | undefined;
49
+ }
50
+
51
+ export function useScreenChrome(): ScreenChrome {
52
+ const nav = useNav();
53
+ const internals = useNavInternals();
54
+
55
+ // The candidate "scoped" entry, if we happen to be rendered inside
56
+ // an EntryScope. May belong to a DIFFERENT nav than `nav` — e.g.
57
+ // when NavHeader is placed in a per-tab `<Stack>`'s chrome slot,
58
+ // it sees the outer (root) EntryScope's entry but its `useNav()`
59
+ // returns the inner per-tab nav. We only honor the pin when the
60
+ // entry is actually on this nav's stack; otherwise we're crossing
61
+ // scopes and the destination-entry path is correct.
62
+ //
63
+ // `useCurrentEntryOptional` is the soft companion to
64
+ // `useCurrentEntry` — it returns `null` outside any EntryScope
65
+ // rather than throwing, which is the right semantic for a chrome
66
+ // consumer that *might* be a Stack slot.
67
+ const candidate: StackEntry | null = useCurrentEntryOptional();
68
+
69
+ const getDestinationEntry = (): StackEntry => {
70
+ const t = nav.transition;
71
+ if (t) {
72
+ return t.kind === 'pop' ? t.underneathEntry : t.topEntry;
73
+ }
74
+ return nav.current;
75
+ };
76
+
77
+ const getEntry = (): StackEntry => {
78
+ if (candidate) {
79
+ const stack = nav.stack;
80
+ if (stack.some((e) => e.key === candidate!.key)) {
81
+ return candidate;
82
+ }
83
+ }
84
+ return getDestinationEntry();
85
+ };
86
+
87
+ return {
88
+ get title() {
89
+ const entry = getEntry();
90
+ const reg = internals.screens.get(entry.key);
91
+ const t = reg?.options.title;
92
+ if (typeof t === 'function') return t();
93
+ if (typeof t === 'string') return t;
94
+ return entry.route;
95
+ },
96
+ get headerShown() {
97
+ const reg = internals.screens.get(getEntry().key);
98
+ return reg?.options.headerShown !== false;
99
+ },
100
+ get canGoBack() {
101
+ const entry = getEntry();
102
+ const stack = nav.stack;
103
+ const idx = stack.findIndex((e) => e.key === entry.key);
104
+ return idx > 0;
105
+ },
106
+ pop() {
107
+ nav.pop();
108
+ },
109
+ get header() {
110
+ const reg = internals.screens.get(getEntry().key);
111
+ return reg?.slots.header;
112
+ },
113
+ get headerLeft() {
114
+ const reg = internals.screens.get(getEntry().key);
115
+ return reg?.slots.headerLeft;
116
+ },
117
+ get headerRight() {
118
+ const reg = internals.screens.get(getEntry().key);
119
+ return reg?.slots.headerRight;
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScreenOptions } from '../types';
2
+ export declare function useScreenOptions(optionsOrFn: ScreenOptions | (() => ScreenOptions)): void;
@@ -22,9 +22,9 @@
22
22
  * pass a getter pay for the subscription; hosts that pass an object don't.
23
23
  */
24
24
  import { effect, onUnmounted } from '@sigx/lynx';
25
- import { useScreenRegistry } from './use-nav-internal.js';
26
- import { mergeOptions } from '../internal/screen-registry.js';
27
- import type { ScreenOptions } from '../types.js';
25
+ import { useScreenRegistry } from './use-nav-internal';
26
+ import { mergeOptions } from '../internal/screen-registry';
27
+ import type { ScreenOptions } from '../types';
28
28
 
29
29
  export function useScreenOptions(
30
30
  optionsOrFn: ScreenOptions | (() => ScreenOptions),
@@ -1,4 +1,4 @@
1
- import { useNav } from './use-nav.js';
1
+ import type { RouteId, RouteSearch } from '../register';
2
2
  /**
3
3
  * Read the typed search/query params for the current screen, asserted against
4
4
  * the named route from the registry.
@@ -7,8 +7,4 @@ import { useNav } from './use-nav.js';
7
7
  * reactivity story — each navigation triggers a remount via the entry-keyed
8
8
  * Stack, so a setup-time snapshot is sufficient.
9
9
  */
10
- export function useSearch(_name) {
11
- const nav = useNav();
12
- return nav.current.search;
13
- }
14
- //# sourceMappingURL=use-search.js.map
10
+ export declare function useSearch<K extends RouteId>(_name: K): RouteSearch<K>;
@@ -1,5 +1,5 @@
1
- import type { RouteId, RouteSearch } from '../register.js';
2
- import { useNav } from './use-nav.js';
1
+ import type { RouteId, RouteSearch } from '../register';
2
+ import { useNav } from './use-nav';
3
3
 
4
4
  /**
5
5
  * Read the typed search/query params for the current screen, asserted against
package/src/href.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { RouteId, RouteParams, RouteSearch } from './register';
2
+ import type { RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav';
3
+ /**
4
+ * A typed reference to a navigation target — what `<Link to={...}>` consumes
5
+ * and what `hrefFor()` produces.
6
+ *
7
+ * Holds both the typed pieces (route name, params, search) and the serialized
8
+ * URL form (when the route declares a `path` template). Either side can drive
9
+ * navigation — typed for in-app links, URL for deep links / sharing.
10
+ */
11
+ export interface Href<K extends RouteId = RouteId> {
12
+ readonly route: K;
13
+ readonly params: RouteParams<K>;
14
+ readonly search: RouteSearch<K>;
15
+ /** URL form. `null` when the route declares no `path` template. */
16
+ readonly url: string | null;
17
+ }
18
+ /**
19
+ * Build a typed Href for a given route. Validates params against the route's
20
+ * schema at runtime; type-checks them at compile time.
21
+ *
22
+ * Overloaded the same way as `nav.push` — one signature for routes without a
23
+ * params schema, one for routes that require params. See `RoutesWithParams`
24
+ * in `./hooks/use-nav.js` for why this isn't expressed as a single conditional.
25
+ *
26
+ * Requires a `<NavigationRoot>` (or an explicit `_setRouteRegistry` call) to
27
+ * have run — the URL form is built against the active route registry. The
28
+ * typed pieces (`route`, `params`, `search`) are returned regardless; `url`
29
+ * is `null` when the route declares no `path` template.
30
+ *
31
+ * Schema validation errors throw — pass already-validated values from
32
+ * `useParams` / `useSearch` to round-trip safely, or wrap in try/catch when
33
+ * building hrefs from external input.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * hrefFor('profile', { id: '42' }); // → { route, params, search: {}, url: '/users/42' }
38
+ * hrefFor('profile', { id: '42' }, { tab: 'about' });
39
+ * hrefFor('home'); // params arg omitted (no schema)
40
+ * ```
41
+ */
42
+ export declare function hrefFor<K extends RoutesWithoutParams>(name: K, search?: RouteSearch<K>): Href<K>;
43
+ export declare function hrefFor<K extends RoutesWithParams>(name: K, params: RouteParams<K>, search?: RouteSearch<K>): Href<K>;
44
+ /**
45
+ * Parse a URL string into a typed Href against the registered routes.
46
+ * Returns `null` if no route's `path` template matches the URL or if the
47
+ * extracted params/search fail the route's schema validation.
48
+ *
49
+ * Accepts both absolute (`myapp://host/path?q`) and pathname-only
50
+ * (`/path?q`) forms — the pathname is matched against each route's
51
+ * compiled template. Iteration order is the registration order; first match
52
+ * wins.
53
+ */
54
+ export declare function parseHref(url: string): Href | null;
package/src/href.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { RouteId, RouteParams, RouteSearch } from './register.js';
2
- import type { RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav.js';
3
- import { buildUrl } from './url/build.js';
4
- import { parseHrefImpl } from './url/parse.js';
5
- import { getRouteRegistry } from './url/registry.js';
6
- import { validateSync } from './url/validate.js';
1
+ import type { RouteId, RouteParams, RouteSearch } from './register';
2
+ import type { RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav';
3
+ import { buildUrl } from './url/build';
4
+ import { parseHrefImpl } from './url/parse';
5
+ import { getRouteRegistry } from './url/registry';
6
+ import { validateSync } from './url/validate';
7
7
 
8
8
  /**
9
9
  * A typed reference to a navigation target — what `<Link to={...}>` consumes
package/src/index.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @sigx/lynx-navigation — type-first native stack router.
3
+ *
4
+ * Phase 0.1 (current): typed registry, stack runtime, NavigationRoot + Stack.
5
+ * Coming next: Screen with slot-based header API, MTS transitions, Tabs.
6
+ */
7
+ export { defineRoutes } from './define-routes';
8
+ export type { Register, RegisteredRoutes, RouteId, RouteParams, RouteSearch } from './register';
9
+ export { useNav } from './hooks/use-nav';
10
+ export type { Nav, RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav';
11
+ export { useParams } from './hooks/use-params';
12
+ export { useSearch } from './hooks/use-search';
13
+ export { useHardwareBack } from './hooks/use-hardware-back';
14
+ export { useLinkingNav } from './hooks/use-linking-nav';
15
+ export type { UseLinkingNavOptions } from './hooks/use-linking-nav';
16
+ export { useIsFocused, useFocusEffect } from './hooks/use-focus';
17
+ export { useScreenOptions } from './hooks/use-screen-options';
18
+ export { useScreenChrome } from './hooks/use-screen-chrome';
19
+ export type { ScreenChrome } from './hooks/use-screen-chrome';
20
+ export { useNavSerializer, NAV_SNAPSHOT_VERSION, } from './hooks/use-nav-serializer';
21
+ export type { NavSnapshot, NavStorageAdapter, UseNavSerializerOptions, } from './hooks/use-nav-serializer';
22
+ export { hrefFor, parseHref } from './href';
23
+ export type { Href } from './href';
24
+ export { _setRouteRegistry, _clearRouteRegistry } from './url/registry';
25
+ export { compilePath } from './url/compile';
26
+ export type { CompiledPath } from './url/compile';
27
+ export { NavigationRoot } from './components/NavigationRoot';
28
+ export { Stack } from './components/Stack';
29
+ export { Screen } from './components/Screen';
30
+ export { Header } from './components/Header';
31
+ export { Tabs, useTabs } from './components/Tabs';
32
+ export type { TabInfo, TabsNav } from './components/Tabs';
33
+ export { TabBar } from './components/TabBar';
34
+ export type { TabRenderContext } from './components/TabBar';
35
+ export { Drawer, useDrawer } from './components/Drawer';
36
+ export type { DrawerNav } from './components/Drawer';
37
+ export { Link } from './components/Link';
38
+ export type { LinkProps } from './components/Link';
39
+ export type { ComponentLike, EmptyParams, InferOutput, ParamsOf, PopOptions, Presentation, PushOptions, RouteDefinition, RouteMap, RouteRequiresParams, ScreenOptions, ScreenSlotFills, SearchOf, StackEntry, StandardSchemaV1, TransitionKind, TransitionRole, TransitionState, } from './types';