@idealyst/navigation 1.0.62 → 1.0.64

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,192 +0,0 @@
1
- import React from "react";
2
-
3
- import { createNativeStackNavigator } from "@react-navigation/native-stack";
4
- import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
5
- import { createDrawerNavigator } from "@react-navigation/drawer";
6
-
7
- import { RouteParam, ScreenOptions } from "./types"
8
- import { TypedNavigator } from "@react-navigation/native";
9
- import { Icon } from '@idealyst/components';
10
-
11
- export const buildRouter = (routeParam: RouteParam, path: string = '') => {
12
- return () => buildNativeRouter(routeParam, path)
13
- }
14
-
15
- /**
16
- * Convert ScreenOptions to React Navigation screen options
17
- */
18
- const convertScreenOptions = (screenOptions?: ScreenOptions) => {
19
- if (!screenOptions) return {};
20
-
21
- const options: any = {};
22
-
23
- if (screenOptions.title) {
24
- options.title = screenOptions.title;
25
- // Set default headerTitle to title, but allow headerTitle to override
26
- if (!screenOptions.headerTitle) {
27
- options.headerTitle = screenOptions.title;
28
- }
29
- }
30
-
31
- if (screenOptions.tabBarLabel) {
32
- options.tabBarLabel = screenOptions.tabBarLabel;
33
- }
34
-
35
- if (screenOptions.tabBarIcon) {
36
- if (typeof screenOptions.tabBarIcon === 'string') {
37
- options.tabBarIcon = ({ focused }: { focused: boolean; color: string; size: number }) => (
38
- <Icon
39
- name={screenOptions.tabBarIcon as any}
40
- color={focused ? 'primary' : 'secondary'}
41
- />
42
- );
43
- } else if (typeof screenOptions.tabBarIcon === 'function') {
44
- options.tabBarIcon = screenOptions.tabBarIcon
45
- } else {
46
- options.tabBarIcon = screenOptions.tabBarIcon;
47
- }
48
- }
49
-
50
- if (screenOptions.tabBarBadge !== undefined) {
51
- options.tabBarBadge = screenOptions.tabBarBadge;
52
- }
53
-
54
- if (screenOptions.tabBarVisible !== undefined) {
55
- options.tabBarStyle = screenOptions.tabBarVisible ? {} : { display: 'none' };
56
- }
57
-
58
- // headerTitle should override the default title in the header
59
- if (screenOptions.headerTitle) {
60
- options.headerTitle = screenOptions.headerTitle;
61
- }
62
-
63
- if (screenOptions.headerBackVisible !== undefined) {
64
- options.headerBackVisible = screenOptions.headerBackVisible;
65
- }
66
-
67
- if (screenOptions.headerLeft) {
68
- options.headerLeft = screenOptions.headerLeft;
69
- }
70
-
71
- if (screenOptions.headerRight) {
72
- options.headerRight = screenOptions.headerRight;
73
- }
74
-
75
- if (screenOptions.platformOptions?.native) {
76
- Object.assign(options, screenOptions.platformOptions.native);
77
- }
78
-
79
- return options;
80
- };
81
-
82
- /**
83
- * Create the router supporting React Navigation
84
- * @param routeParam
85
- * @param path
86
- * @param LastNavigator
87
- * @returns
88
- */
89
- const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNavigator?: TypedNavigator<any>): React.ReactElement => {
90
- const nextPath = (routeParam.path ? path + routeParam.path : path) || '';
91
- const type = routeParam.layout?.type;
92
- const screenOptions = convertScreenOptions(routeParam.screenOptions);
93
-
94
- function buildComponent() {
95
- switch (type) {
96
- case 'stack':
97
- const Stack = createNativeStackNavigator();
98
- return (
99
- <Stack.Navigator
100
- screenOptions={{
101
- // Disable screen optimization to ensure theme updates
102
- freezeOnBlur: false,
103
- }}
104
- >
105
- <Stack.Screen
106
- name={nextPath}
107
- component={routeParam.component}
108
- options={screenOptions}
109
- />
110
- {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Stack))}
111
- </Stack.Navigator>
112
- )
113
- case 'tab':
114
- const Tab = createBottomTabNavigator();
115
- return (
116
- <Tab.Navigator
117
- screenOptions={{
118
- // Disable screen optimization to ensure theme updates
119
- lazy: false,
120
- freezeOnBlur: false,
121
- }}
122
- >
123
- <Tab.Screen
124
- name={nextPath}
125
- component={routeParam.component}
126
- options={screenOptions}
127
- />
128
- {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Tab))}
129
- </Tab.Navigator>
130
- )
131
- case 'drawer':
132
- const Drawer = createDrawerNavigator();
133
- return (
134
- <Drawer.Navigator
135
- screenOptions={{
136
- // Disable screen optimization to ensure theme updates
137
- lazy: false,
138
- freezeOnBlur: false,
139
- }}
140
- >
141
- <Drawer.Screen
142
- name={nextPath}
143
- component={routeParam.component}
144
- options={screenOptions}
145
- />
146
- {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Drawer))}
147
- </Drawer.Navigator>
148
- )
149
- case 'modal':
150
- if (!LastNavigator) {
151
- throw new Error('LastNavigator is required for modal layout');
152
- }
153
- return (
154
- <>
155
- <LastNavigator.Screen
156
- options={{ headerShown: false, presentation: 'modal', ...screenOptions }}
157
- name={nextPath}
158
- component={routeParam.component}
159
- />
160
- {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, LastNavigator))}
161
- </>
162
- )
163
- case undefined:
164
- if (!LastNavigator) {
165
- throw new Error('LastNavigator is required for undefined layout - ' + routeParam.path);
166
- }
167
- return (
168
- <>
169
- <LastNavigator.Screen
170
- name={nextPath}
171
- component={routeParam.component}
172
- options={screenOptions}
173
- />
174
- {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, LastNavigator))}
175
- </>
176
- )
177
- default:
178
- throw new Error(`Unknown layout type: ${type}`);
179
- }
180
- }
181
- const Component = buildComponent();
182
- if (LastNavigator) {
183
- return (
184
- <LastNavigator.Screen
185
- name={nextPath}
186
- component={() => Component}
187
- options={screenOptions}
188
- />
189
- );
190
- }
191
- return Component;
192
- }
@@ -1,366 +0,0 @@
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';
6
-
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" />;
53
- }
54
-
55
- return null;
56
- };
57
-
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
- };
85
-
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;
99
- }
100
-
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
- };
133
-
134
- const activeTab = getActiveTab();
135
-
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
- };
224
-
225
- /**
226
- * Convert ScreenOptions to web-compatible props for layout components
227
- */
228
- const convertScreenOptionsForWeb = (screenOptions?: ScreenOptions) => {
229
- if (!screenOptions) return {};
230
-
231
- const webOptions: any = {};
232
-
233
- // Basic screen info
234
- if (screenOptions.title) {
235
- webOptions.title = screenOptions.title;
236
- }
237
-
238
- if (screenOptions.tabBarLabel) {
239
- webOptions.tabBarLabel = screenOptions.tabBarLabel;
240
- }
241
-
242
- if (screenOptions.tabBarIcon) {
243
- webOptions.tabBarIcon = screenOptions.tabBarIcon;
244
- }
245
-
246
- if (screenOptions.tabBarBadge !== undefined) {
247
- webOptions.tabBarBadge = screenOptions.tabBarBadge;
248
- }
249
-
250
- if (screenOptions.tabBarVisible !== undefined) {
251
- webOptions.tabBarVisible = screenOptions.tabBarVisible;
252
- }
253
-
254
- if (screenOptions.headerTitle) {
255
- webOptions.headerTitle = screenOptions.headerTitle;
256
- }
257
-
258
- if (screenOptions.headerBackVisible !== undefined) {
259
- webOptions.headerBackVisible = screenOptions.headerBackVisible;
260
- }
261
-
262
- if (screenOptions.headerLeft) {
263
- webOptions.headerLeft = screenOptions.headerLeft;
264
- }
265
-
266
- if (screenOptions.headerRight) {
267
- webOptions.headerRight = screenOptions.headerRight;
268
- }
269
-
270
- if (screenOptions.platformOptions?.web) {
271
- Object.assign(webOptions, screenOptions.platformOptions.web);
272
- }
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
-
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
- );
317
-
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
- );
356
-
357
- // Handle nested routes without layout
358
- if (routeParam.routes) {
359
- routeParam.routes.forEach(nestedRoute => {
360
- routes.push(...buildWebRoutes(nestedRoute, currentPath));
361
- });
362
- }
363
- }
364
-
365
- return routes;
366
- };
@@ -1,74 +0,0 @@
1
- import React from "react";
2
-
3
- export type ScreenOptions = {
4
- /**
5
- * Screen title for navigation headers
6
- */
7
- title?: string;
8
-
9
- /**
10
- * Icon component for tab/drawer navigation (React.ComponentType, React.ReactElement, function, or string)
11
- */
12
- tabBarIcon?: React.ComponentType<{ focused: boolean; color: string; size: number }>
13
- | React.ReactElement
14
- | ((props: { focused: boolean; color: string; size: string }) => React.ReactElement)
15
- | string;
16
-
17
- /**
18
- * Label for tab/drawer navigation
19
- */
20
- tabBarLabel?: string;
21
-
22
- /**
23
- * Badge for tab navigation
24
- */
25
- tabBarBadge?: string | number;
26
-
27
- /**
28
- * Whether to show the tab bar for this screen
29
- */
30
- tabBarVisible?: boolean;
31
-
32
- /**
33
- * Custom header title component or string
34
- */
35
- headerTitle?: React.ComponentType | React.ReactElement | string;
36
-
37
- /**
38
- * Custom header left component (overrides back button)
39
- */
40
- headerLeft?: React.ComponentType | React.ReactElement;
41
-
42
- /**
43
- * Whether to show header back button
44
- */
45
- headerBackVisible?: boolean;
46
-
47
- /**
48
- * Custom header right component
49
- */
50
- 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
- };
60
-
61
- export type RouteParam = {
62
- path?: string;
63
- routes?: RouteParam[];
64
- component: React.ComponentType;
65
- layout?: LayoutParam;
66
- screenOptions?: ScreenOptions;
67
- }
68
-
69
- export type LayoutType = 'stack' | 'tab' | 'drawer' | 'modal';
70
-
71
- export type LayoutParam = {
72
- type: LayoutType;
73
- component?: React.ComponentType<{ children?: React.ReactNode }>;
74
- }