@idealyst/navigation 1.0.49 → 1.0.50

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.
@@ -4,13 +4,73 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
4
4
  import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
5
5
  import { createDrawerNavigator } from "@react-navigation/drawer";
6
6
 
7
- import { RouteParam } from "./types"
7
+ import { RouteParam, ScreenOptions } from "./types"
8
8
  import { TypedNavigator } from "@react-navigation/native";
9
+ import { Icon } from '@idealyst/components';
9
10
 
10
11
  export const buildRouter = (routeParam: RouteParam, path: string = '') => {
11
12
  return () => buildNativeRouter(routeParam, path)
12
13
  }
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
+ options.headerTitle = screenOptions.title;
26
+ }
27
+
28
+ if (screenOptions.tabBarLabel) {
29
+ options.tabBarLabel = screenOptions.tabBarLabel;
30
+ }
31
+
32
+ if (screenOptions.tabBarIcon) {
33
+ if (typeof screenOptions.tabBarIcon === 'string') {
34
+ options.tabBarIcon = ({ focused }: { focused: boolean; color: string; size: number }) => (
35
+ <Icon
36
+ name={screenOptions.tabBarIcon as any}
37
+ color={focused ? 'primary' : 'secondary'}
38
+ />
39
+ );
40
+ } else if (typeof screenOptions.tabBarIcon === 'function') {
41
+ options.tabBarIcon = screenOptions.tabBarIcon
42
+ } else {
43
+ options.tabBarIcon = screenOptions.tabBarIcon;
44
+ }
45
+ }
46
+
47
+ if (screenOptions.tabBarBadge !== undefined) {
48
+ options.tabBarBadge = screenOptions.tabBarBadge;
49
+ }
50
+
51
+ if (screenOptions.tabBarVisible !== undefined) {
52
+ options.tabBarStyle = screenOptions.tabBarVisible ? {} : { display: 'none' };
53
+ }
54
+
55
+ if (screenOptions.headerTitle) {
56
+ options.headerTitle = screenOptions.headerTitle;
57
+ }
58
+
59
+ if (screenOptions.headerBackVisible !== undefined) {
60
+ options.headerBackVisible = screenOptions.headerBackVisible;
61
+ }
62
+
63
+ if (screenOptions.headerRight) {
64
+ options.headerRight = screenOptions.headerRight;
65
+ }
66
+
67
+ if (screenOptions.platformOptions?.native) {
68
+ Object.assign(options, screenOptions.platformOptions.native);
69
+ }
70
+
71
+ return options;
72
+ };
73
+
14
74
  /**
15
75
  * Create the router supporting React Navigation
16
76
  * @param routeParam
@@ -21,7 +81,8 @@ export const buildRouter = (routeParam: RouteParam, path: string = '') => {
21
81
  const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNavigator?: TypedNavigator<any>): React.ReactElement => {
22
82
  const nextPath = (routeParam.path ? path + routeParam.path : path) || '';
23
83
  const type = routeParam.layout?.type;
24
- console.log('Registered routes', nextPath, routeParam.routes);
84
+ const screenOptions = convertScreenOptions(routeParam.screenOptions);
85
+
25
86
  switch (type) {
26
87
  case 'stack':
27
88
  const Stack = createNativeStackNavigator();
@@ -32,7 +93,11 @@ const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNaviga
32
93
  freezeOnBlur: false,
33
94
  }}
34
95
  >
35
- <Stack.Screen name={nextPath} component={routeParam.component} />
96
+ <Stack.Screen
97
+ name={nextPath}
98
+ component={routeParam.component}
99
+ options={screenOptions}
100
+ />
36
101
  {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Stack))}
37
102
  </Stack.Navigator>
38
103
  )
@@ -46,7 +111,11 @@ const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNaviga
46
111
  freezeOnBlur: false,
47
112
  }}
48
113
  >
49
- <Tab.Screen name={nextPath} component={routeParam.component} />
114
+ <Tab.Screen
115
+ name={nextPath}
116
+ component={routeParam.component}
117
+ options={screenOptions}
118
+ />
50
119
  {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Tab))}
51
120
  </Tab.Navigator>
52
121
  )
@@ -60,7 +129,11 @@ const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNaviga
60
129
  freezeOnBlur: false,
61
130
  }}
62
131
  >
63
- <Drawer.Screen name={nextPath} component={routeParam.component} />
132
+ <Drawer.Screen
133
+ name={nextPath}
134
+ component={routeParam.component}
135
+ options={screenOptions}
136
+ />
64
137
  {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, Drawer))}
65
138
  </Drawer.Navigator>
66
139
  )
@@ -70,7 +143,11 @@ const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNaviga
70
143
  }
71
144
  return (
72
145
  <>
73
- <LastNavigator.Screen options={{ headerShown: false, presentation: 'modal' }} name={nextPath} component={routeParam.component} />
146
+ <LastNavigator.Screen
147
+ options={{ headerShown: false, presentation: 'modal', ...screenOptions }}
148
+ name={nextPath}
149
+ component={routeParam.component}
150
+ />
74
151
  {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, LastNavigator))}
75
152
  </>
76
153
  )
@@ -80,7 +157,11 @@ const buildNativeRouter = (routeParam: RouteParam, path: string = '', LastNaviga
80
157
  }
81
158
  return (
82
159
  <>
83
- <LastNavigator.Screen name={nextPath} component={routeParam.component} />
160
+ <LastNavigator.Screen
161
+ name={nextPath}
162
+ component={routeParam.component}
163
+ options={screenOptions}
164
+ />
84
165
  {routeParam.routes?.map((route) => buildNativeRouter(route, nextPath, LastNavigator))}
85
166
  </>
86
167
  )
@@ -1,6 +1,217 @@
1
1
  import React from "react";
2
- import { Routes, Route, Outlet } from "react-router-dom";
3
- import { RouteParam } from "./types";
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
+ renderHeaderElement(webScreenOptions.headerLeft)
161
+ ) : (
162
+ renderHeaderElement(
163
+ webScreenOptions.headerTitle ||
164
+ webScreenOptions.title ||
165
+ 'Navigation'
166
+ )
167
+ )}
168
+ </View>
169
+
170
+ {/* Tab Navigation */}
171
+ <View style={{
172
+ flexDirection: 'row',
173
+ alignItems: 'center',
174
+ gap: 8,
175
+ }}>
176
+ {tabRoutes.map((tab) => (
177
+ <TabButton
178
+ key={tab.id}
179
+ tab={tab}
180
+ isActive={activeTab === tab.id}
181
+ onPress={() => {
182
+ let targetPath;
183
+ if (tab.id === 'home') {
184
+ targetPath = '/';
185
+ } else {
186
+ targetPath = `/${tab.path}`;
187
+ }
188
+ navigate(targetPath);
189
+ }}
190
+ />
191
+ ))}
192
+ </View>
193
+
194
+ {/* Right side */}
195
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', flex: 1 }}>
196
+ {renderHeaderElement(webScreenOptions.headerRight)}
197
+ </View>
198
+ </View>
199
+ );
200
+
201
+ return (
202
+ <GeneralLayout
203
+ header={{
204
+ enabled: true,
205
+ content: headerContent,
206
+ }}
207
+ sidebar={{
208
+ enabled: false,
209
+ }}
210
+ >
211
+ <Outlet />
212
+ </GeneralLayout>
213
+ );
214
+ };
4
215
 
5
216
  export const buildRouter = (routeParam: RouteParam, path: string = '') => {
6
217
  return () => (
@@ -10,6 +221,54 @@ export const buildRouter = (routeParam: RouteParam, path: string = '') => {
10
221
  );
11
222
  };
12
223
 
224
+ /**
225
+ * Convert ScreenOptions to web-compatible props for layout components
226
+ */
227
+ const convertScreenOptionsForWeb = (screenOptions?: ScreenOptions) => {
228
+ if (!screenOptions) return {};
229
+
230
+ const webOptions: any = {};
231
+
232
+ // Basic screen info
233
+ if (screenOptions.title) {
234
+ webOptions.title = screenOptions.title;
235
+ }
236
+
237
+ if (screenOptions.tabBarLabel) {
238
+ webOptions.tabBarLabel = screenOptions.tabBarLabel;
239
+ }
240
+
241
+ if (screenOptions.tabBarIcon) {
242
+ webOptions.tabBarIcon = screenOptions.tabBarIcon;
243
+ }
244
+
245
+ if (screenOptions.tabBarBadge !== undefined) {
246
+ webOptions.tabBarBadge = screenOptions.tabBarBadge;
247
+ }
248
+
249
+ if (screenOptions.tabBarVisible !== undefined) {
250
+ webOptions.tabBarVisible = screenOptions.tabBarVisible;
251
+ }
252
+
253
+ if (screenOptions.headerTitle) {
254
+ webOptions.headerTitle = screenOptions.headerTitle;
255
+ }
256
+
257
+ if (screenOptions.headerBackVisible !== undefined) {
258
+ webOptions.headerBackVisible = screenOptions.headerBackVisible;
259
+ }
260
+
261
+ if (screenOptions.headerRight) {
262
+ webOptions.headerRight = screenOptions.headerRight;
263
+ }
264
+
265
+ if (screenOptions.platformOptions?.web) {
266
+ Object.assign(webOptions, screenOptions.platformOptions.web);
267
+ }
268
+
269
+ return webOptions;
270
+ };
271
+
13
272
  /**
14
273
  * Create React Router routes from RouteParam configuration
15
274
  * @param routeParam The route parameter configuration
@@ -23,11 +282,39 @@ const buildWebRoutes = (routeParam: RouteParam, parentPath: string = ''): React.
23
282
  // Handle layout wrapping
24
283
  const LayoutComponent = routeParam.layout?.component;
25
284
  const RouteComponent = routeParam.component;
285
+ const webScreenOptions = convertScreenOptionsForWeb(routeParam.screenOptions);
286
+ const isTabLayout = routeParam.layout?.type === 'tab';
26
287
 
27
- if (LayoutComponent && routeParam.routes) {
28
- // Create a wrapper component that renders the layout with Outlet
288
+ if (isTabLayout && routeParam.routes) {
289
+ // Create simple header-based tab navigation using GeneralLayout
290
+ const SimpleTabLayoutWrapper: React.FC = () => {
291
+ return <SimpleTabLayout routeParam={routeParam} webScreenOptions={webScreenOptions} currentPath={currentPath} />;
292
+ };
293
+
294
+ // Create parent route with simple tab layout
295
+ const layoutRoute = (
296
+ <Route
297
+ key={`simple-tab-layout-${currentPath || 'root'}`}
298
+ path={currentPath || '/'}
299
+ element={<SimpleTabLayoutWrapper />}
300
+ >
301
+ {/* Add index route for the main component */}
302
+ <Route
303
+ index
304
+ element={<RouteComponent {...webScreenOptions} />}
305
+ />
306
+ {/* Add nested routes */}
307
+ {routeParam.routes.reduce((acc, nestedRoute) => {
308
+ return acc.concat(buildWebRoutes(nestedRoute, currentPath));
309
+ }, [] as React.ReactElement[])}
310
+ </Route>
311
+ );
312
+
313
+ routes.push(layoutRoute);
314
+ } else if (LayoutComponent && routeParam.routes) {
315
+ // Create a wrapper component that renders the layout with Outlet and screen options
29
316
  const LayoutWrapper: React.FC = () => (
30
- <LayoutComponent>
317
+ <LayoutComponent {...webScreenOptions}>
31
318
  <Outlet />
32
319
  </LayoutComponent>
33
320
  );
@@ -42,7 +329,7 @@ const buildWebRoutes = (routeParam: RouteParam, parentPath: string = ''): React.
42
329
  {/* Add index route for the main component */}
43
330
  <Route
44
331
  index
45
- element={<RouteComponent />}
332
+ element={<RouteComponent {...webScreenOptions} />}
46
333
  />
47
334
  {/* Add nested routes */}
48
335
  {routeParam.routes.reduce((acc, nestedRoute) => {
@@ -58,7 +345,7 @@ const buildWebRoutes = (routeParam: RouteParam, parentPath: string = ''): React.
58
345
  <Route
59
346
  key={currentPath || 'root'}
60
347
  path={currentPath || '/'}
61
- element={<RouteComponent />}
348
+ element={<RouteComponent {...webScreenOptions} />}
62
349
  />
63
350
  );
64
351
 
@@ -1,10 +1,69 @@
1
1
  import React from "react";
2
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
+
3
61
  export type RouteParam = {
4
62
  path?: string;
5
63
  routes?: RouteParam[];
6
64
  component: React.ComponentType;
7
65
  layout?: LayoutParam;
66
+ screenOptions?: ScreenOptions;
8
67
  }
9
68
 
10
69
  export type LayoutType = 'stack' | 'tab' | 'drawer' | 'modal';