@sigmela/router 0.1.2 โ 0.2.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 +177 -833
- package/lib/module/Navigation.js +1 -10
- package/lib/module/NavigationStack.js +168 -19
- package/lib/module/Router.js +1508 -501
- package/lib/module/RouterContext.js +1 -1
- package/lib/module/ScreenStack/ScreenStack.web.js +343 -117
- package/lib/module/ScreenStack/ScreenStackContext.js +15 -0
- package/lib/module/ScreenStack/animationHelpers.js +72 -0
- package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
- package/lib/module/SplitView/RenderSplitView.native.js +85 -0
- package/lib/module/SplitView/RenderSplitView.web.js +79 -0
- package/lib/module/SplitView/SplitView.js +89 -0
- package/lib/module/SplitView/SplitViewContext.js +4 -0
- package/lib/module/SplitView/index.js +5 -0
- package/lib/module/SplitView/useSplitView.js +11 -0
- package/lib/module/StackRenderer.js +4 -2
- package/lib/module/TabBar/RenderTabBar.native.js +118 -33
- package/lib/module/TabBar/RenderTabBar.web.js +52 -47
- package/lib/module/TabBar/TabBar.js +117 -3
- package/lib/module/TabBar/index.js +4 -1
- package/lib/module/TabBar/useTabBarHeight.js +22 -0
- package/lib/module/index.js +3 -4
- package/lib/module/navigationNode.js +3 -0
- package/lib/module/styles.css +693 -28
- package/lib/typescript/src/NavigationStack.d.ts +25 -13
- package/lib/typescript/src/Router.d.ts +147 -34
- package/lib/typescript/src/RouterContext.d.ts +1 -1
- package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +22 -0
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
- package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
- package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
- package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
- package/lib/typescript/src/SplitView/index.d.ts +5 -0
- package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
- package/lib/typescript/src/StackRenderer.d.ts +2 -1
- package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
- package/lib/typescript/src/TabBar/index.d.ts +3 -0
- package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
- package/lib/typescript/src/createController.d.ts +1 -0
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/navigationNode.d.ts +41 -0
- package/lib/typescript/src/types.d.ts +21 -32
- package/package.json +6 -5
- package/lib/module/web/TransitionStack.js +0 -227
- package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
package/README.md
CHANGED
|
@@ -1,57 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sigmela Router (`@sigmela/router`)
|
|
2
2
|
|
|
3
|
-
Modern, lightweight
|
|
3
|
+
Modern, lightweight navigation for **React Native** and **Web**, built on top of [`react-native-screens`](https://github.com/software-mansion/react-native-screens).
|
|
4
|
+
|
|
5
|
+
This library is **URL-first**: you navigate by **paths** (`/users/42?tab=posts`), and the router derives `params` and `query` for screens.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- ๐ **Deep linking**: automatic stack construction from nested URLs
|
|
18
|
-
- ๐ **Dynamic root**: switch between stacks at runtime (auth flows)
|
|
19
|
-
- ๐จ **Appearance control**: customize tabs, headers and screens
|
|
20
|
-
|
|
21
|
-
## ๐ฆ Installation
|
|
9
|
+
- **Stacks**: predictable stack-based navigation
|
|
10
|
+
- **Tabs**: `TabBar` with native + web renderers (or custom tab bar)
|
|
11
|
+
- **Split view**: master/details navigation (`SplitView`)
|
|
12
|
+
- **Modals & sheets**: via `stackPresentation` (`modal`, `sheet`, โฆ)
|
|
13
|
+
- **Controllers**: async/guarded navigation (only present when ready)
|
|
14
|
+
- **Web History integration**: keeps Router state in sync with `pushState`, `replaceState`, `popstate`
|
|
15
|
+
- **Dynamic root**: swap root navigation tree at runtime (`router.setRoot`)
|
|
16
|
+
- **Type-safe hooks**: `useParams`, `useQueryParams`, `useRoute`, `useCurrentRoute`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
22
19
|
|
|
23
20
|
```bash
|
|
24
|
-
yarn add @sigmela/router
|
|
25
|
-
#
|
|
26
|
-
|
|
21
|
+
yarn add @sigmela/router react-native-screens
|
|
22
|
+
# optional (required only if you use sheet presentation)
|
|
23
|
+
yarn add @sigmela/native-sheet
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
###
|
|
26
|
+
### Peer dependencies
|
|
30
27
|
|
|
31
|
-
- `react
|
|
32
|
-
-
|
|
33
|
-
- `react
|
|
28
|
+
- `react`
|
|
29
|
+
- `react-native`
|
|
30
|
+
- `react-native-screens` (>= `4.18.0`)
|
|
31
|
+
- `@sigmela/native-sheet` (>= `0.0.1`) โ only if you use sheets
|
|
34
32
|
|
|
35
|
-
###
|
|
33
|
+
### Web CSS
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
On web you must import the bundled stylesheet **once**:
|
|
38
36
|
|
|
39
37
|
```ts
|
|
40
38
|
import '@sigmela/router/styles.css';
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
##
|
|
41
|
+
## Quick start
|
|
44
42
|
|
|
45
|
-
###
|
|
43
|
+
### Simple stack
|
|
46
44
|
|
|
47
45
|
```tsx
|
|
48
46
|
import {
|
|
47
|
+
Navigation,
|
|
49
48
|
NavigationStack,
|
|
50
49
|
Router,
|
|
51
|
-
Navigation,
|
|
52
|
-
useRouter,
|
|
53
50
|
useParams,
|
|
54
51
|
useQueryParams,
|
|
52
|
+
useRouter,
|
|
55
53
|
} from '@sigmela/router';
|
|
56
54
|
|
|
57
55
|
function HomeScreen() {
|
|
@@ -81,910 +79,256 @@ export default function App() {
|
|
|
81
79
|
}
|
|
82
80
|
```
|
|
83
81
|
|
|
84
|
-
###
|
|
82
|
+
### Tabs
|
|
85
83
|
|
|
86
84
|
```tsx
|
|
87
|
-
import { NavigationStack, Router,
|
|
85
|
+
import { Navigation, NavigationStack, Router, TabBar } from '@sigmela/router';
|
|
88
86
|
|
|
89
|
-
const homeStack = new NavigationStack().addScreen('/', HomeScreen
|
|
90
|
-
header: { title: 'Home' },
|
|
91
|
-
});
|
|
87
|
+
const homeStack = new NavigationStack().addScreen('/', HomeScreen);
|
|
92
88
|
|
|
93
89
|
const catalogStack = new NavigationStack()
|
|
94
|
-
.addScreen('/catalog', CatalogScreen
|
|
95
|
-
.addScreen('/catalog/products/:productId', ProductScreen
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
.addScreen('/catalog', CatalogScreen)
|
|
91
|
+
.addScreen('/catalog/products/:productId', ProductScreen);
|
|
92
|
+
|
|
93
|
+
const tabBar = new TabBar({ initialIndex: 0 })
|
|
94
|
+
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
95
|
+
.addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });
|
|
96
|
+
|
|
97
|
+
const router = new Router({ root: tabBar });
|
|
98
|
+
|
|
99
|
+
export default function App() {
|
|
100
|
+
return <Navigation router={router} />;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Tabs wrapped by a root stack (like `example/src/navigation/stacks.ts`)
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { NavigationStack, TabBar } from '@sigmela/router';
|
|
110
|
+
|
|
111
|
+
const homeStack = new NavigationStack().addScreen('/', HomeScreen);
|
|
112
|
+
const catalogStack = new NavigationStack().addScreen('/catalog', CatalogScreen);
|
|
101
113
|
|
|
102
114
|
const tabBar = new TabBar()
|
|
103
115
|
.addTab({
|
|
104
116
|
key: 'home',
|
|
105
117
|
stack: homeStack,
|
|
106
118
|
title: 'Home',
|
|
107
|
-
icon:
|
|
119
|
+
icon: require('./assets/home.png'),
|
|
108
120
|
})
|
|
109
121
|
.addTab({
|
|
110
122
|
key: 'catalog',
|
|
111
123
|
stack: catalogStack,
|
|
112
124
|
title: 'Catalog',
|
|
113
|
-
icon:
|
|
125
|
+
icon: require('./assets/catalog.png'),
|
|
114
126
|
});
|
|
115
127
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
// Root stack hosts the tab bar AND top-level modals/overlays.
|
|
129
|
+
export const rootStack = new NavigationStack()
|
|
130
|
+
.addScreen('/', tabBar)
|
|
131
|
+
.addModal('/auth', AuthScreen, { header: { title: 'Login', hidden: true } })
|
|
132
|
+
.addModal('*?modal=promo', PromoModal);
|
|
121
133
|
```
|
|
122
134
|
|
|
123
|
-
##
|
|
124
|
-
|
|
125
|
-
### ๐ Navigation Stack
|
|
135
|
+
## Core concepts
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
### `NavigationStack`
|
|
128
138
|
|
|
129
|
-
|
|
130
|
-
const stack = new NavigationStack()
|
|
131
|
-
.addScreen('/path', Component, options)
|
|
132
|
-
.addModal('/modal', ModalComponent, options)
|
|
133
|
-
.addSheet('/sheet', SheetComponent, options);
|
|
134
|
-
```
|
|
139
|
+
A `NavigationStack` defines a set of routes and how to match them.
|
|
135
140
|
|
|
136
|
-
**Constructor overloads:**
|
|
137
141
|
```tsx
|
|
138
|
-
new NavigationStack()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
const stack = new NavigationStack({ header: { largeTitle: true } })
|
|
143
|
+
.addScreen('/feed', FeedScreen)
|
|
144
|
+
.addScreen('/feed/:id', FeedItemScreen)
|
|
145
|
+
.addModal('/auth', AuthScreen)
|
|
146
|
+
.addSheet('/settings', SettingsSheet);
|
|
142
147
|
```
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
- `addScreen(
|
|
146
|
-
- `addModal(
|
|
147
|
-
- `addSheet(
|
|
148
|
-
- `
|
|
149
|
-
- `getRoutes()` โ get all routes in the stack
|
|
150
|
-
- `getFirstRoute()` โ get the first route (used for initial seeding)
|
|
151
|
-
- `getDefaultOptions()` โ get default options for all screens in this stack
|
|
149
|
+
Key methods:
|
|
150
|
+
- `addScreen(pathPattern, componentOrNode, options?)`
|
|
151
|
+
- `addModal(pathPattern, component, options?)` (shorthand for `stackPresentation: 'modal'`)
|
|
152
|
+
- `addSheet(pathPattern, component, options?)` (shorthand for `stackPresentation: 'sheet'`)
|
|
153
|
+
- `addStack(prefixOrStack, maybeStack?)` โ compose nested stacks under a prefix
|
|
152
154
|
|
|
153
|
-
###
|
|
155
|
+
### `Router`
|
|
154
156
|
|
|
155
|
-
The `Router`
|
|
157
|
+
The `Router` holds navigation state and performs path matching.
|
|
156
158
|
|
|
157
|
-
```
|
|
159
|
+
```ts
|
|
158
160
|
const router = new Router({
|
|
159
|
-
root
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
root, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
|
|
162
|
+
screenOptions, // optional defaults
|
|
163
|
+
debug, // optional
|
|
162
164
|
});
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
- `navigate(path
|
|
167
|
-
- `replace(path
|
|
168
|
-
- `goBack()` โ pop the
|
|
169
|
-
- `
|
|
170
|
-
- `setRoot(nextRoot,
|
|
167
|
+
Navigation:
|
|
168
|
+
- `router.navigate(path)` โ push
|
|
169
|
+
- `router.replace(path, dedupe?)` โ replace top of the active stack
|
|
170
|
+
- `router.goBack()` โ pop top of the active stack
|
|
171
|
+
- `router.reset(path)` โ **web-only**: rebuild Router state as if app loaded at `path`
|
|
172
|
+
- `router.setRoot(nextRoot, { transition? })` โ swap root at runtime
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
- `
|
|
174
|
-
- `
|
|
175
|
-
- `subscribe(
|
|
176
|
-
- `subscribeStack(stackId,
|
|
177
|
-
- `subscribeRoot(
|
|
178
|
-
- `
|
|
174
|
+
State/subscriptions:
|
|
175
|
+
- `router.getState()` โ `{ history: HistoryItem[] }`
|
|
176
|
+
- `router.getActiveRoute()` โ `ActiveRoute | null`
|
|
177
|
+
- `router.subscribe(cb)` โ notify on any history change
|
|
178
|
+
- `router.subscribeStack(stackId, cb)` โ notify when a particular stack slice changes
|
|
179
|
+
- `router.subscribeRoot(cb)` โ notify when root is replaced via `setRoot`
|
|
180
|
+
- `router.getStackHistory(stackId)` โ slice of history for a stack
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
- `getStackHistory(stackId)` โ get history for a specific stack
|
|
182
|
-
- `getRootStackId()` โ get the root stack ID (if root is a stack)
|
|
183
|
-
- `getGlobalStackId()` โ get the global stack ID
|
|
184
|
-
- `hasTabBar()` โ check if root is a TabBar
|
|
182
|
+
> Note: `router.getGlobalStackId()` exists but currently returns `undefined`.
|
|
185
183
|
|
|
186
|
-
###
|
|
184
|
+
### `TabBar`
|
|
187
185
|
|
|
188
|
-
|
|
186
|
+
`TabBar` is a container node that renders one tab at a time.
|
|
189
187
|
|
|
190
188
|
```tsx
|
|
191
|
-
const tabBar = new TabBar({ component
|
|
192
|
-
.addTab({
|
|
193
|
-
|
|
194
|
-
stack?: NavigationStack,
|
|
195
|
-
screen?: Component,
|
|
196
|
-
title?: string,
|
|
197
|
-
icon?: ImageSource | { sfSymbolName | imageSource | templateSource },
|
|
198
|
-
selectedIcon?: ImageSource | { sfSymbolName | imageSource | templateSource },
|
|
199
|
-
});
|
|
189
|
+
const tabBar = new TabBar({ component: CustomTabBar, initialIndex: 0 })
|
|
190
|
+
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
191
|
+
.addTab({ key: 'search', screen: SearchScreen, title: 'Search' });
|
|
200
192
|
```
|
|
201
193
|
|
|
202
|
-
|
|
203
|
-
- `addTab(
|
|
204
|
-
- `
|
|
205
|
-
- `
|
|
206
|
-
- `
|
|
207
|
-
- `
|
|
194
|
+
Key methods:
|
|
195
|
+
- `addTab({ key, stack?, screen?, title?, icon?, selectedIcon?, ... })`
|
|
196
|
+
- `onIndexChange(index)` โ switch active tab
|
|
197
|
+
- `setBadge(index, badge | null)`
|
|
198
|
+
- `setTabBarConfig(partialConfig)`
|
|
199
|
+
- `getState()` and `subscribe(cb)`
|
|
208
200
|
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
Web behavior note:
|
|
202
|
+
- The built-in **web** tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using `router.reset(firstRoutePath)`.
|
|
211
203
|
|
|
212
|
-
###
|
|
204
|
+
### `SplitView`
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
`SplitView` renders **two stacks**: `primary` and `secondary`.
|
|
215
207
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Header configuration (react-native-screens)
|
|
219
|
-
header?: {
|
|
220
|
-
title?: string;
|
|
221
|
-
hidden?: boolean;
|
|
222
|
-
backTitle?: string;
|
|
223
|
-
largeTitle?: boolean;
|
|
224
|
-
// ... all ScreenStackHeaderConfigProps
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// Presentation style
|
|
228
|
-
stackPresentation?:
|
|
229
|
-
| 'push'
|
|
230
|
-
| 'modal'
|
|
231
|
-
| 'transparentModal'
|
|
232
|
-
| 'containedModal'
|
|
233
|
-
| 'containedTransparentModal'
|
|
234
|
-
| 'fullScreenModal'
|
|
235
|
-
| 'formSheet'
|
|
236
|
-
| 'pageSheet'
|
|
237
|
-
| 'sheet';
|
|
238
|
-
|
|
239
|
-
// Animation style
|
|
240
|
-
stackAnimation?:
|
|
241
|
-
| 'default'
|
|
242
|
-
| 'fade'
|
|
243
|
-
| 'flip'
|
|
244
|
-
| 'none'
|
|
245
|
-
| 'simple_push'
|
|
246
|
-
| 'slide_from_right'
|
|
247
|
-
| 'slide_from_left'
|
|
248
|
-
| 'slide_from_bottom'
|
|
249
|
-
| 'fade_from_bottom';
|
|
250
|
-
|
|
251
|
-
// Android: convert modals to sheets
|
|
252
|
-
convertModalToSheetForAndroid?: boolean;
|
|
253
|
-
|
|
254
|
-
// Web helper for default tab icon rendering
|
|
255
|
-
tabBarIcon?: string | { sfSymbolName?: string };
|
|
256
|
-
|
|
257
|
-
// All other react-native-screens Screen props
|
|
258
|
-
// ...
|
|
259
|
-
};
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### ๐ฑ Sheets
|
|
263
|
-
|
|
264
|
-
Sheets are a special presentation style that shows content in a bottom sheet on both iOS and Android.
|
|
208
|
+
- On **native**, `secondary` overlays `primary` when it has at least one screen in its history.
|
|
209
|
+
- On **web**, the layout becomes side-by-side at a fixed breakpoint (currently `>= 640px` in CSS).
|
|
265
210
|
|
|
266
211
|
```tsx
|
|
267
|
-
|
|
268
|
-
.addSheet('/settings', SettingsSheet, {
|
|
269
|
-
header: { title: 'Settings' },
|
|
270
|
-
});
|
|
212
|
+
import { NavigationStack, SplitView } from '@sigmela/router';
|
|
271
213
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
.addScreen('/settings', SettingsSheet, {
|
|
275
|
-
stackPresentation: 'sheet',
|
|
276
|
-
header: { title: 'Settings' },
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Convert modals to sheets on Android:
|
|
280
|
-
const stack = new NavigationStack()
|
|
281
|
-
.addModal('/settings', SettingsSheet, {
|
|
282
|
-
convertModalToSheetForAndroid: true,
|
|
283
|
-
header: { title: 'Settings' },
|
|
284
|
-
});
|
|
285
|
-
```
|
|
214
|
+
const master = new NavigationStack().addScreen('/', ThreadsScreen);
|
|
215
|
+
const detail = new NavigationStack().addScreen('/:threadId', ThreadScreen);
|
|
286
216
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
androidFullScreenTopInset: 40,
|
|
294
|
-
},
|
|
295
|
-
};
|
|
217
|
+
const splitView = new SplitView({
|
|
218
|
+
minWidth: 640, // currently not used by the web renderer; kept for API compatibility
|
|
219
|
+
primary: master,
|
|
220
|
+
secondary: detail,
|
|
221
|
+
primaryMaxWidth: 390,
|
|
222
|
+
});
|
|
296
223
|
|
|
297
|
-
|
|
224
|
+
const root = new NavigationStack().addScreen('/mail', splitView);
|
|
298
225
|
```
|
|
299
226
|
|
|
300
|
-
|
|
227
|
+
## Controllers
|
|
301
228
|
|
|
302
|
-
Controllers
|
|
303
|
-
- Authentication guards
|
|
304
|
-
- Data prefetching
|
|
305
|
-
- Conditional redirects
|
|
306
|
-
- Loading states
|
|
229
|
+
Controllers let you delay/guard navigation. A route can be registered as:
|
|
307
230
|
|
|
308
231
|
```tsx
|
|
309
232
|
import { createController } from '@sigmela/router';
|
|
310
233
|
|
|
311
234
|
const UserDetails = {
|
|
312
235
|
component: UserDetailsScreen,
|
|
313
|
-
controller: createController<
|
|
314
|
-
{ userId: string }, // Path params type
|
|
315
|
-
{ tab?: string } // Query params type
|
|
316
|
-
>(async ({ params, query }, present) => {
|
|
317
|
-
// Check auth
|
|
318
|
-
const isAuthed = await checkAuth();
|
|
319
|
-
if (!isAuthed) {
|
|
320
|
-
router.navigate('/login');
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Prefetch data
|
|
325
|
-
const userData = await fetchUser(params.userId);
|
|
326
|
-
|
|
327
|
-
// Present screen with prefetched data
|
|
328
|
-
present({ userData });
|
|
329
|
-
}),
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// Register with stack
|
|
333
|
-
stack.addScreen('/users/:userId', UserDetails, {
|
|
334
|
-
header: { title: 'User' }
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// In the component, receive passProps
|
|
338
|
-
function UserDetailsScreen({ userData }) {
|
|
339
|
-
// userData is already loaded
|
|
340
|
-
return <Text>{userData.name}</Text>;
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
**Controller signature:**
|
|
345
|
-
```tsx
|
|
346
|
-
type Controller<TParams, TQuery> = (
|
|
347
|
-
input: { params: TParams; query: TQuery },
|
|
348
|
-
present: (passProps?: any) => void
|
|
349
|
-
) => void;
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Controllers receive `params` and `query` parsed from the URL. Call `present(passProps)` to show the screen, passing any data as props. If you don't call `present()`, the screen won't be shown (useful for redirects).
|
|
353
|
-
|
|
354
|
-
## ๐ช Hooks
|
|
355
|
-
|
|
356
|
-
### useRouter
|
|
357
|
-
|
|
358
|
-
Access the router instance:
|
|
359
|
-
|
|
360
|
-
```tsx
|
|
361
|
-
function MyScreen() {
|
|
362
|
-
const router = useRouter();
|
|
363
|
-
|
|
364
|
-
return (
|
|
365
|
-
<Button
|
|
366
|
-
onPress={() => router.navigate('/details')}
|
|
367
|
-
title="Go to details"
|
|
368
|
-
/>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### useParams
|
|
374
|
-
|
|
375
|
-
Get typed path parameters:
|
|
376
|
-
|
|
377
|
-
```tsx
|
|
378
|
-
function UserScreen() {
|
|
379
|
-
const { userId } = useParams<{ userId: string }>();
|
|
380
|
-
return <Text>User ID: {userId}</Text>;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Route: /users/:userId
|
|
384
|
-
// URL: /users/123
|
|
385
|
-
// Result: { userId: '123' }
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### useQueryParams
|
|
389
|
-
|
|
390
|
-
Get typed query parameters:
|
|
391
|
-
|
|
392
|
-
```tsx
|
|
393
|
-
function SearchScreen() {
|
|
394
|
-
const { q, sort } = useQueryParams<{
|
|
395
|
-
q?: string;
|
|
396
|
-
sort?: 'asc' | 'desc'
|
|
397
|
-
}>();
|
|
398
|
-
|
|
399
|
-
return <Text>Query: {q}, Sort: {sort}</Text>;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// URL: /search?q=hello&sort=asc
|
|
403
|
-
// Result: { q: 'hello', sort: 'asc' }
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### useCurrentRoute
|
|
407
|
-
|
|
408
|
-
Subscribe to the currently visible route:
|
|
409
|
-
|
|
410
|
-
```tsx
|
|
411
|
-
function NavigationObserver() {
|
|
412
|
-
const currentRoute = useCurrentRoute();
|
|
413
|
-
|
|
414
|
-
useEffect(() => {
|
|
415
|
-
console.log('Current route:', currentRoute?.path);
|
|
416
|
-
}, [currentRoute]);
|
|
417
|
-
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Returns: { scope, path, params, query, routeId, stackId, tabIndex? } | null
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
### useRoute
|
|
425
|
-
|
|
426
|
-
Access the full route context for the current screen:
|
|
427
|
-
|
|
428
|
-
```tsx
|
|
429
|
-
function MyScreen() {
|
|
430
|
-
const route = useRoute();
|
|
431
|
-
|
|
432
|
-
return (
|
|
433
|
-
<View>
|
|
434
|
-
<Text>Path: {route.path}</Text>
|
|
435
|
-
<Text>Pattern: {route.pattern}</Text>
|
|
436
|
-
<Text>Presentation: {route.presentation}</Text>
|
|
437
|
-
</View>
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Returns: { presentation, params, query, pattern, path }
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### useTabBar
|
|
445
|
-
|
|
446
|
-
Access the TabBar instance (only works inside a tab screen):
|
|
447
|
-
|
|
448
|
-
```tsx
|
|
449
|
-
function HomeScreen() {
|
|
450
|
-
const tabBar = useTabBar();
|
|
451
|
-
const router = useRouter();
|
|
452
|
-
|
|
453
|
-
const handleSwitchTab = () => {
|
|
454
|
-
router.onTabIndexChange(1); // Switch to second tab
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const handleSetBadge = () => {
|
|
458
|
-
tabBar.setBadge(1, '5'); // Show badge on second tab
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
return (
|
|
462
|
-
<View>
|
|
463
|
-
<Button title="Switch Tab" onPress={handleSwitchTab} />
|
|
464
|
-
<Button title="Set Badge" onPress={handleSetBadge} />
|
|
465
|
-
</View>
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## ๐ Advanced Features
|
|
471
|
-
|
|
472
|
-
### ๐ Dynamic Root (Auth Flows)
|
|
473
|
-
|
|
474
|
-
Switch between different root structures at runtime:
|
|
475
|
-
|
|
476
|
-
```tsx
|
|
477
|
-
const authStack = new NavigationStack()
|
|
478
|
-
.addScreen('/login', LoginScreen, { header: { title: 'Login' } });
|
|
479
|
-
|
|
480
|
-
const mainTabBar = new TabBar()
|
|
481
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' });
|
|
482
|
-
|
|
483
|
-
const router = new Router({ root: authStack });
|
|
484
|
-
|
|
485
|
-
// Later, after login:
|
|
486
|
-
function handleLogin() {
|
|
487
|
-
router.setRoot(mainTabBar, { transition: 'fade' });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Or logout:
|
|
491
|
-
function handleLogout() {
|
|
492
|
-
router.setRoot(authStack, { transition: 'fade' });
|
|
493
|
-
}
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
The `transition` option accepts any `stackAnimation` value (`'fade'`, `'slide_from_bottom'`, etc.).
|
|
497
|
-
|
|
498
|
-
### ๐จ Custom Tab Bar
|
|
499
|
-
|
|
500
|
-
Create a custom tab bar component:
|
|
501
|
-
|
|
502
|
-
```tsx
|
|
503
|
-
import { TabBar, type TabBarProps } from '@sigmela/router';
|
|
504
|
-
|
|
505
|
-
function CustomTabBar({ tabs, activeIndex, onTabPress }: TabBarProps) {
|
|
506
|
-
return (
|
|
507
|
-
<View style={styles.tabBar}>
|
|
508
|
-
{tabs.map((tab, index) => (
|
|
509
|
-
<TouchableOpacity
|
|
510
|
-
key={tab.tabKey}
|
|
511
|
-
onPress={() => onTabPress(index)}
|
|
512
|
-
style={[
|
|
513
|
-
styles.tab,
|
|
514
|
-
index === activeIndex && styles.activeTab
|
|
515
|
-
]}
|
|
516
|
-
>
|
|
517
|
-
{tab.icon && <Image source={tab.icon} />}
|
|
518
|
-
<Text style={styles.title}>{tab.title}</Text>
|
|
519
|
-
{tab.badgeValue && <Text style={styles.badge}>{tab.badgeValue}</Text>}
|
|
520
|
-
</TouchableOpacity>
|
|
521
|
-
))}
|
|
522
|
-
</View>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const tabBar = new TabBar({ component: CustomTabBar })
|
|
527
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' });
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### ๐จ Appearance Customization
|
|
531
|
-
|
|
532
|
-
Customize the appearance of tabs, headers, and screens:
|
|
533
|
-
|
|
534
|
-
```tsx
|
|
535
|
-
import type { NavigationAppearance } from '@sigmela/router';
|
|
536
|
-
|
|
537
|
-
const appearance: NavigationAppearance = {
|
|
538
|
-
// Tab bar styling
|
|
539
|
-
tabBar: {
|
|
540
|
-
backgroundColor: '#ffffff',
|
|
541
|
-
iconColor: '#8e8e93',
|
|
542
|
-
iconColorActive: '#007aff',
|
|
543
|
-
badgeBackgroundColor: '#ff3b30',
|
|
544
|
-
androidRippleColor: 'rgba(0,0,0,0.1)',
|
|
545
|
-
androidActiveIndicatorEnabled: true,
|
|
546
|
-
androidActiveIndicatorColor: '#007aff',
|
|
547
|
-
iOSShadowColor: '#000',
|
|
548
|
-
labelVisibilityMode: 'labeled',
|
|
549
|
-
title: {
|
|
550
|
-
fontSize: 11,
|
|
551
|
-
fontFamily: 'System',
|
|
552
|
-
fontWeight: '500',
|
|
553
|
-
color: '#8e8e93',
|
|
554
|
-
activeColor: '#007aff',
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
|
|
558
|
-
// Header styling (applied to all headers)
|
|
559
|
-
header: {
|
|
560
|
-
backgroundColor: '#ffffff',
|
|
561
|
-
tintColor: '#007aff',
|
|
562
|
-
largeTitleColor: '#000',
|
|
563
|
-
},
|
|
564
|
-
|
|
565
|
-
// Screen styling
|
|
566
|
-
screen: {
|
|
567
|
-
backgroundColor: '#f2f2f7',
|
|
568
|
-
},
|
|
569
|
-
|
|
570
|
-
// Sheet styling
|
|
571
|
-
sheet: {
|
|
572
|
-
backgroundColor: '#ffffff',
|
|
573
|
-
cornerRadius: 18,
|
|
574
|
-
androidFullScreenTopInset: 40,
|
|
575
|
-
},
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
export default function App() {
|
|
579
|
-
return <Navigation router={router} appearance={appearance} />;
|
|
580
|
-
}
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### ๐ Tab Badges
|
|
584
|
-
|
|
585
|
-
Show badges on tabs:
|
|
586
|
-
|
|
587
|
-
```tsx
|
|
588
|
-
function SomeScreen() {
|
|
589
|
-
const tabBar = useTabBar();
|
|
590
|
-
const router = useRouter();
|
|
591
|
-
|
|
592
|
-
useEffect(() => {
|
|
593
|
-
// Show badge on second tab
|
|
594
|
-
tabBar.setBadge(1, '3');
|
|
595
|
-
|
|
596
|
-
// Clear badge
|
|
597
|
-
// tabBar.setBadge(1, null);
|
|
598
|
-
}, []);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Or directly via router instance
|
|
602
|
-
router.tabBar?.setBadge(1, '3');
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### ๐ฌ Standalone Stack Renderer
|
|
606
|
-
|
|
607
|
-
Render a specific stack anywhere in your component tree:
|
|
608
|
-
|
|
609
|
-
```tsx
|
|
610
|
-
import { StackRenderer } from '@sigmela/router';
|
|
611
|
-
|
|
612
|
-
function CustomLayout() {
|
|
613
|
-
const myStack = new NavigationStack()
|
|
614
|
-
.addScreen('/nested', NestedScreen);
|
|
615
|
-
|
|
616
|
-
return (
|
|
617
|
-
<View>
|
|
618
|
-
<Header />
|
|
619
|
-
<StackRenderer stack={myStack} appearance={appearance} />
|
|
620
|
-
<Footer />
|
|
621
|
-
</View>
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
## ๐ Web Integration
|
|
627
|
-
|
|
628
|
-
### ๐ History API Sync
|
|
629
|
-
|
|
630
|
-
On web, the router automatically synchronizes with the browser's History API:
|
|
631
|
-
|
|
632
|
-
- `router.navigate('/path')` โ `history.pushState()`
|
|
633
|
-
- `router.replace('/path')` โ `history.replaceState()`
|
|
634
|
-
- `router.goBack()` โ `history.back()`
|
|
635
|
-
- Browser back/forward buttons โ `popstate` event โ router state update
|
|
636
|
-
|
|
637
|
-
You can also call `history.pushState()` or `history.replaceState()` directly, and the router will stay in sync.
|
|
638
|
-
|
|
639
|
-
### ๐ Deep Linking
|
|
640
|
-
|
|
641
|
-
On initial load, the router automatically expands deep URLs into a stack chain:
|
|
642
|
-
|
|
643
|
-
```
|
|
644
|
-
URL: /catalog/products/42
|
|
645
|
-
|
|
646
|
-
Result stack:
|
|
647
|
-
1. /catalog (CatalogScreen)
|
|
648
|
-
2. /catalog/products/42 (ProductScreen)
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
This only works for routes in the same stack. The router builds the longest possible chain of matching routes.
|
|
652
|
-
|
|
653
|
-
### โป๏ธ Replace with Deduplication
|
|
654
|
-
|
|
655
|
-
Avoid no-op replaces on web with the `dedupe` option:
|
|
656
|
-
|
|
657
|
-
```tsx
|
|
658
|
-
// Won't replace if already on /catalog with same params
|
|
659
|
-
router.replace('/catalog', true);
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
This is useful when switching tabs on web to avoid creating duplicate history entries.
|
|
663
|
-
|
|
664
|
-
## ๐ก๏ธ TypeScript
|
|
665
|
-
|
|
666
|
-
### โ
Type-Safe Params
|
|
667
|
-
|
|
668
|
-
```tsx
|
|
669
|
-
import { useParams, useQueryParams } from '@sigmela/router';
|
|
670
|
-
|
|
671
|
-
// Define your param types
|
|
672
|
-
type UserParams = {
|
|
673
|
-
userId: string;
|
|
674
|
-
tab?: string;
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
type UserQuery = {
|
|
678
|
-
ref?: string;
|
|
679
|
-
highlight?: string;
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
function UserScreen() {
|
|
683
|
-
const params = useParams<UserParams>();
|
|
684
|
-
const query = useQueryParams<UserQuery>();
|
|
685
|
-
|
|
686
|
-
// TypeScript knows:
|
|
687
|
-
// params.userId is string
|
|
688
|
-
// params.tab is string | undefined
|
|
689
|
-
// query.ref is string | undefined
|
|
690
|
-
}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### โ
Type-Safe Controllers
|
|
694
|
-
|
|
695
|
-
```tsx
|
|
696
|
-
import { createController } from '@sigmela/router';
|
|
697
|
-
|
|
698
|
-
type ProductParams = { productId: string };
|
|
699
|
-
type ProductQuery = { variant?: string };
|
|
700
|
-
|
|
701
|
-
const ProductDetails = {
|
|
702
|
-
component: ProductScreen,
|
|
703
|
-
controller: createController<ProductParams, ProductQuery>(
|
|
236
|
+
controller: createController<{ userId: string }, { tab?: string }>(
|
|
704
237
|
async ({ params, query }, present) => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
238
|
+
const ok = await checkAuth();
|
|
239
|
+
if (!ok) {
|
|
240
|
+
router.replace('/login', true);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const user = await fetchUser(params.userId);
|
|
245
|
+
present({ user, tab: query.tab });
|
|
710
246
|
}
|
|
711
247
|
),
|
|
712
248
|
};
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
## ๐ API Reference
|
|
716
|
-
|
|
717
|
-
### ๐งญ Router
|
|
718
|
-
|
|
719
|
-
```tsx
|
|
720
|
-
class Router {
|
|
721
|
-
constructor(config: {
|
|
722
|
-
root: TabBar | NavigationStack;
|
|
723
|
-
global?: NavigationStack;
|
|
724
|
-
screenOptions?: ScreenOptions;
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
// Navigation
|
|
728
|
-
navigate(path: string): void;
|
|
729
|
-
replace(path: string, dedupe?: boolean): void;
|
|
730
|
-
goBack(): void;
|
|
731
|
-
onTabIndexChange(index: number): void;
|
|
732
|
-
setRoot(nextRoot: TabBar | NavigationStack, options?: { transition?: RootTransition }): void;
|
|
733
|
-
|
|
734
|
-
// State
|
|
735
|
-
getState(): { history: HistoryItem[]; activeTabIndex?: number };
|
|
736
|
-
getVisibleRoute(): VisibleRoute | null;
|
|
737
|
-
getStackHistory(stackId: string): HistoryItem[];
|
|
738
|
-
getRootStackId(): string | undefined;
|
|
739
|
-
getGlobalStackId(): string | undefined;
|
|
740
|
-
getActiveTabIndex(): number;
|
|
741
|
-
hasTabBar(): boolean;
|
|
742
|
-
getRootTransition(): RootTransition | undefined;
|
|
743
|
-
|
|
744
|
-
// Subscriptions
|
|
745
|
-
subscribe(listener: () => void): () => void;
|
|
746
|
-
subscribeStack(stackId: string, listener: () => void): () => void;
|
|
747
|
-
subscribeActiveTab(listener: () => void): () => void;
|
|
748
|
-
subscribeRoot(listener: () => void): () => void;
|
|
749
|
-
|
|
750
|
-
// Sheet management (internal)
|
|
751
|
-
registerSheetDismisser(key: string, dismisser: () => void): void;
|
|
752
|
-
unregisterSheetDismisser(key: string): void;
|
|
753
|
-
}
|
|
754
|
-
```
|
|
755
249
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
```tsx
|
|
759
|
-
class NavigationStack {
|
|
760
|
-
constructor();
|
|
761
|
-
constructor(id: string);
|
|
762
|
-
constructor(defaultOptions: ScreenOptions);
|
|
763
|
-
constructor(id: string, defaultOptions: ScreenOptions);
|
|
764
|
-
|
|
765
|
-
addScreen(
|
|
766
|
-
path: string,
|
|
767
|
-
component: Component | { component: Component; controller?: Controller },
|
|
768
|
-
options?: ScreenOptions
|
|
769
|
-
): NavigationStack;
|
|
770
|
-
|
|
771
|
-
addModal(
|
|
772
|
-
path: string,
|
|
773
|
-
component: Component | { component: Component; controller?: Controller },
|
|
774
|
-
options?: ScreenOptions
|
|
775
|
-
): NavigationStack;
|
|
776
|
-
|
|
777
|
-
addSheet(
|
|
778
|
-
path: string,
|
|
779
|
-
component: Component | { component: Component; controller?: Controller },
|
|
780
|
-
options?: ScreenOptions
|
|
781
|
-
): NavigationStack;
|
|
782
|
-
|
|
783
|
-
getId(): string;
|
|
784
|
-
getRoutes(): BuiltRoute[];
|
|
785
|
-
getFirstRoute(): BuiltRoute | undefined;
|
|
786
|
-
getDefaultOptions(): ScreenOptions | undefined;
|
|
787
|
-
}
|
|
250
|
+
stack.addScreen('/users/:userId', UserDetails);
|
|
788
251
|
```
|
|
789
252
|
|
|
790
|
-
|
|
253
|
+
If you never call `present()`, the screen is not pushed/replaced.
|
|
791
254
|
|
|
792
|
-
|
|
793
|
-
type TabConfig = {
|
|
794
|
-
key: string;
|
|
795
|
-
stack?: NavigationStack;
|
|
796
|
-
screen?: Component;
|
|
797
|
-
title?: string;
|
|
798
|
-
icon?: ImageSource | {
|
|
799
|
-
sfSymbolName?: string;
|
|
800
|
-
imageSource?: ImageSource;
|
|
801
|
-
templateSource?: ImageSource;
|
|
802
|
-
};
|
|
803
|
-
selectedIcon?: ImageSource | {
|
|
804
|
-
sfSymbolName?: string;
|
|
805
|
-
imageSource?: ImageSource;
|
|
806
|
-
templateSource?: ImageSource;
|
|
807
|
-
};
|
|
808
|
-
};
|
|
255
|
+
## Hooks
|
|
809
256
|
|
|
810
|
-
|
|
811
|
-
constructor(config?: { component?: ComponentType<TabBarProps> });
|
|
257
|
+
### `useRouter()`
|
|
812
258
|
|
|
813
|
-
|
|
814
|
-
setBadge(tabIndex: number, badge: string | null): void;
|
|
815
|
-
onIndexChange(index: number): void;
|
|
816
|
-
getState(): { tabs: InternalTabItem[]; index: number; config: TabBarConfig };
|
|
817
|
-
subscribe(listener: () => void): () => void;
|
|
818
|
-
}
|
|
819
|
-
```
|
|
259
|
+
Access the router instance.
|
|
820
260
|
|
|
821
|
-
###
|
|
261
|
+
### `useCurrentRoute()`
|
|
822
262
|
|
|
823
|
-
|
|
824
|
-
type ScreenOptions = {
|
|
825
|
-
header?: ScreenStackHeaderConfigProps;
|
|
826
|
-
stackPresentation?: StackPresentationTypes;
|
|
827
|
-
stackAnimation?: 'default' | 'fade' | 'flip' | 'none' | 'simple_push'
|
|
828
|
-
| 'slide_from_right' | 'slide_from_left' | 'slide_from_bottom' | 'fade_from_bottom';
|
|
829
|
-
convertModalToSheetForAndroid?: boolean;
|
|
830
|
-
tabBarIcon?: string | { sfSymbolName?: string };
|
|
831
|
-
// ... all react-native-screens Screen props
|
|
832
|
-
};
|
|
263
|
+
Subscribes to `router.getActiveRoute()`.
|
|
833
264
|
|
|
834
|
-
|
|
835
|
-
tabBar?: {
|
|
836
|
-
backgroundColor?: ColorValue;
|
|
837
|
-
badgeBackgroundColor?: ColorValue;
|
|
838
|
-
iconColor?: ColorValue;
|
|
839
|
-
iconColorActive?: ColorValue;
|
|
840
|
-
androidActiveIndicatorEnabled?: boolean;
|
|
841
|
-
androidActiveIndicatorColor?: ColorValue;
|
|
842
|
-
androidRippleColor?: ColorValue;
|
|
843
|
-
labelVisibilityMode?: 'labeled' | 'hidden' | 'selected';
|
|
844
|
-
iOSShadowColor?: ColorValue;
|
|
845
|
-
title?: {
|
|
846
|
-
fontFamily?: string;
|
|
847
|
-
fontSize?: number;
|
|
848
|
-
fontWeight?: string;
|
|
849
|
-
fontStyle?: string;
|
|
850
|
-
color?: string;
|
|
851
|
-
activeColor?: string;
|
|
852
|
-
};
|
|
853
|
-
};
|
|
854
|
-
screen?: StyleProp<ViewStyle>;
|
|
855
|
-
header?: ScreenStackHeaderConfigProps;
|
|
856
|
-
sheet?: {
|
|
857
|
-
backgroundColor?: ColorValue;
|
|
858
|
-
cornerRadius?: number;
|
|
859
|
-
androidFullScreenTopInset?: number;
|
|
860
|
-
};
|
|
861
|
-
};
|
|
265
|
+
Returns `ActiveRoute | null` (shape from `src/types.ts`):
|
|
862
266
|
|
|
863
|
-
|
|
267
|
+
```ts
|
|
268
|
+
type ActiveRoute = {
|
|
864
269
|
routeId: string;
|
|
865
270
|
stackId?: string;
|
|
866
271
|
tabIndex?: number;
|
|
867
|
-
scope: 'global' | 'tab' | 'root';
|
|
868
272
|
path?: string;
|
|
869
273
|
params?: Record<string, unknown>;
|
|
870
274
|
query?: Record<string, unknown>;
|
|
871
275
|
} | null;
|
|
872
|
-
|
|
873
|
-
type TabBarProps = {
|
|
874
|
-
tabs: InternalTabItem[];
|
|
875
|
-
activeIndex: number;
|
|
876
|
-
onTabPress: (index: number) => void;
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
type Controller<TParams, TQuery> = (
|
|
880
|
-
input: { params: TParams; query: TQuery },
|
|
881
|
-
present: (passProps?: any) => void
|
|
882
|
-
) => void;
|
|
883
276
|
```
|
|
884
277
|
|
|
885
|
-
|
|
278
|
+
### `useParams<T>()` / `useQueryParams<T>()`
|
|
886
279
|
|
|
887
|
-
|
|
280
|
+
Returns params/query for the **current screen** (from route context).
|
|
888
281
|
|
|
889
|
-
|
|
890
|
-
import {
|
|
891
|
-
NavigationStack,
|
|
892
|
-
Router,
|
|
893
|
-
Navigation,
|
|
894
|
-
TabBar,
|
|
895
|
-
createController,
|
|
896
|
-
useRouter,
|
|
897
|
-
useParams,
|
|
898
|
-
} from '@sigmela/router';
|
|
899
|
-
|
|
900
|
-
// Auth check controller
|
|
901
|
-
const requireAuth = createController(async (input, present) => {
|
|
902
|
-
const isAuthed = await checkAuth();
|
|
903
|
-
if (!isAuthed) {
|
|
904
|
-
router.navigate('/login');
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
present();
|
|
908
|
-
});
|
|
282
|
+
### `useRoute()`
|
|
909
283
|
|
|
910
|
-
|
|
911
|
-
const homeStack = new NavigationStack()
|
|
912
|
-
.addScreen('/', { component: HomeScreen, controller: requireAuth }, {
|
|
913
|
-
header: { title: 'Home' },
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
const profileStack = new NavigationStack()
|
|
917
|
-
.addScreen('/profile', { component: ProfileScreen, controller: requireAuth }, {
|
|
918
|
-
header: { title: 'Profile' },
|
|
919
|
-
})
|
|
920
|
-
.addScreen('/profile/settings', SettingsScreen, {
|
|
921
|
-
header: { title: 'Settings' },
|
|
922
|
-
});
|
|
284
|
+
Returns route-local context for the current screen:
|
|
923
285
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
286
|
+
```ts
|
|
287
|
+
type RouteLocalContextValue = {
|
|
288
|
+
presentation: StackPresentationTypes;
|
|
289
|
+
params?: Record<string, unknown>;
|
|
290
|
+
query?: Record<string, unknown>;
|
|
291
|
+
pattern?: string;
|
|
292
|
+
path?: string;
|
|
293
|
+
};
|
|
294
|
+
```
|
|
928
295
|
|
|
929
|
-
|
|
930
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home', icon: { sfSymbolName: 'house' } })
|
|
931
|
-
.addTab({ key: 'profile', stack: profileStack, title: 'Profile', icon: { sfSymbolName: 'person' } });
|
|
296
|
+
### `useTabBar()`
|
|
932
297
|
|
|
933
|
-
|
|
934
|
-
.addSheet('/settings-modal', SettingsModalScreen, {
|
|
935
|
-
convertModalToSheetForAndroid: true,
|
|
936
|
-
header: { title: 'Quick Settings' },
|
|
937
|
-
});
|
|
298
|
+
Returns the nearest `TabBar` from context (only inside tab screens).
|
|
938
299
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
root: authStack, // Start with auth
|
|
942
|
-
global: globalStack,
|
|
943
|
-
});
|
|
300
|
+
```tsx
|
|
301
|
+
import { useTabBar } from '@sigmela/router';
|
|
944
302
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const router = useRouter();
|
|
948
|
-
|
|
949
|
-
const handleLogin = async () => {
|
|
950
|
-
await login();
|
|
951
|
-
router.setRoot(mainTabs, { transition: 'fade' });
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
return <Button title="Login" onPress={handleLogin} />;
|
|
955
|
-
}
|
|
303
|
+
function ScreenInsideTabs() {
|
|
304
|
+
const tabBar = useTabBar();
|
|
956
305
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
306
|
+
return (
|
|
307
|
+
<Button
|
|
308
|
+
title="Go to second tab"
|
|
309
|
+
onPress={() => tabBar.onIndexChange(1)}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
960
312
|
}
|
|
961
313
|
```
|
|
962
314
|
|
|
963
|
-
##
|
|
964
|
-
|
|
965
|
-
1. ๐ฏ **Use path-based navigation**: Always navigate with paths (`router.navigate('/path')`) rather than imperative methods. This keeps web and native in sync.
|
|
315
|
+
## Web integration
|
|
966
316
|
|
|
967
|
-
|
|
317
|
+
### History API syncing
|
|
968
318
|
|
|
969
|
-
|
|
319
|
+
On web, Router integrates with the browser History API using custom events:
|
|
970
320
|
|
|
971
|
-
|
|
321
|
+
- `router.navigate('/x')` writes `history.pushState({ __srPath: ... })`
|
|
322
|
+
- `router.replace('/x')` writes `history.replaceState({ __srPath: ... })`
|
|
323
|
+
- Browser back/forward triggers `popstate` and Router updates its state accordingly
|
|
972
324
|
|
|
973
|
-
|
|
325
|
+
Important behavioral detail:
|
|
326
|
+
- `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).
|
|
974
327
|
|
|
975
|
-
|
|
976
|
-
```tsx
|
|
977
|
-
const stack = new NavigationStack({ header: { largeTitle: true } });
|
|
978
|
-
```
|
|
328
|
+
### `syncWithUrl: false`
|
|
979
329
|
|
|
980
|
-
|
|
330
|
+
If a route has `screenOptions.syncWithUrl = false`, Router stores the โrealโ router path in `history.state.__srPath` while keeping the visible URL unchanged.
|
|
981
331
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
## ๐ License
|
|
332
|
+
## License
|
|
985
333
|
|
|
986
334
|
MIT
|
|
987
|
-
|
|
988
|
-
---
|
|
989
|
-
|
|
990
|
-
Built with โค๏ธ by [Sigmela](https://github.com/sigmela)
|