@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,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Screen>` — declarative per-screen options + slot fills.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const ProfileScreen = component(() => () => (
|
|
8
|
+
* <Screen title="Profile" headerShown gestureEnabled>
|
|
9
|
+
* <Screen.HeaderRight>
|
|
10
|
+
* <text bindtap={onEdit}>Edit</text>
|
|
11
|
+
* </Screen.HeaderRight>
|
|
12
|
+
* <view>body…</view>
|
|
13
|
+
* </Screen>
|
|
14
|
+
* ));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `<Screen>` itself renders its `default` slot inline — so the body lives
|
|
18
|
+
* where you'd expect with no extra layout wrapper. The sub-components
|
|
19
|
+
* (`Screen.Header`, `Screen.HeaderLeft`, `Screen.HeaderRight`,
|
|
20
|
+
* `Screen.TabBarItem`) render `null` and write into the entry's
|
|
21
|
+
* `ScreenRegistry`. The navigator's persistent chrome reads from there.
|
|
22
|
+
*
|
|
23
|
+
* Note: `<Screen.TabBarItem>` registers a scoped slot fill on the entry's
|
|
24
|
+
* `ScreenRegistry`, but the built-in `<TabBar>` doesn't read it yet — the
|
|
25
|
+
* fill is exposed for custom tab-bar renderers (pass `renderTab` and look
|
|
26
|
+
* up the active entry's registry yourself).
|
|
27
|
+
*
|
|
28
|
+
* Sub-component placement inside `<Screen>` is conventional — sigx scopes
|
|
29
|
+
* are by component tree, so they work anywhere under the same EntryScope.
|
|
30
|
+
* Placing them as direct children of `<Screen>` keeps the call site
|
|
31
|
+
* declarative and grep-friendly.
|
|
32
|
+
*/
|
|
33
|
+
import { type Define } from '@sigx/lynx';
|
|
34
|
+
type ScreenProps = Define.Prop<'title', string | (() => string)> & Define.Prop<'headerShown', boolean> & Define.Prop<'gestureEnabled', boolean> & Define.Slot<'default'>;
|
|
35
|
+
type SimpleSlotProps = Define.Slot<'default'>;
|
|
36
|
+
/**
|
|
37
|
+
* `<Screen.TabBarItem>` — scoped slot. The default slot is a function that
|
|
38
|
+
* receives `{ active }`; whatever it returns is the tab-bar item content.
|
|
39
|
+
*
|
|
40
|
+
* Sigx's `Define.Slot<'default', { active: boolean }>` would express this
|
|
41
|
+
* directly on the component, but since `<Screen.TabBarItem>`'s parent
|
|
42
|
+
* (the user's tree, not the navigator) doesn't actually pass `active`, we
|
|
43
|
+
* accept a plain default slot whose body is itself a function. The
|
|
44
|
+
* navigator's TabBar invokes that function with the active flag.
|
|
45
|
+
*/
|
|
46
|
+
type TabBarItemProps = Define.Slot<'default'>;
|
|
47
|
+
/**
|
|
48
|
+
* Compound export. `Screen` is callable as a JSX element and exposes the
|
|
49
|
+
* sub-components as properties (`Screen.Header`, etc.) for the declarative
|
|
50
|
+
* call site shown in the file header.
|
|
51
|
+
*/
|
|
52
|
+
export declare const Screen: ((props: {
|
|
53
|
+
gestureEnabled?: boolean | undefined;
|
|
54
|
+
headerShown?: boolean | undefined;
|
|
55
|
+
title?: string | (() => string) | undefined;
|
|
56
|
+
} & {} & {
|
|
57
|
+
slots?: Partial<{
|
|
58
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
59
|
+
}> | undefined;
|
|
60
|
+
} & {} & JSX.IntrinsicAttributes & import("@sigx/runtime-core").ComponentAttributeExtensions & {
|
|
61
|
+
ref?: import("@sigx/runtime-core").Ref<void> | undefined;
|
|
62
|
+
children?: any;
|
|
63
|
+
}) => import("@sigx/runtime-core").JSXElement) & {
|
|
64
|
+
__setup: import("@sigx/runtime-core").SetupFn<{
|
|
65
|
+
gestureEnabled?: boolean | undefined;
|
|
66
|
+
headerShown?: boolean | undefined;
|
|
67
|
+
title?: string | (() => string) | undefined;
|
|
68
|
+
}, ScreenProps, void, {
|
|
69
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
70
|
+
}>;
|
|
71
|
+
__name?: string;
|
|
72
|
+
__islandId?: string;
|
|
73
|
+
__props: {
|
|
74
|
+
gestureEnabled?: boolean | undefined;
|
|
75
|
+
headerShown?: boolean | undefined;
|
|
76
|
+
title?: string | (() => string) | undefined;
|
|
77
|
+
};
|
|
78
|
+
__events: ScreenProps;
|
|
79
|
+
__ref: void;
|
|
80
|
+
__slots: {
|
|
81
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
82
|
+
};
|
|
83
|
+
} & {
|
|
84
|
+
Header: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
|
|
85
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
86
|
+
}>;
|
|
87
|
+
HeaderLeft: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
|
|
88
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
89
|
+
}>;
|
|
90
|
+
HeaderRight: import("@sigx/runtime-core").ComponentFactory<SimpleSlotProps, void, {
|
|
91
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
92
|
+
}>;
|
|
93
|
+
TabBarItem: import("@sigx/runtime-core").ComponentFactory<TabBarItemProps, void, {
|
|
94
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
95
|
+
}>;
|
|
96
|
+
};
|
|
97
|
+
export {};
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
* declarative and grep-friendly.
|
|
32
32
|
*/
|
|
33
33
|
import { component, onUnmounted, type Define } from '@sigx/lynx';
|
|
34
|
-
import { useScreenRegistry } from '../hooks/use-nav-internal
|
|
35
|
-
import { mergeOptions, setSlot } from '../internal/screen-registry
|
|
34
|
+
import { useScreenRegistry } from '../hooks/use-nav-internal';
|
|
35
|
+
import { mergeOptions, setSlot } from '../internal/screen-registry';
|
|
36
|
+
import type { ScreenOptions } from '../types';
|
|
36
37
|
|
|
37
38
|
type ScreenProps =
|
|
38
39
|
& Define.Prop<'title', string | (() => string)>
|
|
@@ -42,15 +43,16 @@ type ScreenProps =
|
|
|
42
43
|
|
|
43
44
|
const ScreenRoot = component<ScreenProps>(({ props, slots }) => {
|
|
44
45
|
const registry = useScreenRegistry();
|
|
45
|
-
// Apply options whenever the component sets up.
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
// Apply options whenever the component sets up. Only set keys that
|
|
47
|
+
// were actually passed — `mergeOptions` treats `undefined` as "clear
|
|
48
|
+
// this key", so building the patch from raw `props.X` would wipe
|
|
49
|
+
// every option a previous `useScreenOptions(...)` (or another `<Screen>`)
|
|
50
|
+
// had set on this same entry.
|
|
51
|
+
const patch: ScreenOptions = {};
|
|
52
|
+
if (props.title !== undefined) patch.title = props.title;
|
|
53
|
+
if (props.headerShown !== undefined) patch.headerShown = props.headerShown;
|
|
54
|
+
if (props.gestureEnabled !== undefined) patch.gestureEnabled = props.gestureEnabled;
|
|
55
|
+
mergeOptions(registry, patch);
|
|
54
56
|
return () => slots.default?.();
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type Define } from '@sigx/lynx';
|
|
2
|
+
type StackProps =
|
|
3
|
+
/**
|
|
4
|
+
* Mint a nested navigator with this route at its base. When set, the
|
|
5
|
+
* `<Stack>` becomes the owner of a new `NavigatorState` and provides
|
|
6
|
+
* `useNav` / `useNavInternals` / `useNavRoutes` to its subtree, so
|
|
7
|
+
* `nav.push('card-route', …)` from inside the stack stays *inside* it
|
|
8
|
+
* (e.g. for per-tab stacks). Routes presented as `modal` / `fullScreen` /
|
|
9
|
+
* `transparent-modal` automatically escalate to the parent navigator
|
|
10
|
+
* via `nav.parent`, walking up until they reach the root — so modals
|
|
11
|
+
* still overlay the whole app.
|
|
12
|
+
*
|
|
13
|
+
* Omit to render the *enclosing* navigator's stack (the default — this
|
|
14
|
+
* is how `<NavigationRoot> → <Stack />` works).
|
|
15
|
+
*/
|
|
16
|
+
Define.Prop<'initialRoute', string>
|
|
17
|
+
/** Initial params for the nested-stack base entry. */
|
|
18
|
+
& Define.Prop<'initialParams', Record<string, unknown>>
|
|
19
|
+
/** Initial search for the nested-stack base entry. */
|
|
20
|
+
& Define.Prop<'initialSearch', Record<string, unknown>>
|
|
21
|
+
/**
|
|
22
|
+
* Optional chrome rendered *above* the active screen, **inside this
|
|
23
|
+
* Stack's nav scope**. The intended use is `<Header />`, which needs
|
|
24
|
+
* to resolve `useNav()` to the per-stack nav (not the enclosing one)
|
|
25
|
+
* so it can react to pushes inside this stack — e.g. show a back
|
|
26
|
+
* button when a card is pushed onto a per-tab stack.
|
|
27
|
+
*
|
|
28
|
+
* Without this, a `<Header />` placed as a sibling of `<Stack>`
|
|
29
|
+
* would see the enclosing nav and never update when pushes happen
|
|
30
|
+
* inside the nested stack.
|
|
31
|
+
*/
|
|
32
|
+
& Define.Slot<'default'>;
|
|
33
|
+
/**
|
|
34
|
+
* Stack navigator — renders the topmost stack entry's component at rest, or
|
|
35
|
+
* the top + underneath entries during a transition.
|
|
36
|
+
*
|
|
37
|
+
* Two modes:
|
|
38
|
+
*
|
|
39
|
+
* **Bound** (no `initialRoute`): renders the enclosing navigator's stack.
|
|
40
|
+
* This is the shape used directly under `<NavigationRoot>` and is what
|
|
41
|
+
* single-stack apps want.
|
|
42
|
+
*
|
|
43
|
+
* **Nested-owner** (`initialRoute="…"`): mints a fresh `NavigatorState` with
|
|
44
|
+
* its own progress `SharedValue` and edge-back gesture, and provides
|
|
45
|
+
* `useNav` / `useNavInternals` / `useNavRoutes` to its subtree. `useNav()`
|
|
46
|
+
* inside this stack returns the nested nav; `nav.parent` points to the
|
|
47
|
+
* enclosing one. Per-tab stacks are the canonical use case:
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <Tabs initialTab="trips">
|
|
51
|
+
* <Tabs.Screen name="trips"><Stack initialRoute="tripsHome" /></Tabs.Screen>
|
|
52
|
+
* <Tabs.Screen name="map"><Stack initialRoute="mapHome" /></Tabs.Screen>
|
|
53
|
+
* </Tabs>
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Modal/fullScreen pushes escalate up the parent chain automatically — so
|
|
57
|
+
* `nav.push('newTrip')` from inside Trips (where `newTrip` is `modal`)
|
|
58
|
+
* walks to root and overlays the whole UI. `replace` stays strictly local
|
|
59
|
+
* (asymmetric with `push`) so a modal `replace` never wipes the root stack.
|
|
60
|
+
*
|
|
61
|
+
* **Render strategy.** Stack always emits the same JSX shape — a
|
|
62
|
+
* relative wrapper containing one `<Layer>` per entry returned by
|
|
63
|
+
* `computeLayers(stack, transition, progress)`. Each Layer is an
|
|
64
|
+
* absolutely-positioned host view with optional MT-bound translate
|
|
65
|
+
* animation. The pure layer-plan function decides:
|
|
66
|
+
*
|
|
67
|
+
* - **Idle.** Topmost non-overlay base + any overlays above it. All
|
|
68
|
+
* static (no transform). Overlays (`modal` / `fullScreen` /
|
|
69
|
+
* `transparent-modal`) keep their underneath mounted; cards
|
|
70
|
+
* replace their underneath in the base layer.
|
|
71
|
+
* - **Card transition.** Both top and underneath animate (slide-in
|
|
72
|
+
* + parallax). After settle, idle rules apply — the underneath
|
|
73
|
+
* unmounts because the new top is the sole base.
|
|
74
|
+
* - **Overlay transition.** The full idle layer stack up through
|
|
75
|
+
* the underneath stays static; only the animated top has a
|
|
76
|
+
* transform. After settle, the overlay either joins the static
|
|
77
|
+
* idle stack (push) or unmounts (pop).
|
|
78
|
+
*
|
|
79
|
+
* Layer keys are `layer-${entry.key}-${animationVariant}`. The variant
|
|
80
|
+
* suffix forces a remount when an entry transitions from animated to
|
|
81
|
+
* static (or vice versa) — `useAnimatedStyle` binds once at setup and
|
|
82
|
+
* can't switch its mapper at runtime. Modal underneath layers never
|
|
83
|
+
* animate, so their key is stable across the modal lifecycle and the
|
|
84
|
+
* subtree's state (per-tab Stack navigators, scroll positions,
|
|
85
|
+
* in-flight inputs) survives.
|
|
86
|
+
*/
|
|
87
|
+
export declare const Stack: import("@sigx/runtime-core").ComponentFactory<StackProps, void, {
|
|
88
|
+
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
89
|
+
}>;
|
|
90
|
+
export {};
|
package/src/components/Stack.tsx
CHANGED
|
@@ -5,24 +5,21 @@ import {
|
|
|
5
5
|
onUnmounted,
|
|
6
6
|
untrack,
|
|
7
7
|
useSharedValue,
|
|
8
|
-
type ComponentFactory,
|
|
9
8
|
type Define,
|
|
10
|
-
type SharedValue,
|
|
11
9
|
} from '@sigx/lynx';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { useNav, type Nav } from '../hooks/use-nav.js';
|
|
10
|
+
import { createNavigatorState } from '../navigator/core';
|
|
11
|
+
import { useNav, type Nav } from '../hooks/use-nav';
|
|
15
12
|
import {
|
|
16
13
|
useCurrentEntry,
|
|
17
14
|
useNavInternals,
|
|
18
15
|
useNavRoutes,
|
|
19
16
|
type NavInternals,
|
|
20
|
-
} from '../hooks/use-nav-internal
|
|
21
|
-
import type { Presentation, StackEntry } from '../types
|
|
22
|
-
import {
|
|
23
|
-
import { EdgeBackHandle } from './EdgeBackHandle
|
|
24
|
-
import {
|
|
25
|
-
import { useTabScreenName, useTabs } from './Tabs
|
|
17
|
+
} from '../hooks/use-nav-internal';
|
|
18
|
+
import type { Presentation, StackEntry } from '../types';
|
|
19
|
+
import { animationVariant, computeLayers, isOverlayPresentation } from '../internal/layer-plan';
|
|
20
|
+
import { EdgeBackHandle } from './EdgeBackHandle';
|
|
21
|
+
import { Layer } from './Layer';
|
|
22
|
+
import { useTabScreenName, useTabs } from './Tabs';
|
|
26
23
|
|
|
27
24
|
type StackProps =
|
|
28
25
|
/**
|
|
@@ -42,7 +39,19 @@ type StackProps =
|
|
|
42
39
|
/** Initial params for the nested-stack base entry. */
|
|
43
40
|
& Define.Prop<'initialParams', Record<string, unknown>>
|
|
44
41
|
/** Initial search for the nested-stack base entry. */
|
|
45
|
-
& Define.Prop<'initialSearch', Record<string, unknown
|
|
42
|
+
& Define.Prop<'initialSearch', Record<string, unknown>>
|
|
43
|
+
/**
|
|
44
|
+
* Optional chrome rendered *above* the active screen, **inside this
|
|
45
|
+
* Stack's nav scope**. The intended use is `<Header />`, which needs
|
|
46
|
+
* to resolve `useNav()` to the per-stack nav (not the enclosing one)
|
|
47
|
+
* so it can react to pushes inside this stack — e.g. show a back
|
|
48
|
+
* button when a card is pushed onto a per-tab stack.
|
|
49
|
+
*
|
|
50
|
+
* Without this, a `<Header />` placed as a sibling of `<Stack>`
|
|
51
|
+
* would see the enclosing nav and never update when pushes happen
|
|
52
|
+
* inside the nested stack.
|
|
53
|
+
*/
|
|
54
|
+
& Define.Slot<'default'>;
|
|
46
55
|
|
|
47
56
|
let _nestedKeyCounter = 0;
|
|
48
57
|
|
|
@@ -74,22 +83,33 @@ let _nestedKeyCounter = 0;
|
|
|
74
83
|
* walks to root and overlays the whole UI. `replace` stays strictly local
|
|
75
84
|
* (asymmetric with `push`) so a modal `replace` never wipes the root stack.
|
|
76
85
|
*
|
|
77
|
-
* **Render strategy
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* absolutely, each with an MT-driven `translateX` that reads from the
|
|
83
|
-
* navigator's progress `SharedValue`. The host's BG thread doesn't tick
|
|
84
|
-
* per frame — `useAnimatedStyle` runs the interpolation entirely on MT.
|
|
86
|
+
* **Render strategy.** Stack always emits the same JSX shape — a
|
|
87
|
+
* relative wrapper containing one `<Layer>` per entry returned by
|
|
88
|
+
* `computeLayers(stack, transition, progress)`. Each Layer is an
|
|
89
|
+
* absolutely-positioned host view with optional MT-bound translate
|
|
90
|
+
* animation. The pure layer-plan function decides:
|
|
85
91
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
92
|
+
* - **Idle.** Topmost non-overlay base + any overlays above it. All
|
|
93
|
+
* static (no transform). Overlays (`modal` / `fullScreen` /
|
|
94
|
+
* `transparent-modal`) keep their underneath mounted; cards
|
|
95
|
+
* replace their underneath in the base layer.
|
|
96
|
+
* - **Card transition.** Both top and underneath animate (slide-in
|
|
97
|
+
* + parallax). After settle, idle rules apply — the underneath
|
|
98
|
+
* unmounts because the new top is the sole base.
|
|
99
|
+
* - **Overlay transition.** The full idle layer stack up through
|
|
100
|
+
* the underneath stays static; only the animated top has a
|
|
101
|
+
* transform. After settle, the overlay either joins the static
|
|
102
|
+
* idle stack (push) or unmounts (pop).
|
|
103
|
+
*
|
|
104
|
+
* Layer keys are `layer-${entry.key}-${animationVariant}`. The variant
|
|
105
|
+
* suffix forces a remount when an entry transitions from animated to
|
|
106
|
+
* static (or vice versa) — `useAnimatedStyle` binds once at setup and
|
|
107
|
+
* can't switch its mapper at runtime. Modal underneath layers never
|
|
108
|
+
* animate, so their key is stable across the modal lifecycle and the
|
|
109
|
+
* subtree's state (per-tab Stack navigators, scroll positions,
|
|
110
|
+
* in-flight inputs) survives.
|
|
91
111
|
*/
|
|
92
|
-
export const Stack = component<StackProps>(({ props }) => {
|
|
112
|
+
export const Stack = component<StackProps>(({ props, slots }) => {
|
|
93
113
|
// Capture enclosing scope's nav + routes + internals BEFORE any of the
|
|
94
114
|
// defineProvide calls below override them for descendants. These are
|
|
95
115
|
// always the "outer" values regardless of whether this Stack is bound
|
|
@@ -226,89 +246,113 @@ export const Stack = component<StackProps>(({ props }) => {
|
|
|
226
246
|
internals = parentInternals;
|
|
227
247
|
}
|
|
228
248
|
|
|
249
|
+
// Per-stack chrome (slots.default) renders *inside* this Stack's
|
|
250
|
+
// nav scope so a `<Header />` placed there resolves `useNav()` to
|
|
251
|
+
// the per-stack nav. Wrapping the active body in a flex-column
|
|
252
|
+
// with the slot above does that without disturbing layer-fill
|
|
253
|
+
// semantics — the slot takes natural height, the body keeps
|
|
254
|
+
// flex-fill.
|
|
255
|
+
const flexColumnFill = {
|
|
256
|
+
flexGrow: 1,
|
|
257
|
+
flexShrink: 1,
|
|
258
|
+
flexBasis: 0,
|
|
259
|
+
minHeight: 0,
|
|
260
|
+
display: 'flex',
|
|
261
|
+
flexDirection: 'column',
|
|
262
|
+
} as const;
|
|
263
|
+
|
|
229
264
|
return () => {
|
|
230
|
-
const
|
|
231
|
-
const
|
|
265
|
+
const chrome = slots.default?.();
|
|
266
|
+
const layers = computeLayers(nav.stack, nav.transition, internals.progress);
|
|
232
267
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// handle so the user can pan from the left edge to start a back
|
|
256
|
-
// transition. `position: absolute` doesn't disturb the screen's
|
|
257
|
-
// own layout — the handle only intercepts touches in the leftmost
|
|
258
|
-
// 20px, and only when they pan rightward past `MIN_DISTANCE`.
|
|
259
|
-
if (nav.canGoBack && internals.edgeSwipeEnabled) {
|
|
260
|
-
return (
|
|
261
|
-
<view
|
|
262
|
-
style={{
|
|
263
|
-
position: 'relative',
|
|
264
|
-
width: '100%',
|
|
265
|
-
height: '100%',
|
|
266
|
-
}}
|
|
267
|
-
>
|
|
268
|
-
<EntryScope key={top.key} entry={top}>
|
|
269
|
-
{body}
|
|
270
|
-
</EntryScope>
|
|
271
|
-
<EdgeBackHandle key="edge-back" />
|
|
272
|
-
</view>
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
return (
|
|
276
|
-
<EntryScope key={top.key} entry={top}>
|
|
277
|
-
{body}
|
|
278
|
-
</EntryScope>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
268
|
+
const renderLayerNode = (layer: typeof layers[number] | undefined) =>
|
|
269
|
+
layer ? (
|
|
270
|
+
<Layer
|
|
271
|
+
key={`layer-${layer.entry.key}-${animationVariant(layer.animation)}`}
|
|
272
|
+
entry={layer.entry}
|
|
273
|
+
routes={routes}
|
|
274
|
+
animation={layer.animation}
|
|
275
|
+
/>
|
|
276
|
+
) : null;
|
|
277
|
+
// sigx's reconciler treats a single array-valued JSX child as
|
|
278
|
+
// one "slot": when the array's *length* changes between
|
|
279
|
+
// renders, keyed children inside can be remounted even if
|
|
280
|
+
// their keys are stable. To make stacked-overlay state
|
|
281
|
+
// preservation work (modal A still mounted after modal B
|
|
282
|
+
// pushes on top), each layer is emitted as its own separate
|
|
283
|
+
// JSX child slot rather than as an array. The slots are
|
|
284
|
+
// position-stable across renders — the only thing that
|
|
285
|
+
// changes is a slot turning from `null` to a Layer (mount) or
|
|
286
|
+
// vice versa (unmount). MAX_LAYERS caps the supported stack
|
|
287
|
+
// depth; in practice apps rarely stack more than 2-3 overlays.
|
|
288
|
+
// If you hit the cap, increase the constant — the unrolled
|
|
289
|
+
// shape is just verbose, not algorithmically limited.
|
|
281
290
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
291
|
+
// Edge-swipe handle on top, gated on:
|
|
292
|
+
// - `internals.edgeSwipeEnabled` — opt-out flag (also off
|
|
293
|
+
// when the navigator has no progress SharedValue, i.e.
|
|
294
|
+
// animations disabled — no in-flight gesture to animate).
|
|
295
|
+
// - `nav.canGoBack` — something to pop back to.
|
|
296
|
+
// - `!nav.transition` — no animation already running.
|
|
297
|
+
// - The current top is a card (not an overlay). Edge-swipe
|
|
298
|
+
// is the iOS-style horizontal pop gesture for card stacks;
|
|
299
|
+
// using it to dismiss a modal would be the wrong axis +
|
|
300
|
+
// the wrong dismissal semantic.
|
|
301
|
+
//
|
|
302
|
+
// The handle only intercepts touches in the leftmost 20px and
|
|
303
|
+
// ignores small drags, so placing it last (highest z) doesn't
|
|
304
|
+
// disturb screen touches.
|
|
305
|
+
const top = nav.current;
|
|
306
|
+
const edgeHandle = (
|
|
307
|
+
internals.edgeSwipeEnabled
|
|
308
|
+
&& nav.canGoBack
|
|
309
|
+
&& !nav.transition
|
|
310
|
+
&& !isOverlayPresentation(top.presentation)
|
|
311
|
+
)
|
|
312
|
+
? <EdgeBackHandle key="edge-back" />
|
|
313
|
+
: null;
|
|
286
314
|
|
|
287
|
-
|
|
315
|
+
const body = (
|
|
288
316
|
<view
|
|
289
317
|
style={{
|
|
290
318
|
position: 'relative',
|
|
291
319
|
width: '100%',
|
|
292
|
-
|
|
320
|
+
// Flex-fill so the layer container has a real
|
|
321
|
+
// height — `<Layer>`s anchor via `position:
|
|
322
|
+
// absolute; top/right/bottom/left: 0`, which
|
|
323
|
+
// needs a sized relative parent.
|
|
324
|
+
...flexColumnFill,
|
|
325
|
+
// Clip any animated layer that translates off-
|
|
326
|
+
// screen so the slide doesn't bleed past the
|
|
327
|
+
// Stack's bounds.
|
|
293
328
|
overflow: 'hidden',
|
|
294
329
|
}}
|
|
295
330
|
>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
331
|
+
{renderLayerNode(layers[0])}
|
|
332
|
+
{renderLayerNode(layers[1])}
|
|
333
|
+
{renderLayerNode(layers[2])}
|
|
334
|
+
{renderLayerNode(layers[3])}
|
|
335
|
+
{renderLayerNode(layers[4])}
|
|
336
|
+
{renderLayerNode(layers[5])}
|
|
337
|
+
{renderLayerNode(layers[6])}
|
|
338
|
+
{renderLayerNode(layers[7])}
|
|
339
|
+
{renderLayerNode(layers[8])}
|
|
340
|
+
{renderLayerNode(layers[9])}
|
|
341
|
+
{renderLayerNode(layers[10])}
|
|
342
|
+
{renderLayerNode(layers[11])}
|
|
343
|
+
{renderLayerNode(layers[12])}
|
|
344
|
+
{renderLayerNode(layers[13])}
|
|
345
|
+
{renderLayerNode(layers[14])}
|
|
346
|
+
{renderLayerNode(layers[15])}
|
|
347
|
+
{edgeHandle}
|
|
348
|
+
</view>
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (chrome == null) return body as never;
|
|
352
|
+
return (
|
|
353
|
+
<view style={flexColumnFill}>
|
|
354
|
+
{chrome}
|
|
355
|
+
<view style={flexColumnFill}>{body}</view>
|
|
312
356
|
</view>
|
|
313
357
|
);
|
|
314
358
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<TabBar>` — headless default chrome for `<Tabs>`.
|
|
3
|
+
*
|
|
4
|
+
* Renders the active-tab buttons reading from the enclosing `useTabs()`
|
|
5
|
+
* navigator. Intentionally **unstyled** — this lives in the (theme-less)
|
|
6
|
+
* navigation package, so it ships pure structure + accessibility wiring.
|
|
7
|
+
* Themed chrome belongs in a UI-kit package: see `<NavTabBar />` in
|
|
8
|
+
* `@sigx/lynx-daisyui` for the daisy-themed equivalent.
|
|
9
|
+
*
|
|
10
|
+
* Use this directly only if you want to handle styling yourself via the
|
|
11
|
+
* `renderTab` prop. For a "looks like a tab bar out of the box" component,
|
|
12
|
+
* pull `<NavTabBar />` from `@sigx/lynx-daisyui` (or your own UI kit).
|
|
13
|
+
*
|
|
14
|
+
* Customization:
|
|
15
|
+
* - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
|
|
16
|
+
* default button rendering for each tab. `ctx.active` tells the
|
|
17
|
+
* consumer whether this tab is currently focused; `ctx.onPress`
|
|
18
|
+
* activates the tab. **Recommended** for any visual treatment.
|
|
19
|
+
*
|
|
20
|
+
* Accessibility (baked into the default button — the one structural
|
|
21
|
+
* concern this component keeps):
|
|
22
|
+
* - `accessibility-label` from `info.accessibilityLabel ?? info.label ?? info.name`.
|
|
23
|
+
* - `accessibility-element="true"` so screen readers see the whole pill.
|
|
24
|
+
* - `accessibility-trait="button"` and a `selected` flag on the active
|
|
25
|
+
* one so VoiceOver/TalkBack announces focus state on tab switch.
|
|
26
|
+
*/
|
|
27
|
+
import { type JSXElement } from '@sigx/lynx';
|
|
28
|
+
import { type TabInfo } from './Tabs';
|
|
29
|
+
/** Rendering context passed to a `renderTab` consumer. */
|
|
30
|
+
export interface TabRenderContext {
|
|
31
|
+
/** True when this tab is currently active. Reactive — re-runs render on change. */
|
|
32
|
+
readonly active: boolean;
|
|
33
|
+
/** Activates this tab. Use as a `bindtap` handler on the rendered node. */
|
|
34
|
+
onPress(): void;
|
|
35
|
+
}
|
|
36
|
+
export declare const TabBar: import("@sigx/runtime-core").ComponentFactory<{
|
|
37
|
+
renderTab?: ((info: TabInfo, ctx: TabRenderContext) => JSXElement) | undefined;
|
|
38
|
+
}, void, {}>;
|
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<TabBar>` — default chrome for `<Tabs>`.
|
|
2
|
+
* `<TabBar>` — headless default chrome for `<Tabs>`.
|
|
3
3
|
*
|
|
4
|
-
* Renders
|
|
5
|
-
* navigator.
|
|
6
|
-
*
|
|
4
|
+
* Renders the active-tab buttons reading from the enclosing `useTabs()`
|
|
5
|
+
* navigator. Intentionally **unstyled** — this lives in the (theme-less)
|
|
6
|
+
* navigation package, so it ships pure structure + accessibility wiring.
|
|
7
|
+
* Themed chrome belongs in a UI-kit package: see `<NavTabBar />` in
|
|
8
|
+
* `@sigx/lynx-daisyui` for the daisy-themed equivalent.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
10
|
+
* Use this directly only if you want to handle styling yourself via the
|
|
11
|
+
* `renderTab` prop. For a "looks like a tab bar out of the box" component,
|
|
12
|
+
* pull `<NavTabBar />` from `@sigx/lynx-daisyui` (or your own UI kit).
|
|
13
|
+
*
|
|
14
|
+
* Customization:
|
|
9
15
|
* - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
|
|
10
16
|
* default button rendering for each tab. `ctx.active` tells the
|
|
11
17
|
* consumer whether this tab is currently focused; `ctx.onPress`
|
|
12
|
-
* activates the tab.
|
|
13
|
-
*
|
|
14
|
-
* Accessibility:
|
|
15
|
-
* - Each default button gets `accessibility-label` from
|
|
16
|
-
* `info.accessibilityLabel ?? info.label ?? info.name`.
|
|
17
|
-
* - Each default button gets `accessibility-element="true"` so screen
|
|
18
|
-
* readers see the whole pill, not just the inner `<text>`.
|
|
19
|
-
* - Each default button gets `accessibility-trait="button"` and a
|
|
20
|
-
* `selected` flag on the active one so VoiceOver/TalkBack announces
|
|
21
|
-
* focus state on tab switch.
|
|
18
|
+
* activates the tab. **Recommended** for any visual treatment.
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* Accessibility (baked into the default button — the one structural
|
|
21
|
+
* concern this component keeps):
|
|
22
|
+
* - `accessibility-label` from `info.accessibilityLabel ?? info.label ?? info.name`.
|
|
23
|
+
* - `accessibility-element="true"` so screen readers see the whole pill.
|
|
24
|
+
* - `accessibility-trait="button"` and a `selected` flag on the active
|
|
25
|
+
* one so VoiceOver/TalkBack announces focus state on tab switch.
|
|
27
26
|
*/
|
|
28
27
|
import {
|
|
29
28
|
component,
|
|
30
29
|
type Define,
|
|
31
30
|
type JSXElement,
|
|
32
31
|
} from '@sigx/lynx';
|
|
33
|
-
import { useTabs, type TabInfo } from './Tabs
|
|
32
|
+
import { useTabs, type TabInfo } from './Tabs';
|
|
34
33
|
|
|
35
34
|
/** Rendering context passed to a `renderTab` consumer. */
|
|
36
35
|
export interface TabRenderContext {
|
|
@@ -46,8 +45,9 @@ type TabBarProps =
|
|
|
46
45
|
/**
|
|
47
46
|
* Default per-tab button. Plain `<view>` with a `<text>` inside, an
|
|
48
47
|
* `accessibility-*` cluster for screen readers, and a tap handler. No
|
|
49
|
-
* styling beyond a minimal active-state
|
|
50
|
-
* branded chrome pass `renderTab
|
|
48
|
+
* styling beyond a minimal active-state opacity hint — consumers that
|
|
49
|
+
* want branded chrome pass `renderTab` or use a UI-kit-provided tab bar
|
|
50
|
+
* (e.g. `<NavTabBar />` from `@sigx/lynx-daisyui`).
|
|
51
51
|
*/
|
|
52
52
|
const DefaultTabButton = component<
|
|
53
53
|
& Define.Prop<'info', TabInfo, true>
|