@idealyst/navigation 1.0.61 → 1.0.62
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/package.json +3 -3
- package/src/context/NavigatorContext.native.tsx +5 -3
- package/src/context/NavigatorContext.web.tsx +14 -6
- package/src/context/types.ts +2 -2
- package/src/examples/CustomStackLayout.tsx +63 -0
- package/src/examples/CustomTabLayout.tsx +44 -0
- package/src/examples/ExampleHybridRouter.tsx +34 -32
- package/src/examples/ExampleStackRouter.tsx +22 -24
- package/src/examples/ExampleTabRouter.tsx +42 -37
- package/src/layouts/DefaultStackLayout.tsx +57 -0
- package/src/layouts/DefaultTabLayout.tsx +101 -0
- package/src/layouts/DefaultTabLayout.web.tsx +105 -0
- package/src/layouts/index.ts +4 -1
- package/src/routing/router.native.tsx +40 -179
- package/src/routing/router.web.tsx +192 -335
- package/src/routing/types.ts +62 -20
- package/src/routing_old/index.native.tsx +2 -0
- package/src/routing_old/index.ts +2 -0
- package/src/routing_old/index.web.tsx +2 -0
- package/src/routing_old/router.native.tsx +192 -0
- package/src/routing_old/router.web.tsx +366 -0
- package/src/routing_old/types.ts +74 -0
- /package/src/{routing → routing_old}/README.md +0 -0
|
@@ -1,366 +1,223 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { View, Text, Icon, Pressable } from '@idealyst/components';
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { NavigatorParam, RouteParam, TabNavigatorParam, StackNavigatorParam } from './types'
|
|
3
|
+
import { DefaultTabLayout } from '../layouts/DefaultTabLayout'
|
|
4
|
+
import { DefaultStackLayout } from '../layouts/DefaultStackLayout'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Tab Button Component
|
|
25
|
-
const TabButton: React.FC<TabButtonProps> = ({ tab, isActive, onPress }) => {
|
|
26
|
-
// Render icon - supports React elements, functions, and string names
|
|
27
|
-
const renderIcon = (icon: TabRoute['icon']) => {
|
|
28
|
-
if (!icon) return null;
|
|
29
|
-
|
|
30
|
-
if (typeof icon === 'function') {
|
|
31
|
-
// Function-based icon that receives state - pass explicit colors
|
|
32
|
-
const IconComponent = icon as (props: { focused: boolean; color: string; size: string }) => React.ReactElement;
|
|
33
|
-
return IconComponent({
|
|
34
|
-
focused: isActive,
|
|
35
|
-
color: isActive ? 'blue' : 'black.900',
|
|
36
|
-
size: 'sm'
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (React.isValidElement(icon)) {
|
|
41
|
-
return icon;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (typeof icon === 'string') {
|
|
45
|
-
// Fallback for string icons (though this breaks transpiler support)
|
|
46
|
-
return <Icon name={icon as any} size="md" color={isActive ? 'white' : 'secondary'} />;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Handle React.ComponentType
|
|
50
|
-
if (typeof icon === 'object' && 'type' in icon) {
|
|
51
|
-
const IconComponent = icon as React.ComponentType<{ focused: boolean; color: string; size: string }>;
|
|
52
|
-
return <IconComponent focused={isActive} color={isActive ? 'white' : 'secondary'} size="sm" />;
|
|
6
|
+
/**
|
|
7
|
+
* Build the Web navigator using custom layout components
|
|
8
|
+
* @param params Navigator configuration
|
|
9
|
+
* @param parentPath Parent route path for nested routing
|
|
10
|
+
* @returns React Router component
|
|
11
|
+
*/
|
|
12
|
+
export const buildNavigator = (params: NavigatorParam, parentPath = '') => {
|
|
13
|
+
const NavigatorComponent = () => {
|
|
14
|
+
switch (params.layout) {
|
|
15
|
+
case 'tab':
|
|
16
|
+
return <TabNavigator params={params} parentPath={parentPath} />
|
|
17
|
+
case 'stack':
|
|
18
|
+
return <StackNavigator params={params} parentPath={parentPath} />
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unsupported navigator layout: ${(params as any).layout}`)
|
|
53
21
|
}
|
|
54
|
-
|
|
55
|
-
return null;
|
|
56
|
-
};
|
|
22
|
+
}
|
|
57
23
|
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
<View
|
|
61
|
-
style={{
|
|
62
|
-
paddingHorizontal: 12,
|
|
63
|
-
paddingVertical: 8,
|
|
64
|
-
borderRadius: 6,
|
|
65
|
-
flexDirection: 'row',
|
|
66
|
-
alignItems: 'center',
|
|
67
|
-
}}
|
|
68
|
-
>
|
|
69
|
-
{tab.icon ? renderIcon(tab.icon) : null}
|
|
70
|
-
{tab.label && (
|
|
71
|
-
<Text
|
|
72
|
-
size="small"
|
|
73
|
-
color={isActive ? 'blue' : 'black.900'}
|
|
74
|
-
style={{
|
|
75
|
-
marginLeft: tab.icon ? 8 : 0,
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
{tab.label}
|
|
79
|
-
</Text>
|
|
80
|
-
)}
|
|
81
|
-
</View>
|
|
82
|
-
</Pressable>
|
|
83
|
-
);
|
|
84
|
-
};
|
|
24
|
+
return NavigatorComponent
|
|
25
|
+
}
|
|
85
26
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
tabBarLabel?: string;
|
|
95
|
-
tabBarIcon?: TabRoute['icon'];
|
|
96
|
-
[key: string]: any;
|
|
97
|
-
};
|
|
98
|
-
currentPath: string;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize path for navigation - convert empty string to '/'
|
|
29
|
+
*/
|
|
30
|
+
const normalizePath = (path: string): string => {
|
|
31
|
+
if (path === '' || path === '/') {
|
|
32
|
+
return '/'
|
|
33
|
+
}
|
|
34
|
+
return path.startsWith('/') ? path : `/${path}`
|
|
99
35
|
}
|
|
100
36
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Main route (home/index)
|
|
109
|
-
{
|
|
110
|
-
id: 'home',
|
|
111
|
-
path: '',
|
|
112
|
-
label: webScreenOptions.tabBarLabel || webScreenOptions.title || 'Home',
|
|
113
|
-
icon: webScreenOptions.tabBarIcon,
|
|
114
|
-
},
|
|
115
|
-
// Child routes
|
|
116
|
-
...routeParam.routes!.map((route): TabRoute => {
|
|
117
|
-
const routeOptions = convertScreenOptionsForWeb(route.screenOptions);
|
|
118
|
-
return {
|
|
119
|
-
id: route.path || '',
|
|
120
|
-
path: route.path || '',
|
|
121
|
-
label: routeOptions.tabBarLabel || routeOptions.title || route.path || '',
|
|
122
|
-
icon: routeOptions.tabBarIcon,
|
|
123
|
-
};
|
|
124
|
-
}),
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
// Determine active tab based on current location
|
|
128
|
-
const getActiveTab = () => {
|
|
129
|
-
const path = location.pathname.replace(/^\//, ''); // Remove leading slash
|
|
130
|
-
const activeTabId = path || 'home';
|
|
131
|
-
return activeTabId;
|
|
132
|
-
};
|
|
37
|
+
/**
|
|
38
|
+
* Build full path by combining parent path with child path
|
|
39
|
+
*/
|
|
40
|
+
const buildFullPath = (parentPath: string, childPath: string): string => {
|
|
41
|
+
if (childPath === '/') {
|
|
42
|
+
return parentPath || '/'
|
|
43
|
+
}
|
|
133
44
|
|
|
134
|
-
const
|
|
45
|
+
const normalizedParent = parentPath === '/' ? '' : parentPath
|
|
46
|
+
const normalizedChild = normalizePath(childPath)
|
|
135
47
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!element) return null;
|
|
139
|
-
if (React.isValidElement(element)) return element;
|
|
140
|
-
if (typeof element === 'string') return <Text size="large" weight="semibold">{element}</Text>;
|
|
141
|
-
if (typeof element === 'function') {
|
|
142
|
-
const Component = element as React.ComponentType;
|
|
143
|
-
return <Component />;
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Create simple header navigation
|
|
149
|
-
const headerContent = (
|
|
150
|
-
<View style={{
|
|
151
|
-
flexDirection: 'row',
|
|
152
|
-
alignItems: 'center',
|
|
153
|
-
justifyContent: 'space-between',
|
|
154
|
-
width: '100%',
|
|
155
|
-
flex: 1
|
|
156
|
-
}}>
|
|
157
|
-
{/* Left side */}
|
|
158
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
|
159
|
-
{webScreenOptions.headerLeft && (
|
|
160
|
-
<View style={{ marginRight: 12 }}>
|
|
161
|
-
{renderHeaderElement(webScreenOptions.headerLeft)}
|
|
162
|
-
</View>
|
|
163
|
-
)}
|
|
164
|
-
{renderHeaderElement(
|
|
165
|
-
webScreenOptions.headerTitle ||
|
|
166
|
-
webScreenOptions.title ||
|
|
167
|
-
'Navigation'
|
|
168
|
-
)}
|
|
169
|
-
</View>
|
|
170
|
-
|
|
171
|
-
{/* Tab Navigation */}
|
|
172
|
-
<View style={{
|
|
173
|
-
flexDirection: 'row',
|
|
174
|
-
alignItems: 'center',
|
|
175
|
-
gap: 8,
|
|
176
|
-
}}>
|
|
177
|
-
{tabRoutes.map((tab) => (
|
|
178
|
-
<TabButton
|
|
179
|
-
key={tab.id}
|
|
180
|
-
tab={tab}
|
|
181
|
-
isActive={activeTab === tab.id}
|
|
182
|
-
onPress={() => {
|
|
183
|
-
let targetPath;
|
|
184
|
-
if (tab.id === 'home') {
|
|
185
|
-
targetPath = '/';
|
|
186
|
-
} else {
|
|
187
|
-
targetPath = `/${tab.path}`;
|
|
188
|
-
}
|
|
189
|
-
navigate(targetPath);
|
|
190
|
-
}}
|
|
191
|
-
/>
|
|
192
|
-
))}
|
|
193
|
-
</View>
|
|
194
|
-
|
|
195
|
-
{/* Right side */}
|
|
196
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', flex: 1 }}>
|
|
197
|
-
{renderHeaderElement(webScreenOptions.headerRight)}
|
|
198
|
-
</View>
|
|
199
|
-
</View>
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<GeneralLayout
|
|
204
|
-
header={{
|
|
205
|
-
enabled: true,
|
|
206
|
-
content: headerContent,
|
|
207
|
-
}}
|
|
208
|
-
sidebar={{
|
|
209
|
-
enabled: false,
|
|
210
|
-
}}
|
|
211
|
-
>
|
|
212
|
-
<Outlet />
|
|
213
|
-
</GeneralLayout>
|
|
214
|
-
);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export const buildRouter = (routeParam: RouteParam, path: string = '') => {
|
|
218
|
-
return () => (
|
|
219
|
-
<Routes>
|
|
220
|
-
{buildWebRoutes(routeParam, path)}
|
|
221
|
-
</Routes>
|
|
222
|
-
);
|
|
223
|
-
};
|
|
48
|
+
return `${normalizedParent}${normalizedChild}`
|
|
49
|
+
}
|
|
224
50
|
|
|
225
51
|
/**
|
|
226
|
-
*
|
|
52
|
+
* Check if current path matches a route, considering parent path
|
|
227
53
|
*/
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
54
|
+
const pathMatches = (currentPath: string, routePath: string, parentPath: string): boolean => {
|
|
55
|
+
const fullRoutePath = buildFullPath(parentPath, routePath)
|
|
56
|
+
return currentPath === fullRoutePath
|
|
57
|
+
}
|
|
232
58
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Tab Navigator Component for web using custom layout components
|
|
61
|
+
*/
|
|
62
|
+
const TabNavigator: React.FC<{ params: TabNavigatorParam; parentPath: string }> = ({
|
|
63
|
+
params,
|
|
64
|
+
parentPath
|
|
65
|
+
}) => {
|
|
66
|
+
// Get current path from window location
|
|
67
|
+
const getCurrentPath = () => {
|
|
68
|
+
return window.location.pathname
|
|
236
69
|
}
|
|
237
70
|
|
|
238
|
-
|
|
239
|
-
webOptions.tabBarLabel = screenOptions.tabBarLabel;
|
|
240
|
-
}
|
|
71
|
+
const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
|
|
241
72
|
|
|
242
|
-
|
|
243
|
-
|
|
73
|
+
// Listen for navigation changes
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
const handlePopState = () => {
|
|
76
|
+
setCurrentPath(getCurrentPath())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
window.addEventListener('popstate', handlePopState)
|
|
80
|
+
return () => window.removeEventListener('popstate', handlePopState)
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
// Navigate function
|
|
84
|
+
const navigateToRoute = (routePath: string) => {
|
|
85
|
+
const fullPath = buildFullPath(parentPath, routePath)
|
|
86
|
+
window.history.pushState({}, '', fullPath)
|
|
87
|
+
setCurrentPath(fullPath)
|
|
244
88
|
}
|
|
89
|
+
|
|
90
|
+
// Get current route component
|
|
91
|
+
const getCurrentRoute = () => {
|
|
92
|
+
// Find matching route
|
|
93
|
+
const currentRoute = params.routes.find(route => {
|
|
94
|
+
return pathMatches(currentPath, route.path, parentPath)
|
|
95
|
+
}) || params.routes[0] // fallback to first route
|
|
96
|
+
|
|
97
|
+
if (!currentRoute || currentRoute.type !== 'screen') {
|
|
98
|
+
return () => React.createElement('div', {}, `Route not found: ${currentPath}`)
|
|
99
|
+
}
|
|
245
100
|
|
|
246
|
-
|
|
247
|
-
webOptions.tabBarBadge = screenOptions.tabBarBadge;
|
|
101
|
+
return currentRoute.component
|
|
248
102
|
}
|
|
249
103
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
104
|
+
// Transform routes to include full paths for layout component
|
|
105
|
+
const routesWithFullPaths = params.routes.map(route => ({
|
|
106
|
+
...route,
|
|
107
|
+
fullPath: buildFullPath(parentPath, route.path)
|
|
108
|
+
}))
|
|
253
109
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
110
|
+
// Use custom layout component or default
|
|
111
|
+
const LayoutComponent = params.layoutComponent || DefaultTabLayout
|
|
257
112
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
113
|
+
return (
|
|
114
|
+
<LayoutComponent
|
|
115
|
+
options={params.options}
|
|
116
|
+
routes={routesWithFullPaths}
|
|
117
|
+
ContentComponent={getCurrentRoute()}
|
|
118
|
+
onNavigate={navigateToRoute}
|
|
119
|
+
currentPath={currentPath}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
261
123
|
|
|
262
|
-
|
|
263
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Stack Navigator Component for web using custom layout components
|
|
126
|
+
*/
|
|
127
|
+
const StackNavigator: React.FC<{ params: StackNavigatorParam; parentPath: string }> = ({
|
|
128
|
+
params,
|
|
129
|
+
parentPath
|
|
130
|
+
}) => {
|
|
131
|
+
// Get current path from window location
|
|
132
|
+
const getCurrentPath = () => {
|
|
133
|
+
return window.location.pathname
|
|
264
134
|
}
|
|
265
135
|
|
|
266
|
-
|
|
267
|
-
webOptions.headerRight = screenOptions.headerRight;
|
|
268
|
-
}
|
|
136
|
+
const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
|
|
269
137
|
|
|
270
|
-
|
|
271
|
-
|
|
138
|
+
// Listen for navigation changes
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
const handlePopState = () => {
|
|
141
|
+
setCurrentPath(getCurrentPath())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
window.addEventListener('popstate', handlePopState)
|
|
145
|
+
return () => window.removeEventListener('popstate', handlePopState)
|
|
146
|
+
}, [])
|
|
147
|
+
|
|
148
|
+
// Navigate function
|
|
149
|
+
const navigateToRoute = (routePath: string) => {
|
|
150
|
+
const fullPath = buildFullPath(parentPath, routePath)
|
|
151
|
+
window.history.pushState({}, '', fullPath)
|
|
152
|
+
setCurrentPath(fullPath)
|
|
272
153
|
}
|
|
273
|
-
|
|
274
|
-
return webOptions;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Create React Router routes from RouteParam configuration
|
|
279
|
-
* @param routeParam The route parameter configuration
|
|
280
|
-
* @param parentPath The parent path for nested routes
|
|
281
|
-
* @returns Array of React Router Route elements
|
|
282
|
-
*/
|
|
283
|
-
const buildWebRoutes = (routeParam: RouteParam, parentPath: string = ''): React.ReactElement[] => {
|
|
284
|
-
const routes: React.ReactElement[] = [];
|
|
285
|
-
const currentPath = routeParam.path ? `${parentPath}${routeParam.path}` : parentPath;
|
|
286
154
|
|
|
287
|
-
//
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (isTabLayout && routeParam.routes) {
|
|
294
|
-
// Create simple header-based tab navigation using GeneralLayout
|
|
295
|
-
const SimpleTabLayoutWrapper: React.FC = () => {
|
|
296
|
-
return <SimpleTabLayout routeParam={routeParam} webScreenOptions={webScreenOptions} currentPath={currentPath} />;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Create parent route with simple tab layout
|
|
300
|
-
const layoutRoute = (
|
|
301
|
-
<Route
|
|
302
|
-
key={`simple-tab-layout-${currentPath || 'root'}`}
|
|
303
|
-
path={currentPath || '/'}
|
|
304
|
-
element={<SimpleTabLayoutWrapper />}
|
|
305
|
-
>
|
|
306
|
-
{/* Add index route for the main component */}
|
|
307
|
-
<Route
|
|
308
|
-
index
|
|
309
|
-
element={<RouteComponent {...webScreenOptions} />}
|
|
310
|
-
/>
|
|
311
|
-
{/* Add nested routes */}
|
|
312
|
-
{routeParam.routes.reduce((acc, nestedRoute) => {
|
|
313
|
-
return acc.concat(buildWebRoutes(nestedRoute, currentPath));
|
|
314
|
-
}, [] as React.ReactElement[])}
|
|
315
|
-
</Route>
|
|
316
|
-
);
|
|
155
|
+
// Get current route component
|
|
156
|
+
const getCurrentRoute = () => {
|
|
157
|
+
// Find matching route
|
|
158
|
+
const currentRoute = params.routes.find(route => {
|
|
159
|
+
return pathMatches(currentPath, route.path, parentPath)
|
|
160
|
+
})
|
|
317
161
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
<Route
|
|
336
|
-
index
|
|
337
|
-
element={<RouteComponent {...webScreenOptions} />}
|
|
338
|
-
/>
|
|
339
|
-
{/* Add nested routes */}
|
|
340
|
-
{routeParam.routes.reduce((acc, nestedRoute) => {
|
|
341
|
-
return acc.concat(buildWebRoutes(nestedRoute, currentPath));
|
|
342
|
-
}, [] as React.ReactElement[])}
|
|
343
|
-
</Route>
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
routes.push(layoutRoute);
|
|
347
|
-
} else {
|
|
348
|
-
// Simple route without layout
|
|
349
|
-
routes.push(
|
|
350
|
-
<Route
|
|
351
|
-
key={currentPath || 'root'}
|
|
352
|
-
path={currentPath || '/'}
|
|
353
|
-
element={<RouteComponent {...webScreenOptions} />}
|
|
354
|
-
/>
|
|
355
|
-
);
|
|
162
|
+
// If no exact match, check if current path starts with any navigator route
|
|
163
|
+
if (!currentRoute) {
|
|
164
|
+
const navigatorRoute = params.routes.find(route => {
|
|
165
|
+
if (route.type === 'navigator') {
|
|
166
|
+
const fullRoutePath = buildFullPath(parentPath, route.path)
|
|
167
|
+
return currentPath.startsWith(fullRoutePath)
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (navigatorRoute && navigatorRoute.type === 'navigator') {
|
|
173
|
+
return () => {
|
|
174
|
+
const NestedNavigator = buildNavigator(navigatorRoute, buildFullPath(parentPath, navigatorRoute.path))
|
|
175
|
+
return React.createElement(NestedNavigator)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
356
179
|
|
|
357
|
-
//
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
180
|
+
// Fallback to first route if no match
|
|
181
|
+
if (!currentRoute) {
|
|
182
|
+
const fallbackRoute = params.routes[0]
|
|
183
|
+
if (fallbackRoute.type === 'screen') {
|
|
184
|
+
return fallbackRoute.component
|
|
185
|
+
} else if (fallbackRoute.type === 'navigator') {
|
|
186
|
+
const NestedNavigator = buildNavigator(fallbackRoute, buildFullPath(parentPath, fallbackRoute.path))
|
|
187
|
+
return () => React.createElement(NestedNavigator)
|
|
188
|
+
}
|
|
362
189
|
}
|
|
190
|
+
|
|
191
|
+
if (currentRoute) {
|
|
192
|
+
if (currentRoute.type === 'screen') {
|
|
193
|
+
return currentRoute.component
|
|
194
|
+
} else if (currentRoute.type === 'navigator') {
|
|
195
|
+
// Nested navigator
|
|
196
|
+
const NestedNavigator = buildNavigator(currentRoute, buildFullPath(parentPath, currentRoute.path))
|
|
197
|
+
return () => React.createElement(NestedNavigator)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If all else fails, return a not found component
|
|
202
|
+
return () => React.createElement('div', {}, `Route not found: ${currentPath}`)
|
|
363
203
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
204
|
+
|
|
205
|
+
// Transform routes to include full paths for layout component
|
|
206
|
+
const routesWithFullPaths = params.routes.map(route => ({
|
|
207
|
+
...route,
|
|
208
|
+
fullPath: buildFullPath(parentPath, route.path)
|
|
209
|
+
}))
|
|
210
|
+
|
|
211
|
+
// Use custom layout component or default
|
|
212
|
+
const LayoutComponent = params.layoutComponent || DefaultStackLayout
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<LayoutComponent
|
|
216
|
+
options={params.options}
|
|
217
|
+
routes={routesWithFullPaths}
|
|
218
|
+
ContentComponent={getCurrentRoute()}
|
|
219
|
+
onNavigate={navigateToRoute}
|
|
220
|
+
currentPath={currentPath}
|
|
221
|
+
/>
|
|
222
|
+
)
|
|
223
|
+
}
|
package/src/routing/types.ts
CHANGED
|
@@ -6,6 +6,12 @@ export type ScreenOptions = {
|
|
|
6
6
|
*/
|
|
7
7
|
title?: string;
|
|
8
8
|
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tab bar specific screen options
|
|
13
|
+
*/
|
|
14
|
+
export type TabBarScreenOptions = {
|
|
9
15
|
/**
|
|
10
16
|
* Icon component for tab/drawer navigation (React.ComponentType, React.ReactElement, function, or string)
|
|
11
17
|
*/
|
|
@@ -28,6 +34,10 @@ export type ScreenOptions = {
|
|
|
28
34
|
* Whether to show the tab bar for this screen
|
|
29
35
|
*/
|
|
30
36
|
tabBarVisible?: boolean;
|
|
37
|
+
} & ScreenOptions
|
|
38
|
+
|
|
39
|
+
export type NavigatorOptions = {
|
|
40
|
+
|
|
31
41
|
|
|
32
42
|
/**
|
|
33
43
|
* Custom header title component or string
|
|
@@ -48,27 +58,59 @@ export type ScreenOptions = {
|
|
|
48
58
|
* Custom header right component
|
|
49
59
|
*/
|
|
50
60
|
headerRight?: React.ComponentType | React.ReactElement;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type BaseNavigatorParam = {
|
|
64
|
+
path: string
|
|
65
|
+
type: 'navigator'
|
|
66
|
+
options?: NavigatorOptions
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type TabNavigatorParam = {
|
|
70
|
+
layout: 'tab'
|
|
71
|
+
routes: RouteParam<TabBarScreenOptions>[]
|
|
72
|
+
layoutComponent?: TabLayoutComponent
|
|
73
|
+
} & BaseNavigatorParam
|
|
74
|
+
|
|
75
|
+
export type StackNavigatorParam = {
|
|
76
|
+
layout: 'stack'
|
|
77
|
+
routes: RouteParam<ScreenOptions>[]
|
|
78
|
+
layoutComponent?: StackLayoutComponent
|
|
79
|
+
} & BaseNavigatorParam
|
|
80
|
+
|
|
81
|
+
export type NavigatorParam = TabNavigatorParam | StackNavigatorParam
|
|
82
|
+
|
|
83
|
+
export type ScreenParam<T = ScreenOptions> = {
|
|
84
|
+
path: string
|
|
85
|
+
type: 'screen'
|
|
86
|
+
options?: T
|
|
87
|
+
component: React.ComponentType
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type RouteParam<T = ScreenOptions> = NavigatorParam | ScreenParam<T>;
|
|
60
91
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
screenOptions?: ScreenOptions;
|
|
92
|
+
/**
|
|
93
|
+
* Extended route type with full path for layout components
|
|
94
|
+
*/
|
|
95
|
+
export type RouteWithFullPath<T = ScreenOptions> = RouteParam<T> & {
|
|
96
|
+
fullPath: string
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
export type
|
|
99
|
+
export type TabLayoutProps = {
|
|
100
|
+
options?: NavigatorOptions
|
|
101
|
+
routes: RouteWithFullPath<TabBarScreenOptions>[]
|
|
102
|
+
ContentComponent: React.ComponentType
|
|
103
|
+
onNavigate: (path: string) => void
|
|
104
|
+
currentPath: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type StackLayoutProps = {
|
|
108
|
+
options?: NavigatorOptions
|
|
109
|
+
routes: RouteWithFullPath<ScreenOptions>[]
|
|
110
|
+
ContentComponent: React.ComponentType
|
|
111
|
+
onNavigate: (path: string) => void
|
|
112
|
+
currentPath: string
|
|
113
|
+
}
|
|
70
114
|
|
|
71
|
-
export type
|
|
72
|
-
|
|
73
|
-
component?: React.ComponentType<{ children?: React.ReactNode }>;
|
|
74
|
-
}
|
|
115
|
+
export type TabLayoutComponent = React.ComponentType<TabLayoutProps>
|
|
116
|
+
export type StackLayoutComponent = React.ComponentType<StackLayoutProps>
|