@sigmela/router 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +346 -0
- package/lib/module/Navigation.js +74 -0
- package/lib/module/NavigationStack.js +72 -0
- package/lib/module/Router.js +571 -0
- package/lib/module/RouterContext.js +33 -0
- package/lib/module/ScreenStackItem.js +61 -0
- package/lib/module/StackRenderer.js +29 -0
- package/lib/module/TabBar/RenderTabBar.js +122 -0
- package/lib/module/TabBar/TabBar.js +74 -0
- package/lib/module/TabBar/TabBarContext.js +4 -0
- package/lib/module/TabBar/useTabBar.js +11 -0
- package/lib/module/createController.js +5 -0
- package/lib/module/index.js +14 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +3 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Navigation.d.ts +8 -0
- package/lib/typescript/src/NavigationStack.d.ts +30 -0
- package/lib/typescript/src/Router.d.ts +70 -0
- package/lib/typescript/src/RouterContext.d.ts +19 -0
- package/lib/typescript/src/ScreenStackItem.d.ts +12 -0
- package/lib/typescript/src/StackRenderer.d.ts +6 -0
- package/lib/typescript/src/TabBar/RenderTabBar.d.ts +8 -0
- package/lib/typescript/src/TabBar/TabBar.d.ts +43 -0
- package/lib/typescript/src/TabBar/TabBarContext.d.ts +3 -0
- package/lib/typescript/src/TabBar/useTabBar.d.ts +2 -0
- package/lib/typescript/src/createController.d.ts +14 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/types.d.ts +244 -0
- package/package.json +166 -0
- package/src/Navigation.tsx +102 -0
- package/src/NavigationStack.ts +106 -0
- package/src/Router.ts +684 -0
- package/src/RouterContext.tsx +58 -0
- package/src/ScreenStackItem.tsx +64 -0
- package/src/StackRenderer.tsx +41 -0
- package/src/TabBar/RenderTabBar.tsx +154 -0
- package/src/TabBar/TabBar.ts +106 -0
- package/src/TabBar/TabBarContext.ts +4 -0
- package/src/TabBar/useTabBar.ts +10 -0
- package/src/createController.ts +27 -0
- package/src/index.ts +24 -0
- package/src/types.ts +272 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 https://github.com/bogoslavskiy
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Router for React Native
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@sigmela/router)
|
|
4
|
+
|
|
5
|
+
Lightweight, predictable navigation for React Native built on top of react-native-screens. It provides:
|
|
6
|
+
- Stack navigation with URL-like paths and typed params
|
|
7
|
+
- Bottom tab navigation via a simple builder API
|
|
8
|
+
- A global overlay stack (e.g., auth modal) rendered above tabs/root
|
|
9
|
+
- An imperative API with idempotent navigation and O(1) per-stack updates
|
|
10
|
+
|
|
11
|
+
Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yarn add @sigmela/router react-native-screens
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Make sure react-native-screens is properly installed and configured in your app.
|
|
18
|
+
|
|
19
|
+
Quick start (single stack)
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { NavigationStack, Router, Navigation, useRouter, useParams, useQueryParams } from '@sigmela/router';
|
|
23
|
+
|
|
24
|
+
function HomeScreen() {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
return (
|
|
27
|
+
<Button
|
|
28
|
+
title="Open details"
|
|
29
|
+
onPress={() => router.navigate('/details/42?from=home')}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DetailsScreen() {
|
|
35
|
+
const { id } = useParams<{ id: string }>();
|
|
36
|
+
const { from } = useQueryParams<{ from?: string }>();
|
|
37
|
+
return <Text>{`Details id=${id} from=${from ?? 'n/a'}`}</Text>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rootStack = new NavigationStack()
|
|
41
|
+
.addScreen('/', HomeScreen, { header: { title: 'Home' } })
|
|
42
|
+
.addScreen('/details/:id', DetailsScreen, { header: { title: 'Details' } });
|
|
43
|
+
|
|
44
|
+
const router = new Router({ root: rootStack });
|
|
45
|
+
|
|
46
|
+
export default function App() {
|
|
47
|
+
return <Navigation router={router} />;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Quick start (tabs + global stack)
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { NavigationStack, Router, Navigation, TabBar } from '@sigmela/router';
|
|
55
|
+
|
|
56
|
+
const homeStack = new NavigationStack()
|
|
57
|
+
.addScreen('/', HomeScreen, { header: { title: 'Home' } });
|
|
58
|
+
|
|
59
|
+
const catalogStack = new NavigationStack()
|
|
60
|
+
.addScreen('/catalog', CatalogScreen, { header: { title: 'Catalog' } })
|
|
61
|
+
.addScreen('/catalog/products/:productId', ProductScreen, {
|
|
62
|
+
header: { title: 'Product' }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const globalStack = new NavigationStack()
|
|
66
|
+
.addModal('/auth', AuthScreen, {
|
|
67
|
+
header: { title: 'Sign in' },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const tabBar = new TabBar({ labeled: true })
|
|
71
|
+
.addTab({ stack: homeStack, title: 'Home', icon: { sfSymbol: 'house' } })
|
|
72
|
+
.addTab({ stack: catalogStack, title: 'Catalog', icon: { sfSymbol: 'bag' } });
|
|
73
|
+
|
|
74
|
+
const router = new Router({ root: tabBar, global: globalStack });
|
|
75
|
+
|
|
76
|
+
export default function App() {
|
|
77
|
+
return <Navigation router={router} />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Navigation Appearance
|
|
82
|
+
|
|
83
|
+
You can customize the navigation appearance using the `appearance` prop:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Navigation, NavigationAppearance } from '@sigmela/router';
|
|
87
|
+
|
|
88
|
+
const appearance: NavigationAppearance = {
|
|
89
|
+
tabBar: {
|
|
90
|
+
// Android-specific
|
|
91
|
+
backgroundColor: '#ffffff',
|
|
92
|
+
tabBarItemStyle: {
|
|
93
|
+
titleFontColor: '#999999',
|
|
94
|
+
titleFontColorActive: '#007AFF',
|
|
95
|
+
titleFontSize: 12,
|
|
96
|
+
titleFontWeight: '600',
|
|
97
|
+
iconColor: '#999999',
|
|
98
|
+
iconColorActive: '#007AFF',
|
|
99
|
+
rippleColor: '#00000020',
|
|
100
|
+
activeIndicatorColor: '#007AFF',
|
|
101
|
+
},
|
|
102
|
+
// iOS-specific
|
|
103
|
+
tintColor: '#007AFF',
|
|
104
|
+
standardAppearance: {
|
|
105
|
+
tabBarBackgroundColor: '#ffffff',
|
|
106
|
+
tabBarShadowColor: 'transparent',
|
|
107
|
+
},
|
|
108
|
+
scrollEdgeAppearance: {
|
|
109
|
+
tabBarBackgroundColor: 'rgba(255,255,255,0.9)',
|
|
110
|
+
tabBarShadowColor: 'transparent',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
screenStyle: {
|
|
114
|
+
backgroundColor: '#ffffff',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default function App() {
|
|
119
|
+
return <Navigation router={router} appearance={appearance} />;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Core concepts
|
|
124
|
+
|
|
125
|
+
- Router: central coordinator. Holds slices (histories) per stack, active tab index, and the visible route.
|
|
126
|
+
- NavigationStack: define stack routes with path patterns using path-to-regexp.
|
|
127
|
+
- TabBar: builder for bottom tabs; each tab may reference a stack or a single screen.
|
|
128
|
+
- Global stack: a separate stack rendered above tabs/root, ideal for modals like auth.
|
|
129
|
+
- Navigation component: renders current root layer (tabs or root stack) and the global overlay.
|
|
130
|
+
|
|
131
|
+
API reference
|
|
132
|
+
|
|
133
|
+
NavigationStack
|
|
134
|
+
|
|
135
|
+
- constructor(idOrOptions?, maybeOptions?)
|
|
136
|
+
- Overloads:
|
|
137
|
+
- new NavigationStack()
|
|
138
|
+
- new NavigationStack(id: string)
|
|
139
|
+
- new NavigationStack(defaultOptions: ScreenOptions)
|
|
140
|
+
- new NavigationStack(id: string, defaultOptions: ScreenOptions)
|
|
141
|
+
- addScreen(path: string, component: React.ComponentType, options?: ScreenOptions): this
|
|
142
|
+
- addModal(path: string, component: React.ComponentType, options?: ScreenOptions): this
|
|
143
|
+
- Convenience method that automatically sets `stackPresentation: 'modal'`
|
|
144
|
+
- getId(): string
|
|
145
|
+
- getDefaultOptions(): ScreenOptions | undefined
|
|
146
|
+
|
|
147
|
+
Router
|
|
148
|
+
|
|
149
|
+
- constructor({ root, global?, screenOptions? })
|
|
150
|
+
- root: TabBar | NavigationStack
|
|
151
|
+
- global: optional NavigationStack rendered on top (modal layer)
|
|
152
|
+
- screenOptions: global ScreenOptions overrides merged into each screen
|
|
153
|
+
- navigate(path: string): void
|
|
154
|
+
- Matches a route by pathname, switches tab if needed, pushes a new history item
|
|
155
|
+
- Duplicate navigate to the same top screen with the same params is ignored
|
|
156
|
+
- replace(path: string): void
|
|
157
|
+
- Replaces the top history item. If the top stack changes, both stack slices are updated incrementally to avoid stale entries [[memory:6631860]].
|
|
158
|
+
- goBack(): void
|
|
159
|
+
- Pops from the highest priority layer that can pop: global → current tab → root
|
|
160
|
+
- “Seed” screens (the very first screen of a stack) are protected from popping
|
|
161
|
+
- setRoot(nextRoot: TabBar | NavigationStack, options?: { transition?: ScreenOptions['stackAnimation'] }): void
|
|
162
|
+
- Switch between auth flow and main app, etc.; reseeds the new root
|
|
163
|
+
- transition is applied to the root layer when changing
|
|
164
|
+
- onTabIndexChange(index: number): void and setActiveTabIndex(index: number): void
|
|
165
|
+
- ensureTabSeed(index: number): void
|
|
166
|
+
- Ensures the first screen of a tab stack is seeded when the tab becomes active
|
|
167
|
+
- getVisibleRoute(): {
|
|
168
|
+
routeId: string; stackId?: string; tabIndex?: number; scope: 'global' | 'tab' | 'root'; params?; query?; path?; pattern?
|
|
169
|
+
} | null
|
|
170
|
+
- subscribe(listener): unsubscribe
|
|
171
|
+
- subscribeStack(stackId, listener): unsubscribe
|
|
172
|
+
- subscribeActiveTab(listener): unsubscribe
|
|
173
|
+
- getStackHistory(stackId): HistoryItem[] (useful for debugging/analytics)
|
|
174
|
+
- hasTabBar(): boolean, getRootStackId(): string | undefined, getGlobalStackId(): string | undefined, getRootTransition(): ScreenOptions['stackAnimation'] | undefined
|
|
175
|
+
|
|
176
|
+
Components
|
|
177
|
+
|
|
178
|
+
- Navigation: top-level view that renders the root layer and the global overlay. Usage: `<Navigation router={router} appearance={appearance} />`.
|
|
179
|
+
- StackRenderer: renders a single `NavigationStack` (advanced use, usually not needed directly).
|
|
180
|
+
|
|
181
|
+
Hooks
|
|
182
|
+
|
|
183
|
+
- useRouter(): Router
|
|
184
|
+
- useCurrentRoute(): VisibleRoute
|
|
185
|
+
- useParams<TParams>(): TParams
|
|
186
|
+
- useQueryParams<TQuery>(): TQuery
|
|
187
|
+
- useRoute(): { params, query, pattern?, path? }
|
|
188
|
+
|
|
189
|
+
TabBar builder
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
new TabBar({
|
|
193
|
+
sidebarAdaptable?: boolean,
|
|
194
|
+
disablePageAnimations?: boolean,
|
|
195
|
+
hapticFeedbackEnabled?: boolean,
|
|
196
|
+
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent',
|
|
197
|
+
minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never',
|
|
198
|
+
})
|
|
199
|
+
.addTab({
|
|
200
|
+
stack?: NavigationStack,
|
|
201
|
+
screen?: React.ComponentType,
|
|
202
|
+
title?: string,
|
|
203
|
+
badge?: string,
|
|
204
|
+
icon?: ImageSource | AppleIcon | (({ focused }: { focused: boolean }) => ImageSource | AppleIcon),
|
|
205
|
+
activeTintColor?: ColorValue,
|
|
206
|
+
hidden?: boolean,
|
|
207
|
+
testID?: string,
|
|
208
|
+
role?: 'search',
|
|
209
|
+
freezeOnBlur?: boolean,
|
|
210
|
+
lazy?: boolean,
|
|
211
|
+
iconInsets?: { top?: number; bottom?: number; left?: number; right?: number },
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
You can update badges at runtime via:
|
|
216
|
+
- setBadge(tabIndex, badge: string | null)
|
|
217
|
+
- setTabBarConfig(partial)
|
|
218
|
+
|
|
219
|
+
For styling, use the `appearance` prop on the Navigation component instead.
|
|
220
|
+
|
|
221
|
+
Screen options
|
|
222
|
+
|
|
223
|
+
ScreenOptions map directly to props of react-native-screens `ScreenStackItem` (e.g., header, stackPresentation, stackAnimation, gestureEnabled, etc.).
|
|
224
|
+
- **header**: controls the navigation header. If not specified, the header is hidden by default.
|
|
225
|
+
- Per-screen options come from `addScreen(path, component, options)`
|
|
226
|
+
- Per-stack defaults via `new NavigationStack(defaultOptions)`
|
|
227
|
+
- Global overrides via `new Router({ screenOptions })`
|
|
228
|
+
The effective options are merged in this order: stack defaults → per-screen → router overrides.
|
|
229
|
+
|
|
230
|
+
Header configuration:
|
|
231
|
+
```tsx
|
|
232
|
+
// Header with title (visible)
|
|
233
|
+
{ header: { title: 'My Screen' } }
|
|
234
|
+
|
|
235
|
+
// Hidden header (explicit)
|
|
236
|
+
{ header: { hidden: true } }
|
|
237
|
+
|
|
238
|
+
// No header specified = hidden by default
|
|
239
|
+
{ /* header will be hidden automatically */ }
|
|
240
|
+
|
|
241
|
+
// Custom header with background color
|
|
242
|
+
{ header: { title: 'Settings', backgroundColor: '#007AFF' } }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Modal screens:
|
|
246
|
+
```tsx
|
|
247
|
+
// Using addModal - automatically sets stackPresentation: 'modal'
|
|
248
|
+
const stack = new NavigationStack()
|
|
249
|
+
.addModal('/auth', AuthScreen, {
|
|
250
|
+
header: { title: 'Sign In' }
|
|
251
|
+
})
|
|
252
|
+
.addModal('/settings', SettingsScreen, {
|
|
253
|
+
header: { title: 'Settings' }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Equivalent to using addScreen with explicit modal presentation
|
|
257
|
+
const stack = new NavigationStack()
|
|
258
|
+
.addScreen('/auth', AuthScreen, {
|
|
259
|
+
stackPresentation: 'modal',
|
|
260
|
+
header: { title: 'Sign In' }
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Paths, params and query
|
|
265
|
+
|
|
266
|
+
- Paths use path-to-regexp under the hood. Examples:
|
|
267
|
+
- `/users/:userId`
|
|
268
|
+
- `/orders/:year/:month`
|
|
269
|
+
- Params are exposed via `useParams()`; query params via `useQueryParams()` and are parsed with query-string.
|
|
270
|
+
- When you call `router.navigate('/users/123?tab=posts')`, your screen receives `{ userId: '123' }` as params and `{ tab: 'posts' }` as query.
|
|
271
|
+
|
|
272
|
+
### Controllers: delay screen presentation and pass props
|
|
273
|
+
|
|
274
|
+
You can attach a controller to a route to perform checks or async work before the screen is shown. If a controller is present, the screen is NOT pushed until the controller calls `present(passProps)`.
|
|
275
|
+
|
|
276
|
+
Definition:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { createController } from '@sigmela/router';
|
|
280
|
+
|
|
281
|
+
type ProductParams = { productId: string };
|
|
282
|
+
type ProductQuery = { coupon?: string };
|
|
283
|
+
|
|
284
|
+
export const ProductController = createController<ProductParams, ProductQuery>((input, present) => {
|
|
285
|
+
// input.params and input.query are typed
|
|
286
|
+
// Do any sync/async work here (auth, data prefetch, A/B logic, etc.)
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
present({ preloadedTitle: 'From controller' }); // props passed to the screen
|
|
289
|
+
}, 300);
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Attach to a route:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
new NavigationStack()
|
|
297
|
+
.addScreen('/catalog/products/:productId', {
|
|
298
|
+
controller: ProductController,
|
|
299
|
+
component: ProductScreen,
|
|
300
|
+
}, {
|
|
301
|
+
header: { title: 'Product' },
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
In your screen you can receive `passProps` from the controller alongside route params/query:
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
type ProductScreenProps = { preloadedTitle?: string };
|
|
309
|
+
|
|
310
|
+
function ProductScreen(props: ProductScreenProps) {
|
|
311
|
+
const { productId } = useParams<ProductParams>();
|
|
312
|
+
const { coupon } = useQueryParams<ProductQuery>();
|
|
313
|
+
return <Text>{props.preloadedTitle} #{productId} coupon={coupon ?? '—'}</Text>;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Notes:
|
|
318
|
+
- `navigate()` and `replace()` both respect controllers.
|
|
319
|
+
- If the controller never calls `present()`, the screen will not be shown (useful for redirects).
|
|
320
|
+
- Props passed to `present()` are injected into the route component as regular props.
|
|
321
|
+
|
|
322
|
+
Behavior highlights (verified by tests)
|
|
323
|
+
|
|
324
|
+
- Initial seeding: the first screen of the active tab (or root stack) is pushed automatically.
|
|
325
|
+
- Duplicate navigate to the same top screen with the same params is ignored.
|
|
326
|
+
- goBack pops from the global stack first (if any), then from the current tab’s stack, then from the root stack; seed screens are protected.
|
|
327
|
+
- Navigating to a route inside a tab switches the active tab and seeds it if needed.
|
|
328
|
+
- setRoot switches between TabBar and NavigationStack, applies an optional transition, rebuilds the registry, and reseeds the new root; subscribers to `subscribeRoot` are notified.
|
|
329
|
+
- replace updates old/new stack slices atomically to avoid stale entries and keeps per-stack updates O(1) [[memory:6631860]].
|
|
330
|
+
|
|
331
|
+
TypeScript
|
|
332
|
+
|
|
333
|
+
Helpful exports:
|
|
334
|
+
- Types: `TabConfig`, `TabBarConfig`, `NavigationProps`, `NavigationAppearance`, `HistoryItem`
|
|
335
|
+
- Components: `Navigation`, `StackRenderer`, `TabBar`
|
|
336
|
+
- Hooks: `useRouter`, `useCurrentRoute`, `useParams`, `useQueryParams`
|
|
337
|
+
- Core classes: `Router`, `NavigationStack`
|
|
338
|
+
|
|
339
|
+
Requirements
|
|
340
|
+
|
|
341
|
+
- React 18+
|
|
342
|
+
- React Native (with react-native-screens)
|
|
343
|
+
|
|
344
|
+
License
|
|
345
|
+
|
|
346
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { ScreenStack, ScreenStackItem as RNNScreenStackItem } from 'react-native-screens';
|
|
4
|
+
import { memo, useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
5
|
+
import { RenderTabBar } from "./TabBar/RenderTabBar.js";
|
|
6
|
+
import { ScreenStackItem } from "./ScreenStackItem.js";
|
|
7
|
+
import { RouterContext } from "./RouterContext.js";
|
|
8
|
+
import { StyleSheet } from 'react-native';
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
const EMPTY_HISTORY = [];
|
|
11
|
+
function useStackHistory(router, stackId) {
|
|
12
|
+
const subscribe = useCallback(cb => stackId ? router.subscribeStack(stackId, cb) : () => {}, [router, stackId]);
|
|
13
|
+
const get = useCallback(() => stackId ? router.getStackHistory(stackId) : EMPTY_HISTORY, [router, stackId]);
|
|
14
|
+
return useSyncExternalStore(subscribe, get, get);
|
|
15
|
+
}
|
|
16
|
+
export const Navigation = /*#__PURE__*/memo(({
|
|
17
|
+
router,
|
|
18
|
+
appearance
|
|
19
|
+
}) => {
|
|
20
|
+
const [root, setRoot] = useState(() => ({
|
|
21
|
+
hasTabBar: router.hasTabBar(),
|
|
22
|
+
rootId: router.getRootStackId()
|
|
23
|
+
}));
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return router.subscribeRoot(() => {
|
|
26
|
+
setRoot({
|
|
27
|
+
hasTabBar: router.hasTabBar(),
|
|
28
|
+
rootId: router.getRootStackId()
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}, [router]);
|
|
32
|
+
const {
|
|
33
|
+
hasTabBar,
|
|
34
|
+
rootId
|
|
35
|
+
} = root;
|
|
36
|
+
const rootTransition = router.getRootTransition();
|
|
37
|
+
const globalId = router.getGlobalStackId();
|
|
38
|
+
const rootItems = useStackHistory(router, rootId);
|
|
39
|
+
const globalItems = useStackHistory(router, globalId);
|
|
40
|
+
return /*#__PURE__*/_jsx(RouterContext.Provider, {
|
|
41
|
+
value: router,
|
|
42
|
+
children: /*#__PURE__*/_jsxs(ScreenStack, {
|
|
43
|
+
style: styles.flex,
|
|
44
|
+
children: [hasTabBar && /*#__PURE__*/_jsx(RNNScreenStackItem, {
|
|
45
|
+
screenId: "root-tabbar",
|
|
46
|
+
headerConfig: {
|
|
47
|
+
hidden: true
|
|
48
|
+
},
|
|
49
|
+
style: styles.flex,
|
|
50
|
+
stackAnimation: rootTransition,
|
|
51
|
+
children: /*#__PURE__*/_jsx(RenderTabBar, {
|
|
52
|
+
tabBar: router.tabBar,
|
|
53
|
+
appearance: appearance?.tabBar
|
|
54
|
+
})
|
|
55
|
+
}), rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
56
|
+
stackId: rootId,
|
|
57
|
+
item: item,
|
|
58
|
+
stackAnimation: rootTransition,
|
|
59
|
+
screenStyle: appearance?.screenStyle,
|
|
60
|
+
headerAppearance: appearance?.header
|
|
61
|
+
}, item.key)), globalItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
62
|
+
stackId: globalId,
|
|
63
|
+
item: item,
|
|
64
|
+
screenStyle: appearance?.screenStyle,
|
|
65
|
+
headerAppearance: appearance?.header
|
|
66
|
+
}, item.key))]
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
const styles = StyleSheet.create({
|
|
71
|
+
flex: {
|
|
72
|
+
flex: 1
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { nanoid } from 'nanoid/non-secure';
|
|
4
|
+
import { match } from 'path-to-regexp';
|
|
5
|
+
export class NavigationStack {
|
|
6
|
+
routes = [];
|
|
7
|
+
|
|
8
|
+
// Overloads
|
|
9
|
+
|
|
10
|
+
constructor(idOrOptions, maybeOptions) {
|
|
11
|
+
if (typeof idOrOptions === 'string') {
|
|
12
|
+
this.stackId = idOrOptions ?? `stack-${nanoid()}`;
|
|
13
|
+
this.defaultOptions = maybeOptions;
|
|
14
|
+
} else {
|
|
15
|
+
this.stackId = `stack-${nanoid()}`;
|
|
16
|
+
this.defaultOptions = idOrOptions;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
getId() {
|
|
20
|
+
return this.stackId;
|
|
21
|
+
}
|
|
22
|
+
addScreen(path, mixedComponent, options) {
|
|
23
|
+
const {
|
|
24
|
+
component,
|
|
25
|
+
controller
|
|
26
|
+
} = this.extractComponent(mixedComponent);
|
|
27
|
+
const routeId = `${this.stackId}-route-${this.routes.length}`;
|
|
28
|
+
const matcher = match(path);
|
|
29
|
+
this.routes.push({
|
|
30
|
+
routeId,
|
|
31
|
+
path,
|
|
32
|
+
match: p => {
|
|
33
|
+
const result = matcher(p);
|
|
34
|
+
return result ? {
|
|
35
|
+
params: result.params ?? {}
|
|
36
|
+
} : false;
|
|
37
|
+
},
|
|
38
|
+
component,
|
|
39
|
+
controller,
|
|
40
|
+
options
|
|
41
|
+
});
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
addModal(path, mixedComponent, options) {
|
|
45
|
+
return this.addScreen(path, mixedComponent, {
|
|
46
|
+
...options,
|
|
47
|
+
stackPresentation: 'modal'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
getRoutes() {
|
|
51
|
+
return this.routes.slice();
|
|
52
|
+
}
|
|
53
|
+
getFirstRoute() {
|
|
54
|
+
return this.routes[0];
|
|
55
|
+
}
|
|
56
|
+
getDefaultOptions() {
|
|
57
|
+
return this.defaultOptions;
|
|
58
|
+
}
|
|
59
|
+
extractComponent(component) {
|
|
60
|
+
const componentWithController = component;
|
|
61
|
+
if (componentWithController?.component) {
|
|
62
|
+
return {
|
|
63
|
+
controller: componentWithController.controller,
|
|
64
|
+
component: componentWithController.component
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
component: component,
|
|
69
|
+
controller: undefined
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|