@sigmela/router 0.0.16 â 0.1.1
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 +861 -118
- package/lib/module/NavigationStack.js +6 -0
- package/lib/module/Router.js +27 -8
- package/lib/module/ScreenStackItem/ScreenStackItem.js +25 -14
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +70 -0
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +5 -0
- package/lib/module/ScreenStackSheetItem/index.js +3 -0
- package/lib/typescript/src/NavigationStack.d.ts +1 -0
- package/lib/typescript/src/Router.d.ts +3 -0
- package/lib/typescript/src/RouterContext.d.ts +2 -3
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.native.d.ts +11 -0
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.web.d.ts +2 -0
- package/lib/typescript/src/ScreenStackSheetItem/index.d.ts +2 -0
- package/lib/typescript/src/types.d.ts +10 -1
- package/package.json +14 -5
package/README.md
CHANGED
|
@@ -1,33 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
# React Native Router
|
|
2
2
|
|
|
3
|
-
Modern,
|
|
3
|
+
Modern, lightweight, and type-safe navigation for React Native and Web built on top of react-native-screens. Predictable stack-based architecture with tabs, global modals, sheets, controllers, and full Web History API integration.
|
|
4
4
|
|
|
5
|
-
Features
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- URL-first navigation
|
|
10
|
-
-
|
|
11
|
-
-
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ðŠķ **Lightweight**: minimal dependencies, small bundle size
|
|
8
|
+
- ⥠**Fast & performant**: optimized for performance with minimal re-renders
|
|
9
|
+
- ð **URL-first navigation**: path as source of truth, params in URL
|
|
10
|
+
- ðĄïļ **Type safety**: typed `useParams` and `useQueryParams`
|
|
11
|
+
- âïļ **Chainable API**: `new NavigationStack().addScreen('/users/:id', User)`
|
|
12
|
+
- ðą **Sheets & modals**: native presentation styles including iOS/Android sheets
|
|
13
|
+
- ðŊ **Bottom tabs**: native and web renderers, custom tab bars
|
|
14
|
+
- ð **Global stack**: modals and overlays on top of main navigation
|
|
15
|
+
- ðŪ **Controllers**: async navigation with guards and data prefetch
|
|
16
|
+
- ð **Web History API**: full synchronization with pushState/replaceState/popstate
|
|
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
|
|
12
22
|
|
|
13
|
-
Installation
|
|
14
23
|
```bash
|
|
15
|
-
yarn add @sigmela/router react-native-screens
|
|
24
|
+
yarn add @sigmela/router @sigmela/native-sheet react-native-screens
|
|
16
25
|
# or
|
|
17
|
-
npm i @sigmela/router react-native-screens
|
|
26
|
+
npm i @sigmela/router @sigmela/native-sheet react-native-screens
|
|
18
27
|
```
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- react
|
|
29
|
+
### ð Dependencies
|
|
30
|
+
|
|
31
|
+
- `react-native-screens` >= 4.16.0
|
|
32
|
+
- `@sigmela/native-sheet` >= 0.0.1 (optional, required for sheets)
|
|
33
|
+
- `react` and `react-native` (matching your app versions)
|
|
34
|
+
|
|
35
|
+
### ð Web CSS
|
|
36
|
+
|
|
37
|
+
For web, import the bundled stylesheet once in your entry point:
|
|
23
38
|
|
|
24
|
-
Web CSS
|
|
25
|
-
- Import the bundled stylesheet once in your web entry to enable transitions and default tab styles:
|
|
26
39
|
```ts
|
|
27
40
|
import '@sigmela/router/styles.css';
|
|
28
41
|
```
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
## ð Quick Start
|
|
44
|
+
|
|
45
|
+
### ð Simple Stack
|
|
46
|
+
|
|
31
47
|
```tsx
|
|
32
48
|
import {
|
|
33
49
|
NavigationStack,
|
|
@@ -51,7 +67,7 @@ function HomeScreen() {
|
|
|
51
67
|
function DetailsScreen() {
|
|
52
68
|
const { id } = useParams<{ id: string }>();
|
|
53
69
|
const { from } = useQueryParams<{ from?: string }>();
|
|
54
|
-
return <Text>
|
|
70
|
+
return <Text>Details: id={id}, from={from ?? 'n/a'}</Text>;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
const rootStack = new NavigationStack()
|
|
@@ -65,7 +81,8 @@ export default function App() {
|
|
|
65
81
|
}
|
|
66
82
|
```
|
|
67
83
|
|
|
68
|
-
|
|
84
|
+
### ðŊ Tabs + Global Stack
|
|
85
|
+
|
|
69
86
|
```tsx
|
|
70
87
|
import { NavigationStack, Router, Navigation, TabBar } from '@sigmela/router';
|
|
71
88
|
|
|
@@ -79,17 +96,15 @@ const catalogStack = new NavigationStack()
|
|
|
79
96
|
header: { title: 'Product' },
|
|
80
97
|
});
|
|
81
98
|
|
|
82
|
-
const globalStack = new NavigationStack()
|
|
83
|
-
header: { title: 'Sign in' }
|
|
84
|
-
});
|
|
99
|
+
const globalStack = new NavigationStack()
|
|
100
|
+
.addModal('/auth', AuthScreen, { header: { title: 'Sign in' } });
|
|
85
101
|
|
|
86
102
|
const tabBar = new TabBar()
|
|
87
103
|
.addTab({
|
|
88
104
|
key: 'home',
|
|
89
105
|
stack: homeStack,
|
|
90
106
|
title: 'Home',
|
|
91
|
-
|
|
92
|
-
icon: { sfSymbolName: 'house' },
|
|
107
|
+
icon: { sfSymbolName: 'house' }, // iOS SF Symbols
|
|
93
108
|
})
|
|
94
109
|
.addTab({
|
|
95
110
|
key: 'catalog',
|
|
@@ -105,43 +120,458 @@ export default function App() {
|
|
|
105
120
|
}
|
|
106
121
|
```
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
## ð§Đ Core Concepts
|
|
124
|
+
|
|
125
|
+
### ð Navigation Stack
|
|
126
|
+
|
|
127
|
+
A `NavigationStack` is a container for screens. Stacks can be used as the root, inside tabs, or as a global overlay.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const stack = new NavigationStack()
|
|
131
|
+
.addScreen('/path', Component, options)
|
|
132
|
+
.addModal('/modal', ModalComponent, options)
|
|
133
|
+
.addSheet('/sheet', SheetComponent, options);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Constructor overloads:**
|
|
137
|
+
```tsx
|
|
138
|
+
new NavigationStack()
|
|
139
|
+
new NavigationStack(id: string)
|
|
140
|
+
new NavigationStack(defaultOptions: ScreenOptions)
|
|
141
|
+
new NavigationStack(id: string, defaultOptions: ScreenOptions)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Methods:**
|
|
145
|
+
- `addScreen(path, component, options?)` â add a regular screen
|
|
146
|
+
- `addModal(path, component, options?)` â add a modal (shorthand for `stackPresentation: 'modal'`)
|
|
147
|
+
- `addSheet(path, component, options?)` â add a sheet (shorthand for `stackPresentation: 'sheet'`)
|
|
148
|
+
- `getId()` â get the unique stack ID
|
|
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
|
|
152
|
+
|
|
153
|
+
### ð§ Router
|
|
154
|
+
|
|
155
|
+
The `Router` manages navigation state and history.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
const router = new Router({
|
|
159
|
+
root: tabBar | stack, // TabBar or NavigationStack
|
|
160
|
+
global?: stack, // Optional global overlay stack
|
|
161
|
+
screenOptions?: ScreenOptions // Global screen defaults
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Navigation methods:**
|
|
166
|
+
- `navigate(path: string)` â push a new screen
|
|
167
|
+
- `replace(path: string, dedupe?: boolean)` â replace the top screen
|
|
168
|
+
- `goBack()` â pop the current screen
|
|
169
|
+
- `onTabIndexChange(index: number)` â switch to a specific tab
|
|
170
|
+
- `setRoot(nextRoot, options?)` â dynamically switch root (e.g., login â main app)
|
|
171
|
+
|
|
172
|
+
**State methods:**
|
|
173
|
+
- `getVisibleRoute()` â get currently visible route with params and query
|
|
174
|
+
- `getState()` â get the full router state
|
|
175
|
+
- `subscribe(listener)` â subscribe to all navigation changes
|
|
176
|
+
- `subscribeStack(stackId, listener)` â subscribe to specific stack changes
|
|
177
|
+
- `subscribeRoot(listener)` â subscribe to root structure changes
|
|
178
|
+
- `subscribeActiveTab(listener)` â subscribe to tab changes
|
|
179
|
+
|
|
180
|
+
**Stack history methods:**
|
|
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
|
|
185
|
+
|
|
186
|
+
### ð TabBar
|
|
187
|
+
|
|
188
|
+
A `TabBar` manages multiple stacks as tabs.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const tabBar = new TabBar({ component?: CustomTabBarComponent })
|
|
192
|
+
.addTab({
|
|
193
|
+
key: string,
|
|
194
|
+
stack?: NavigationStack,
|
|
195
|
+
screen?: Component,
|
|
196
|
+
title?: string,
|
|
197
|
+
icon?: ImageSource | { sfSymbolName | imageSource | templateSource },
|
|
198
|
+
selectedIcon?: ImageSource | { sfSymbolName | imageSource | templateSource },
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Methods:**
|
|
203
|
+
- `addTab(config)` â add a new tab
|
|
204
|
+
- `setBadge(index, badge)` â set badge text on a tab
|
|
205
|
+
- `onIndexChange(index)` â change active tab (usually called via router)
|
|
206
|
+
- `getState()` â get current tab state
|
|
207
|
+
- `subscribe(listener)` â subscribe to tab bar changes
|
|
208
|
+
|
|
209
|
+
**Tab configuration:**
|
|
210
|
+
Each tab can have either a `stack` (NavigationStack) or a single `screen` (Component). If using a stack, the tab will support full navigation within that tab.
|
|
211
|
+
|
|
212
|
+
### âïļ Screen Options
|
|
213
|
+
|
|
214
|
+
Screen options control the appearance and behavior of individual screens:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
type ScreenOptions = {
|
|
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.
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
const stack = new NavigationStack()
|
|
268
|
+
.addSheet('/settings', SettingsSheet, {
|
|
269
|
+
header: { title: 'Settings' },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Or using addScreen with explicit option:
|
|
273
|
+
const stack = new NavigationStack()
|
|
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
|
+
```
|
|
286
|
+
|
|
287
|
+
**Sheet appearance:**
|
|
288
|
+
```tsx
|
|
289
|
+
const appearance: NavigationAppearance = {
|
|
290
|
+
sheet: {
|
|
291
|
+
backgroundColor: '#fff',
|
|
292
|
+
cornerRadius: 18,
|
|
293
|
+
androidFullScreenTopInset: 40,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
<Navigation router={router} appearance={appearance} />
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### ðŪ Controllers
|
|
301
|
+
|
|
302
|
+
Controllers run before a screen is presented. They're useful for:
|
|
303
|
+
- Authentication guards
|
|
304
|
+
- Data prefetching
|
|
305
|
+
- Conditional redirects
|
|
306
|
+
- Loading states
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { createController } from '@sigmela/router';
|
|
310
|
+
|
|
311
|
+
const UserDetails = {
|
|
312
|
+
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
|
+
|
|
109
502
|
```tsx
|
|
110
503
|
import { TabBar, type TabBarProps } from '@sigmela/router';
|
|
111
504
|
|
|
112
|
-
function
|
|
505
|
+
function CustomTabBar({ tabs, activeIndex, onTabPress }: TabBarProps) {
|
|
113
506
|
return (
|
|
114
|
-
<
|
|
115
|
-
{tabs.map((
|
|
116
|
-
<
|
|
117
|
-
{
|
|
118
|
-
|
|
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>
|
|
119
521
|
))}
|
|
120
|
-
</
|
|
522
|
+
</View>
|
|
121
523
|
);
|
|
122
524
|
}
|
|
123
525
|
|
|
124
|
-
const tabBar = new TabBar({ component:
|
|
125
|
-
.addTab({ key: 'home', stack: homeStack, title: 'Home' })
|
|
126
|
-
.addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });
|
|
526
|
+
const tabBar = new TabBar({ component: CustomTabBar })
|
|
527
|
+
.addTab({ key: 'home', stack: homeStack, title: 'Home' });
|
|
127
528
|
```
|
|
128
529
|
|
|
129
|
-
Appearance
|
|
130
|
-
|
|
530
|
+
### ðĻ Appearance Customization
|
|
531
|
+
|
|
532
|
+
Customize the appearance of tabs, headers, and screens:
|
|
533
|
+
|
|
131
534
|
```tsx
|
|
132
535
|
import type { NavigationAppearance } from '@sigmela/router';
|
|
133
536
|
|
|
134
537
|
const appearance: NavigationAppearance = {
|
|
538
|
+
// Tab bar styling
|
|
135
539
|
tabBar: {
|
|
136
|
-
backgroundColor: '#
|
|
540
|
+
backgroundColor: '#ffffff',
|
|
137
541
|
iconColor: '#8e8e93',
|
|
138
|
-
iconColorActive: '#
|
|
139
|
-
|
|
140
|
-
// Android-only options:
|
|
542
|
+
iconColorActive: '#007aff',
|
|
543
|
+
badgeBackgroundColor: '#ff3b30',
|
|
141
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
|
+
},
|
|
142
556
|
},
|
|
557
|
+
|
|
558
|
+
// Header styling (applied to all headers)
|
|
143
559
|
header: {
|
|
144
|
-
backgroundColor: '#
|
|
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,
|
|
145
575
|
},
|
|
146
576
|
};
|
|
147
577
|
|
|
@@ -150,98 +580,411 @@ export default function App() {
|
|
|
150
580
|
}
|
|
151
581
|
```
|
|
152
582
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
|
|
198
695
|
```tsx
|
|
199
696
|
import { createController } from '@sigmela/router';
|
|
200
697
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
698
|
+
type ProductParams = { productId: string };
|
|
699
|
+
type ProductQuery = { variant?: string };
|
|
700
|
+
|
|
701
|
+
const ProductDetails = {
|
|
702
|
+
component: ProductScreen,
|
|
703
|
+
controller: createController<ProductParams, ProductQuery>(
|
|
704
|
+
async ({ params, query }, present) => {
|
|
705
|
+
// params.productId is typed as string
|
|
706
|
+
// query.variant is typed as string | undefined
|
|
707
|
+
|
|
708
|
+
const product = await fetchProduct(params.productId);
|
|
709
|
+
present({ product });
|
|
208
710
|
}
|
|
209
|
-
|
|
210
|
-
}),
|
|
711
|
+
),
|
|
211
712
|
};
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## ð API Reference
|
|
212
716
|
|
|
213
|
-
|
|
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
|
+
}
|
|
214
754
|
```
|
|
215
755
|
|
|
216
|
-
|
|
217
|
-
- On web, the router listens to `pushState`, `replaceState`, and `popstate`. You can navigate by calling `router.navigate('/path')` or by updating `window.history` yourself; the router will stay in sync.
|
|
218
|
-
- Initial load deep links are expanded into a stack chain: `/a/b/c` seeds the stack with `/a` â `/a/b` â `/a/b/c` if those routes exist in the same stack.
|
|
219
|
-
- `goBack()` pops within the active stack (or global). The router avoids creating duplicate entries when switching tabs by using `replace` under the hood in the web tab bar.
|
|
756
|
+
### ð NavigationStack
|
|
220
757
|
|
|
221
|
-
Root switching (auth flows)
|
|
222
|
-
Switch between a login stack and the main tab bar at runtime. Optionally pass a transition for the change.
|
|
223
758
|
```tsx
|
|
224
|
-
|
|
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
|
+
}
|
|
225
788
|
```
|
|
226
789
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
790
|
+
### ð TabBar
|
|
791
|
+
|
|
792
|
+
```tsx
|
|
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
|
+
};
|
|
809
|
+
|
|
810
|
+
class TabBar {
|
|
811
|
+
constructor(config?: { component?: ComponentType<TabBarProps> });
|
|
812
|
+
|
|
813
|
+
addTab(config: TabConfig): TabBar;
|
|
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
|
+
```
|
|
820
|
+
|
|
821
|
+
### ð Types
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
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
|
+
};
|
|
833
|
+
|
|
834
|
+
type NavigationAppearance = {
|
|
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
|
+
};
|
|
862
|
+
|
|
863
|
+
type VisibleRoute = {
|
|
864
|
+
routeId: string;
|
|
865
|
+
stackId?: string;
|
|
866
|
+
tabIndex?: number;
|
|
867
|
+
scope: 'global' | 'tab' | 'root';
|
|
868
|
+
path?: string;
|
|
869
|
+
params?: Record<string, unknown>;
|
|
870
|
+
query?: Record<string, unknown>;
|
|
871
|
+
} | 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
|
+
```
|
|
884
|
+
|
|
885
|
+
## ðĄ Examples
|
|
886
|
+
|
|
887
|
+
### ð Full Example with Auth Flow
|
|
888
|
+
|
|
889
|
+
```tsx
|
|
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
|
+
});
|
|
909
|
+
|
|
910
|
+
// Stacks
|
|
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
|
+
});
|
|
923
|
+
|
|
924
|
+
const authStack = new NavigationStack()
|
|
925
|
+
.addScreen('/login', LoginScreen, {
|
|
926
|
+
header: { title: 'Login', hidden: true },
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const mainTabs = new TabBar()
|
|
930
|
+
.addTab({ key: 'home', stack: homeStack, title: 'Home', icon: { sfSymbolName: 'house' } })
|
|
931
|
+
.addTab({ key: 'profile', stack: profileStack, title: 'Profile', icon: { sfSymbolName: 'person' } });
|
|
932
|
+
|
|
933
|
+
const globalStack = new NavigationStack()
|
|
934
|
+
.addSheet('/settings-modal', SettingsModalScreen, {
|
|
935
|
+
convertModalToSheetForAndroid: true,
|
|
936
|
+
header: { title: 'Quick Settings' },
|
|
937
|
+
});
|
|
231
938
|
|
|
232
|
-
//
|
|
233
|
-
|
|
939
|
+
// Router
|
|
940
|
+
const router = new Router({
|
|
941
|
+
root: authStack, // Start with auth
|
|
942
|
+
global: globalStack,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Login screen
|
|
946
|
+
function LoginScreen() {
|
|
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
|
+
}
|
|
956
|
+
|
|
957
|
+
// App
|
|
958
|
+
export default function App() {
|
|
959
|
+
return <Navigation router={router} appearance={appearance} />;
|
|
960
|
+
}
|
|
234
961
|
```
|
|
235
962
|
|
|
236
|
-
|
|
237
|
-
|
|
963
|
+
## ðĄ Tips & Best Practices
|
|
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.
|
|
966
|
+
|
|
967
|
+
2. ðĄïļ **Type your params**: Use TypeScript generics with `useParams` and `useQueryParams` for type safety.
|
|
968
|
+
|
|
969
|
+
3. ðŪ **Controllers for guards**: Use controllers for auth checks, data prefetching, and conditional navigation instead of doing it in the component.
|
|
970
|
+
|
|
971
|
+
4. ð **Global stack for overlays**: Use the global stack for modals/sheets that should appear on top of everything (auth, alerts, etc.).
|
|
972
|
+
|
|
973
|
+
5. ð·ïļ **Stack IDs**: Optionally provide custom stack IDs for debugging: `new NavigationStack('my-stack-id')`.
|
|
974
|
+
|
|
975
|
+
6. âïļ **Default options**: Set default options at the stack level to avoid repetition:
|
|
976
|
+
```tsx
|
|
977
|
+
const stack = new NavigationStack({ header: { largeTitle: true } });
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
7. ð **Web CSS**: Don't forget to import `@sigmela/router/styles.css` in your web entry point.
|
|
981
|
+
|
|
982
|
+
8. ðą **Sheet vs Modal**: Use `addSheet()` for bottom sheets, `addModal()` for full-screen modals. On Android, use `convertModalToSheetForAndroid: true` to convert modals to sheets automatically.
|
|
238
983
|
|
|
239
|
-
|
|
240
|
-
- Prefer path-based navigation throughout your app: it keeps web and native in sync.
|
|
241
|
-
- Type your params and query with `useParams<T>()` and `useQueryParams<T>()` to get end-to-end type safety.
|
|
242
|
-
- On the web, remember to import `@sigmela/router/styles.css` once.
|
|
984
|
+
## ð License
|
|
243
985
|
|
|
244
|
-
License
|
|
245
986
|
MIT
|
|
246
987
|
|
|
988
|
+
---
|
|
247
989
|
|
|
990
|
+
Built with âĪïļ by [Sigmela](https://github.com/sigmela)
|