@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
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `<Tabs>` — Lynx tab navigator.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```tsx
7
+ * <NavigationRoot routes={routes} initialRoute="root">
8
+ * <Stack />
9
+ * </NavigationRoot>
10
+ *
11
+ * // The route "root" component renders:
12
+ * <Tabs initialTab="feed">
13
+ * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
14
+ * <Stack initialRoute="feedHome" />
15
+ * </Tabs.Screen>
16
+ * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
17
+ * <Stack initialRoute="profileHome" />
18
+ * </Tabs.Screen>
19
+ * <TabBar />
20
+ * </Tabs>
21
+ * ```
22
+ *
23
+ * Tab bodies stay mounted across switches (the inactive ones render with
24
+ * `display: 'none'`), so each tab's nested `<Stack>` keeps its history when
25
+ * the user flips back to it. The active tab is reactive via `useTabs()`.
26
+ *
27
+ * Per-tab stacks: each `<Tabs.Screen>` can host a `<Stack initialRoute="…">`
28
+ * which mints its own navigator. `useNav()` inside that subtree resolves to
29
+ * the tab's stack, so `nav.push('card-route', …)` stays inside the tab.
30
+ * Routes presented as `modal` / `fullScreen` / `transparent-modal` escalate
31
+ * up `nav.parent` to the root navigator automatically — they overlay the
32
+ * tabs UI (TabBar included) and dismiss back into the originating tab.
33
+ */
34
+ import { type Define, type JSXElement } from '@sigx/lynx';
35
+ /** Metadata about a registered `<Tabs.Screen>`. */
36
+ export interface TabInfo {
37
+ /** Stable tab id, used by `setActive`. */
38
+ readonly name: string;
39
+ /** Optional icon node — passed through to the default tab bar. */
40
+ readonly icon?: JSXElement;
41
+ /** Optional human-readable label. Defaults to `name`. */
42
+ readonly label?: string;
43
+ /**
44
+ * Accessibility label announced by screen readers. Falls back to
45
+ * `label`, then `name`. Surfaced as `accessibility-label` on the
46
+ * default `<TabBar>` button.
47
+ */
48
+ readonly accessibilityLabel?: string;
49
+ }
50
+ /** Reactive controller exposed by `useTabs()`. */
51
+ export interface TabsNav {
52
+ /** Currently-active tab name. Reactive — accessing inside render/effect tracks. */
53
+ readonly active: string;
54
+ /** Switch the active tab. Triggers reactive updates in any consumer. */
55
+ setActive(name: string): void;
56
+ /** Snapshot of registered tabs in registration order. Reactive. */
57
+ readonly tabs: ReadonlyArray<TabInfo>;
58
+ }
59
+ /**
60
+ * Access the enclosing Tabs navigator. Throws when called outside `<Tabs>`.
61
+ */
62
+ export declare const useTabs: import("@sigx/runtime-core").InjectableFunction<TabsNav>;
63
+ /**
64
+ * @internal
65
+ * Provided by each `<Tabs.Screen>` so a nested `<Stack initialRoute>` can
66
+ * discover *which* tab it's hosted by, and gate its focus state on that
67
+ * tab being active. Throws when called outside a `<Tabs.Screen>` body so
68
+ * the gate degrades to "always active" via the caller's try/catch.
69
+ */
70
+ export declare const useTabScreenName: import("@sigx/runtime-core").InjectableFunction<string>;
71
+ type TabsProps = Define.Prop<'initialTab', string> & Define.Slot<'default'>;
72
+ type TabsScreenProps = Define.Prop<'name', string, true> & Define.Prop<'icon', JSXElement> & Define.Prop<'label', string> & Define.Prop<'accessibilityLabel', string> & Define.Slot<'default'>;
73
+ /**
74
+ * Compound export. `Tabs` is the parent component; `Tabs.Screen` registers
75
+ * an individual tab. Matches the `Screen` / `Screen.Header` shape used
76
+ * elsewhere in this package and the daisyui `Modal` / `Modal.Header`
77
+ * convention.
78
+ */
79
+ export declare const Tabs: ((props: {
80
+ initialTab?: string | undefined;
81
+ } & {} & {
82
+ slots?: Partial<{
83
+ default: () => JSXElement | JSXElement[] | null;
84
+ }> | undefined;
85
+ } & {} & JSX.IntrinsicAttributes & import("@sigx/runtime-core").ComponentAttributeExtensions & {
86
+ ref?: import("@sigx/runtime-core").Ref<void> | undefined;
87
+ children?: any;
88
+ }) => JSXElement) & {
89
+ __setup: import("@sigx/runtime-core").SetupFn<{
90
+ initialTab?: string | undefined;
91
+ }, TabsProps, void, {
92
+ default: () => JSXElement | JSXElement[] | null;
93
+ }>;
94
+ __name?: string;
95
+ __islandId?: string;
96
+ __props: {
97
+ initialTab?: string | undefined;
98
+ };
99
+ __events: TabsProps;
100
+ __ref: void;
101
+ __slots: {
102
+ default: () => JSXElement | JSXElement[] | null;
103
+ };
104
+ } & {
105
+ Screen: import("@sigx/runtime-core").ComponentFactory<TabsScreenProps, void, {
106
+ default: () => JSXElement | JSXElement[] | null;
107
+ }>;
108
+ };
109
+ export {};
@@ -4,31 +4,32 @@
4
4
  * Usage:
5
5
  *
6
6
  * ```tsx
7
- * <NavigationRoot routes={routes}>
8
- * <Tabs initialTab="feed">
9
- * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
10
- * <FeedView />
11
- * </Tabs.Screen>
12
- * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
13
- * <ProfileView />
14
- * </Tabs.Screen>
15
- * </Tabs>
7
+ * <NavigationRoot routes={routes} initialRoute="root">
8
+ * <Stack />
16
9
  * </NavigationRoot>
17
- * ```
18
10
  *
19
- * Scope of this slice (v0.1): pure UI primitive. Each tab's body stays
20
- * mounted for state preservation (the inactive ones render with
21
- * `display: 'none'`). Active tab is reactive via `useTabs()`.
11
+ * // The route "root" component renders:
12
+ * <Tabs initialTab="feed">
13
+ * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
14
+ * <Stack initialRoute="feedHome" />
15
+ * </Tabs.Screen>
16
+ * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
17
+ * <Stack initialRoute="profileHome" />
18
+ * </Tabs.Screen>
19
+ * <TabBar />
20
+ * </Tabs>
21
+ * ```
22
22
  *
23
- * Out of scope (deferred to a nested-navigators slice):
24
- * - Per-tab `<Stack>` with its own navigator state machine
25
- * - `nav.parent` chain into the Tabs nav
26
- * - Named navigators (`useNav('root')`)
23
+ * Tab bodies stay mounted across switches (the inactive ones render with
24
+ * `display: 'none'`), so each tab's nested `<Stack>` keeps its history when
25
+ * the user flips back to it. The active tab is reactive via `useTabs()`.
27
26
  *
28
- * Those build on multi-navigator-state plumbing that isn't ready yet.
29
- * For now, the inner content of a `<Tabs.Screen>` shares the same nav as
30
- * its outer `<NavigationRoot>` usable for shallow tab apps, but full
31
- * nested routing comes later.
27
+ * Per-tab stacks: each `<Tabs.Screen>` can host a `<Stack initialRoute="…">`
28
+ * which mints its own navigator. `useNav()` inside that subtree resolves to
29
+ * the tab's stack, so `nav.push('card-route', …)` stays inside the tab.
30
+ * Routes presented as `modal` / `fullScreen` / `transparent-modal` escalate
31
+ * up `nav.parent` to the root navigator automatically — they overlay the
32
+ * tabs UI (TabBar included) and dismiss back into the originating tab.
32
33
  */
33
34
  import {
34
35
  component,
@@ -98,6 +99,19 @@ const useTabsRegistrar = defineInjectable<TabsRegistrar>(() => {
98
99
  );
99
100
  });
100
101
 
102
+ /**
103
+ * @internal
104
+ * Provided by each `<Tabs.Screen>` so a nested `<Stack initialRoute>` can
105
+ * discover *which* tab it's hosted by, and gate its focus state on that
106
+ * tab being active. Throws when called outside a `<Tabs.Screen>` body so
107
+ * the gate degrades to "always active" via the caller's try/catch.
108
+ */
109
+ export const useTabScreenName = defineInjectable<string>(() => {
110
+ throw new Error(
111
+ '[lynx-navigation] useTabScreenName() called outside a <Tabs.Screen> body.',
112
+ );
113
+ });
114
+
101
115
  type TabsProps =
102
116
  & Define.Prop<'initialTab', string>
103
117
  & Define.Slot<'default'>;
@@ -186,17 +200,35 @@ const TabsScreen = component<TabsScreenProps>(({ props, slots }) => {
186
200
  });
187
201
  onUnmounted(() => registrar.unregister(name));
188
202
 
203
+ // Expose this screen's tab name so a nested `<Stack initialRoute>` body
204
+ // can gate its locally-focused state on `tabs.active === name`.
205
+ defineProvide(useTabScreenName, () => name);
206
+
189
207
  return () => {
190
208
  // `display: none` keeps the body mounted so per-tab state survives
191
209
  // tab switches. Read activeSignal here so re-activating triggers a
192
210
  // re-render with display restored.
211
+ //
212
+ // Flex-fill long-form (`flex-grow/shrink/basis`) instead of
213
+ // `height: '100%'`. The percentage form only resolves against an
214
+ // explicit parent height, which means consumers had to wrap us
215
+ // in a `flexFill + height: '100%'` view to make us visible — and
216
+ // every Lynx app got that wrong (myself included) until we hit
217
+ // it on the showcase. With flex-fill we just take whatever space
218
+ // our parent flex container gives us; the parent only needs to
219
+ // be a flex column with a known height (e.g. SafeAreaView, which
220
+ // now defaults to that).
193
221
  const active = registrar.activeSignal.value === name;
194
222
  return (
195
223
  <view
196
224
  style={{
197
225
  display: active ? 'flex' : 'none',
226
+ flexDirection: 'column',
198
227
  width: '100%',
199
- height: '100%',
228
+ flexGrow: 1,
229
+ flexShrink: 1,
230
+ flexBasis: 0,
231
+ minHeight: 0,
200
232
  }}
201
233
  >
202
234
  {slots.default?.()}
@@ -1,3 +1,4 @@
1
+ import type { RouteMap } from './types';
1
2
  /**
2
3
  * Define a typed route registry.
3
4
  *
@@ -26,7 +27,4 @@
26
27
  * }
27
28
  * ```
28
29
  */
29
- export function defineRoutes(routes) {
30
- return routes;
31
- }
32
- //# sourceMappingURL=define-routes.js.map
30
+ export declare function defineRoutes<const T extends RouteMap>(routes: T): T;
@@ -1,4 +1,4 @@
1
- import type { RouteMap } from './types.js';
1
+ import type { RouteMap } from './types';
2
2
 
3
3
  /**
4
4
  * Define a typed route registry.
@@ -1,6 +1,4 @@
1
- import { computed, effect, onUnmounted, untrack, } from '@sigx/lynx';
2
- import { useNav } from './use-nav.js';
3
- import { useCurrentEntry } from './use-nav-internal.js';
1
+ import { type Computed } from '@sigx/lynx';
4
2
  /**
5
3
  * Reactive "is this screen the focused entry?" signal.
6
4
  *
@@ -21,14 +19,7 @@ import { useCurrentEntry } from './use-nav-internal.js';
21
19
  * });
22
20
  * ```
23
21
  */
24
- export function useIsFocused() {
25
- const nav = useNav();
26
- // Capture the entry's key once at setup. The entry object provided
27
- // through `defineProvide` may carry reactive dependencies; we only care
28
- // about the immutable key of the entry this screen was mounted for.
29
- const myKey = useCurrentEntry().key;
30
- return computed(() => nav.current.key === myKey);
31
- }
22
+ export declare function useIsFocused(): Computed<boolean>;
32
23
  /**
33
24
  * Run `cb` whenever this screen gains focus; run the returned cleanup when it
34
25
  * loses focus or unmounts. Mirrors React Navigation's `useFocusEffect`.
@@ -51,31 +42,4 @@ export function useIsFocused() {
51
42
  * });
52
43
  * ```
53
44
  */
54
- export function useFocusEffect(cb) {
55
- const isFocused = useIsFocused();
56
- let cleanup;
57
- const runner = effect(() => {
58
- const focused = isFocused.value;
59
- // Always tear down any previous focus session before starting a new
60
- // one (or before going dormant on blur). Wrap `cb` in `untrack` so
61
- // signals read inside the user-provided callback can't retrigger the
62
- // outer effect and stack subscriptions.
63
- if (typeof cleanup === 'function') {
64
- const fn = cleanup;
65
- cleanup = undefined;
66
- fn();
67
- }
68
- if (focused) {
69
- cleanup = untrack(() => cb());
70
- }
71
- });
72
- onUnmounted(() => {
73
- if (typeof cleanup === 'function') {
74
- const fn = cleanup;
75
- cleanup = undefined;
76
- fn();
77
- }
78
- runner.stop();
79
- });
80
- }
81
- //# sourceMappingURL=use-focus.js.map
45
+ export declare function useFocusEffect(cb: () => void | (() => void)): void;
@@ -5,8 +5,8 @@ import {
5
5
  untrack,
6
6
  type Computed,
7
7
  } from '@sigx/lynx';
8
- import { useNav } from './use-nav.js';
9
- import { useCurrentEntry } from './use-nav-internal.js';
8
+ import { useNav } from './use-nav';
9
+ import { useCurrentEntry } from './use-nav-internal';
10
10
 
11
11
  /**
12
12
  * Reactive "is this screen the focused entry?" signal.
@@ -34,7 +34,13 @@ export function useIsFocused(): Computed<boolean> {
34
34
  // through `defineProvide` may carry reactive dependencies; we only care
35
35
  // about the immutable key of the entry this screen was mounted for.
36
36
  const myKey = useCurrentEntry().key;
37
- return computed(() => nav.current.key === myKey);
37
+ // AND in `nav.isLocallyFocused` so a screen in a nested stack (e.g. a
38
+ // per-tab `<Stack>`) reports unfocused when its enclosing tab is
39
+ // inactive, or when a modal on the root nav covers everything — even
40
+ // though it's still the top of its own (paused) stack. Root nav's
41
+ // `isLocallyFocused` is permanently true, so this reduces to the
42
+ // previous behavior for un-nested apps.
43
+ return computed(() => nav.current.key === myKey && nav.isLocallyFocused);
38
44
  }
39
45
 
40
46
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Wire the Android hardware back button to the active navigator.
3
+ *
4
+ * Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
5
+ * `BackHandler` (which the native side dispatches from
6
+ * `MainActivity.onBackPressed`). On press the handler walks to the
7
+ * deepest currently-focused navigator (per-tab `<Stack>`s register with
8
+ * their parent), then walks back up the `parent` chain looking for the
9
+ * first nav that `canGoBack`:
10
+ *
11
+ * - If any nav in the chain can go back → `nav.pop()` on that nav.
12
+ * - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
13
+ * keeps the bundle warm; iOS: rejects, since iOS doesn't permit
14
+ * programmatic termination).
15
+ *
16
+ * The traversal means you only need to call this once at the root — a
17
+ * back press from inside a tab pops that tab's nested stack first, only
18
+ * exiting the app once every level is at its base entry.
19
+ *
20
+ * Call this once in any component under `<NavigationRoot>` (typically a
21
+ * thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
22
+ * hook is a no-op there.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const BackHandlerWiring = component(() => {
27
+ * useHardwareBack();
28
+ * return () => null;
29
+ * });
30
+ *
31
+ * <NavigationRoot routes={routes}>
32
+ * <BackHandlerWiring />
33
+ * <Stack />
34
+ * </NavigationRoot>
35
+ * ```
36
+ */
37
+ export declare function useHardwareBack(): void;
@@ -1,19 +1,26 @@
1
1
  import { onMounted } from '@sigx/lynx';
2
2
  import { BackHandler } from '@sigx/lynx-linking';
3
- import { useNav } from './use-nav.js';
3
+ import { useNav, type Nav } from './use-nav';
4
4
 
5
5
  /**
6
6
  * Wire the Android hardware back button to the active navigator.
7
7
  *
8
8
  * Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
9
9
  * `BackHandler` (which the native side dispatches from
10
- * `MainActivity.onBackPressed`). On press:
10
+ * `MainActivity.onBackPressed`). On press the handler walks to the
11
+ * deepest currently-focused navigator (per-tab `<Stack>`s register with
12
+ * their parent), then walks back up the `parent` chain looking for the
13
+ * first nav that `canGoBack`:
11
14
  *
12
- * - If `nav.canGoBack` → `nav.pop()`.
15
+ * - If any nav in the chain can go back → `nav.pop()` on that nav.
13
16
  * - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
14
17
  * keeps the bundle warm; iOS: rejects, since iOS doesn't permit
15
18
  * programmatic termination).
16
19
  *
20
+ * The traversal means you only need to call this once at the root — a
21
+ * back press from inside a tab pops that tab's nested stack first, only
22
+ * exiting the app once every level is at its base entry.
23
+ *
17
24
  * Call this once in any component under `<NavigationRoot>` (typically a
18
25
  * thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
19
26
  * hook is a no-op there.
@@ -35,13 +42,40 @@ export function useHardwareBack(): void {
35
42
  const nav = useNav();
36
43
  onMounted(() => {
37
44
  const sub = BackHandler.addEventListener(() => {
38
- if (nav.canGoBack) {
39
- nav.pop();
40
- return true;
45
+ // Walk down to the deepest focused nav. Per-tab `<Stack>`s
46
+ // register themselves via `parent._children.add(nav)`; only one
47
+ // child per level is `isLocallyFocused` at a time, so the
48
+ // traversal is unambiguous. Falls back to the starting nav if
49
+ // no nested stacks are wired up.
50
+ let active: Nav = nav;
51
+ // Loop instead of recursion so a deeply-nested tree doesn't blow
52
+ // the stack on a synchronous back press.
53
+ outer: while (active._children.size > 0) {
54
+ for (const child of active._children) {
55
+ if (child.isLocallyFocused) {
56
+ active = child;
57
+ continue outer;
58
+ }
59
+ }
60
+ // No focused child at this level — stop drilling.
61
+ break;
62
+ }
63
+ // Walk back up the chain looking for the first nav that has
64
+ // something to pop. This is what makes "back press in trips
65
+ // tab with empty inner stack" fall through to root (which might
66
+ // have a modal on top) before exiting.
67
+ let cur: Nav | null = active;
68
+ while (cur) {
69
+ if (cur.canGoBack) {
70
+ cur.pop();
71
+ return true;
72
+ }
73
+ cur = cur.parent;
41
74
  }
42
- // At the root — leave the app. Promise is fire-and-forget; we
43
- // don't await because we want the back press to feel instant
44
- // (Android starts the move-to-back transition immediately).
75
+ // At the root with nothing to pop — leave the app. Promise is
76
+ // fire-and-forget; we don't await because we want the back
77
+ // press to feel instant (Android starts the move-to-back
78
+ // transition immediately).
45
79
  void BackHandler.exitApp();
46
80
  return true;
47
81
  });
@@ -0,0 +1,91 @@
1
+ import { type Href } from '../href';
2
+ import { type Nav } from './use-nav';
3
+ import type { RouteMap } from '../types';
4
+ export interface UseLinkingNavOptions {
5
+ /**
6
+ * Schemes/prefixes to strip before parsing. Matched in order; the first
7
+ * match wins. Example: `['myapp://', 'https://myapp.com']` lets
8
+ * `https://myapp.com/users/42` parse against the same routes as
9
+ * `/users/42`.
10
+ *
11
+ * After stripping, a leading `/` is added if missing so the result is a
12
+ * valid pathname.
13
+ */
14
+ prefixes?: string[];
15
+ /**
16
+ * Custom handler invoked instead of the default dispatch. Use this when
17
+ * you need to intercept (e.g. for auth callbacks, analytics) before
18
+ * routing. If you call `nav.push` / `nav.replace` from here, the default
19
+ * dispatch is skipped — return `void`.
20
+ */
21
+ onURL?: (url: string, nav: Nav) => void;
22
+ /**
23
+ * Called when an incoming URL doesn't match any registered route's `path`
24
+ * template (or fails schema validation). Defaults to a no-op so unknown
25
+ * URLs are dropped silently. Use this to surface "page not found" UX or
26
+ * to forward to a catch-all route.
27
+ */
28
+ onUnmatched?: (url: string) => void;
29
+ /**
30
+ * Whether to use `nav.replace` instead of `nav.push` for the cold-start
31
+ * initial URL. Defaults to `true` — restoring an app into a deep link
32
+ * shouldn't leave a stray "initial route" entry beneath it that the back
33
+ * button can return to.
34
+ *
35
+ * Runtime URLs (from `addEventListener`) always `push`.
36
+ */
37
+ replaceInitial?: boolean;
38
+ }
39
+ /**
40
+ * Bridge `@sigx/lynx-linking` URL events into a `@sigx/lynx-navigation`
41
+ * navigator. Call once inside a `<NavigationRoot>` subtree.
42
+ *
43
+ * Handles both delivery modes:
44
+ * - **cold start** — `Linking.getInitialURL()` is read on mount and, if
45
+ * present, dispatched (replacing the initial route by default).
46
+ * - **warm start** — `Linking.addEventListener('url', ...)` subscribes for
47
+ * URLs delivered while the app is already running; each one is pushed.
48
+ *
49
+ * URL → route dispatch goes through `parseHref`, which matches the URL's
50
+ * pathname against the route registry seeded by `<NavigationRoot>`. Routes
51
+ * without a `path` template are never matched by deep links — only typed
52
+ * `<Link>` / `nav.push` calls reach them.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * import { useLinkingNav } from '@sigx/lynx-navigation';
57
+ *
58
+ * const DeepLinks = component(() => {
59
+ * useLinkingNav({
60
+ * prefixes: ['myapp://', 'https://myapp.com'],
61
+ * onUnmatched: (url) => console.warn('Unknown deep link:', url),
62
+ * });
63
+ * return () => null;
64
+ * });
65
+ *
66
+ * <NavigationRoot routes={routes}>
67
+ * <DeepLinks />
68
+ * <Stack />
69
+ * </NavigationRoot>
70
+ * ```
71
+ */
72
+ export declare function useLinkingNav(opts?: UseLinkingNavOptions): void;
73
+ /**
74
+ * Strip the first matching prefix from `url`, returning a pathname-like
75
+ * string. If no prefixes are provided, or none match, the original URL is
76
+ * returned unchanged so `parseHref` can still handle scheme-prefixed forms
77
+ * via `@sigx/lynx-linking`'s `parse`.
78
+ *
79
+ * Exported for unit testing — not part of the package public API.
80
+ */
81
+ export declare function _stripPrefix(url: string, prefixes?: string[]): string;
82
+ /**
83
+ * Call the right `nav.push` / `nav.replace` overload for `href`. The
84
+ * overloads differ in positional layout: routes with a params schema take
85
+ * `(name, params, search?, options?)`; routes without take `(name, search?,
86
+ * options?)`. Calling the wrong shape silently shifts `search` into the
87
+ * `options` slot, so we look the route up in the registry and branch.
88
+ *
89
+ * Exported for unit testing — not part of the package public API.
90
+ */
91
+ export declare function _navigateToHref(nav: Nav, routes: RouteMap, href: Href, kind: 'push' | 'replace'): void;
@@ -1,9 +1,9 @@
1
1
  import { onMounted } from '@sigx/lynx';
2
2
  import { Linking } from '@sigx/lynx-linking';
3
- import { parseHref, type Href } from '../href.js';
4
- import { useNav, type Nav } from './use-nav.js';
5
- import { useNavRoutes } from './use-nav-internal.js';
6
- import type { RouteMap } from '../types.js';
3
+ import { parseHref, type Href } from '../href';
4
+ import { useNav, type Nav } from './use-nav';
5
+ import { useNavRoutes } from './use-nav-internal';
6
+ import type { RouteMap } from '../types';
7
7
 
8
8
  export interface UseLinkingNavOptions {
9
9
  /**
@@ -0,0 +1,91 @@
1
+ import { type SharedValue } from '@sigx/lynx';
2
+ import type { ScreenRegistry } from '../internal/screen-registry';
3
+ import type { RouteMap, StackEntry } from '../types';
4
+ /**
5
+ * Internal injectable: the `StackEntry` the calling screen was rendered for.
6
+ *
7
+ * Provided by `<EntryScope>` which `<Stack>` and `<ScreenContainer>` wrap
8
+ * around each screen component mount. Screens use this to derive their own
9
+ * focus state (`useIsFocused`, `useFocusEffect`) without having to track
10
+ * `entry.key` themselves.
11
+ *
12
+ * Default throws so calling `useIsFocused()` outside a screen mounted by a
13
+ * navigator surfaces a clear error rather than silently returning `false`.
14
+ */
15
+ export declare const useCurrentEntry: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown>>;
16
+ /**
17
+ * Soft companion to {@link useCurrentEntry} — returns the current scope's
18
+ * entry if any, `null` when called outside an `<EntryScope>` instead of
19
+ * throwing. Provided alongside the strict version by `<EntryScope>`.
20
+ *
21
+ * Used by chrome consumers (`useScreenChrome`) where "no scoped entry"
22
+ * is a legitimate state (a Stack chrome slot lives outside the screen's
23
+ * EntryScope) and the caller wants to soft-fallback to the navigator's
24
+ * destination entry rather than crash.
25
+ */
26
+ export declare const useCurrentEntryOptional: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown> | null>;
27
+ /**
28
+ * Internal injectable: the route registry passed into `<NavigationRoot>`.
29
+ * Components (Stack, Screen) read this to look up route definitions by name.
30
+ *
31
+ * Not exported from the package barrel — use `useNav()` for navigation, and
32
+ * the registry is implicit from `<NavigationRoot routes={...}>`.
33
+ */
34
+ export declare const useNavRoutes: import("@sigx/runtime-core").InjectableFunction<RouteMap>;
35
+ /**
36
+ * Internal injectable: low-level navigator handles used by the edge-back
37
+ * gesture. Holds the progress SharedValue (so gesture worklets can write it
38
+ * directly on MT) plus BG-side begin/commit/cancel functions invoked via
39
+ * `runOnBackground` from gesture worklets.
40
+ *
41
+ * `progress` is `null` when the navigator was created with `animated={false}`
42
+ * (e.g. tests). `beginBackGesture` is also a no-op in that case.
43
+ */
44
+ export interface NavInternals {
45
+ /** MT-driven transition progress; null when animations are disabled. */
46
+ readonly progress: SharedValue<number> | null;
47
+ /**
48
+ * Set transition state for a gesture-driven pop. Does not start any
49
+ * automatic animation — the gesture worklet writes `progress` directly
50
+ * per frame, then animates to the commit/cancel endpoint on release.
51
+ */
52
+ beginBackGesture(): void;
53
+ /** Commit the back gesture: pop top entry + clear transition. */
54
+ commitBackGesture(): void;
55
+ /** Cancel the back gesture: clear transition without popping. */
56
+ cancelBackGesture(): void;
57
+ /** Whether the user opted into the edge-swipe-back gesture. */
58
+ readonly edgeSwipeEnabled: boolean;
59
+ /**
60
+ * Cross-entry screen registry controller. `<EntryScope>` calls
61
+ * `register` on mount and `unregister` on unmount. Persistent chrome
62
+ * (HeaderBar / TabBar — later slices) calls `get(entryKey)` to read
63
+ * the focused screen's options + slot fills without remounting itself.
64
+ */
65
+ readonly screens: {
66
+ register(registry: ScreenRegistry): void;
67
+ /**
68
+ * Identity-checked: only removes the entry if `registry` is the
69
+ * one currently registered under its `entry.key`. A no-op when
70
+ * a newer registry has already taken that slot (which happens
71
+ * at the transition→idle handoff, where a fresh `<EntryScope>`
72
+ * for the same entry mounts before the old one's unmount fires).
73
+ */
74
+ unregister(registry: ScreenRegistry): void;
75
+ get(entryKey: string): ScreenRegistry | undefined;
76
+ };
77
+ }
78
+ export declare const useNavInternals: import("@sigx/runtime-core").InjectableFunction<NavInternals>;
79
+ /**
80
+ * Internal injectable: the calling screen's `ScreenRegistry`.
81
+ *
82
+ * Provided by `<EntryScope>` alongside `useCurrentEntry`. The `<Screen>`
83
+ * component and its slot-filling sub-components write options and slot
84
+ * fills here; the navigator's persistent chrome (HeaderBar, TabBar — later
85
+ * slices) reads from this registry via `getScreenRegistry(key)` on the
86
+ * navigator state, which keys into a cross-entry map.
87
+ *
88
+ * Throws when used outside an EntryScope so calling `<Screen>` at the app
89
+ * root surfaces a clear error rather than silently no-op'ing.
90
+ */
91
+ export declare const useScreenRegistry: import("@sigx/runtime-core").InjectableFunction<ScreenRegistry>;