@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.
@@ -1,366 +1,223 @@
1
- import React from "react";
2
- import { Routes, Route, Outlet, useLocation, useNavigate } from "react-router-dom";
3
- import { RouteParam, ScreenOptions } from "./types";
4
- import { GeneralLayout } from "../layouts/GeneralLayout";
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
- // Types for TabButton
8
- interface TabRoute {
9
- id: string;
10
- path: string;
11
- label: string;
12
- icon?: React.ComponentType<{ focused: boolean; color: string; size: string }>
13
- | React.ReactElement
14
- | ((props: { focused: boolean; color: string; size: string }) => React.ReactElement)
15
- | string;
16
- }
17
-
18
- interface TabButtonProps {
19
- tab: TabRoute;
20
- isActive: boolean;
21
- onPress: () => void;
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
- <Pressable onPress={onPress}>
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
- // Types for SimpleTabLayout
87
- interface SimpleTabLayoutProps {
88
- routeParam: RouteParam;
89
- webScreenOptions: {
90
- title?: string;
91
- headerTitle?: React.ComponentType | React.ReactElement | string;
92
- headerLeft?: React.ComponentType | React.ReactElement;
93
- headerRight?: React.ComponentType | React.ReactElement;
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
- // Simple Tab Layout Component
102
- const SimpleTabLayout: React.FC<SimpleTabLayoutProps> = ({ routeParam, webScreenOptions }) => {
103
- const location = useLocation();
104
- const navigate = useNavigate();
105
-
106
- // Build tab links from routes with screen options
107
- const tabRoutes: TabRoute[] = [
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 activeTab = getActiveTab();
45
+ const normalizedParent = parentPath === '/' ? '' : parentPath
46
+ const normalizedChild = normalizePath(childPath)
135
47
 
136
- // Helper to render header element (component, element, or string)
137
- const renderHeaderElement = (element: React.ComponentType | React.ReactElement | string | undefined) => {
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
- * Convert ScreenOptions to web-compatible props for layout components
52
+ * Check if current path matches a route, considering parent path
227
53
  */
228
- const convertScreenOptionsForWeb = (screenOptions?: ScreenOptions) => {
229
- if (!screenOptions) return {};
230
-
231
- const webOptions: any = {};
54
+ const pathMatches = (currentPath: string, routePath: string, parentPath: string): boolean => {
55
+ const fullRoutePath = buildFullPath(parentPath, routePath)
56
+ return currentPath === fullRoutePath
57
+ }
232
58
 
233
- // Basic screen info
234
- if (screenOptions.title) {
235
- webOptions.title = screenOptions.title;
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
- if (screenOptions.tabBarLabel) {
239
- webOptions.tabBarLabel = screenOptions.tabBarLabel;
240
- }
71
+ const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
241
72
 
242
- if (screenOptions.tabBarIcon) {
243
- webOptions.tabBarIcon = screenOptions.tabBarIcon;
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
- if (screenOptions.tabBarBadge !== undefined) {
247
- webOptions.tabBarBadge = screenOptions.tabBarBadge;
101
+ return currentRoute.component
248
102
  }
249
103
 
250
- if (screenOptions.tabBarVisible !== undefined) {
251
- webOptions.tabBarVisible = screenOptions.tabBarVisible;
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
- if (screenOptions.headerTitle) {
255
- webOptions.headerTitle = screenOptions.headerTitle;
256
- }
110
+ // Use custom layout component or default
111
+ const LayoutComponent = params.layoutComponent || DefaultTabLayout
257
112
 
258
- if (screenOptions.headerBackVisible !== undefined) {
259
- webOptions.headerBackVisible = screenOptions.headerBackVisible;
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
- if (screenOptions.headerLeft) {
263
- webOptions.headerLeft = screenOptions.headerLeft;
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
- if (screenOptions.headerRight) {
267
- webOptions.headerRight = screenOptions.headerRight;
268
- }
136
+ const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
269
137
 
270
- if (screenOptions.platformOptions?.web) {
271
- Object.assign(webOptions, screenOptions.platformOptions.web);
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
- // Handle layout wrapping
288
- const LayoutComponent = routeParam.layout?.component;
289
- const RouteComponent = routeParam.component;
290
- const webScreenOptions = convertScreenOptionsForWeb(routeParam.screenOptions);
291
- const isTabLayout = routeParam.layout?.type === 'tab';
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
- routes.push(layoutRoute);
319
- } else if (LayoutComponent && routeParam.routes) {
320
- // Create a wrapper component that renders the layout with Outlet and screen options
321
- const LayoutWrapper: React.FC = () => (
322
- <LayoutComponent {...webScreenOptions}>
323
- <Outlet />
324
- </LayoutComponent>
325
- );
326
-
327
- // Create parent route with layout
328
- const layoutRoute = (
329
- <Route
330
- key={`layout-${currentPath || 'root'}`}
331
- path={currentPath || '/'}
332
- element={<LayoutWrapper />}
333
- >
334
- {/* Add index route for the main component */}
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
- // Handle nested routes without layout
358
- if (routeParam.routes) {
359
- routeParam.routes.forEach(nestedRoute => {
360
- routes.push(...buildWebRoutes(nestedRoute, currentPath));
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
- return routes;
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
+ }
@@ -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
- * Additional platform-specific options
54
- */
55
- platformOptions?: {
56
- native?: Record<string, any>;
57
- web?: Record<string, any>;
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
- export type RouteParam = {
62
- path?: string;
63
- routes?: RouteParam[];
64
- component: React.ComponentType;
65
- layout?: LayoutParam;
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 LayoutType = 'stack' | 'tab' | 'drawer' | 'modal';
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 LayoutParam = {
72
- type: LayoutType;
73
- component?: React.ComponentType<{ children?: React.ReactNode }>;
74
- }
115
+ export type TabLayoutComponent = React.ComponentType<TabLayoutProps>
116
+ export type StackLayoutComponent = React.ComponentType<StackLayoutProps>
@@ -0,0 +1,2 @@
1
+ export * from './router.native';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export * from './router.web';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export * from './router.web';
2
+ export * from './types';