@sigmela/router 0.3.7 → 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/CHANGELOG.md +23 -0
- package/LICENSE +2 -1
- package/README.md +108 -479
- package/dist/Router.d.ts +190 -0
- package/dist/Router.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +56 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/OverlayContentScope.d.ts +8 -0
- package/dist/components/OverlayContentScope.d.ts.map +1 -0
- package/dist/components/RouterCloseButton.d.ts +9 -0
- package/dist/components/RouterCloseButton.d.ts.map +1 -0
- package/dist/components/Sheet.d.ts +53 -0
- package/dist/components/Sheet.d.ts.map +1 -0
- package/dist/components/SheetSurface.d.ts +27 -0
- package/dist/components/SheetSurface.d.ts.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/pageScrollLock.d.ts +3 -0
- package/dist/components/pageScrollLock.d.ts.map +1 -0
- package/dist/components/sheetDragEngine.d.ts +27 -0
- package/dist/components/sheetDragEngine.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3649 -0
- package/dist/index.js.map +1 -0
- package/dist/navigationHistory.d.ts +17 -0
- package/dist/navigationHistory.d.ts.map +1 -0
- package/dist/router.css +971 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/package.json +69 -174
- package/lib/module/Drawer/Drawer.js +0 -250
- package/lib/module/Drawer/DrawerContext.js +0 -4
- package/lib/module/Drawer/DrawerIcon.web.js +0 -47
- package/lib/module/Drawer/RenderDrawer.native.js +0 -248
- package/lib/module/Drawer/RenderDrawer.web.js +0 -197
- package/lib/module/Drawer/useDrawer.js +0 -11
- package/lib/module/Navigation.js +0 -80
- package/lib/module/NavigationStack.js +0 -254
- package/lib/module/Router.js +0 -2171
- package/lib/module/RouterContext.js +0 -33
- package/lib/module/ScreenStack/ScreenStack.native.js +0 -3
- package/lib/module/ScreenStack/ScreenStack.web.js +0 -561
- package/lib/module/ScreenStack/ScreenStackContext.js +0 -21
- package/lib/module/ScreenStack/animationHelpers.js +0 -87
- package/lib/module/ScreenStack/index.js +0 -3
- package/lib/module/ScreenStackItem/ScreenStackItem.js +0 -70
- package/lib/module/ScreenStackItem/ScreenStackItem.types.js +0 -3
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +0 -171
- package/lib/module/ScreenStackItem/index.js +0 -3
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +0 -70
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +0 -5
- package/lib/module/ScreenStackSheetItem/index.js +0 -3
- package/lib/module/SplitView/RenderSplitView.native.js +0 -101
- package/lib/module/SplitView/RenderSplitView.web.js +0 -107
- package/lib/module/SplitView/SplitView.js +0 -97
- package/lib/module/SplitView/SplitViewContext.js +0 -4
- package/lib/module/SplitView/index.js +0 -5
- package/lib/module/SplitView/useSplitView.js +0 -11
- package/lib/module/StackRenderer.js +0 -42
- package/lib/module/TabBar/RenderTabBar.native.js +0 -418
- package/lib/module/TabBar/RenderTabBar.web.js +0 -218
- package/lib/module/TabBar/TabBar.js +0 -208
- package/lib/module/TabBar/TabBarContext.js +0 -4
- package/lib/module/TabBar/TabIcon.web.js +0 -47
- package/lib/module/TabBar/index.js +0 -6
- package/lib/module/TabBar/useTabBar.js +0 -11
- package/lib/module/TabBar/useTabBarHeight.js +0 -22
- package/lib/module/createController.js +0 -5
- package/lib/module/index.js +0 -15
- package/lib/module/navigationNode.js +0 -3
- package/lib/module/package.json +0 -1
- package/lib/module/styles.css +0 -1111
- package/lib/module/types.js +0 -13
- package/lib/typescript/package.json +0 -1
- package/lib/typescript/src/Drawer/Drawer.d.ts +0 -100
- package/lib/typescript/src/Drawer/DrawerContext.d.ts +0 -3
- package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +0 -7
- package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +0 -8
- package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +0 -8
- package/lib/typescript/src/Drawer/useDrawer.d.ts +0 -2
- package/lib/typescript/src/Navigation.d.ts +0 -17
- package/lib/typescript/src/NavigationStack.d.ts +0 -46
- package/lib/typescript/src/Router.d.ts +0 -232
- package/lib/typescript/src/RouterContext.d.ts +0 -18
- package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +0 -2
- package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -9
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +0 -31
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +0 -6
- package/lib/typescript/src/ScreenStack/index.d.ts +0 -2
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +0 -3
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +0 -14
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +0 -3
- package/lib/typescript/src/ScreenStackItem/index.d.ts +0 -3
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.native.d.ts +0 -11
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.web.d.ts +0 -2
- package/lib/typescript/src/ScreenStackSheetItem/index.d.ts +0 -2
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +0 -19
- package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +0 -8
- package/lib/typescript/src/SplitView/SplitView.d.ts +0 -33
- package/lib/typescript/src/SplitView/SplitViewContext.d.ts +0 -3
- package/lib/typescript/src/SplitView/index.d.ts +0 -5
- package/lib/typescript/src/SplitView/useSplitView.d.ts +0 -2
- package/lib/typescript/src/StackRenderer.d.ts +0 -8
- package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +0 -8
- package/lib/typescript/src/TabBar/RenderTabBar.web.d.ts +0 -8
- package/lib/typescript/src/TabBar/TabBar.d.ts +0 -112
- package/lib/typescript/src/TabBar/TabBarContext.d.ts +0 -3
- package/lib/typescript/src/TabBar/TabIcon.web.d.ts +0 -7
- package/lib/typescript/src/TabBar/index.d.ts +0 -5
- package/lib/typescript/src/TabBar/useTabBar.d.ts +0 -2
- package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +0 -18
- package/lib/typescript/src/createController.d.ts +0 -15
- package/lib/typescript/src/index.d.ts +0 -21
- package/lib/typescript/src/navigationNode.d.ts +0 -41
- package/lib/typescript/src/types.d.ts +0 -140
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[semantic versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
First standalone release, extracted from the Sigmela merchant web app.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Route primitives: `Router.Screen`, `Stack`, `Redirect`, `Modal`, `Sheet`,
|
|
14
|
+
`TabBar`/`TabBarItem`, `Drawer`/`DrawerGroup`/`DrawerSection`/`DrawerItem`,
|
|
15
|
+
`SplitView` and `Protected`.
|
|
16
|
+
- Hooks: `useRouter`, `useCurrentRoute`, `useParams`, `useSearchParams`,
|
|
17
|
+
`useRoute`, `useHistoryState`, `useCurrentModal`, `useDrawer`.
|
|
18
|
+
- Overlays with their own local push stack, gesture dismissal, and a shared
|
|
19
|
+
close path for button, backdrop, Escape and swipe.
|
|
20
|
+
- Content-sized modals via `<Modal presentation="content">`: the surface grows
|
|
21
|
+
with its content, never drops below half the viewport, and leaves the page
|
|
22
|
+
behind undimmed.
|
|
23
|
+
- Theming through `--router-*` CSS custom properties with built-in fallbacks.
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Sigmela
|
|
4
|
+
|
|
4
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
6
7
|
in the Software without restriction, including without limitation the rights
|
package/README.md
CHANGED
|
@@ -1,537 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sigmela/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A router for React web apps that behave like native apps: screen stacks with
|
|
4
|
+
animated push/pop, modals and bottom sheets with swipe-to-dismiss, tab bars,
|
|
5
|
+
drawers and split views — all declared as JSX routes.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Stacks**: predictable stack-based navigation
|
|
10
|
-
- **Tabs**: `TabBar` with native + web renderers (or custom tab bar)
|
|
11
|
-
- **Drawer**: side-panel navigation (`Drawer`)
|
|
12
|
-
- **Split view**: master/details navigation (`SplitView`)
|
|
13
|
-
- **Modals & sheets**: via `stackPresentation` (`modal`, `sheet`, …)
|
|
14
|
-
- **Controllers**: async/guarded navigation (only present when ready)
|
|
15
|
-
- **Appearance**: global styling via `NavigationAppearance` (tab bar colors, fonts, blur effects, etc.)
|
|
16
|
-
- **Web History integration**: keeps Router state in sync with `pushState`, `replaceState`, `popstate`
|
|
17
|
-
- **Dynamic root**: swap root navigation tree at runtime (`router.setRoot`)
|
|
18
|
-
- **Type-safe hooks**: `useParams`, `useQueryParams`, `useRoute`, `useCurrentRoute`
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
7
|
+
Built on [wouter](https://github.com/molefrog/wouter) for matching and
|
|
8
|
+
[motion](https://motion.dev) for transitions.
|
|
21
9
|
|
|
22
10
|
```bash
|
|
23
|
-
|
|
24
|
-
# optional (required only if you use sheet presentation)
|
|
25
|
-
yarn add @sigmela/native-sheet
|
|
11
|
+
npm install @sigmela/router
|
|
26
12
|
```
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- `react`
|
|
31
|
-
- `react-native`
|
|
32
|
-
- `react-native-screens` (>= `4.24.0`)
|
|
33
|
-
- `@sigmela/native-sheet` (>= `0.0.1`) — only if you use sheets
|
|
34
|
-
|
|
35
|
-
### Web CSS
|
|
36
|
-
|
|
37
|
-
On web you must import the bundled stylesheet **once**:
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import '@sigmela/router/styles.css';
|
|
41
|
-
```
|
|
14
|
+
`react` and `react-dom` (>= 19) are peer dependencies.
|
|
42
15
|
|
|
43
16
|
## Quick start
|
|
44
17
|
|
|
45
|
-
### Simple stack
|
|
46
|
-
|
|
47
|
-
```tsx
|
|
48
|
-
import {
|
|
49
|
-
Navigation,
|
|
50
|
-
NavigationStack,
|
|
51
|
-
Router,
|
|
52
|
-
useParams,
|
|
53
|
-
useQueryParams,
|
|
54
|
-
useRouter,
|
|
55
|
-
} from '@sigmela/router';
|
|
56
|
-
|
|
57
|
-
function HomeScreen() {
|
|
58
|
-
const router = useRouter();
|
|
59
|
-
return (
|
|
60
|
-
<Button
|
|
61
|
-
title="Open details"
|
|
62
|
-
onPress={() => router.navigate('/details/42?from=home')}
|
|
63
|
-
/>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function DetailsScreen() {
|
|
68
|
-
const { id } = useParams<{ id: string }>();
|
|
69
|
-
const { from } = useQueryParams<{ from?: string }>();
|
|
70
|
-
return <Text>Details: id={id}, from={from ?? 'n/a'}</Text>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const rootStack = new NavigationStack()
|
|
74
|
-
.addScreen('/', HomeScreen, { header: { title: 'Home' } })
|
|
75
|
-
.addScreen('/details/:id', DetailsScreen, { header: { title: 'Details' } });
|
|
76
|
-
|
|
77
|
-
const router = new Router({ roots: { app: rootStack }, root: 'app' });
|
|
78
|
-
|
|
79
|
-
export default function App() {
|
|
80
|
-
return <Navigation router={router} />;
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Tabs
|
|
85
|
-
|
|
86
|
-
```tsx
|
|
87
|
-
import { Navigation, NavigationStack, Router, TabBar } from '@sigmela/router';
|
|
88
|
-
|
|
89
|
-
const homeStack = new NavigationStack().addScreen('/', HomeScreen);
|
|
90
|
-
|
|
91
|
-
const catalogStack = new NavigationStack()
|
|
92
|
-
.addScreen('/catalog', CatalogScreen)
|
|
93
|
-
.addScreen('/catalog/products/:productId', ProductScreen);
|
|
94
|
-
|
|
95
|
-
const tabBar = new TabBar({ initialIndex: 0 })
|
|
96
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
97
|
-
.addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });
|
|
98
|
-
|
|
99
|
-
const router = new Router({ roots: { app: tabBar }, root: 'app' });
|
|
100
|
-
|
|
101
|
-
export default function App() {
|
|
102
|
-
return <Navigation router={router} />;
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Tabs wrapped by a root stack (like `example/src/navigation/stacks.ts`)
|
|
107
|
-
|
|
108
|
-
In the example app, tabs are mounted as a **screen** inside a root `NavigationStack`. This lets you keep tab navigation plus define modals/overlays at the root level.
|
|
109
|
-
|
|
110
|
-
```tsx
|
|
111
|
-
import { NavigationStack, TabBar } from '@sigmela/router';
|
|
112
|
-
|
|
113
|
-
const homeStack = new NavigationStack().addScreen('/', HomeScreen);
|
|
114
|
-
const catalogStack = new NavigationStack().addScreen('/catalog', CatalogScreen);
|
|
115
|
-
|
|
116
|
-
const tabBar = new TabBar()
|
|
117
|
-
.addTab({
|
|
118
|
-
key: 'home',
|
|
119
|
-
stack: homeStack,
|
|
120
|
-
title: 'Home',
|
|
121
|
-
icon: require('./assets/home.png'),
|
|
122
|
-
})
|
|
123
|
-
.addTab({
|
|
124
|
-
key: 'catalog',
|
|
125
|
-
stack: catalogStack,
|
|
126
|
-
title: 'Catalog',
|
|
127
|
-
icon: require('./assets/catalog.png'),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Root stack hosts the tab bar AND top-level modals/overlays.
|
|
131
|
-
export const rootStack = new NavigationStack()
|
|
132
|
-
.addScreen('/', tabBar)
|
|
133
|
-
.addModal('/auth', AuthScreen, { header: { title: 'Login', hidden: true } })
|
|
134
|
-
.addModal('*?modal=promo', PromoModal);
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Core concepts
|
|
138
|
-
|
|
139
|
-
### `NavigationStack`
|
|
140
|
-
|
|
141
|
-
A `NavigationStack` defines a set of routes and how to match them.
|
|
142
|
-
|
|
143
|
-
```tsx
|
|
144
|
-
const stack = new NavigationStack({ header: { largeTitle: true } })
|
|
145
|
-
.addScreen('/feed', FeedScreen)
|
|
146
|
-
.addScreen('/feed/:id', FeedItemScreen)
|
|
147
|
-
.addModal('/auth', AuthScreen)
|
|
148
|
-
.addSheet('/settings', SettingsSheet);
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Key methods:
|
|
152
|
-
- `addScreen(pathPattern, componentOrNode, options?)`
|
|
153
|
-
- `addModal(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'modal'`)
|
|
154
|
-
- `addSheet(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'sheet'`)
|
|
155
|
-
- `addStack(prefixOrStack, maybeStack?)` — compose nested stacks under a prefix
|
|
156
|
-
|
|
157
|
-
#### Provider Context
|
|
158
|
-
|
|
159
|
-
You can wrap an entire stack with a React context provider by passing a `provider` option:
|
|
160
|
-
|
|
161
18
|
```tsx
|
|
162
|
-
import {
|
|
163
|
-
|
|
164
|
-
const stack = new NavigationStack({
|
|
165
|
-
header: { largeTitle: true },
|
|
166
|
-
provider: ThemeProvider,
|
|
167
|
-
})
|
|
168
|
-
.addScreen('/feed', FeedScreen)
|
|
169
|
-
.addScreen('/feed/:id', FeedItemScreen);
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
The `provider` component wraps the entire stack renderer, making the context available to all screens in the stack. This is useful for:
|
|
173
|
-
- **Theme providers**: Apply theme context to all screens
|
|
174
|
-
- **Auth providers**: Share authentication state across screens
|
|
175
|
-
- **Localization**: Provide i18n context to the entire stack
|
|
176
|
-
|
|
177
|
-
**Composing multiple providers:**
|
|
178
|
-
|
|
179
|
-
If you need multiple providers, create a composed component:
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
const ComposedProvider = ({ children }) => (
|
|
183
|
-
<ThemeProvider>
|
|
184
|
-
<AuthProvider>
|
|
185
|
-
<I18nProvider>
|
|
186
|
-
{children}
|
|
187
|
-
</I18nProvider>
|
|
188
|
-
</AuthProvider>
|
|
189
|
-
</ThemeProvider>
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const stack = new NavigationStack({ provider: ComposedProvider })
|
|
193
|
-
.addScreen('/', HomeScreen);
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**Important:** The provider should be a stable reference (not an inline arrow function) to avoid unnecessary re-renders.
|
|
197
|
-
|
|
198
|
-
### Modal Stacks (Stack in Stack)
|
|
199
|
-
|
|
200
|
-
You can pass an entire `NavigationStack` to `addModal()` or `addSheet()` to create a multi-screen flow inside a modal:
|
|
201
|
-
|
|
202
|
-
```tsx
|
|
203
|
-
// Define a flow with multiple screens
|
|
204
|
-
const emailVerifyStack = new NavigationStack()
|
|
205
|
-
.addScreen('/verify', EmailInputScreen)
|
|
206
|
-
.addScreen('/verify/sent', EmailSentScreen);
|
|
207
|
-
|
|
208
|
-
// Mount the entire stack as a modal
|
|
209
|
-
const rootStack = new NavigationStack()
|
|
210
|
-
.addScreen('/', HomeScreen)
|
|
211
|
-
.addModal('/verify', emailVerifyStack);
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
**How it works:**
|
|
215
|
-
- Navigating to `/verify` opens the modal with `EmailInputScreen`
|
|
216
|
-
- Inside the modal, `router.navigate('/verify/sent')` pushes `EmailSentScreen` within the same modal
|
|
217
|
-
- `router.goBack()` navigates back inside the modal stack
|
|
218
|
-
- `router.dismiss()` closes the entire modal from any depth
|
|
219
|
-
|
|
220
|
-
**Example screen with navigation inside modal:**
|
|
19
|
+
import { Router, useRouter } from '@sigmela/router';
|
|
221
20
|
|
|
222
|
-
|
|
223
|
-
function EmailInputScreen() {
|
|
224
|
-
const router = useRouter();
|
|
225
|
-
|
|
21
|
+
function App() {
|
|
226
22
|
return (
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
|
|
23
|
+
<Router>
|
|
24
|
+
<Router.Screen path="/" element={HomeScreen} />
|
|
25
|
+
<Router.Stack path="/products">
|
|
26
|
+
<Router.Screen element={ProductListScreen} />
|
|
27
|
+
<Router.Screen path=":id" element={ProductScreen} />
|
|
28
|
+
<Router.Modal path=":id/edit" element={EditProductModal} />
|
|
29
|
+
<Router.Sheet path=":id/share" element={ShareSheet} />
|
|
30
|
+
</Router.Stack>
|
|
31
|
+
</Router>
|
|
231
32
|
);
|
|
232
33
|
}
|
|
233
34
|
|
|
234
|
-
function
|
|
35
|
+
function ProductCard({ id }: { id: string }) {
|
|
235
36
|
const router = useRouter();
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
<View>
|
|
239
|
-
<Button title="Back" onPress={() => router.goBack()} />
|
|
240
|
-
<Button title="Done" onPress={() => router.dismiss()} />
|
|
241
|
-
</View>
|
|
242
|
-
);
|
|
37
|
+
return <button onClick={() => router.navigate(`/products/${id}`)}>Open</button>;
|
|
243
38
|
}
|
|
244
39
|
```
|
|
245
40
|
|
|
246
|
-
|
|
41
|
+
Styles ship with the bundle: `dist/index.js` imports `router.css`, so bundlers
|
|
42
|
+
that handle CSS from dependencies (Vite, webpack, Next, Parcel) pick it up
|
|
43
|
+
automatically — no extra import needed.
|
|
247
44
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
45
|
+
Environments without a CSS loader — plain Node, `vitest` with the `node`
|
|
46
|
+
environment, some SSR pipelines — cannot resolve that import. Alias or stub
|
|
47
|
+
`@sigmela/router/router.css` there, and load the stylesheet explicitly on the
|
|
48
|
+
client:
|
|
251
49
|
|
|
252
50
|
```ts
|
|
253
|
-
|
|
254
|
-
roots: { app: root }, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
|
|
255
|
-
root: 'app',
|
|
256
|
-
screenOptions, // optional defaults
|
|
257
|
-
debug, // optional
|
|
258
|
-
});
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Navigation:
|
|
262
|
-
- `router.navigate(path)` — push
|
|
263
|
-
- `router.replace(path, dedupe?)` — replace top of the active stack
|
|
264
|
-
- `router.goBack()` — pop top of the active stack
|
|
265
|
-
- `router.dismiss()` — close the nearest modal or sheet (including all screens in a modal stack)
|
|
266
|
-
- `router.reset(path)` — **web-only**: rebuild Router state as if app loaded at `path`
|
|
267
|
-
- `router.setRoot(rootKey, { transition? })` — swap root at runtime (`rootKey` from `config.roots`)
|
|
268
|
-
|
|
269
|
-
State/subscriptions:
|
|
270
|
-
- `router.getState()` → `{ history: HistoryItem[] }`
|
|
271
|
-
- `router.getActiveRoute()` → `ActiveRoute | null`
|
|
272
|
-
- `router.subscribe(cb)` — notify on any history change
|
|
273
|
-
- `router.subscribeStack(stackId, cb)` — notify when a particular stack slice changes
|
|
274
|
-
- `router.subscribeRoot(cb)` — notify when root is replaced via `setRoot`
|
|
275
|
-
- `router.getStackHistory(stackId)` — slice of history for a stack
|
|
276
|
-
|
|
277
|
-
### `TabBar`
|
|
278
|
-
|
|
279
|
-
`TabBar` is a container node that renders one tab at a time.
|
|
280
|
-
|
|
281
|
-
```tsx
|
|
282
|
-
const tabBar = new TabBar({ component: CustomTabBar, initialIndex: 0 })
|
|
283
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
284
|
-
.addTab({ key: 'search', screen: SearchScreen, title: 'Search' });
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
Key methods:
|
|
288
|
-
- `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, selectedIcon?, ... })`
|
|
289
|
-
- `onIndexChange(index)` — switch active tab
|
|
290
|
-
- `setBadge(index, badge | null)`
|
|
291
|
-
- `setTabBarConfig(partialConfig)`
|
|
292
|
-
- `getState()` and `subscribe(cb)`
|
|
293
|
-
|
|
294
|
-
Notes:
|
|
295
|
-
- Exactly one of `stack`, `node`, `screen` must be provided.
|
|
296
|
-
- Use `prefix` to mount a tab's routes under a base path (e.g. `/mail`).
|
|
297
|
-
- All `TabsScreenProps` from `react-native-screens` are forwarded to native. This includes lifecycle events (`onWillAppear`, `onDidAppear`, `onWillDisappear`, `onDidDisappear`), accessibility props (`testID`, `accessibilityLabel`, `tabBarItemTestID`, `tabBarItemAccessibilityLabel`), `orientation`, `systemItem`, `freezeContents`, `placeholder`, `scrollEdgeEffects`, badge styling, and more.
|
|
298
|
-
|
|
299
|
-
#### `setTabBarConfig()`
|
|
300
|
-
|
|
301
|
-
Runtime tab bar configuration:
|
|
302
|
-
|
|
303
|
-
```tsx
|
|
304
|
-
tabBar.setTabBarConfig({
|
|
305
|
-
bottomAccessory: (environment) => <MiniPlayer layout={environment} />, // iOS 26+
|
|
306
|
-
experimentalControlNavigationStateInJS: true,
|
|
307
|
-
});
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
Web behavior note:
|
|
311
|
-
- The built-in **web** tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using `router.reset(firstRoutePath)`.
|
|
312
|
-
|
|
313
|
-
### `Drawer`
|
|
314
|
-
|
|
315
|
-
`Drawer` provides side-panel navigation, similar to `TabBar` but with a slide-out panel.
|
|
316
|
-
|
|
317
|
-
```tsx
|
|
318
|
-
import { Drawer, NavigationStack } from '@sigmela/router';
|
|
319
|
-
|
|
320
|
-
const homeStack = new NavigationStack().addScreen('/', HomeScreen);
|
|
321
|
-
const settingsStack = new NavigationStack().addScreen('/settings', SettingsScreen);
|
|
322
|
-
|
|
323
|
-
const drawer = new Drawer({ width: 280 })
|
|
324
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
325
|
-
.addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Key methods:
|
|
329
|
-
- `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, ... })`
|
|
330
|
-
- `open()`, `close()`, `toggle()` — manage drawer state
|
|
331
|
-
- `getIsOpen()` — current open state
|
|
332
|
-
- `subscribeOpenState(listener)` — subscribe to open/close changes
|
|
333
|
-
- `onIndexChange(index)` — switch active tab
|
|
334
|
-
- `setBadge(index, badge | null)`
|
|
335
|
-
|
|
336
|
-
### `SplitView`
|
|
337
|
-
|
|
338
|
-
`SplitView` renders **two stacks**: `primary` and `secondary`.
|
|
339
|
-
|
|
340
|
-
- On **native**, `secondary` overlays `primary` when it has at least one screen in its history.
|
|
341
|
-
- On **web**, the layout becomes side-by-side at a fixed breakpoint (`minWidth`, default `640px`).
|
|
342
|
-
|
|
343
|
-
```tsx
|
|
344
|
-
import { NavigationStack, SplitView, TabBar } from '@sigmela/router';
|
|
345
|
-
|
|
346
|
-
const master = new NavigationStack().addScreen('/', ThreadsScreen);
|
|
347
|
-
const detail = new NavigationStack().addScreen('/:threadId', ThreadScreen);
|
|
348
|
-
|
|
349
|
-
const splitView = new SplitView({
|
|
350
|
-
minWidth: 640,
|
|
351
|
-
primary: master,
|
|
352
|
-
secondary: detail,
|
|
353
|
-
primaryMaxWidth: 390,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Mount SplitView directly as a tab (no wrapper stack needed).
|
|
357
|
-
const tabBar = new TabBar()
|
|
358
|
-
.addTab({ key: 'mail', node: splitView, prefix: '/mail', title: 'Mail' })
|
|
359
|
-
.addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
|
|
51
|
+
import '@sigmela/router/router.css';
|
|
360
52
|
```
|
|
361
53
|
|
|
362
|
-
##
|
|
363
|
-
|
|
364
|
-
Controllers let you delay/guard navigation. A route can be registered as:
|
|
54
|
+
## Route primitives
|
|
365
55
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const user = await fetchUser(params.userId);
|
|
380
|
-
present({ user, tab: query.tab });
|
|
381
|
-
}
|
|
382
|
-
),
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
stack.addScreen('/users/:userId', UserDetails);
|
|
386
|
-
```
|
|
56
|
+
| Element | Purpose |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `Router.Screen` | A routed screen inside the current stack |
|
|
59
|
+
| `Router.Stack` | Groups screens that push and pop with a shared animation |
|
|
60
|
+
| `Router.Redirect` | Declarative redirect |
|
|
61
|
+
| `Router.Modal` | Full-cover or content-sized overlay above the current screen |
|
|
62
|
+
| `Router.Sheet` | Bottom sheet, content-sized or fullscreen |
|
|
63
|
+
| `Router.TabBar` / `Router.TabBarItem` | Tab navigation with per-tab stacks |
|
|
64
|
+
| `Router.Drawer` / `Router.DrawerGroup` / `Router.DrawerSection` / `Router.DrawerItem` | Persistent side navigation |
|
|
65
|
+
| `Router.SplitView` | Primary/secondary panes above a width breakpoint |
|
|
66
|
+
| `Router.Protected` | Guarded subtree with a redirect target |
|
|
387
67
|
|
|
388
|
-
|
|
68
|
+
### Overlays with their own stack
|
|
389
69
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Pass `NavigationAppearance` to `<Navigation>` to customize styling globally:
|
|
70
|
+
`Router.Modal` and `Router.Sheet` can own a local push stack. The children share
|
|
71
|
+
one surface and leave the browser URL on the underlying screen:
|
|
393
72
|
|
|
394
73
|
```tsx
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
iconColor: '#999999',
|
|
401
|
-
iconColorActive: '#007AFF',
|
|
402
|
-
badgeBackgroundColor: '#FF3B30',
|
|
403
|
-
iOSShadowColor: '#00000020',
|
|
404
|
-
title: {
|
|
405
|
-
fontFamily: 'Inter',
|
|
406
|
-
fontSize: 10,
|
|
407
|
-
color: '#999999',
|
|
408
|
-
activeColor: '#007AFF',
|
|
409
|
-
activeFontSize: 12, // Android: active tab title font size
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
// Android-specific
|
|
413
|
-
androidActiveIndicatorEnabled: true,
|
|
414
|
-
androidActiveIndicatorColor: '#007AFF20',
|
|
415
|
-
androidRippleColor: '#007AFF10',
|
|
416
|
-
labelVisibilityMode: 'labeled',
|
|
417
|
-
|
|
418
|
-
// Tab bar behavior
|
|
419
|
-
hidden: false, // hide/show the tab bar
|
|
420
|
-
tintColor: '#007AFF', // iOS: selected tab tint + glow color
|
|
421
|
-
controllerMode: 'automatic', // iOS 18+: 'automatic' | 'tabBar' | 'tabSidebar'
|
|
422
|
-
minimizeBehavior: 'automatic', // iOS 26+: 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'
|
|
423
|
-
nativeContainerBackgroundColor: '#fff', // native container background
|
|
424
|
-
iOSBlurEffect: 'systemDefault', // iOS: tab bar blur effect
|
|
425
|
-
},
|
|
426
|
-
header: { /* ScreenStackHeaderConfigProps */ },
|
|
427
|
-
sheet: {
|
|
428
|
-
cornerRadius: 16,
|
|
429
|
-
backgroundColor: '#ffffff',
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
export default function App() {
|
|
434
|
-
return <Navigation router={router} appearance={appearance} />;
|
|
435
|
-
}
|
|
74
|
+
<Router.Modal path="/auth/email">
|
|
75
|
+
<Router.Screen element={AuthEmailModal} />
|
|
76
|
+
<Router.Screen path="verify" element={AuthEmailVerifyModal} />
|
|
77
|
+
<Router.Screen path="name" element={AuthEmailNameModal} />
|
|
78
|
+
</Router.Modal>
|
|
436
79
|
```
|
|
437
80
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Access the router instance.
|
|
443
|
-
|
|
444
|
-
### `useCurrentRoute()`
|
|
81
|
+
`navigate()` and `replace()` resolve matching children inside the active
|
|
82
|
+
overlay. `goBack()` pops the local stack first, then closes the overlay from its
|
|
83
|
+
root. The close button, backdrop tap, Escape, swipe dismissal and
|
|
84
|
+
`useCurrentModal().close()` all dismiss the whole flow from any depth.
|
|
445
85
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
Returns `ActiveRoute | null` (shape from `src/types.ts`):
|
|
449
|
-
|
|
450
|
-
```ts
|
|
451
|
-
type ActiveRoute = {
|
|
452
|
-
routeId: string;
|
|
453
|
-
stackId?: string;
|
|
454
|
-
tabIndex?: number;
|
|
455
|
-
path?: string;
|
|
456
|
-
params?: Record<string, unknown>;
|
|
457
|
-
query?: Record<string, unknown>;
|
|
458
|
-
} | null;
|
|
459
|
-
```
|
|
86
|
+
### Content-sized modals
|
|
460
87
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
### `useRoute()`
|
|
466
|
-
|
|
467
|
-
Returns route-local context for the current screen:
|
|
468
|
-
|
|
469
|
-
```ts
|
|
470
|
-
type RouteLocalContextValue = {
|
|
471
|
-
presentation: StackPresentationTypes;
|
|
472
|
-
params?: Record<string, unknown>;
|
|
473
|
-
query?: Record<string, unknown>;
|
|
474
|
-
pattern?: string;
|
|
475
|
-
path?: string;
|
|
476
|
-
};
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### `useTabBar()`
|
|
480
|
-
|
|
481
|
-
Returns the nearest `TabBar` from context (only inside tab screens).
|
|
88
|
+
A modal covers the viewport by default. Content that renders `<Modal>` with
|
|
89
|
+
`presentation="content"` sizes the surface to its content instead — floored at
|
|
90
|
+
half the viewport, capped at full height, with the page behind left undimmed:
|
|
482
91
|
|
|
483
92
|
```tsx
|
|
484
|
-
import {
|
|
485
|
-
|
|
486
|
-
function ScreenInsideTabs() {
|
|
487
|
-
const tabBar = useTabBar();
|
|
93
|
+
import { Modal } from '@sigmela/router';
|
|
488
94
|
|
|
95
|
+
function ProductDetail({ product }) {
|
|
489
96
|
return (
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
/>
|
|
97
|
+
<Modal presentation={product.image ? 'fullscreen' : 'content'}>
|
|
98
|
+
{/* … */}
|
|
99
|
+
</Modal>
|
|
494
100
|
);
|
|
495
101
|
}
|
|
496
102
|
```
|
|
497
103
|
|
|
498
|
-
|
|
104
|
+
## Hooks
|
|
499
105
|
|
|
500
|
-
|
|
106
|
+
| Hook | Returns |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `useRouter()` | `navigate`, `replace`, `goBack`, and overlay controls |
|
|
109
|
+
| `useCurrentRoute()` | The active route: `pathname`, `search`, `searchParams` |
|
|
110
|
+
| `useParams()` | Path parameters of the current route |
|
|
111
|
+
| `useSearchParams()` | Query string as `URLSearchParams` |
|
|
112
|
+
| `useRoute(pattern)` | Match test for an arbitrary pattern |
|
|
113
|
+
| `useHistoryState()` | State passed through `navigate(path, { state })` |
|
|
114
|
+
| `useCurrentModal()` | `close()` / `id` for the overlay a component renders in |
|
|
115
|
+
| `useDrawer()` | Drawer open state and controls |
|
|
501
116
|
|
|
502
|
-
|
|
117
|
+
## Theming
|
|
503
118
|
|
|
504
|
-
|
|
119
|
+
The stylesheet reads CSS custom properties, so an app can restyle every surface
|
|
120
|
+
without overriding selectors. Set them on `:root` or on an app shell element:
|
|
505
121
|
|
|
506
|
-
|
|
507
|
-
|
|
122
|
+
**Base** — `--router-bg`, `--router-text`, `--router-line`, `--router-scrim`,
|
|
123
|
+
`--router-container`, `--router-card-shadow`, `--router-glass-bg`,
|
|
124
|
+
`--router-transparent`, `--router-divider`
|
|
508
125
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return <Button title="Open menu" onPress={() => drawer.open()} />;
|
|
512
|
-
}
|
|
513
|
-
```
|
|
126
|
+
**Overlay/footer** — `--router-footer-fade-bg`,
|
|
127
|
+
`--router-footer-fade-transparent`
|
|
514
128
|
|
|
515
|
-
|
|
129
|
+
**Drawer** — `--router-drawer-bg`, `--router-drawer-text`,
|
|
130
|
+
`--router-drawer-hover-bg`, `--router-drawer-active-bg`,
|
|
131
|
+
`--router-drawer-badge-bg`, `--router-drawer-badge-text`,
|
|
132
|
+
`--router-drawer-focus-ring`, `--router-drawer-overlay-color`,
|
|
133
|
+
`--router-drawer-overlay-bg`, `--router-drawer-overlay-open-bg`
|
|
516
134
|
|
|
517
|
-
|
|
135
|
+
**Close button** — `--router-close-button-bg`, `--router-close-button-text`,
|
|
136
|
+
`--router-close-button-light-text`, `--router-close-button-glass-bg`,
|
|
137
|
+
`--router-close-button-shadow`, `--router-close-button-shine`,
|
|
138
|
+
`--router-mask-fill`
|
|
518
139
|
|
|
519
|
-
|
|
140
|
+
**Typography/layout** — `--router-title-font-family`,
|
|
141
|
+
`--router-title-font-size`, `--router-title-line-height`,
|
|
142
|
+
`--router-title-font-weight`, `--router-safe-area-bottom`
|
|
520
143
|
|
|
521
|
-
|
|
144
|
+
Every token falls back to a sensible default, so the router renders correctly
|
|
145
|
+
with no theming at all.
|
|
522
146
|
|
|
523
|
-
|
|
147
|
+
## Scope
|
|
524
148
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
149
|
+
The router owns navigation, overlay surfaces and their gestures. It never
|
|
150
|
+
imports application features, i18n, data layers or design-system components —
|
|
151
|
+
screens and layouts are supplied by the host app as JSX.
|
|
528
152
|
|
|
529
|
-
|
|
530
|
-
- `router.goBack()` **does not call** `history.back()`. It pops Router state and updates the URL via `replaceState` (so it doesn’t grow/rewind the browser stack).
|
|
153
|
+
## Development
|
|
531
154
|
|
|
532
|
-
|
|
155
|
+
```bash
|
|
156
|
+
npm install
|
|
157
|
+
npm run typecheck
|
|
158
|
+
npm test # 58 contract tests
|
|
159
|
+
npm run lint
|
|
160
|
+
npm run build # dist/index.js + dist/router.css + type declarations
|
|
161
|
+
```
|
|
533
162
|
|
|
534
|
-
|
|
163
|
+
`ROUTER_CONTRACT.md` documents the invariants the contract tests protect.
|
|
535
164
|
|
|
536
165
|
## License
|
|
537
166
|
|