@sigx/lynx-navigation 0.1.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/LICENSE +21 -0
- package/dist/components/EdgeBackHandle.d.ts +2 -0
- package/dist/components/EdgeBackHandle.d.ts.map +1 -0
- package/dist/components/Link.d.ts +61 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +54 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/NavigationRoot.d.ts +37 -0
- package/dist/components/NavigationRoot.d.ts.map +1 -0
- package/dist/components/NavigationRoot.js +41 -0
- package/dist/components/NavigationRoot.js.map +1 -0
- package/dist/components/ScreenContainer.d.ts +18 -0
- package/dist/components/ScreenContainer.d.ts.map +1 -0
- package/dist/components/Stack.d.ts +21 -0
- package/dist/components/Stack.d.ts.map +1 -0
- package/dist/components/Stack.js +39 -0
- package/dist/components/Stack.js.map +1 -0
- package/dist/define-routes.d.ts +31 -0
- package/dist/define-routes.d.ts.map +1 -0
- package/dist/define-routes.js +32 -0
- package/dist/define-routes.js.map +1 -0
- package/dist/hooks/use-hardware-back.d.ts +31 -0
- package/dist/hooks/use-hardware-back.d.ts.map +1 -0
- package/dist/hooks/use-nav-internal.d.ts +37 -0
- package/dist/hooks/use-nav-internal.d.ts.map +1 -0
- package/dist/hooks/use-nav-internal.js +12 -0
- package/dist/hooks/use-nav-internal.js.map +1 -0
- package/dist/hooks/use-nav.d.ts +77 -0
- package/dist/hooks/use-nav.d.ts.map +1 -0
- package/dist/hooks/use-nav.js +11 -0
- package/dist/hooks/use-nav.js.map +1 -0
- package/dist/hooks/use-params.d.ts +19 -0
- package/dist/hooks/use-params.d.ts.map +1 -0
- package/dist/hooks/use-params.js +22 -0
- package/dist/hooks/use-params.js.map +1 -0
- package/dist/hooks/use-search.d.ts +11 -0
- package/dist/hooks/use-search.d.ts.map +1 -0
- package/dist/hooks/use-search.js +14 -0
- package/dist/hooks/use-search.js.map +1 -0
- package/dist/href.d.ts +40 -0
- package/dist/href.d.ts.map +1 -0
- package/dist/href.js +14 -0
- package/dist/href.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/screen-width.d.ts +16 -0
- package/dist/internal/screen-width.d.ts.map +1 -0
- package/dist/navigator/core.d.ts +51 -0
- package/dist/navigator/core.d.ts.map +1 -0
- package/dist/navigator/core.js +149 -0
- package/dist/navigator/core.js.map +1 -0
- package/dist/register.d.ts +38 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +2 -0
- package/dist/register.js.map +1 -0
- package/dist/types.d.ts +162 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
- package/src/components/EdgeBackHandle.tsx +161 -0
- package/src/components/Link.tsx +113 -0
- package/src/components/NavigationRoot.tsx +85 -0
- package/src/components/ScreenContainer.tsx +101 -0
- package/src/components/Stack.tsx +99 -0
- package/src/define-routes.ts +33 -0
- package/src/hooks/use-hardware-back.ts +50 -0
- package/src/hooks/use-nav-internal.ts +47 -0
- package/src/hooks/use-nav.ts +118 -0
- package/src/hooks/use-params.ts +23 -0
- package/src/hooks/use-search.ts +15 -0
- package/src/href.ts +58 -0
- package/src/index.ts +38 -0
- package/src/internal/screen-width.ts +34 -0
- package/src/navigator/core.ts +386 -0
- package/src/register.ts +41 -0
- package/src/types.ts +171 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineInjectable, type SharedValue } from '@sigx/lynx';
|
|
2
|
+
import type { RouteMap } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal injectable: the route registry passed into `<NavigationRoot>`.
|
|
6
|
+
* Components (Stack, Screen) read this to look up route definitions by name.
|
|
7
|
+
*
|
|
8
|
+
* Not exported from the package barrel — use `useNav()` for navigation, and
|
|
9
|
+
* the registry is implicit from `<NavigationRoot routes={...}>`.
|
|
10
|
+
*/
|
|
11
|
+
export const useNavRoutes = defineInjectable<RouteMap>(() => {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'[lynx-navigation] No <NavigationRoot> found in the component tree.',
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Internal injectable: low-level navigator handles used by the edge-back
|
|
19
|
+
* gesture. Holds the progress SharedValue (so gesture worklets can write it
|
|
20
|
+
* directly on MT) plus BG-side begin/commit/cancel functions invoked via
|
|
21
|
+
* `runOnBackground` from gesture worklets.
|
|
22
|
+
*
|
|
23
|
+
* `progress` is `null` when the navigator was created with `animated={false}`
|
|
24
|
+
* (e.g. tests). `beginBackGesture` is also a no-op in that case.
|
|
25
|
+
*/
|
|
26
|
+
export interface NavInternals {
|
|
27
|
+
/** MT-driven transition progress; null when animations are disabled. */
|
|
28
|
+
readonly progress: SharedValue<number> | null;
|
|
29
|
+
/**
|
|
30
|
+
* Set transition state for a gesture-driven pop. Does not start any
|
|
31
|
+
* automatic animation — the gesture worklet writes `progress` directly
|
|
32
|
+
* per frame, then animates to the commit/cancel endpoint on release.
|
|
33
|
+
*/
|
|
34
|
+
beginBackGesture(): void;
|
|
35
|
+
/** Commit the back gesture: pop top entry + clear transition. */
|
|
36
|
+
commitBackGesture(): void;
|
|
37
|
+
/** Cancel the back gesture: clear transition without popping. */
|
|
38
|
+
cancelBackGesture(): void;
|
|
39
|
+
/** Whether the user opted into the edge-swipe-back gesture. */
|
|
40
|
+
readonly edgeSwipeEnabled: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useNavInternals = defineInjectable<NavInternals>(() => {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'[lynx-navigation] No <NavigationRoot> found in the component tree.',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { defineInjectable } from '@sigx/lynx';
|
|
2
|
+
import type { RegisteredRoutes, RouteId, RouteParams, RouteSearch } from '../register.js';
|
|
3
|
+
import type {
|
|
4
|
+
PopOptions,
|
|
5
|
+
PushOptions,
|
|
6
|
+
RouteRequiresParams,
|
|
7
|
+
StackEntry,
|
|
8
|
+
TransitionState,
|
|
9
|
+
} from '../types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subset of registered route names that declare a `params` schema (and so
|
|
13
|
+
* require a `params` argument when navigating).
|
|
14
|
+
*
|
|
15
|
+
* Computed via mapped-type filtering rather than a conditional inside the
|
|
16
|
+
* method signature: when a conditional like `RouteRequiresParams<R[K]>` is
|
|
17
|
+
* embedded inside a generic method parameter, TS evaluates it at definition
|
|
18
|
+
* time with K bound to the *whole* union of route ids — which distributes
|
|
19
|
+
* `RouteRequiresParams` over every route and collapses the result to
|
|
20
|
+
* `boolean`, breaking the conditional. Filtering once at the type level avoids
|
|
21
|
+
* the issue and produces clean overload candidates.
|
|
22
|
+
*/
|
|
23
|
+
export type RoutesWithParams = {
|
|
24
|
+
[K in RouteId]: RouteRequiresParams<RegisteredRoutes[K]> extends true ? K : never;
|
|
25
|
+
}[RouteId];
|
|
26
|
+
|
|
27
|
+
/** Routes that don't declare a `params` schema. */
|
|
28
|
+
export type RoutesWithoutParams = Exclude<RouteId, RoutesWithParams>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The navigator handle returned by `useNav()`.
|
|
32
|
+
*
|
|
33
|
+
* Read access (`current`, `stack`, `canGoBack`) is reactive — these properties
|
|
34
|
+
* are getters that read from the underlying stack signal, so accessing them
|
|
35
|
+
* inside a component's render function (or inside `effect` / `computed`) takes
|
|
36
|
+
* a reactive dependency. Mutating methods (`push`, `pop`, etc.) trigger the
|
|
37
|
+
* dependents to update.
|
|
38
|
+
*
|
|
39
|
+
* `push` and `replace` are split into two overloads — one for routes without a
|
|
40
|
+
* params schema (no params arg) and one for routes with a params schema
|
|
41
|
+
* (params required). See `RoutesWithParams` above for why this isn't a single
|
|
42
|
+
* conditional return type.
|
|
43
|
+
*/
|
|
44
|
+
export interface Nav {
|
|
45
|
+
/** Push a route that has no params schema. */
|
|
46
|
+
push<K extends RoutesWithoutParams>(
|
|
47
|
+
name: K,
|
|
48
|
+
search?: RouteSearch<K>,
|
|
49
|
+
options?: PushOptions,
|
|
50
|
+
): void;
|
|
51
|
+
/** Push a route that requires params. */
|
|
52
|
+
push<K extends RoutesWithParams>(
|
|
53
|
+
name: K,
|
|
54
|
+
params: RouteParams<K>,
|
|
55
|
+
search?: RouteSearch<K>,
|
|
56
|
+
options?: PushOptions,
|
|
57
|
+
): void;
|
|
58
|
+
|
|
59
|
+
/** Replace the top entry — same overload pattern as push. */
|
|
60
|
+
replace<K extends RoutesWithoutParams>(
|
|
61
|
+
name: K,
|
|
62
|
+
search?: RouteSearch<K>,
|
|
63
|
+
options?: PushOptions,
|
|
64
|
+
): void;
|
|
65
|
+
replace<K extends RoutesWithParams>(
|
|
66
|
+
name: K,
|
|
67
|
+
params: RouteParams<K>,
|
|
68
|
+
search?: RouteSearch<K>,
|
|
69
|
+
options?: PushOptions,
|
|
70
|
+
): void;
|
|
71
|
+
|
|
72
|
+
/** Pop one or more entries off the top of the stack. */
|
|
73
|
+
pop(count?: number, options?: PopOptions): void;
|
|
74
|
+
|
|
75
|
+
/** Pop entries until the named route is at the top. */
|
|
76
|
+
popTo<K extends RouteId>(name: K): void;
|
|
77
|
+
|
|
78
|
+
/** Pop all the way to the root entry. */
|
|
79
|
+
popToRoot(): void;
|
|
80
|
+
|
|
81
|
+
/** Wholesale-replace the stack. */
|
|
82
|
+
reset(state: { stack: ReadonlyArray<StackEntry> }): void;
|
|
83
|
+
|
|
84
|
+
/** Dismiss the topmost modal stack (no-op if none active). */
|
|
85
|
+
dismiss(): void;
|
|
86
|
+
|
|
87
|
+
/** Currently-focused entry. Reactive via property access. */
|
|
88
|
+
readonly current: StackEntry;
|
|
89
|
+
|
|
90
|
+
/** Full stack, top last. Reactive. */
|
|
91
|
+
readonly stack: ReadonlyArray<StackEntry>;
|
|
92
|
+
|
|
93
|
+
/** Whether the user can go back from the current entry. Reactive. */
|
|
94
|
+
readonly canGoBack: boolean;
|
|
95
|
+
|
|
96
|
+
/** Parent navigator (e.g. the Tabs above this Stack), or null at the root. */
|
|
97
|
+
readonly parent: Nav | null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* In-flight transition, or null when navigation is at rest. Reactive —
|
|
101
|
+
* `<Stack>` reads this to decide whether to render one screen or two
|
|
102
|
+
* (during a slide transition both the outgoing and incoming screens
|
|
103
|
+
* stay mounted with `useAnimatedStyle`-driven transforms).
|
|
104
|
+
*/
|
|
105
|
+
readonly transition: TransitionState | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Access the innermost navigator. Provided by `<NavigationRoot>` via
|
|
110
|
+
* `defineProvide`. Throws when called outside a NavigationRoot subtree.
|
|
111
|
+
*
|
|
112
|
+
* Mirrors `@sigx/router`'s `useRouter` pattern (`packages/router/src/router.ts:30`).
|
|
113
|
+
*/
|
|
114
|
+
export const useNav = defineInjectable<Nav>(() => {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'[lynx-navigation] useNav() called but no <NavigationRoot> is mounted in the component tree.',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RouteId, RouteParams } from '../register.js';
|
|
2
|
+
import { useNav } from './use-nav.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the typed params for the current screen, asserted against the named
|
|
6
|
+
* route from the registry.
|
|
7
|
+
*
|
|
8
|
+
* Returns the current entry's params snapshot. The `name` arg is the type
|
|
9
|
+
* discriminator at compile time; we don't currently runtime-check that the
|
|
10
|
+
* caller's route matches the active entry — the dev-mode warning lands in a
|
|
11
|
+
* later slice along with schema validation.
|
|
12
|
+
*
|
|
13
|
+
* **Reactivity**: each `nav.push` / `replace` produces a new entry with a
|
|
14
|
+
* fresh `key`. `<Stack>` keys the rendered component on `entry.key`, so the
|
|
15
|
+
* screen component fully remounts on every navigation — useParams runs again
|
|
16
|
+
* during the new mount and reads the new params. There is no "in-place params
|
|
17
|
+
* update for the same mounted screen" path in v0.1, so a snapshot at setup
|
|
18
|
+
* time is correct.
|
|
19
|
+
*/
|
|
20
|
+
export function useParams<K extends RouteId>(_name: K): RouteParams<K> {
|
|
21
|
+
const nav = useNav();
|
|
22
|
+
return nav.current.params as RouteParams<K>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteId, RouteSearch } from '../register.js';
|
|
2
|
+
import { useNav } from './use-nav.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the typed search/query params for the current screen, asserted against
|
|
6
|
+
* the named route from the registry.
|
|
7
|
+
*
|
|
8
|
+
* Returns the current entry's search snapshot. See `useParams` for the
|
|
9
|
+
* reactivity story — each navigation triggers a remount via the entry-keyed
|
|
10
|
+
* Stack, so a setup-time snapshot is sufficient.
|
|
11
|
+
*/
|
|
12
|
+
export function useSearch<K extends RouteId>(_name: K): RouteSearch<K> {
|
|
13
|
+
const nav = useNav();
|
|
14
|
+
return nav.current.search as RouteSearch<K>;
|
|
15
|
+
}
|
package/src/href.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RouteId, RouteParams, RouteSearch } from './register.js';
|
|
2
|
+
import type { RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A typed reference to a navigation target — what `<Link to={...}>` consumes
|
|
6
|
+
* and what `hrefFor()` produces.
|
|
7
|
+
*
|
|
8
|
+
* Holds both the typed pieces (route name, params, search) and the serialized
|
|
9
|
+
* URL form (when the route declares a `path` template). Either side can drive
|
|
10
|
+
* navigation — typed for in-app links, URL for deep links / sharing.
|
|
11
|
+
*/
|
|
12
|
+
export interface Href<K extends RouteId = RouteId> {
|
|
13
|
+
readonly route: K;
|
|
14
|
+
readonly params: RouteParams<K>;
|
|
15
|
+
readonly search: RouteSearch<K>;
|
|
16
|
+
/** URL form. `null` when the route declares no `path` template. */
|
|
17
|
+
readonly url: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a typed Href for a given route. Validates params against the route's
|
|
22
|
+
* schema at runtime; type-checks them at compile time.
|
|
23
|
+
*
|
|
24
|
+
* Overloaded the same way as `nav.push` — one signature for routes without a
|
|
25
|
+
* params schema, one for routes that require params. See `RoutesWithParams`
|
|
26
|
+
* in `./hooks/use-nav.js` for why this isn't expressed as a single conditional.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* hrefFor('profile', { id: '42' }); // → { route, params, search: {}, url: '/users/42' }
|
|
31
|
+
* hrefFor('profile', { id: '42' }, { tab: 'about' });
|
|
32
|
+
* hrefFor('home'); // params arg omitted (no schema)
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function hrefFor<K extends RoutesWithoutParams>(name: K, search?: RouteSearch<K>): Href<K>;
|
|
36
|
+
export function hrefFor<K extends RoutesWithParams>(
|
|
37
|
+
name: K,
|
|
38
|
+
params: RouteParams<K>,
|
|
39
|
+
search?: RouteSearch<K>,
|
|
40
|
+
): Href<K>;
|
|
41
|
+
export function hrefFor(name: string, ..._args: unknown[]): Href {
|
|
42
|
+
void name;
|
|
43
|
+
void _args;
|
|
44
|
+
throw new Error(
|
|
45
|
+
'hrefFor() runtime is not implemented yet — this is the type spike.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a URL string into a typed Href against the registered routes.
|
|
51
|
+
* Returns `null` if no route's `path` template matches.
|
|
52
|
+
*/
|
|
53
|
+
export function parseHref(url: string): Href | null {
|
|
54
|
+
void url;
|
|
55
|
+
throw new Error(
|
|
56
|
+
'parseHref() runtime is not implemented yet — this is the type spike.',
|
|
57
|
+
);
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigx/lynx-navigation — type-first native stack router.
|
|
3
|
+
*
|
|
4
|
+
* Phase 0.1 (current): typed registry, stack runtime, NavigationRoot + Stack.
|
|
5
|
+
* Coming next: Screen with slot-based header API, MTS transitions, Tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { defineRoutes } from './define-routes.js';
|
|
9
|
+
export type { Register, RegisteredRoutes, RouteId, RouteParams, RouteSearch } from './register.js';
|
|
10
|
+
export { useNav } from './hooks/use-nav.js';
|
|
11
|
+
export type { Nav, RoutesWithoutParams, RoutesWithParams } from './hooks/use-nav.js';
|
|
12
|
+
export { useParams } from './hooks/use-params.js';
|
|
13
|
+
export { useSearch } from './hooks/use-search.js';
|
|
14
|
+
export { useHardwareBack } from './hooks/use-hardware-back.js';
|
|
15
|
+
export { hrefFor, parseHref } from './href.js';
|
|
16
|
+
export type { Href } from './href.js';
|
|
17
|
+
export { NavigationRoot } from './components/NavigationRoot.js';
|
|
18
|
+
export { Stack } from './components/Stack.js';
|
|
19
|
+
export { Link } from './components/Link.js';
|
|
20
|
+
export type { LinkProps } from './components/Link.js';
|
|
21
|
+
export type {
|
|
22
|
+
ComponentLike,
|
|
23
|
+
EmptyParams,
|
|
24
|
+
InferOutput,
|
|
25
|
+
ParamsOf,
|
|
26
|
+
PopOptions,
|
|
27
|
+
Presentation,
|
|
28
|
+
PushOptions,
|
|
29
|
+
RouteDefinition,
|
|
30
|
+
RouteMap,
|
|
31
|
+
RouteRequiresParams,
|
|
32
|
+
SearchOf,
|
|
33
|
+
StackEntry,
|
|
34
|
+
StandardSchemaV1,
|
|
35
|
+
TransitionKind,
|
|
36
|
+
TransitionRole,
|
|
37
|
+
TransitionState,
|
|
38
|
+
} from './types.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logical screen width (in dp) read from `lynx.SystemInfo` at module load.
|
|
3
|
+
* Falls back to 400 (typical phone) if SystemInfo isn't available — module
|
|
4
|
+
* load happens BG-side after the bundle initializes, by which time
|
|
5
|
+
* `lynx.SystemInfo` is populated, so the fallback only fires in tests / SSR /
|
|
6
|
+
* non-Lynx hosts.
|
|
7
|
+
*
|
|
8
|
+
* Used by:
|
|
9
|
+
* - `<ScreenContainer>` for the slide-from-right transform output range.
|
|
10
|
+
* - `<EdgeBackHandle>` for the gesture commit threshold (`dx / width`).
|
|
11
|
+
*
|
|
12
|
+
* Both must agree, otherwise the commit threshold and the animation
|
|
13
|
+
* geometry won't line up. Single shared constant avoids drift.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
declare const lynx:
|
|
17
|
+
| { SystemInfo?: { pixelWidth?: number; pixelRatio?: number } }
|
|
18
|
+
| undefined;
|
|
19
|
+
|
|
20
|
+
function readScreenWidth(): number {
|
|
21
|
+
try {
|
|
22
|
+
const info = typeof lynx !== 'undefined' ? lynx?.SystemInfo : undefined;
|
|
23
|
+
const pw = info?.pixelWidth;
|
|
24
|
+
const pr = info?.pixelRatio || 1;
|
|
25
|
+
if (typeof pw === 'number' && pw > 0) {
|
|
26
|
+
return Math.round(pw / pr);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// Lynx globals not present (test env / SSR) — use fallback.
|
|
30
|
+
}
|
|
31
|
+
return 400;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const SCREEN_WIDTH = readScreenWidth();
|