@idealyst/navigation 1.1.7 → 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.7",
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.7",
47
- "@idealyst/theme": "^1.1.7",
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.7",
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.7",
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
 
@@ -126,35 +126,40 @@ const parseParameterizedPath = (path: string, rootRoute: any): { routeName: stri
126
126
  const cleanPath = route.path.startsWith('/') ? route.path.slice(1) : route.path;
127
127
  const fullRoutePath = pathPrefix ? `${pathPrefix}/${cleanPath}` : route.path;
128
128
 
129
- if (routeSegments.length === segments.length - startIndex) {
130
- let isMatch = true;
131
- const extractedParams: Record<string, string> = {};
132
-
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) {
133
137
  for (let i = 0; i < routeSegments.length; i++) {
134
138
  const routeSegment = routeSegments[i];
135
139
  const pathSegment = segments[startIndex + i];
136
-
140
+
137
141
  if (routeSegment.startsWith(':')) {
138
142
  // Parameter segment - extract value
139
143
  const paramName = routeSegment.slice(1);
140
144
  extractedParams[paramName] = pathSegment;
141
145
  } else if (routeSegment !== pathSegment) {
142
146
  // Literal segment must match exactly
143
- isMatch = false;
147
+ prefixMatches = false;
144
148
  break;
145
149
  }
146
150
  }
147
-
148
- if (isMatch) {
149
- return { route, params: extractedParams, fullPath: fullRoutePath };
150
- }
151
151
  }
152
-
153
- // Check nested routes
154
- 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) {
155
160
  const nestedMatch = findMatchingRoute(
156
- route.routes,
157
- segments,
161
+ route.routes,
162
+ segments,
158
163
  startIndex + routeSegments.length,
159
164
  fullRoutePath
160
165
  );
@@ -230,12 +235,9 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
230
235
  };
231
236
 
232
237
  if (params.replace) {
233
- // Use CommonActions.reset to replace the current route
238
+ // Use StackActions.replace to replace the current screen in the stack
234
239
  navigation.dispatch(
235
- CommonActions.reset({
236
- index: 0,
237
- routes: [{ name: parsed.routeName, params: navigationParams }],
238
- })
240
+ StackActions.replace(parsed.routeName, navigationParams)
239
241
  );
240
242
  } else {
241
243
  // Navigate to the pattern route with extracted parameters
@@ -243,6 +245,30 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
243
245
  }
244
246
  };
245
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
+
246
272
  const RouteComponent = useMemo(() => {
247
273
  // Memoize the navigator to prevent unnecessary re-renders
248
274
  return memo(buildNavigator(route));
@@ -260,6 +286,7 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
260
286
  <NavigatorContext.Provider value={{
261
287
  route,
262
288
  navigate,
289
+ replace,
263
290
  canGoBack,
264
291
  goBack,
265
292
  }}>
@@ -330,12 +357,9 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
330
357
  };
331
358
 
332
359
  if (params.replace) {
333
- // Use CommonActions.reset to replace the current route
360
+ // Use StackActions.replace to replace the current screen in the stack
334
361
  navigation.dispatch(
335
- CommonActions.reset({
336
- index: 0,
337
- routes: [{ name: parsed.routeName, params: navigationParams }],
338
- })
362
+ StackActions.replace(parsed.routeName, navigationParams)
339
363
  );
340
364
  } else {
341
365
  // Navigate to the pattern route with extracted parameters
@@ -343,6 +367,30 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
343
367
  }
344
368
  };
345
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
+
346
394
  const canGoBack = () => navigation.canGoBack();
347
395
 
348
396
  const goBack = () => {
@@ -352,7 +400,7 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
352
400
  };
353
401
 
354
402
  return (
355
- <DrawerNavigatorContext.Provider value={{ navigate, route, canGoBack, goBack }}>
403
+ <DrawerNavigatorContext.Provider value={{ navigate, replace, route, canGoBack, goBack }}>
356
404
  {children}
357
405
  </DrawerNavigatorContext.Provider>
358
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,7 +142,7 @@ 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
  }