@idealyst/navigation 1.1.6 → 1.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/navigation",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Cross-platform navigation library for React and React Native",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -43,8 +43,10 @@
43
43
  "publish:npm": "npm publish"
44
44
  },
45
45
  "peerDependencies": {
46
- "@idealyst/components": "^1.1.6",
47
- "@idealyst/theme": "^1.1.6",
46
+ "@idealyst/camera": "^1.1.6",
47
+ "@idealyst/components": "^1.1.8",
48
+ "@idealyst/microphone": "^1.1.7",
49
+ "@idealyst/theme": "^1.1.8",
48
50
  "@react-navigation/bottom-tabs": ">=7.0.0",
49
51
  "@react-navigation/drawer": ">=7.0.0",
50
52
  "@react-navigation/native": ">=7.0.0",
@@ -59,11 +61,21 @@
59
61
  "react-router": ">=6.0.0",
60
62
  "react-router-dom": ">=6.0.0"
61
63
  },
64
+ "peerDependenciesMeta": {
65
+ "@idealyst/camera": {
66
+ "optional": true
67
+ },
68
+ "@idealyst/microphone": {
69
+ "optional": true
70
+ }
71
+ },
62
72
  "devDependencies": {
63
- "@idealyst/components": "^1.1.6",
73
+ "@idealyst/camera": "^1.1.6",
74
+ "@idealyst/components": "^1.1.8",
64
75
  "@idealyst/datagrid": "^1.0.93",
65
76
  "@idealyst/datepicker": "^1.0.93",
66
- "@idealyst/theme": "^1.1.6",
77
+ "@idealyst/microphone": "^1.1.7",
78
+ "@idealyst/theme": "^1.1.8",
67
79
  "@types/react": "^19.1.8",
68
80
  "@types/react-dom": "^19.1.6",
69
81
  "react": "^19.1.0",
@@ -1,6 +1,6 @@
1
1
  import React, { createContext, memo, useContext, useMemo } from 'react';
2
2
  import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
3
- import { useNavigation, DarkTheme, DefaultTheme, NavigationContainer, CommonActions } from '@react-navigation/native';
3
+ import { useNavigation, DarkTheme, DefaultTheme, NavigationContainer, CommonActions, StackActions } from '@react-navigation/native';
4
4
  import { buildNavigator, NavigatorParam, NOT_FOUND_SCREEN_NAME } from '../routing';
5
5
  import { useUnistyles } from 'react-native-unistyles';
6
6
 
@@ -89,6 +89,29 @@ function hasNotFoundComponent(route: NavigatorParam): boolean {
89
89
  return false;
90
90
  }
91
91
 
92
+ /**
93
+ * Normalize a path and substitute variables
94
+ */
95
+ function normalizePath(path: string, vars?: Record<string, string>): string {
96
+ let normalizedPath = path;
97
+
98
+ // Convert empty string to '/'
99
+ if (normalizedPath === '' || normalizedPath === '/') {
100
+ normalizedPath = '/';
101
+ } else if (!normalizedPath.startsWith('/')) {
102
+ normalizedPath = `/${normalizedPath}`;
103
+ }
104
+
105
+ // Substitute variables in the path if provided
106
+ if (vars) {
107
+ Object.entries(vars).forEach(([key, value]) => {
108
+ normalizedPath = normalizedPath.replace(`:${key}`, value);
109
+ });
110
+ }
111
+
112
+ return normalizedPath;
113
+ }
114
+
92
115
  // Utility function to parse path with parameters and find matching route
93
116
  const parseParameterizedPath = (path: string, rootRoute: any): { routeName: string, params: Record<string, string> } | null => {
94
117
  // Handle absolute paths like /event/123
@@ -103,35 +126,40 @@ const parseParameterizedPath = (path: string, rootRoute: any): { routeName: stri
103
126
  const cleanPath = route.path.startsWith('/') ? route.path.slice(1) : route.path;
104
127
  const fullRoutePath = pathPrefix ? `${pathPrefix}/${cleanPath}` : route.path;
105
128
 
106
- if (routeSegments.length === segments.length - startIndex) {
107
- let isMatch = true;
108
- const extractedParams: Record<string, string> = {};
109
-
129
+ // Check if route segments match the path segments at current position
130
+ const remainingSegments = segments.length - startIndex;
131
+
132
+ // Check if this route's segments match as a prefix (for nested routes) or exactly
133
+ let prefixMatches = routeSegments.length <= remainingSegments;
134
+ const extractedParams: Record<string, string> = {};
135
+
136
+ if (prefixMatches) {
110
137
  for (let i = 0; i < routeSegments.length; i++) {
111
138
  const routeSegment = routeSegments[i];
112
139
  const pathSegment = segments[startIndex + i];
113
-
140
+
114
141
  if (routeSegment.startsWith(':')) {
115
142
  // Parameter segment - extract value
116
143
  const paramName = routeSegment.slice(1);
117
144
  extractedParams[paramName] = pathSegment;
118
145
  } else if (routeSegment !== pathSegment) {
119
146
  // Literal segment must match exactly
120
- isMatch = false;
147
+ prefixMatches = false;
121
148
  break;
122
149
  }
123
150
  }
124
-
125
- if (isMatch) {
126
- return { route, params: extractedParams, fullPath: fullRoutePath };
127
- }
128
151
  }
129
-
130
- // Check nested routes
131
- if (route.routes) {
152
+
153
+ // Exact match - route segments consume all remaining path segments
154
+ if (prefixMatches && routeSegments.length === remainingSegments) {
155
+ return { route, params: extractedParams, fullPath: fullRoutePath };
156
+ }
157
+
158
+ // Check nested routes ONLY if this route's path is a prefix of the target path
159
+ if (prefixMatches && route.routes) {
132
160
  const nestedMatch = findMatchingRoute(
133
- route.routes,
134
- segments,
161
+ route.routes,
162
+ segments,
135
163
  startIndex + routeSegments.length,
136
164
  fullRoutePath
137
165
  );
@@ -166,14 +194,17 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
166
194
  return;
167
195
  }
168
196
 
197
+ // Normalize path and substitute variables (e.g., /visit/:id with vars { id: "123" } becomes /visit/123)
198
+ const normalizedPath = normalizePath(params.path, params.vars);
199
+
169
200
  // Parse parameterized path for mobile
170
- const parsed = parseParameterizedPath(params.path, route);
201
+ const parsed = parseParameterizedPath(normalizedPath, route);
171
202
 
172
203
  if (!parsed) {
173
204
  // Invalid route - try to find a handler
174
- const handler = findInvalidRouteHandler(route, params.path);
205
+ const handler = findInvalidRouteHandler(route, normalizedPath);
175
206
  if (handler) {
176
- const redirectParams = handler(params.path);
207
+ const redirectParams = handler(normalizedPath);
177
208
  if (redirectParams) {
178
209
  // Handler returned NavigateParams - redirect
179
210
  return navigate(
@@ -187,14 +218,13 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
187
218
  // Navigate to 404 screen if configured
188
219
  if (route.notFoundComponent) {
189
220
  navigation.navigate(NOT_FOUND_SCREEN_NAME as never, {
190
- path: params.path,
191
- params: params.vars
221
+ path: normalizedPath,
192
222
  } as never);
193
223
  return;
194
224
  }
195
225
 
196
226
  // No handler and no 404 screen - log warning
197
- console.warn(`Navigation: Invalid route "${params.path}" and no handler or notFoundComponent configured.`);
227
+ console.warn(`Navigation: Invalid route "${normalizedPath}" and no handler or notFoundComponent configured.`);
198
228
  return;
199
229
  }
200
230
 
@@ -205,12 +235,9 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
205
235
  };
206
236
 
207
237
  if (params.replace) {
208
- // Use CommonActions.reset to replace the current route
238
+ // Use StackActions.replace to replace the current screen in the stack
209
239
  navigation.dispatch(
210
- CommonActions.reset({
211
- index: 0,
212
- routes: [{ name: parsed.routeName, params: navigationParams }],
213
- })
240
+ StackActions.replace(parsed.routeName, navigationParams)
214
241
  );
215
242
  } else {
216
243
  // Navigate to the pattern route with extracted parameters
@@ -218,6 +245,30 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
218
245
  }
219
246
  };
220
247
 
248
+ const replace = (params: Omit<NavigateParams, 'replace'>) => {
249
+ // Normalize path and substitute variables
250
+ const normalizedPath = normalizePath(params.path, params.vars);
251
+
252
+ // Parse parameterized path for mobile
253
+ const parsed = parseParameterizedPath(normalizedPath, route);
254
+
255
+ if (!parsed) {
256
+ console.warn(`Navigation: Cannot replace to invalid route "${normalizedPath}".`);
257
+ return;
258
+ }
259
+
260
+ // Merge route params with navigation state
261
+ const navigationParams = {
262
+ ...parsed.params,
263
+ ...(params.state || {}),
264
+ };
265
+
266
+ // Use StackActions.replace to replace the current screen
267
+ navigation.dispatch(
268
+ StackActions.replace(parsed.routeName, navigationParams)
269
+ );
270
+ };
271
+
221
272
  const RouteComponent = useMemo(() => {
222
273
  // Memoize the navigator to prevent unnecessary re-renders
223
274
  return memo(buildNavigator(route));
@@ -235,6 +286,7 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
235
286
  <NavigatorContext.Provider value={{
236
287
  route,
237
288
  navigate,
289
+ replace,
238
290
  canGoBack,
239
291
  goBack,
240
292
  }}>
@@ -245,7 +297,7 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
245
297
 
246
298
  const NavigatorProvider = ({ route }: NavigatorProviderProps) => {
247
299
  const {rt} = useUnistyles()
248
-
300
+
249
301
  const isDarkMode = rt.themeName === 'dark';
250
302
 
251
303
  return (
@@ -264,14 +316,17 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
264
316
  return;
265
317
  }
266
318
 
319
+ // Normalize path and substitute variables (e.g., /visit/:id with vars { id: "123" } becomes /visit/123)
320
+ const normalizedPath = normalizePath(params.path, params.vars);
321
+
267
322
  // Parse parameterized path for mobile
268
- const parsed = parseParameterizedPath(params.path, route);
323
+ const parsed = parseParameterizedPath(normalizedPath, route);
269
324
 
270
325
  if (!parsed) {
271
326
  // Invalid route - try to find a handler
272
- const handler = findInvalidRouteHandler(route, params.path);
327
+ const handler = findInvalidRouteHandler(route, normalizedPath);
273
328
  if (handler) {
274
- const redirectParams = handler(params.path);
329
+ const redirectParams = handler(normalizedPath);
275
330
  if (redirectParams) {
276
331
  // Handler returned NavigateParams - redirect
277
332
  return navigate(
@@ -285,14 +340,13 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
285
340
  // Navigate to 404 screen if configured
286
341
  if (route.notFoundComponent) {
287
342
  navigation.navigate(NOT_FOUND_SCREEN_NAME as never, {
288
- path: params.path,
289
- params: params.vars
343
+ path: normalizedPath,
290
344
  } as never);
291
345
  return;
292
346
  }
293
347
 
294
348
  // No handler and no 404 screen - log warning
295
- console.warn(`Navigation: Invalid route "${params.path}" and no handler or notFoundComponent configured.`);
349
+ console.warn(`Navigation: Invalid route "${normalizedPath}" and no handler or notFoundComponent configured.`);
296
350
  return;
297
351
  }
298
352
 
@@ -303,12 +357,9 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
303
357
  };
304
358
 
305
359
  if (params.replace) {
306
- // Use CommonActions.reset to replace the current route
360
+ // Use StackActions.replace to replace the current screen in the stack
307
361
  navigation.dispatch(
308
- CommonActions.reset({
309
- index: 0,
310
- routes: [{ name: parsed.routeName, params: navigationParams }],
311
- })
362
+ StackActions.replace(parsed.routeName, navigationParams)
312
363
  );
313
364
  } else {
314
365
  // Navigate to the pattern route with extracted parameters
@@ -316,6 +367,30 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
316
367
  }
317
368
  };
318
369
 
370
+ const replace = (params: Omit<NavigateParams, 'replace'>) => {
371
+ // Normalize path and substitute variables
372
+ const normalizedPath = normalizePath(params.path, params.vars);
373
+
374
+ // Parse parameterized path for mobile
375
+ const parsed = parseParameterizedPath(normalizedPath, route);
376
+
377
+ if (!parsed) {
378
+ console.warn(`Navigation: Cannot replace to invalid route "${normalizedPath}".`);
379
+ return;
380
+ }
381
+
382
+ // Merge route params with navigation state
383
+ const navigationParams = {
384
+ ...parsed.params,
385
+ ...(params.state || {}),
386
+ };
387
+
388
+ // Use StackActions.replace to replace the current screen
389
+ navigation.dispatch(
390
+ StackActions.replace(parsed.routeName, navigationParams)
391
+ );
392
+ };
393
+
319
394
  const canGoBack = () => navigation.canGoBack();
320
395
 
321
396
  const goBack = () => {
@@ -325,7 +400,7 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
325
400
  };
326
401
 
327
402
  return (
328
- <DrawerNavigatorContext.Provider value={{ navigate, route, canGoBack, goBack }}>
403
+ <DrawerNavigatorContext.Provider value={{ navigate, replace, route, canGoBack, goBack }}>
329
404
  {children}
330
405
  </DrawerNavigatorContext.Provider>
331
406
  );
@@ -5,6 +5,7 @@ import { buildNavigator, NavigatorParam } from '../routing';
5
5
 
6
6
  const NavigatorContext = createContext<NavigatorContextValue>({
7
7
  navigate: () => {},
8
+ replace: () => {},
8
9
  route: undefined,
9
10
  canGoBack: () => false,
10
11
  goBack: () => {},
@@ -278,10 +279,16 @@ export const NavigatorProvider = ({
278
279
  }
279
280
  };
280
281
 
282
+ const replace = (params: Omit<NavigateParams, 'replace'>) => {
283
+ // On web, replace just delegates to navigate (no special handling needed)
284
+ navigateFunction(params);
285
+ };
286
+
281
287
  return (
282
288
  <NavigatorContext.Provider value={{
283
289
  route,
284
290
  navigate: navigateFunction,
291
+ replace,
285
292
  canGoBack,
286
293
  goBack,
287
294
  }}>
@@ -34,6 +34,13 @@ export type NavigatorProviderProps = {
34
34
  export type NavigatorContextValue = {
35
35
  route: NavigatorParam | undefined;
36
36
  navigate: (params: NavigateParams) => void;
37
+ /**
38
+ * Replace the current screen with a new one. The current screen unmounts
39
+ * and the new screen takes its place in the navigation stack.
40
+ * On native, this uses StackActions.replace() to swap the current screen.
41
+ * On web, this behaves the same as navigate (no special handling needed).
42
+ */
43
+ replace: (params: Omit<NavigateParams, 'replace'>) => void;
37
44
  /**
38
45
  * Returns true if there is a parent route in the route hierarchy to navigate back to.
39
46
  * On web, this checks for parent routes (not browser history).
@@ -2,6 +2,8 @@ import React from 'react';
2
2
  import { AvatarExamples, BadgeExamples, ButtonExamples, CardExamples, CheckboxExamples, DialogExamples, DividerExamples, IconExamples, InputExamples, LinkExamples, PopoverExamples, ScreenExamples, SelectExamples, SliderExamples, SVGImageExamples, TextExamples, ViewExamples, ThemeExtensionExamples, SwitchExamples, RadioButtonExamples, ProgressExamples, TextAreaExamples, TabBarExamples, TooltipExamples, AccordionExamples, ListExamples, TableExamples, MenuExamples, ImageExamples, VideoExamples, AlertExamples, SkeletonExamples, ChipExamples, BreadcrumbExamples } from '@idealyst/components/examples';
3
3
  import { DataGridShowcase } from '@idealyst/datagrid/examples';
4
4
  import { DatePickerExamples } from '@idealyst/datepicker/examples';
5
+ import { CameraExamples } from '@idealyst/camera/examples';
6
+ import { MicrophoneExamples } from '@idealyst/microphone/examples';
5
7
  import { Text, View, Card, Screen, Icon, Button } from '@idealyst/components';
6
8
  import { NavigatorParam, RouteParam, NotFoundComponentProps } from '../routing';
7
9
  import { ExampleWebLayout } from './ExampleWebLayout';
@@ -329,6 +331,8 @@ const ExampleNavigationRouter: NavigatorParam = {
329
331
  { path: "breadcrumb", type: 'screen', component: BreadcrumbExamples, options: { title: "Breadcrumb" } },
330
332
  { path: "image", type: 'screen', component: ImageExamples, options: { title: "Image" } },
331
333
  { path: "video", type: 'screen', component: VideoExamples, options: { title: "Video" } },
334
+ { path: "camera", type: 'screen', component: CameraExamples, options: { title: "Camera" } },
335
+ { path: "microphone", type: 'screen', component: MicrophoneExamples, options: { title: "Microphone" } },
332
336
  { path: "datagrid", type: 'screen', component: DataGridShowcase, options: { title: "Data Grid" } },
333
337
  { path: "datepicker", type: 'screen', component: DatePickerExamples, options: { title: "Date Picker" } },
334
338
  { path: "theme-extension", type: 'screen', component: ThemeExtensionExamples, options: { title: "Theme Extension" } },
@@ -77,6 +77,8 @@ const componentGroups: ComponentGroup[] = [
77
77
  { label: 'Image', path: '/image', icon: 'image' },
78
78
  { label: 'SVG Image', path: '/svg-image', icon: 'image' },
79
79
  { label: 'Video', path: '/video', icon: 'video' },
80
+ { label: 'Camera', path: '/camera', icon: 'camera' },
81
+ { label: 'Microphone', path: '/microphone', icon: 'microphone' },
80
82
  ],
81
83
  },
82
84
  {
@@ -91,6 +93,7 @@ const componentGroups: ComponentGroup[] = [
91
93
  title: 'Theme',
92
94
  items: [
93
95
  { label: 'Theme Extension', path: '/theme-extension', icon: 'palette' },
96
+ { label: 'StyleBuilder Test', path: '/stylebuilder-test', icon: 'test-tube' },
94
97
  ],
95
98
  },
96
99
  ];
@@ -13,10 +13,11 @@ export const ExampleWebLayout: React.FC = () => {
13
13
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
14
14
  const [showSearch, setShowSearch] = useState(false);
15
15
  const currentTheme = UnistylesRuntime.themeName || 'light';
16
- const { theme } = useUnistyles();
17
16
 
17
+ const { theme } = useUnistyles();
18
+
18
19
  const cycleTheme = () => {
19
- const nextTheme = getNextTheme(currentTheme);
20
+ const nextTheme = UnistylesRuntime.themeName === 'light' ? 'dark' : 'light';
20
21
  UnistylesRuntime.setTheme(nextTheme as any);
21
22
  };
22
23
 
@@ -4,13 +4,14 @@ import { UnistylesRuntime } from 'react-native-unistyles';
4
4
  import ExampleSearchDialog from './ExampleSearchDialog';
5
5
 
6
6
  export default function HeaderRight() {
7
- const [isDark, setIsDark] = useState(false);
7
+ // const [isDark, setIsDark] = useState(false);
8
8
  const [showDialog, setShowDialog] = useState(false);
9
+ const isDark = false
9
10
 
10
11
  const toggleTheme = () => {
11
- const newTheme = isDark ? 'light' : 'dark';
12
+ const newTheme = UnistylesRuntime.themeName === 'light' ? 'dark' : 'light';
12
13
  UnistylesRuntime.setTheme(newTheme);
13
- setIsDark(!isDark);
14
+ // setIsDark(!isDark);
14
15
  };
15
16
 
16
17
  return (
@@ -7,73 +7,7 @@ import { createDrawerNavigator } from "@react-navigation/drawer";
7
7
  import { DrawerContentWrapper } from './DrawerContentWrapper.native';
8
8
  import { HeaderWrapper } from './HeaderWrapper.native';
9
9
  import React from 'react';
10
- import { useUnistyles } from 'react-native-unistyles';
11
- import { useIsFocused } from '@react-navigation/native';
12
10
 
13
- /**
14
- * Wrapper that makes screen components reactive to theme changes
15
- * Only updates when the screen is focused
16
- */
17
- const ThemeAwareScreenWrapper: React.FC<{
18
- Component: React.ComponentType<any>;
19
- [key: string]: any;
20
- }> = ({ Component, ...props }) => {
21
- const isFocused = useIsFocused();
22
-
23
- // Force update mechanism
24
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
25
-
26
- // Subscribe to theme changes
27
- const { rt } = useUnistyles();
28
-
29
- // Force re-render when theme changes (only when focused)
30
- React.useEffect(() => {
31
- if (isFocused) {
32
- console.log('[ThemeAwareScreenWrapper] Theme changed, forcing update. New theme:', rt.themeName);
33
- forceUpdate();
34
- }
35
- }, [rt.themeName, isFocused]);
36
-
37
- // Log when component renders
38
- React.useEffect(() => {
39
- if (isFocused) {
40
- console.log('[ThemeAwareScreenWrapper] Screen rendered with theme:', rt.themeName);
41
- }
42
- });
43
-
44
- // Only render when focused to optimize performance
45
- if (!isFocused) {
46
- return null;
47
- }
48
-
49
- return <Component {...props} />;
50
- };
51
-
52
- /**
53
- * Cache for wrapped components to maintain stable references across renders
54
- */
55
- const wrappedComponentCache = new WeakMap<React.ComponentType<any>, React.ComponentType<any>>();
56
-
57
- /**
58
- * Creates a theme-aware component wrapper with a stable reference
59
- * This prevents React Navigation warnings about inline components
60
- */
61
- const createThemeAwareComponent = (OriginalComponent: React.ComponentType<any>) => {
62
- // Check cache first to return the same wrapped component reference
63
- if (wrappedComponentCache.has(OriginalComponent)) {
64
- return wrappedComponentCache.get(OriginalComponent)!;
65
- }
66
-
67
- const Wrapped = React.memo((props: any) => (
68
- <ThemeAwareScreenWrapper Component={OriginalComponent} {...props} />
69
- ));
70
- Wrapped.displayName = `ThemeAware(${OriginalComponent.displayName || OriginalComponent.name || 'Component'})`;
71
-
72
- // Store in cache for future lookups
73
- wrappedComponentCache.set(OriginalComponent, Wrapped);
74
-
75
- return Wrapped;
76
- };
77
11
 
78
12
  /**
79
13
  * Internal screen name for 404 pages
@@ -208,29 +142,69 @@ const buildScreen = (params: RouteParam, Navigator: TypedNavigator, parentPath =
208
142
  // Determine the component - wrap screens with ThemeAwareScreenWrapper
209
143
  let component: React.ComponentType<any>;
210
144
  if (params.type === 'screen') {
211
- component = createThemeAwareComponent(params.component);
145
+ component = params.component
212
146
  } else {
213
147
  component = buildNavigator(params, fullPath);
214
148
  }
215
149
 
216
- // Build screen options, adding fullScreen presentation if specified
217
- let screenOptions = params.options;
218
- if (params.type === 'screen' && params.options?.fullScreen) {
219
- screenOptions = {
220
- ...params.options,
221
- // Use fullScreenModal presentation to bypass parent navigator chrome
222
- presentation: 'fullScreenModal',
223
- // Hide header for true fullScreen experience
224
- headerShown: params.options.headerShown ?? false,
225
- } as ScreenOptions;
226
- }
150
+ // Build screen options
151
+ // React Navigation expects headerLeft/headerRight to be functions returning elements
152
+ const buildScreenOptions = (navProps: any) => {
153
+ let options = params.options || {};
154
+
155
+ // Handle fullScreen presentation
156
+ if (params.type === 'screen' && options?.fullScreen) {
157
+ options = {
158
+ ...options,
159
+ presentation: 'fullScreenModal',
160
+ headerShown: options.headerShown ?? false,
161
+ };
162
+ }
163
+
164
+ // Wrap headerLeft if it's a component
165
+ if (options.headerLeft) {
166
+ const HeaderLeftContent = options.headerLeft as React.ComponentType<any>;
167
+ options = {
168
+ ...options,
169
+ headerLeft: () => (
170
+ <HeaderWrapper
171
+ content={HeaderLeftContent}
172
+ route={params as NavigatorParam}
173
+ navigation={navProps.navigation}
174
+ />
175
+ ),
176
+ };
177
+ }
178
+
179
+ // Wrap headerRight if it's a component
180
+ if (options.headerRight) {
181
+ const HeaderRightContent = options.headerRight as React.ComponentType<any>;
182
+ options = {
183
+ ...options,
184
+ headerRight: () => (
185
+ <HeaderWrapper
186
+ content={HeaderRightContent}
187
+ route={params as NavigatorParam}
188
+ navigation={navProps.navigation}
189
+ />
190
+ ),
191
+ };
192
+ }
193
+
194
+ return options;
195
+ };
196
+
197
+ // Use function form of options to access navigation props for header wrappers
198
+ const screenOptions = (params.options?.headerLeft || params.options?.headerRight)
199
+ ? buildScreenOptions
200
+ : params.options;
227
201
 
228
202
  return (
229
203
  <Navigator.Screen
230
204
  key={`${fullPath}-${index}`}
231
205
  name={fullPath}
232
206
  component={component}
233
- options={screenOptions}
207
+ options={screenOptions as any}
234
208
  />
235
209
  )
236
210
  }