@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 +17 -5
- package/src/context/NavigatorContext.native.tsx +115 -40
- package/src/context/NavigatorContext.web.tsx +7 -0
- package/src/context/types.ts +7 -0
- package/src/examples/ExampleNavigationRouter.tsx +4 -0
- package/src/examples/ExampleSidebar.tsx +3 -0
- package/src/examples/ExampleWebLayout.tsx +3 -2
- package/src/examples/HeaderRight.tsx +4 -3
- package/src/routing/router.native.tsx +53 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/navigation",
|
|
3
|
-
"version": "1.1.
|
|
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/
|
|
47
|
-
"@idealyst/
|
|
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/
|
|
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/
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
131
|
-
if (
|
|
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(
|
|
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,
|
|
205
|
+
const handler = findInvalidRouteHandler(route, normalizedPath);
|
|
175
206
|
if (handler) {
|
|
176
|
-
const redirectParams = handler(
|
|
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:
|
|
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 "${
|
|
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
|
|
238
|
+
// Use StackActions.replace to replace the current screen in the stack
|
|
209
239
|
navigation.dispatch(
|
|
210
|
-
|
|
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(
|
|
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,
|
|
327
|
+
const handler = findInvalidRouteHandler(route, normalizedPath);
|
|
273
328
|
if (handler) {
|
|
274
|
-
const redirectParams = handler(
|
|
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:
|
|
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 "${
|
|
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
|
|
360
|
+
// Use StackActions.replace to replace the current screen in the stack
|
|
307
361
|
navigation.dispatch(
|
|
308
|
-
|
|
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
|
}}>
|
package/src/context/types.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
145
|
+
component = params.component
|
|
212
146
|
} else {
|
|
213
147
|
component = buildNavigator(params, fullPath);
|
|
214
148
|
}
|
|
215
149
|
|
|
216
|
-
// Build screen options
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
}
|