@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.
- package/README.md +189 -7
- 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 +83 -13
- 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 +30 -21
- 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-focus.d.ts.map +1 -1
- package/dist/hooks/use-hardware-back.d.ts +9 -2
- package/dist/hooks/use-hardware-back.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 +38 -3
- 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 +31 -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 +333 -92
- 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 +54 -22
- package/{dist/define-routes.js → src/define-routes.d.ts} +2 -4
- package/src/define-routes.ts +1 -1
- package/{dist/hooks/use-focus.js → src/hooks/use-focus.d.ts} +3 -39
- package/src/hooks/use-focus.ts +9 -3
- package/src/hooks/use-hardware-back.d.ts +37 -0
- package/src/hooks/use-hardware-back.ts +43 -9
- 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 +40 -3
- 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 +90 -10
- 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 -75
- 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 -140
- package/dist/components/Tabs.js.map +0 -1
- package/dist/define-routes.js.map +0 -1
- package/dist/hooks/use-focus.js.map +0 -1
- package/dist/hooks/use-hardware-back.js +0 -50
- 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 -344
- 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
|
@@ -4,31 +4,32 @@
|
|
|
4
4
|
* Usage:
|
|
5
5
|
*
|
|
6
6
|
* ```tsx
|
|
7
|
-
* <NavigationRoot routes={routes}>
|
|
8
|
-
* <
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
|
|
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;
|
package/src/define-routes.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
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;
|
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.
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
43
|
-
// don't await because we want the back
|
|
44
|
-
// (Android starts the move-to-back
|
|
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
|
|
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>;
|