@sigx/lynx-navigation 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -8
- package/dist/components/EntryScope.d.ts +1 -1
- package/dist/components/EntryScope.d.ts.map +1 -1
- package/dist/components/Layer.d.ts +34 -0
- package/dist/components/Layer.d.ts.map +1 -0
- package/dist/components/Link.d.ts +2 -2
- package/dist/components/Link.d.ts.map +1 -1
- package/dist/components/NavigationRoot.d.ts +2 -2
- package/dist/components/NavigationRoot.d.ts.map +1 -1
- package/dist/components/Screen.d.ts +6 -6
- package/dist/components/Screen.d.ts.map +1 -1
- package/dist/components/Stack.d.ts +41 -16
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/TabBar.d.ts +19 -20
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/define-routes.d.ts +1 -1
- package/dist/define-routes.d.ts.map +1 -1
- package/dist/hooks/use-linking-nav.d.ts +3 -3
- package/dist/hooks/use-linking-nav.d.ts.map +1 -1
- package/dist/hooks/use-nav-internal.d.ts +21 -3
- package/dist/hooks/use-nav-internal.d.ts.map +1 -1
- package/dist/hooks/use-nav-serializer.d.ts +1 -1
- package/dist/hooks/use-nav-serializer.d.ts.map +1 -1
- package/dist/hooks/use-nav.d.ts +2 -2
- package/dist/hooks/use-nav.d.ts.map +1 -1
- package/dist/hooks/use-params.d.ts +1 -1
- package/dist/hooks/use-params.d.ts.map +1 -1
- package/dist/hooks/use-screen-chrome.d.ts +19 -0
- package/dist/hooks/use-screen-chrome.d.ts.map +1 -0
- package/dist/hooks/use-screen-options.d.ts +1 -1
- package/dist/hooks/use-screen-options.d.ts.map +1 -1
- package/dist/hooks/use-search.d.ts +1 -1
- package/dist/hooks/use-search.d.ts.map +1 -1
- package/dist/href.d.ts +2 -2
- package/dist/href.d.ts.map +1 -1
- package/dist/index.d.ts +33 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1160 -29
- package/dist/index.js.map +1 -1
- package/dist/internal/layer-plan.d.ts +69 -0
- package/dist/internal/layer-plan.d.ts.map +1 -0
- package/dist/internal/screen-registry.d.ts +1 -1
- package/dist/internal/screen-registry.d.ts.map +1 -1
- package/dist/internal/screen-width.d.ts +9 -7
- package/dist/internal/screen-width.d.ts.map +1 -1
- package/dist/navigator/core.d.ts +5 -4
- package/dist/navigator/core.d.ts.map +1 -1
- package/dist/register.d.ts +1 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/url/index.d.ts +6 -6
- package/dist/url/index.d.ts.map +1 -1
- package/dist/url/parse.d.ts +1 -1
- package/dist/url/parse.d.ts.map +1 -1
- package/dist/url/registry.d.ts +2 -2
- package/dist/url/registry.d.ts.map +1 -1
- package/dist/url/validate.d.ts +1 -1
- package/dist/url/validate.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/components/Drawer.d.ts +55 -0
- package/src/components/EdgeBackHandle.d.ts +1 -0
- package/src/components/EdgeBackHandle.tsx +2 -2
- package/{dist/components/EntryScope.js → src/components/EntryScope.d.ts} +7 -15
- package/src/components/EntryScope.tsx +15 -4
- package/src/components/Header.d.ts +6 -0
- package/src/components/Header.tsx +3 -3
- package/src/components/Layer.d.ts +33 -0
- package/src/components/Layer.tsx +96 -0
- package/src/components/Link.d.ts +60 -0
- package/src/components/Link.tsx +4 -4
- package/src/components/NavigationRoot.d.ts +36 -0
- package/src/components/NavigationRoot.tsx +6 -6
- package/src/components/Screen.d.ts +97 -0
- package/src/components/Screen.tsx +13 -11
- package/src/components/Stack.d.ts +90 -0
- package/src/components/Stack.tsx +142 -98
- package/src/components/TabBar.d.ts +38 -0
- package/src/components/TabBar.tsx +22 -22
- package/src/components/Tabs.d.ts +109 -0
- package/src/components/Tabs.tsx +15 -1
- package/{dist/define-routes.js → src/define-routes.d.ts} +2 -4
- package/src/define-routes.ts +1 -1
- package/src/hooks/use-focus.d.ts +45 -0
- package/src/hooks/use-focus.ts +2 -2
- package/src/hooks/use-hardware-back.d.ts +37 -0
- package/src/hooks/use-hardware-back.ts +1 -1
- package/src/hooks/use-linking-nav.d.ts +91 -0
- package/src/hooks/use-linking-nav.ts +4 -4
- package/src/hooks/use-nav-internal.d.ts +91 -0
- package/src/hooks/use-nav-internal.ts +24 -3
- package/src/hooks/use-nav-serializer.d.ts +82 -0
- package/src/hooks/use-nav-serializer.ts +3 -3
- package/src/hooks/use-nav.d.ts +111 -0
- package/src/hooks/use-nav.ts +2 -2
- package/{dist/hooks/use-params.js → src/hooks/use-params.d.ts} +2 -6
- package/src/hooks/use-params.ts +2 -2
- package/src/hooks/use-screen-chrome.d.ts +18 -0
- package/src/hooks/use-screen-chrome.ts +122 -0
- package/src/hooks/use-screen-options.d.ts +2 -0
- package/src/hooks/use-screen-options.ts +3 -3
- package/{dist/hooks/use-search.js → src/hooks/use-search.d.ts} +2 -6
- package/src/hooks/use-search.ts +2 -2
- package/src/href.d.ts +54 -0
- package/src/href.ts +6 -6
- package/src/index.d.ts +39 -0
- package/src/index.ts +33 -31
- package/src/internal/layer-plan.d.ts +68 -0
- package/src/internal/layer-plan.ts +187 -0
- package/{dist/internal/screen-registry.js → src/internal/screen-registry.d.ts} +21 -32
- package/src/internal/screen-registry.ts +1 -1
- package/src/internal/screen-width.d.ts +17 -0
- package/src/internal/screen-width.ts +22 -14
- package/src/navigator/core.d.ts +96 -0
- package/src/navigator/core.ts +17 -6
- package/src/register.d.ts +37 -0
- package/src/register.ts +1 -1
- package/src/types.d.ts +217 -0
- package/src/url/build.d.ts +15 -0
- package/src/url/build.ts +2 -2
- package/src/url/compile.d.ts +34 -0
- package/src/url/format.d.ts +28 -0
- package/src/url/index.ts +6 -6
- package/src/url/parse.d.ts +20 -0
- package/src/url/parse.ts +6 -6
- package/{dist/url/registry.js → src/url/registry.d.ts} +12 -28
- package/src/url/registry.ts +3 -3
- package/src/url/validate.d.ts +23 -0
- package/src/url/validate.ts +1 -1
- package/dist/components/Drawer.js +0 -74
- package/dist/components/Drawer.js.map +0 -1
- package/dist/components/EdgeBackHandle.js +0 -144
- package/dist/components/EdgeBackHandle.js.map +0 -1
- package/dist/components/EntryScope.js.map +0 -1
- package/dist/components/Header.js +0 -103
- package/dist/components/Header.js.map +0 -1
- package/dist/components/Link.js +0 -51
- package/dist/components/Link.js.map +0 -1
- package/dist/components/NavigationRoot.js +0 -67
- package/dist/components/NavigationRoot.js.map +0 -1
- package/dist/components/Screen.js +0 -94
- package/dist/components/Screen.js.map +0 -1
- package/dist/components/ScreenContainer.d.ts +0 -18
- package/dist/components/ScreenContainer.d.ts.map +0 -1
- package/dist/components/ScreenContainer.js +0 -77
- package/dist/components/ScreenContainer.js.map +0 -1
- package/dist/components/Stack.js +0 -221
- package/dist/components/Stack.js.map +0 -1
- package/dist/components/TabBar.js +0 -63
- package/dist/components/TabBar.js.map +0 -1
- package/dist/components/Tabs.js +0 -154
- package/dist/components/Tabs.js.map +0 -1
- package/dist/define-routes.js.map +0 -1
- package/dist/hooks/use-focus.js +0 -87
- package/dist/hooks/use-focus.js.map +0 -1
- package/dist/hooks/use-hardware-back.js +0 -84
- package/dist/hooks/use-hardware-back.js.map +0 -1
- package/dist/hooks/use-linking-nav.js +0 -109
- package/dist/hooks/use-linking-nav.js.map +0 -1
- package/dist/hooks/use-nav-internal.js +0 -44
- package/dist/hooks/use-nav-internal.js.map +0 -1
- package/dist/hooks/use-nav-serializer.js +0 -181
- package/dist/hooks/use-nav-serializer.js.map +0 -1
- package/dist/hooks/use-nav.js +0 -11
- package/dist/hooks/use-nav.js.map +0 -1
- package/dist/hooks/use-params.js.map +0 -1
- package/dist/hooks/use-screen-options.js +0 -43
- package/dist/hooks/use-screen-options.js.map +0 -1
- package/dist/hooks/use-search.js.map +0 -1
- package/dist/href.js +0 -57
- package/dist/href.js.map +0 -1
- package/dist/internal/screen-registry.js.map +0 -1
- package/dist/internal/screen-width.js +0 -30
- package/dist/internal/screen-width.js.map +0 -1
- package/dist/navigator/core.js +0 -383
- package/dist/navigator/core.js.map +0 -1
- package/dist/register.js +0 -2
- package/dist/register.js.map +0 -1
- package/dist/types.js +0 -9
- package/dist/types.js.map +0 -1
- package/dist/url/build.js +0 -30
- package/dist/url/build.js.map +0 -1
- package/dist/url/compile.js +0 -83
- package/dist/url/compile.js.map +0 -1
- package/dist/url/format.js +0 -102
- package/dist/url/format.js.map +0 -1
- package/dist/url/index.js +0 -13
- package/dist/url/index.js.map +0 -1
- package/dist/url/parse.js +0 -94
- package/dist/url/parse.js.map +0 -1
- package/dist/url/registry.js.map +0 -1
- package/dist/url/validate.js +0 -37
- package/dist/url/validate.js.map +0 -1
- 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 {};
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -208,13 +208,27 @@ const TabsScreen = component<TabsScreenProps>(({ props, slots }) => {
|
|
|
208
208
|
// `display: none` keeps the body mounted so per-tab state survives
|
|
209
209
|
// tab switches. Read activeSignal here so re-activating triggers a
|
|
210
210
|
// re-render with display restored.
|
|
211
|
+
//
|
|
212
|
+
// Flex-fill long-form (`flex-grow/shrink/basis`) instead of
|
|
213
|
+
// `height: '100%'`. The percentage form only resolves against an
|
|
214
|
+
// explicit parent height, which means consumers had to wrap us
|
|
215
|
+
// in a `flexFill + height: '100%'` view to make us visible — and
|
|
216
|
+
// every Lynx app got that wrong (myself included) until we hit
|
|
217
|
+
// it on the showcase. With flex-fill we just take whatever space
|
|
218
|
+
// our parent flex container gives us; the parent only needs to
|
|
219
|
+
// be a flex column with a known height (e.g. SafeAreaView, which
|
|
220
|
+
// now defaults to that).
|
|
211
221
|
const active = registrar.activeSignal.value === name;
|
|
212
222
|
return (
|
|
213
223
|
<view
|
|
214
224
|
style={{
|
|
215
225
|
display: active ? 'flex' : 'none',
|
|
226
|
+
flexDirection: 'column',
|
|
216
227
|
width: '100%',
|
|
217
|
-
|
|
228
|
+
flexGrow: 1,
|
|
229
|
+
flexShrink: 1,
|
|
230
|
+
flexBasis: 0,
|
|
231
|
+
minHeight: 0,
|
|
218
232
|
}}
|
|
219
233
|
>
|
|
220
234
|
{slots.default?.()}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RouteMap } from './types';
|
|
1
2
|
/**
|
|
2
3
|
* Define a typed route registry.
|
|
3
4
|
*
|
|
@@ -26,7 +27,4 @@
|
|
|
26
27
|
* }
|
|
27
28
|
* ```
|
|
28
29
|
*/
|
|
29
|
-
export function defineRoutes(routes)
|
|
30
|
-
return routes;
|
|
31
|
-
}
|
|
32
|
-
//# sourceMappingURL=define-routes.js.map
|
|
30
|
+
export declare function defineRoutes<const T extends RouteMap>(routes: T): T;
|
package/src/define-routes.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Computed } from '@sigx/lynx';
|
|
2
|
+
/**
|
|
3
|
+
* Reactive "is this screen the focused entry?" signal.
|
|
4
|
+
*
|
|
5
|
+
* Must be called from inside a component rendered as a route by `<Stack>` (or
|
|
6
|
+
* any other navigator that uses `<EntryScope>`); throws otherwise. The
|
|
7
|
+
* returned `Computed` reads `nav.current.key` and compares it to the entry
|
|
8
|
+
* the calling screen was mounted for, so any nav mutation that changes the
|
|
9
|
+
* top entry flips the value.
|
|
10
|
+
*
|
|
11
|
+
* Note: screens stay mounted when something is pushed on top of them — they
|
|
12
|
+
* just lose focus. Pop the new top off and they regain focus.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const Profile = component(() => {
|
|
17
|
+
* const isFocused = useIsFocused();
|
|
18
|
+
* return () => <text>{isFocused.value ? 'visible' : 'hidden'}</text>;
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function useIsFocused(): Computed<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Run `cb` whenever this screen gains focus; run the returned cleanup when it
|
|
25
|
+
* loses focus or unmounts. Mirrors React Navigation's `useFocusEffect`.
|
|
26
|
+
*
|
|
27
|
+
* Lifecycle:
|
|
28
|
+
* - cb runs immediately if the screen is already focused at mount.
|
|
29
|
+
* - When the screen loses focus (something pushed on top), cleanup runs.
|
|
30
|
+
* - When focus returns (the cover is popped), `cb` runs again — yielding a
|
|
31
|
+
* fresh cleanup for the next blur.
|
|
32
|
+
* - On unmount, cleanup runs once if still focused.
|
|
33
|
+
*
|
|
34
|
+
* Common uses: subscribe to a data source while visible, track an analytics
|
|
35
|
+
* "screen view" event, start/stop a polling loop.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* useFocusEffect(() => {
|
|
40
|
+
* const id = setInterval(refresh, 5000);
|
|
41
|
+
* return () => clearInterval(id);
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function useFocusEffect(cb: () => void | (() => void)): void;
|
package/src/hooks/use-focus.ts
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
untrack,
|
|
6
6
|
type Computed,
|
|
7
7
|
} from '@sigx/lynx';
|
|
8
|
-
import { useNav } from './use-nav
|
|
9
|
-
import { useCurrentEntry } from './use-nav-internal
|
|
8
|
+
import { useNav } from './use-nav';
|
|
9
|
+
import { useCurrentEntry } from './use-nav-internal';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Reactive "is this screen the focused entry?" signal.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire the Android hardware back button to the active navigator.
|
|
3
|
+
*
|
|
4
|
+
* Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
|
|
5
|
+
* `BackHandler` (which the native side dispatches from
|
|
6
|
+
* `MainActivity.onBackPressed`). On press the handler walks to the
|
|
7
|
+
* deepest currently-focused navigator (per-tab `<Stack>`s register with
|
|
8
|
+
* their parent), then walks back up the `parent` chain looking for the
|
|
9
|
+
* first nav that `canGoBack`:
|
|
10
|
+
*
|
|
11
|
+
* - If any nav in the chain can go back → `nav.pop()` on that nav.
|
|
12
|
+
* - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
|
|
13
|
+
* keeps the bundle warm; iOS: rejects, since iOS doesn't permit
|
|
14
|
+
* programmatic termination).
|
|
15
|
+
*
|
|
16
|
+
* The traversal means you only need to call this once at the root — a
|
|
17
|
+
* back press from inside a tab pops that tab's nested stack first, only
|
|
18
|
+
* exiting the app once every level is at its base entry.
|
|
19
|
+
*
|
|
20
|
+
* Call this once in any component under `<NavigationRoot>` (typically a
|
|
21
|
+
* thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
|
|
22
|
+
* hook is a no-op there.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const BackHandlerWiring = component(() => {
|
|
27
|
+
* useHardwareBack();
|
|
28
|
+
* return () => null;
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* <NavigationRoot routes={routes}>
|
|
32
|
+
* <BackHandlerWiring />
|
|
33
|
+
* <Stack />
|
|
34
|
+
* </NavigationRoot>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function useHardwareBack(): void;
|
|
@@ -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
|
|
4
|
-
import { useNav, type Nav } from './use-nav
|
|
5
|
-
import { useNavRoutes } from './use-nav-internal
|
|
6
|
-
import type { RouteMap } from '../types
|
|
3
|
+
import { parseHref, type Href } from '../href';
|
|
4
|
+
import { useNav, type Nav } from './use-nav';
|
|
5
|
+
import { useNavRoutes } from './use-nav-internal';
|
|
6
|
+
import type { RouteMap } from '../types';
|
|
7
7
|
|
|
8
8
|
export interface UseLinkingNavOptions {
|
|
9
9
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type SharedValue } from '@sigx/lynx';
|
|
2
|
+
import type { ScreenRegistry } from '../internal/screen-registry';
|
|
3
|
+
import type { RouteMap, StackEntry } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Internal injectable: the `StackEntry` the calling screen was rendered for.
|
|
6
|
+
*
|
|
7
|
+
* Provided by `<EntryScope>` which `<Stack>` and `<ScreenContainer>` wrap
|
|
8
|
+
* around each screen component mount. Screens use this to derive their own
|
|
9
|
+
* focus state (`useIsFocused`, `useFocusEffect`) without having to track
|
|
10
|
+
* `entry.key` themselves.
|
|
11
|
+
*
|
|
12
|
+
* Default throws so calling `useIsFocused()` outside a screen mounted by a
|
|
13
|
+
* navigator surfaces a clear error rather than silently returning `false`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const useCurrentEntry: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown>>;
|
|
16
|
+
/**
|
|
17
|
+
* Soft companion to {@link useCurrentEntry} — returns the current scope's
|
|
18
|
+
* entry if any, `null` when called outside an `<EntryScope>` instead of
|
|
19
|
+
* throwing. Provided alongside the strict version by `<EntryScope>`.
|
|
20
|
+
*
|
|
21
|
+
* Used by chrome consumers (`useScreenChrome`) where "no scoped entry"
|
|
22
|
+
* is a legitimate state (a Stack chrome slot lives outside the screen's
|
|
23
|
+
* EntryScope) and the caller wants to soft-fallback to the navigator's
|
|
24
|
+
* destination entry rather than crash.
|
|
25
|
+
*/
|
|
26
|
+
export declare const useCurrentEntryOptional: import("@sigx/runtime-core").InjectableFunction<StackEntry<string, unknown, unknown> | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Internal injectable: the route registry passed into `<NavigationRoot>`.
|
|
29
|
+
* Components (Stack, Screen) read this to look up route definitions by name.
|
|
30
|
+
*
|
|
31
|
+
* Not exported from the package barrel — use `useNav()` for navigation, and
|
|
32
|
+
* the registry is implicit from `<NavigationRoot routes={...}>`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const useNavRoutes: import("@sigx/runtime-core").InjectableFunction<RouteMap>;
|
|
35
|
+
/**
|
|
36
|
+
* Internal injectable: low-level navigator handles used by the edge-back
|
|
37
|
+
* gesture. Holds the progress SharedValue (so gesture worklets can write it
|
|
38
|
+
* directly on MT) plus BG-side begin/commit/cancel functions invoked via
|
|
39
|
+
* `runOnBackground` from gesture worklets.
|
|
40
|
+
*
|
|
41
|
+
* `progress` is `null` when the navigator was created with `animated={false}`
|
|
42
|
+
* (e.g. tests). `beginBackGesture` is also a no-op in that case.
|
|
43
|
+
*/
|
|
44
|
+
export interface NavInternals {
|
|
45
|
+
/** MT-driven transition progress; null when animations are disabled. */
|
|
46
|
+
readonly progress: SharedValue<number> | null;
|
|
47
|
+
/**
|
|
48
|
+
* Set transition state for a gesture-driven pop. Does not start any
|
|
49
|
+
* automatic animation — the gesture worklet writes `progress` directly
|
|
50
|
+
* per frame, then animates to the commit/cancel endpoint on release.
|
|
51
|
+
*/
|
|
52
|
+
beginBackGesture(): void;
|
|
53
|
+
/** Commit the back gesture: pop top entry + clear transition. */
|
|
54
|
+
commitBackGesture(): void;
|
|
55
|
+
/** Cancel the back gesture: clear transition without popping. */
|
|
56
|
+
cancelBackGesture(): void;
|
|
57
|
+
/** Whether the user opted into the edge-swipe-back gesture. */
|
|
58
|
+
readonly edgeSwipeEnabled: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Cross-entry screen registry controller. `<EntryScope>` calls
|
|
61
|
+
* `register` on mount and `unregister` on unmount. Persistent chrome
|
|
62
|
+
* (HeaderBar / TabBar — later slices) calls `get(entryKey)` to read
|
|
63
|
+
* the focused screen's options + slot fills without remounting itself.
|
|
64
|
+
*/
|
|
65
|
+
readonly screens: {
|
|
66
|
+
register(registry: ScreenRegistry): void;
|
|
67
|
+
/**
|
|
68
|
+
* Identity-checked: only removes the entry if `registry` is the
|
|
69
|
+
* one currently registered under its `entry.key`. A no-op when
|
|
70
|
+
* a newer registry has already taken that slot (which happens
|
|
71
|
+
* at the transition→idle handoff, where a fresh `<EntryScope>`
|
|
72
|
+
* for the same entry mounts before the old one's unmount fires).
|
|
73
|
+
*/
|
|
74
|
+
unregister(registry: ScreenRegistry): void;
|
|
75
|
+
get(entryKey: string): ScreenRegistry | undefined;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export declare const useNavInternals: import("@sigx/runtime-core").InjectableFunction<NavInternals>;
|
|
79
|
+
/**
|
|
80
|
+
* Internal injectable: the calling screen's `ScreenRegistry`.
|
|
81
|
+
*
|
|
82
|
+
* Provided by `<EntryScope>` alongside `useCurrentEntry`. The `<Screen>`
|
|
83
|
+
* component and its slot-filling sub-components write options and slot
|
|
84
|
+
* fills here; the navigator's persistent chrome (HeaderBar, TabBar — later
|
|
85
|
+
* slices) reads from this registry via `getScreenRegistry(key)` on the
|
|
86
|
+
* navigator state, which keys into a cross-entry map.
|
|
87
|
+
*
|
|
88
|
+
* Throws when used outside an EntryScope so calling `<Screen>` at the app
|
|
89
|
+
* root surfaces a clear error rather than silently no-op'ing.
|
|
90
|
+
*/
|
|
91
|
+
export declare const useScreenRegistry: import("@sigx/runtime-core").InjectableFunction<ScreenRegistry>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineInjectable, type SharedValue } from '@sigx/lynx';
|
|
2
|
-
import type { ScreenRegistry } from '../internal/screen-registry
|
|
3
|
-
import type { RouteMap, StackEntry } from '../types
|
|
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
|
-
|
|
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
|
|
3
|
-
import { useNavRoutes } from './use-nav-internal
|
|
4
|
-
import type { StackEntry } from '../types
|
|
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
|