@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
@@ -7,8 +7,8 @@ import {
7
7
  } from '@sigx/lynx';
8
8
  import { isLazyComponent } from '@sigx/lynx';
9
9
  import { withTiming } from '@sigx/lynx-motion';
10
- import type { Nav } from '../hooks/use-nav.js';
11
- import type { ScreenRegistry } from '../internal/screen-registry.js';
10
+ import type { Nav } from '../hooks/use-nav';
11
+ import type { ScreenRegistry } from '../internal/screen-registry';
12
12
  import type {
13
13
  PopOptions,
14
14
  Presentation,
@@ -16,7 +16,7 @@ import type {
16
16
  RouteMap,
17
17
  StackEntry,
18
18
  TransitionState,
19
- } from '../types.js';
19
+ } from '../types';
20
20
 
21
21
  /**
22
22
  * The reactive backing state for one navigator instance.
@@ -61,9 +61,18 @@ export interface NavigatorState {
61
61
  */
62
62
  readonly _screens: {
63
63
  register(registry: ScreenRegistry): void;
64
- unregister(entryKey: string): void;
64
+ /** Identity-checked: no-op when a newer registry has taken the slot. */
65
+ unregister(registry: ScreenRegistry): void;
65
66
  get(entryKey: string): ScreenRegistry | undefined;
66
67
  };
68
+ /**
69
+ * Internal: set `nav.isLocallyFocused` from outside.
70
+ *
71
+ * `<Stack>` calls this when its host entry's locally-focused state
72
+ * changes (top of parent + parent focused + enclosing tab active). For
73
+ * the root nav this stays `true` for the lifetime of the navigator.
74
+ */
75
+ readonly _setLocallyFocused: (focused: boolean) => void;
67
76
  }
68
77
 
69
78
  /**
@@ -146,6 +155,24 @@ export interface CreateNavigatorOptions {
146
155
  * that don't have an MT runtime.
147
156
  */
148
157
  progress?: SharedValue<number>;
158
+ /**
159
+ * Parent navigator. Set when this navigator is nested under another
160
+ * (e.g. a per-tab `<Stack initialRoute>` under root). Drives the
161
+ * `nav.parent` getter and the modal-escalation behaviour of `push`:
162
+ * a push of a route whose resolved presentation is not `'card'`
163
+ * recurses via `parent.push(...)`, walking up the chain until it
164
+ * lands on a navigator with no parent (the root).
165
+ *
166
+ * Leave undefined for the root navigator.
167
+ */
168
+ parent?: Nav | null;
169
+ /**
170
+ * Whether this navigator is considered "locally focused" at creation
171
+ * time. Defaults to true for the root nav; nested stacks pass `false`
172
+ * here and then flip the flag via `_setLocallyFocused` once their
173
+ * host-entry/tab-active state is computed.
174
+ */
175
+ initialLocallyFocused?: boolean;
149
176
  }
150
177
 
151
178
  /**
@@ -154,9 +181,13 @@ export interface CreateNavigatorOptions {
154
181
  * can subscribe to it.
155
182
  */
156
183
  export function createNavigatorState(opts: CreateNavigatorOptions): NavigatorState {
157
- const { routes, initial, progress } = opts;
184
+ const { routes, initial, progress, parent = null } = opts;
158
185
 
159
186
  const stackSignal: Signal<StackEntry[]> = signal<StackEntry[]>([initial]);
187
+ const focusedBox: Signal<{ value: boolean }> = signal<{ value: boolean }>({
188
+ value: opts.initialLocallyFocused ?? true,
189
+ });
190
+ const children = new Set<Nav>();
160
191
  // `signal(null)` would wrap as a primitive (no `$set`), so wrap in an
161
192
  // object to get the standard `{ value }`-style API. Reading `.value`
162
193
  // tracks; writing triggers re-render of `<Stack>`.
@@ -231,14 +262,33 @@ export function createNavigatorState(opts: CreateNavigatorOptions): NavigatorSta
231
262
  }
232
263
 
233
264
  const push: Nav['push'] = ((name: string, ...args: unknown[]) => {
234
- if (isTransitioning()) return;
235
- const { params, search, options } = unpackArgs(name, args, routes);
236
265
  if (!routes[name]) {
237
266
  throw new Error(
238
267
  `[lynx-navigation] push('${name}'): route is not registered. ` +
239
268
  `Known routes: ${Object.keys(routes).join(', ') || '(none)'}`,
240
269
  );
241
270
  }
271
+ const { params, search, options } = unpackArgs(name, args, routes);
272
+
273
+ // Escalate non-card presentations up the parent chain. Modals,
274
+ // fullScreen, and transparent-modal routes belong on the root
275
+ // navigator so they overlay tab UI and persistent chrome. We resolve
276
+ // the presentation the same way `makeEntry` does so the escalation
277
+ // decision matches what would actually be shown.
278
+ const resolvedPresentation =
279
+ (options?.presentation ?? routes[name].presentation ?? 'card') as Presentation;
280
+ if (resolvedPresentation !== 'card' && parent) {
281
+ // Walk straight to the root — every navigator with a parent
282
+ // delegates non-card pushes upward, so a chain of any depth
283
+ // collapses to a single push on the topmost nav.
284
+ // Forward original args verbatim so overloads (`push(name)`,
285
+ // `push(name, params)`, `push(name, params, search)`,
286
+ // `push(name, params, search, options)`) keep their meaning.
287
+ (parent.push as (n: string, ...a: unknown[]) => void)(name, ...args);
288
+ return;
289
+ }
290
+
291
+ if (isTransitioning()) return;
242
292
  preloadRouteComponent(routes[name].component);
243
293
  const newEntry = makeEntry(name, params, search, options, routes);
244
294
  const cur = getStack();
@@ -411,18 +461,38 @@ export function createNavigatorState(opts: CreateNavigatorOptions): NavigatorSta
411
461
  return stackSignal.length > 1;
412
462
  },
413
463
  get parent() {
414
- return null;
464
+ return parent;
465
+ },
466
+ get isLocallyFocused() {
467
+ return focusedBox.value;
468
+ },
469
+ get _children() {
470
+ return children;
415
471
  },
416
472
  get transition() {
417
473
  return transitionBox.value;
418
474
  },
419
475
  };
420
476
 
477
+ if (parent) {
478
+ // Register with parent so root-level traversals (hardware back,
479
+ // future deepest-focused queries) can reach this nav. The matching
480
+ // `_children.delete(nav)` happens when the owning `<Stack>` unmounts;
481
+ // see Stack.tsx.
482
+ parent._children.add(nav);
483
+ }
484
+
485
+ function setLocallyFocused(focused: boolean): void {
486
+ if (focusedBox.value === focused) return;
487
+ focusedBox.value = focused;
488
+ }
489
+
421
490
  return {
422
491
  nav,
423
492
  routes,
424
493
  _gesture: { beginBackGesture, commitBackGesture, cancelBackGesture },
425
494
  _screens: createScreenRegistries(),
495
+ _setLocallyFocused: setLocallyFocused,
426
496
  };
427
497
  }
428
498
 
@@ -451,8 +521,18 @@ function createScreenRegistries(): NavigatorState['_screens'] {
451
521
  // would self-loop, so we untrack the bump.
452
522
  untrack(() => { version.v = version.v + 1; });
453
523
  },
454
- unregister(key: string) {
455
- byKey.delete(key);
524
+ // Identity-checked unregister: deletes the entry only if the
525
+ // currently-registered registry is the *same instance* the caller
526
+ // holds. Without this, the transition→idle handoff (which can
527
+ // mount a new `<EntryScope>` for the same entry-key before the
528
+ // old one unmounts) would let the old scope's `onUnmounted` wipe
529
+ // out the fresh registry — leaving `screens.get(key)` returning
530
+ // undefined and chrome consumers (NavHeader) falling back to the
531
+ // route-name as title with all slot fills gone.
532
+ unregister(reg: ScreenRegistry) {
533
+ const cur = byKey.get(reg.entry.key);
534
+ if (cur !== reg) return;
535
+ byKey.delete(reg.entry.key);
456
536
  untrack(() => { version.v = version.v + 1; });
457
537
  },
458
538
  get(key: string) {
@@ -0,0 +1,37 @@
1
+ import type { ParamsOf, RouteMap, SearchOf } from './types';
2
+ /**
3
+ * Module-augmentation surface for the user's typed route map.
4
+ *
5
+ * Apps register their routes by augmenting this interface — the rest of the
6
+ * library's typed APIs (useNav, useParams, useSearch, <Link>) read from
7
+ * `RegisteredRoutes` so all inference is global.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // In your app's main.ts (or a routes.ts that's imported early):
12
+ * import type { routes } from './routes';
13
+ *
14
+ * declare module '@sigx/lynx-navigation' {
15
+ * interface Register { routes: typeof routes }
16
+ * }
17
+ * ```
18
+ *
19
+ * If `Register.routes` is not augmented the library falls back to a permissive
20
+ * `RouteMap` so non-augmented usage still type-checks (just without precise
21
+ * inference). The recommended pattern is always to augment.
22
+ */
23
+ export interface Register {
24
+ }
25
+ /**
26
+ * The user's registered route map, or a permissive fallback when not
27
+ * augmented. All higher-level types derive from this.
28
+ */
29
+ export type RegisteredRoutes = Register extends {
30
+ routes: infer R;
31
+ } ? R : RouteMap;
32
+ /** Union of registered route names (string literal union when registered). */
33
+ export type RouteId = keyof RegisteredRoutes & string;
34
+ /** Params type for a registered route name. */
35
+ export type RouteParams<K extends RouteId> = ParamsOf<RegisteredRoutes[K]>;
36
+ /** Search type for a registered route name. */
37
+ export type RouteSearch<K extends RouteId> = SearchOf<RegisteredRoutes[K]>;
package/src/register.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ParamsOf, RouteMap, SearchOf } from './types.js';
1
+ import type { ParamsOf, RouteMap, SearchOf } from './types';
2
2
 
3
3
  /**
4
4
  * Module-augmentation surface for the user's typed route map.
package/src/types.d.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Core types for @sigx/lynx-navigation.
3
+ *
4
+ * The type machinery here is the differentiating DX: route names, params, and
5
+ * search are all inferred end-to-end from the user's `defineRoutes` call so
6
+ * `nav.push('profile', { id: 42 })` is a TS error if `id` is typed as string.
7
+ */
8
+ /**
9
+ * Minimal Standard Schema spec subset — see https://standardschema.dev.
10
+ * Inlined so we don't depend on `@standard-schema/spec` for the type spike.
11
+ * Compatible with Zod, Valibot, ArkType, etc.
12
+ */
13
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
14
+ readonly '~standard': {
15
+ readonly version: 1;
16
+ readonly vendor: string;
17
+ readonly types?: {
18
+ readonly input: Input;
19
+ readonly output: Output;
20
+ };
21
+ };
22
+ }
23
+ /**
24
+ * Infer the validated output type of a Standard Schema, falling back to
25
+ * `unknown` for non-schema values.
26
+ */
27
+ export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer O> ? O : unknown;
28
+ /** Empty record — what `ParamsOf` returns when a route declares no schema. */
29
+ export type EmptyParams = Record<string, never>;
30
+ /**
31
+ * How a route entry is presented on the stack.
32
+ * `card` is the default push; `modal`/`fullScreen` slide up; `transparent-modal`
33
+ * preserves the underlying screen visible (e.g. for popovers).
34
+ */
35
+ export type Presentation = 'card' | 'modal' | 'fullScreen' | 'transparent-modal';
36
+ /**
37
+ * A route definition entry.
38
+ *
39
+ * Users construct this via `defineRoutes({...})`. The `params` and `search`
40
+ * schemas drive runtime validation AND TS inference for `useParams`,
41
+ * `useSearch`, `nav.push`, `<Link>`, etc.
42
+ *
43
+ * `component` accepts an eager component factory or a lazy import — both shapes
44
+ * resolve through sigx's `<Suspense>` boundary at render time.
45
+ */
46
+ export interface RouteDefinition<Params extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined, Search extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined> {
47
+ /** Component factory or lazy importer. */
48
+ component: ComponentLike;
49
+ /**
50
+ * Fallback shown while a lazy `component` is loading.
51
+ *
52
+ * Set this only on routes whose `component` was created with `lazy(...)`.
53
+ * The fallback is rendered inside a `<Suspense>` boundary wrapping the
54
+ * screen mount, so the user sees this UI while the screen's chunk is
55
+ * being fetched. When omitted, lazy routes still work — the caller is
56
+ * responsible for placing its own `<Suspense>` boundary (e.g. above the
57
+ * `<NavigationRoot>` or inside the screen component).
58
+ *
59
+ * Accepts a component factory (`MyLoadingScreen`) or a function returning
60
+ * JSX (`() => <Spinner />`). Eager routes ignore this field.
61
+ */
62
+ fallback?: ComponentLike | (() => unknown);
63
+ /** Standard-Schema validator for path params. Optional. */
64
+ params?: Params;
65
+ /** Standard-Schema validator for query/search params. Optional. */
66
+ search?: Search;
67
+ /** Optional URL pattern for deep-link serialization (e.g. `/users/:id`). */
68
+ path?: string;
69
+ /** Default presentation when this route is pushed. */
70
+ presentation?: Presentation;
71
+ /** Nested routes — share the URL/path namespace and may inherit options. */
72
+ children?: Record<string, RouteDefinition>;
73
+ }
74
+ /**
75
+ * The component shape we accept on a route. Kept structural so we don't pull
76
+ * `ComponentFactory` from sigx at type level (avoids a hard dep on sigx purely
77
+ * for types in the spike). Refined to a real ComponentFactory in Phase 0.1
78
+ * runtime work.
79
+ */
80
+ export type ComponentLike = ((...args: any[]) => unknown) | (() => Promise<{
81
+ default: (...args: any[]) => unknown;
82
+ }>);
83
+ /**
84
+ * Map of route definitions, as returned by `defineRoutes`. Keys are route
85
+ * names; values are typed RouteDefinitions.
86
+ */
87
+ export type RouteMap = Record<string, RouteDefinition>;
88
+ /**
89
+ * Extract params type from a single RouteDefinition.
90
+ * Falls back to `EmptyParams` when the route declares no schema.
91
+ *
92
+ * We use a structural `params: infer S` match (without an `extends
93
+ * StandardSchemaV1` constraint on `S`) because TS conditional types treat the
94
+ * generic-defaulted `StandardSchemaV1<unknown, unknown>` as invariant in this
95
+ * position — a schema typed `StandardSchemaV1<{id:string}>` does not match
96
+ * `extends StandardSchemaV1` reliably under `<const T>` inference. `InferOutput`
97
+ * gracefully handles non-schema `S` by returning `unknown`.
98
+ */
99
+ export type ParamsOf<R> = R extends {
100
+ params: infer S;
101
+ } ? InferOutput<S> : EmptyParams;
102
+ /**
103
+ * Extract search type from a single RouteDefinition.
104
+ * Falls back to `EmptyParams` when the route declares no schema.
105
+ */
106
+ export type SearchOf<R> = R extends {
107
+ search: infer S;
108
+ } ? InferOutput<S> : EmptyParams;
109
+ /**
110
+ * Whether a route requires a `params` argument when calling `nav.push` etc.
111
+ * True iff the route definition has a `params` field.
112
+ */
113
+ export type RouteRequiresParams<R> = R extends {
114
+ params: object;
115
+ } ? true : false;
116
+ /**
117
+ * Per-entry state stored on the stack signal.
118
+ *
119
+ * `key` is unique per entry — needed because the same route can appear more
120
+ * than once (e.g. profile A → message → profile A again). Focus state and
121
+ * scroll position are keyed by `key`, not by route name.
122
+ */
123
+ export interface StackEntry<R extends string = string, P = unknown, S = unknown> {
124
+ readonly key: string;
125
+ readonly route: R;
126
+ readonly params: P;
127
+ readonly search: S;
128
+ /** User state — survives suspend/restore. */
129
+ state: unknown;
130
+ readonly presentation: Presentation;
131
+ }
132
+ /** Options accepted by `nav.push` / `nav.replace`. */
133
+ export interface PushOptions {
134
+ /** Override the route's default presentation for this navigation. */
135
+ presentation?: Presentation;
136
+ /** User state to attach to the new entry. Survives suspend/restore. */
137
+ state?: unknown;
138
+ /**
139
+ * Skip the slide animation (instant swap). Defaults to true on platforms
140
+ * where `useAnimatedStyle` isn't available (test renderer); defaults to
141
+ * false on real Lynx. Tests can force `false` to keep assertions
142
+ * deterministic.
143
+ */
144
+ animated?: boolean;
145
+ }
146
+ /** Options accepted by `nav.pop`. */
147
+ export interface PopOptions {
148
+ /** Skip the slide animation (instant swap). See `PushOptions.animated`. */
149
+ animated?: boolean;
150
+ }
151
+ /**
152
+ * Direction of an in-flight transition.
153
+ * - `push`: a new entry is animating in (progress 0 → 1).
154
+ * - `pop`: the current top is animating out (progress 0 → 1, then committed).
155
+ */
156
+ export type TransitionKind = 'push' | 'pop';
157
+ /** Role of a screen during a transition — determines its transform formula. */
158
+ export type TransitionRole = 'top' | 'underneath';
159
+ /**
160
+ * Snapshot of an in-flight transition. Stored on the navigator state so the
161
+ * `<Stack>` component knows to render two entries (`topEntry` above
162
+ * `underneathEntry`) and bind their transforms to `progress`.
163
+ *
164
+ * `progress` is a `SharedValue<number>` (re-exported as `unknown` here to
165
+ * avoid a hard dep on `@sigx/lynx`'s SharedValue type at the contract level —
166
+ * the runtime `<Stack>` casts as needed). The value runs 0 → 1 in both push
167
+ * and pop, with the role/kind pair determining the visual direction.
168
+ */
169
+ export interface TransitionState {
170
+ readonly kind: TransitionKind;
171
+ readonly topEntry: StackEntry;
172
+ readonly underneathEntry: StackEntry;
173
+ /** Animation progress signal — typed loosely; cast at the runtime boundary. */
174
+ readonly progress: unknown;
175
+ }
176
+ /**
177
+ * Per-screen display options written by `<Screen>` into its entry's registry.
178
+ *
179
+ * Read by persistent navigator chrome (the `<HeaderBar>` shipped in the
180
+ * `header` slice; `<TabBar>` later). All fields are optional — consumers
181
+ * apply sensible defaults (headerShown defaults to true, gestureEnabled to
182
+ * true, title falls back to the route name).
183
+ *
184
+ * `title` accepts a function so the header can be derived from reactive
185
+ * state (e.g. a user's display name signal). Plain strings are wrapped in
186
+ * a thunk by consumers when read.
187
+ */
188
+ export interface ScreenOptions {
189
+ /** Header title. Either a static string or a getter (re-tracked each render). */
190
+ title?: string | (() => string);
191
+ /** When false, the navigator's header is hidden for this screen. Default true. */
192
+ headerShown?: boolean;
193
+ /** When false, the iOS edge-swipe-back gesture is disabled for this screen. Default true. */
194
+ gestureEnabled?: boolean;
195
+ }
196
+ /**
197
+ * Slot fills written by `<Screen.Header>` / `<Screen.HeaderLeft>` /
198
+ * `<Screen.HeaderRight>` / `<Screen.TabBarItem>`.
199
+ *
200
+ * Each fill is the rendered output of that sub-component's `default` slot,
201
+ * captured as a thunk so the navigator's persistent chrome can call it at
202
+ * render time. `tabBarItem` is a scoped slot — the consumer passes
203
+ * `{ active }` so the same screen's tab-bar item can style itself
204
+ * differently when focused vs. not.
205
+ */
206
+ export interface ScreenSlotFills {
207
+ /** Full header replacement. When set, takes precedence over title + headerLeft/Right. */
208
+ header?: () => unknown;
209
+ /** Left-side header content (typically back arrow override). */
210
+ headerLeft?: () => unknown;
211
+ /** Right-side header content (typically action buttons). */
212
+ headerRight?: () => unknown;
213
+ /** Tab-bar item — scoped slot receives `{ active }` indicating focus. */
214
+ tabBarItem?: (ctx: {
215
+ active: boolean;
216
+ }) => unknown;
217
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Typed → URL: build the `url` field of an Href from a route's params/search.
3
+ *
4
+ * Mirror of parse.ts. Used by `hrefFor()` after schema validation succeeds.
5
+ */
6
+ /**
7
+ * Build the URL form of a route + params + search, or `null` if the route
8
+ * declares no `path` template (typed navigation still works — only deep-link
9
+ * serialization is unavailable).
10
+ *
11
+ * Params must already be valid for the route's schema (callers run this after
12
+ * `validateSync`). Search values are stringified as-is — schema-validated
13
+ * inputs survive round-tripping because `parseHref` re-runs the same schema.
14
+ */
15
+ export declare function buildUrl(routeName: string, params: Record<string, unknown> | undefined, search: Record<string, unknown> | undefined): string | null;
package/src/url/build.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Mirror of parse.ts. Used by `hrefFor()` after schema validation succeeds.
5
5
  */
6
6
 
7
- import { formatSearch } from './format.js';
8
- import { getCompiledPath, getRouteRegistry } from './registry.js';
7
+ import { formatSearch } from './format';
8
+ import { getCompiledPath, getRouteRegistry } from './registry';
9
9
 
10
10
  /**
11
11
  * Build the URL form of a route + params + search, or `null` if the route
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Path template compiler.
3
+ *
4
+ * Turns a route's `path` (e.g. `/users/:id/posts/:postId`) into a compiled
5
+ * object that can both match a URL pathname against it and format a typed
6
+ * params object back into a URL.
7
+ *
8
+ * Supported syntax (intentionally minimal for v1):
9
+ * - Literal segments: `/users`, `/users/me`
10
+ * - Named params: `:id` (matches `[^/]+`)
11
+ * - Trailing slashes tolerated on match
12
+ *
13
+ * Out of scope for v1 (future-compatible — additions won't break v1 paths):
14
+ * - Wildcards `*`
15
+ * - Optional params `:id?`
16
+ * - Typed/constrained params `:id<number>` or `:id(\\d+)`
17
+ */
18
+ /** Result of compiling a path template — used by parse + format. */
19
+ export interface CompiledPath {
20
+ readonly source: string;
21
+ readonly paramNames: readonly string[];
22
+ /** Regex that matches a URL pathname. Captures are param values in order. */
23
+ readonly regex: RegExp;
24
+ /**
25
+ * Render a URL pathname for this template given param values. Each value
26
+ * is `encodeURIComponent`-encoded. Throws if a required `:name` is missing.
27
+ */
28
+ format(params: Record<string, string | number>): string;
29
+ }
30
+ /**
31
+ * Compile a path template. Throws on malformed input (duplicate param names,
32
+ * unexpected `:` syntax). Pure — safe to memoize.
33
+ */
34
+ export declare function compilePath(template: string): CompiledPath;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Format helpers: typed params/search → URL string.
3
+ *
4
+ * Used by `hrefFor()` to render the `url` field of an Href.
5
+ */
6
+ /**
7
+ * Serialize an object as a `key=value&key=value` querystring.
8
+ *
9
+ * Keys are sorted to make the output deterministic (useful for tests and
10
+ * persistence diffs). `undefined`/`null` values are skipped. Non-primitive
11
+ * values are JSON-stringified on the way out — `parseSearch` returns the
12
+ * raw string and leaves any JSON decoding to the route's `search` schema
13
+ * (e.g. a Zod `transform`), so the round-trip is intentionally one-way at
14
+ * this layer.
15
+ *
16
+ * Returns `''` (empty string) when there are no entries — callers join with
17
+ * `?` only when the result is non-empty.
18
+ */
19
+ export declare function formatSearch(search: Record<string, unknown> | undefined): string;
20
+ /**
21
+ * Parse a `key=value&key=value` querystring into a string-keyed bag. Values
22
+ * are decoded but kept as strings — typed coercion happens in the route's
23
+ * `search` schema (e.g. Zod's `z.coerce.number()`).
24
+ *
25
+ * Multiple occurrences of the same key produce an array. Schemas that don't
26
+ * expect arrays will reject this — that's the right failure mode.
27
+ */
28
+ export declare function parseSearch(query: string): Record<string, string | string[]>;
package/src/url/index.ts CHANGED
@@ -5,14 +5,14 @@
5
5
  * `parseHref` in ../href.ts plus `_setRouteRegistry` for tests/bootstrap.
6
6
  */
7
7
 
8
- export { compilePath, type CompiledPath } from './compile.js';
9
- export { buildUrl } from './build.js';
10
- export { parseHrefImpl } from './parse.js';
11
- export { formatSearch, parseSearch } from './format.js';
8
+ export { compilePath, type CompiledPath } from './compile';
9
+ export { buildUrl } from './build';
10
+ export { parseHrefImpl } from './parse';
11
+ export { formatSearch, parseSearch } from './format';
12
12
  export {
13
13
  _setRouteRegistry,
14
14
  _clearRouteRegistry,
15
15
  getRouteRegistry,
16
16
  getCompiledPath,
17
- } from './registry.js';
18
- export { validateSync, type ValidateOutcome } from './validate.js';
17
+ } from './registry';
18
+ export { validateSync, type ValidateOutcome } from './validate';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * URL → typed Href parser.
3
+ *
4
+ * Walks every registered route with a `path`, tries to match its compiled
5
+ * regex against the URL's pathname, and on a hit validates the extracted
6
+ * params + search through the route's Standard Schema. First match wins;
7
+ * iteration order follows `Object.keys` of the registered routes map.
8
+ *
9
+ * Validation failures return `null` rather than throwing — deep-link handlers
10
+ * fall back to the initial route on a bad URL instead of crashing the app.
11
+ */
12
+ import type { Href } from '../href';
13
+ /**
14
+ * Parse a URL string against the active route registry.
15
+ *
16
+ * Accepts both absolute URLs (`myapp://host/users/42?tab=about`) and
17
+ * pathname-only forms (`/users/42?tab=about`). Returns `null` if no route's
18
+ * `path` matches the URL or if schema validation rejects the extracted bits.
19
+ */
20
+ export declare function parseHrefImpl(url: string): Href | null;
package/src/url/parse.ts CHANGED
@@ -10,13 +10,13 @@
10
10
  * fall back to the initial route on a bad URL instead of crashing the app.
11
11
  */
12
12
 
13
- import type { Href } from '../href.js';
14
- import type { RouteId } from '../register.js';
13
+ import type { Href } from '../href';
14
+ import type { RouteId } from '../register';
15
15
  import { parse as parseUrl } from '@sigx/lynx-linking';
16
- import type { CompiledPath } from './compile.js';
17
- import { parseSearch } from './format.js';
18
- import { getCompiledPath, getRouteRegistry } from './registry.js';
19
- import { validateSync } from './validate.js';
16
+ import type { CompiledPath } from './compile';
17
+ import { parseSearch } from './format';
18
+ import { getCompiledPath, getRouteRegistry } from './registry';
19
+ import { validateSync } from './validate';
20
20
 
21
21
  /**
22
22
  * Parse a URL string against the active route registry.
@@ -11,8 +11,13 @@
11
11
  * directly. The leading underscore is a convention: not part of the supported
12
12
  * public API (test/integration use only).
13
13
  */
14
- import { compilePath } from './compile.js';
15
- let current = null;
14
+ import type { CompiledPath } from './compile';
15
+ import type { RouteMap } from '../types';
16
+ interface RegistryState {
17
+ readonly routes: RouteMap;
18
+ /** Lazy-compiled paths keyed by route name. */
19
+ readonly compiled: Map<string, CompiledPath>;
20
+ }
16
21
  /**
17
22
  * Set the active route registry. Called by `<NavigationRoot>` on setup and
18
23
  * available to tests/bootstrap code as `_setRouteRegistry`.
@@ -22,35 +27,14 @@ let current = null;
22
27
  * specific registry for a one-off call, pass it explicitly to the helper
23
28
  * (parseHrefWithRoutes / hrefForWithRoutes — currently internal).
24
29
  */
25
- export function _setRouteRegistry(routes) {
26
- current = { routes, compiled: new Map() };
27
- }
30
+ export declare function _setRouteRegistry(routes: RouteMap): void;
28
31
  /** Clear the registry. Mainly for tests that want to assert the unset path. */
29
- export function _clearRouteRegistry() {
30
- current = null;
31
- }
32
+ export declare function _clearRouteRegistry(): void;
32
33
  /** Get the active registry or throw a friendly error if none is set. */
33
- export function getRouteRegistry() {
34
- if (!current) {
35
- throw new Error('[lynx-navigation] No route registry set — render a <NavigationRoot> first, or call _setRouteRegistry() for tests.');
36
- }
37
- return current;
38
- }
34
+ export declare function getRouteRegistry(): RegistryState;
39
35
  /**
40
36
  * Look up (or lazily compile) the path template for a route name. Returns
41
37
  * `null` when the route exists but declares no `path`.
42
38
  */
43
- export function getCompiledPath(registry, name) {
44
- const def = registry.routes[name];
45
- if (!def)
46
- return null;
47
- if (!def.path)
48
- return null;
49
- let compiled = registry.compiled.get(name);
50
- if (!compiled) {
51
- compiled = compilePath(def.path);
52
- registry.compiled.set(name, compiled);
53
- }
54
- return compiled;
55
- }
56
- //# sourceMappingURL=registry.js.map
39
+ export declare function getCompiledPath(registry: RegistryState, name: string): CompiledPath | null;
40
+ export {};
@@ -12,9 +12,9 @@
12
12
  * public API (test/integration use only).
13
13
  */
14
14
 
15
- import type { CompiledPath } from './compile.js';
16
- import { compilePath } from './compile.js';
17
- import type { RouteMap } from '../types.js';
15
+ import type { CompiledPath } from './compile';
16
+ import { compilePath } from './compile';
17
+ import type { RouteMap } from '../types';
18
18
 
19
19
  interface RegistryState {
20
20
  readonly routes: RouteMap;