@idealyst/navigation 1.0.78 → 1.0.79

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.0.78",
3
+ "version": "1.0.79",
4
4
  "description": "Cross-platform navigation library for React and React Native",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -18,8 +18,10 @@
18
18
  },
19
19
  "exports": {
20
20
  ".": {
21
- "import": "./src/index.ts",
22
- "require": "./src/index.ts",
21
+ "react-native": "./src/index.native.ts",
22
+ "default": "./src/index.web.ts",
23
+ "import": "./src/index.web.ts",
24
+ "require": "./src/index.web.ts",
23
25
  "types": "./src/index.ts"
24
26
  },
25
27
  "./examples": {
@@ -38,8 +40,8 @@
38
40
  "publish:npm": "npm publish"
39
41
  },
40
42
  "peerDependencies": {
41
- "@idealyst/components": "^1.0.78",
42
- "@idealyst/theme": "^1.0.78",
43
+ "@idealyst/components": "^1.0.79",
44
+ "@idealyst/theme": "^1.0.79",
43
45
  "@react-navigation/bottom-tabs": "^7.0.0",
44
46
  "@react-navigation/drawer": "^7.0.0",
45
47
  "@react-navigation/native": "^7.0.0",
@@ -1,33 +1,101 @@
1
1
  import React, { createContext, memo, useContext, useMemo } from 'react';
2
- import { NavigateParams, NavigatorProviderProps } from './types';
3
- import { useNavigation, useNavigationState, DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
2
+ import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
3
+ import { useNavigation, useNavigationState, DarkTheme, DefaultTheme, NavigationContainer, useRoute } from '@react-navigation/native';
4
4
  import { buildNavigator } from '../routing';
5
5
  import { useUnistyles } from 'react-native-unistyles';
6
6
 
7
- const NavigatorContext = createContext<{
8
- navigate: (params: NavigateParams) => void;
9
- }>({
7
+ const NavigatorContext = createContext<NavigatorContextValue>({
10
8
  navigate: () => {},
9
+ params: {},
11
10
  });
12
11
 
12
+ // Utility function to parse path with parameters and find matching route
13
+ const parseParameterizedPath = (path: string, routes: any[]): { routeName: string, params: Record<string, string> } | null => {
14
+ // Handle absolute paths like /event/123
15
+ if (path.startsWith('/')) {
16
+ const pathSegments = path.split('/').filter(Boolean);
17
+
18
+ // Find matching route by checking patterns like /event/:id
19
+ const findMatchingRoute = (routeList: any[], segments: string[], startIndex = 0): any => {
20
+ for (const route of routeList) {
21
+ const routeSegments = route.path.split('/').filter(Boolean);
22
+
23
+ if (routeSegments.length === segments.length - startIndex) {
24
+ let isMatch = true;
25
+ const extractedParams: Record<string, string> = {};
26
+
27
+ for (let i = 0; i < routeSegments.length; i++) {
28
+ const routeSegment = routeSegments[i];
29
+ const pathSegment = segments[startIndex + i];
30
+
31
+ if (routeSegment.startsWith(':')) {
32
+ // Parameter segment - extract value
33
+ const paramName = routeSegment.slice(1);
34
+ extractedParams[paramName] = pathSegment;
35
+ } else if (routeSegment !== pathSegment) {
36
+ // Literal segment must match exactly
37
+ isMatch = false;
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (isMatch) {
43
+ return { route, params: extractedParams };
44
+ }
45
+ }
46
+
47
+ // Check nested routes
48
+ if (route.routes) {
49
+ const nestedMatch = findMatchingRoute(route.routes, segments, startIndex + route.path.split('/').filter(Boolean).length);
50
+ if (nestedMatch) {
51
+ return nestedMatch;
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ };
57
+
58
+ const match = findMatchingRoute([route], pathSegments);
59
+ if (match) {
60
+ return {
61
+ routeName: match.route.path,
62
+ params: match.params
63
+ };
64
+ }
65
+ }
66
+
67
+ return null;
68
+ };
69
+
13
70
  const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
14
71
 
15
72
  const navigation = useNavigation();
73
+ const currentRoute = useRoute();
16
74
 
17
75
  const navigate = (params: NavigateParams) => {
18
- console.log('navigate', params);
19
- navigation.navigate(params.path as never);
76
+
77
+ // Parse parameterized path for mobile
78
+ const parsed = parseParameterizedPath(params.path, [route]);
79
+
80
+ if (parsed) {
81
+ // Navigate to the pattern route with extracted parameters
82
+ navigation.navigate(parsed.routeName as never, parsed.params as never);
83
+ } else {
84
+ // Fallback to direct navigation
85
+ navigation.navigate(params.path as never, params.vars as never);
86
+ }
20
87
  };
21
88
 
22
89
  const RouteComponent = useMemo(() => {
23
90
  // Memoize the navigator to prevent unnecessary re-renders
24
91
  return memo(buildNavigator(route));
25
92
  }, [route]);
26
-
27
- console.log('UnwrappedNavigatorProvider render', RouteComponent);
28
93
 
29
94
  return (
30
- <NavigatorContext.Provider value={{ navigate }}>
95
+ <NavigatorContext.Provider value={{
96
+ navigate,
97
+ params: currentRoute.params || {}
98
+ }}>
31
99
  <RouteComponent />
32
100
  </NavigatorContext.Provider>
33
101
  )
@@ -1,16 +1,19 @@
1
1
  import React, { createContext, memo, useContext, useMemo } from 'react';
2
- import { NavigateParams, NavigatorProviderProps } from './types';
2
+ import { useNavigate, useParams } from 'react-router';
3
+ import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
3
4
  import { buildNavigator } from '../routing';
4
5
 
5
- const NavigatorContext = createContext<{
6
- navigate: (params: NavigateParams) => void;
7
- }>({
6
+ const NavigatorContext = createContext<NavigatorContextValue>({
8
7
  navigate: () => {},
8
+ params: {},
9
9
  });
10
10
 
11
11
  export const NavigatorProvider = ({
12
12
  route,
13
13
  }: NavigatorProviderProps) => {
14
+ const reactRouterNavigate = useNavigate()
15
+ const params = useParams()
16
+
14
17
  const navigateFunction = (params: NavigateParams) => {
15
18
  if (params.path) {
16
19
  // Normalize path - convert empty string to '/'
@@ -21,10 +24,15 @@ export const NavigatorProvider = ({
21
24
  path = `/${path}`
22
25
  }
23
26
 
24
- // Use HTML5 history API for proper navigation without hash
25
- window.history.pushState({}, '', path);
26
- // Trigger a popstate event to update any listening components
27
- window.dispatchEvent(new PopStateEvent('popstate'));
27
+ // Substitute variables in the path if provided
28
+ if (params.vars) {
29
+ Object.entries(params.vars).forEach(([key, value]) => {
30
+ path = path.replace(`:${key}`, value);
31
+ });
32
+ }
33
+
34
+ // Use React Router's navigate function
35
+ reactRouterNavigate(path);
28
36
  }
29
37
  };
30
38
 
@@ -34,7 +42,10 @@ export const NavigatorProvider = ({
34
42
  }, [route]);
35
43
 
36
44
  return (
37
- <NavigatorContext.Provider value={{ navigate: navigateFunction }}>
45
+ <NavigatorContext.Provider value={{
46
+ navigate: navigateFunction,
47
+ params: params || {}
48
+ }}>
38
49
  <RouteComponent />
39
50
  </NavigatorContext.Provider>
40
51
  );
@@ -10,4 +10,12 @@ export type NavigateParams = {
10
10
 
11
11
  export type NavigatorProviderProps = {
12
12
  route: NavigatorParam;
13
+ };
14
+
15
+ /**
16
+ * Context value that includes navigation function and current route parameters
17
+ */
18
+ export type NavigatorContextValue = {
19
+ navigate: (params: NavigateParams) => void;
20
+ params: Record<string, string>;
13
21
  };
@@ -0,0 +1,4 @@
1
+ // Native-specific exports
2
+ export * from './index';
3
+
4
+ // No React Router exports needed for native
package/src/index.ts CHANGED
@@ -1,8 +1,4 @@
1
- // Web-specific exports
1
+ // Cross-platform exports
2
2
  export * from './context';
3
-
4
- // Layout components
5
3
  export * from './layouts';
6
-
7
- // Routing utilities and components
8
4
  export * from './routing';
@@ -0,0 +1,5 @@
1
+ // Web-specific exports
2
+ export * from './index';
3
+
4
+ // Re-export React Router components to ensure single instance
5
+ export { Outlet } from 'react-router';
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { View, Text } from '@idealyst/components'
3
3
  import { StackLayoutProps } from '../routing/types'
4
+ import { Outlet } from 'react-router'
4
5
 
5
6
  export interface DefaultStackLayoutProps extends StackLayoutProps {
6
7
  onNavigate: (path: string) => void
@@ -14,7 +15,6 @@ export interface DefaultStackLayoutProps extends StackLayoutProps {
14
15
  export const DefaultStackLayout: React.FC<DefaultStackLayoutProps> = ({
15
16
  options,
16
17
  routes,
17
- ContentComponent,
18
18
  onNavigate,
19
19
  currentPath
20
20
  }) => {
@@ -50,7 +50,7 @@ export const DefaultStackLayout: React.FC<DefaultStackLayoutProps> = ({
50
50
 
51
51
  {/* Content Area */}
52
52
  <View style={{ flex: 1, padding: 20 }}>
53
- <ContentComponent />
53
+ <Outlet />
54
54
  </View>
55
55
  </View>
56
56
  )
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { View, Text, Button, Icon } from '@idealyst/components'
3
3
  import { TabLayoutProps } from '../routing/types'
4
+ import { Outlet } from 'react-router'
4
5
 
5
6
  export interface DefaultTabLayoutProps extends TabLayoutProps {}
6
7
 
@@ -11,7 +12,6 @@ export interface DefaultTabLayoutProps extends TabLayoutProps {}
11
12
  export const DefaultTabLayout: React.FC<DefaultTabLayoutProps> = ({
12
13
  options,
13
14
  routes,
14
- ContentComponent,
15
15
  onNavigate,
16
16
  currentPath
17
17
  }) => {
@@ -94,7 +94,7 @@ export const DefaultTabLayout: React.FC<DefaultTabLayoutProps> = ({
94
94
 
95
95
  {/* Content Area */}
96
96
  <View style={{ flex: 1, padding: 20 }}>
97
- <ContentComponent />
97
+ <Outlet />
98
98
  </View>
99
99
  </View>
100
100
  )
@@ -1,2 +1,6 @@
1
1
  export * from './router.native';
2
- export * from './types';
2
+ export * from './types';
3
+
4
+ // Added for consistency
5
+ const Outlet = null
6
+ export { Outlet };
@@ -1,2 +1,4 @@
1
1
  export * from './router.web';
2
- export * from './types';
2
+ export * from './types';
3
+
4
+ export { Outlet } from 'react-router';
@@ -1,223 +1,78 @@
1
1
  import React from 'react'
2
+ import { Routes, Route } from 'react-router'
2
3
  import { DefaultStackLayout } from '../layouts/DefaultStackLayout'
3
4
  import { DefaultTabLayout } from '../layouts/DefaultTabLayout'
4
- import { NavigatorParam, StackNavigatorParam, TabNavigatorParam } from './types'
5
+ import { NavigatorParam, RouteParam } from './types'
5
6
 
6
7
  /**
7
- * Build the Web navigator using custom layout components
8
+ * Build the Web navigator using React Router v7 nested routes
8
9
  * @param params Navigator configuration
9
10
  * @param parentPath Parent route path for nested routing
10
11
  * @returns React Router component
11
12
  */
12
13
  export const buildNavigator = (params: NavigatorParam, parentPath = '') => {
13
- const NavigatorComponent = () => {
14
- switch (params.layout) {
15
- case 'tab':
16
- return <TabNavigator params={params} parentPath={parentPath} />
17
- case 'stack':
18
- return <StackNavigator params={params} parentPath={parentPath} />
19
- default:
20
- throw new Error(`Unsupported navigator layout: ${(params as any).layout}`)
21
- }
22
- }
23
-
24
- return NavigatorComponent
25
- }
26
-
27
- /**
28
- * Normalize path for navigation - convert empty string to '/'
29
- */
30
- const normalizePath = (path: string): string => {
31
- if (path === '' || path === '/') {
32
- return '/'
33
- }
34
- return path.startsWith('/') ? path : `/${path}`
35
- }
36
-
37
- /**
38
- * Build full path by combining parent path with child path
39
- */
40
- const buildFullPath = (parentPath: string, childPath: string): string => {
41
- if (childPath === '/') {
42
- return parentPath || '/'
43
- }
44
-
45
- const normalizedParent = parentPath === '/' ? '' : parentPath
46
- const normalizedChild = normalizePath(childPath)
47
-
48
- return `${normalizedParent}${normalizedChild}`
14
+ return () => (
15
+ <Routes>
16
+ {params.routes.map((child, index) => buildRoute(child, index))}
17
+ </Routes>
18
+ )
49
19
  }
50
20
 
51
- /**
52
- * Check if current path matches a route, considering parent path
53
- */
54
- const pathMatches = (currentPath: string, routePath: string, parentPath: string): boolean => {
55
- const fullRoutePath = buildFullPath(parentPath, routePath)
56
- return currentPath === fullRoutePath
57
- }
58
21
 
59
22
  /**
60
- * Tab Navigator Component for web using custom layout components
23
+ * Build Route - handles both screens and nested navigators
24
+ * @param params Route configuration
25
+ * @param index Route index for key generation
26
+ * @param isNested Whether this is a nested route (should strip leading slash)
27
+ * @returns React Router Route element
61
28
  */
62
- const TabNavigator: React.FC<{ params: TabNavigatorParam; parentPath: string }> = ({
63
- params,
64
- parentPath
65
- }) => {
66
- // Get current path from window location
67
- const getCurrentPath = () => {
68
- return window.location.pathname
69
- }
70
-
71
- const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
72
-
73
- // Listen for navigation changes
74
- React.useEffect(() => {
75
- const handlePopState = () => {
76
- setCurrentPath(getCurrentPath())
77
- }
78
-
79
- window.addEventListener('popstate', handlePopState)
80
- return () => window.removeEventListener('popstate', handlePopState)
81
- }, [])
82
-
83
- // Navigate function
84
- const navigateToRoute = (routePath: string) => {
85
- const fullPath = buildFullPath(parentPath, routePath)
86
- window.history.pushState({}, '', fullPath)
87
- setCurrentPath(fullPath)
88
- }
29
+ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
30
+ // For nested routes, strip leading slash to make path relative
31
+ const routePath = isNested && params.path.startsWith('/')
32
+ ? params.path.slice(1)
33
+ : params.path;
89
34
 
90
- // Get current route component
91
- const getCurrentRoute = () => {
92
- // Find matching route
93
- const currentRoute = params.routes.find(route => {
94
- return pathMatches(currentPath, route.path, parentPath)
95
- }) || params.routes[0] // fallback to first route
96
-
97
- if (!currentRoute || currentRoute.type !== 'screen') {
98
- return () => React.createElement('div', {}, `Route not found: ${currentPath}`)
99
- }
100
-
101
- return currentRoute.component
102
- }
103
-
104
- // Transform routes to include full paths for layout component
105
- const routesWithFullPaths = params.routes.map(route => ({
106
- ...route,
107
- fullPath: buildFullPath(parentPath, route.path)
108
- }))
109
-
110
- // Use custom layout component or default
111
- const LayoutComponent = params.layoutComponent || DefaultTabLayout
112
-
113
- return (
114
- <LayoutComponent
115
- options={params.options}
116
- routes={routesWithFullPaths}
117
- ContentComponent={getCurrentRoute()}
118
- onNavigate={navigateToRoute}
119
- currentPath={currentPath}
120
- />
121
- )
122
- }
123
-
124
- /**
125
- * Stack Navigator Component for web using custom layout components
126
- */
127
- const StackNavigator: React.FC<{ params: StackNavigatorParam; parentPath: string }> = ({
128
- params,
129
- parentPath
130
- }) => {
131
- // Get current path from window location
132
- const getCurrentPath = () => {
133
- return window.location.pathname
134
- }
135
-
136
- const [currentPath, setCurrentPath] = React.useState(getCurrentPath)
137
-
138
- // Listen for navigation changes
139
- React.useEffect(() => {
140
- const handlePopState = () => {
141
- setCurrentPath(getCurrentPath())
142
- }
35
+ if (params.type === 'screen') {
143
36
 
144
- window.addEventListener('popstate', handlePopState)
145
- return () => window.removeEventListener('popstate', handlePopState)
146
- }, [])
147
-
148
- // Navigate function
149
- const navigateToRoute = (routePath: string) => {
150
- const fullPath = buildFullPath(parentPath, routePath)
151
- window.history.pushState({}, '', fullPath)
152
- setCurrentPath(fullPath)
153
- }
154
-
155
- // Get current route component
156
- const getCurrentRoute = () => {
157
- // Find matching route
158
- const currentRoute = params.routes.find(route => {
159
- return pathMatches(currentPath, route.path, parentPath)
160
- })
37
+ // If the route path is empty (root route in navigator), make it an index route
38
+ const routeProps = routePath === '' ? { index: true } : { path: routePath };
161
39
 
162
- // If no exact match, check if current path starts with any navigator route
163
- if (!currentRoute) {
164
- const navigatorRoute = params.routes.find(route => {
165
- if (route.type === 'navigator') {
166
- const fullRoutePath = buildFullPath(parentPath, route.path)
167
- return currentPath.startsWith(fullRoutePath + '/') || currentPath === fullRoutePath
168
- }
169
- return false
170
- })
171
-
172
- if (navigatorRoute && navigatorRoute.type === 'navigator') {
173
- return () => {
174
- const NestedNavigator = buildNavigator(navigatorRoute, buildFullPath(parentPath, navigatorRoute.path))
175
- return React.createElement(NestedNavigator)
176
- }
177
- }
178
- }
40
+ return (
41
+ <Route
42
+ key={`${params.path}-${index}`}
43
+ {...routeProps}
44
+ element={React.createElement(params.component)}
45
+ />
46
+ );
47
+ } else if (params.type === 'navigator') {
48
+ // Get the layout component directly
49
+ const LayoutComponent = params.layoutComponent ||
50
+ (params.layout === 'tab' ? DefaultTabLayout : DefaultStackLayout);
179
51
 
180
- // Fallback to first route if no match
181
- if (!currentRoute) {
182
- const fallbackRoute = params.routes[0]
183
- if (fallbackRoute.type === 'screen') {
184
- return fallbackRoute.component
185
- } else if (fallbackRoute.type === 'navigator') {
186
- const NestedNavigator = buildNavigator(fallbackRoute, buildFullPath(parentPath, fallbackRoute.path))
187
- return () => React.createElement(NestedNavigator)
188
- }
189
- }
190
-
191
- if (currentRoute) {
192
- if (currentRoute.type === 'screen') {
193
- return currentRoute.component
194
- } else if (currentRoute.type === 'navigator') {
195
- // Nested navigator
196
- const NestedNavigator = buildNavigator(currentRoute, buildFullPath(parentPath, currentRoute.path))
197
- return () => React.createElement(NestedNavigator)
198
- }
199
- }
52
+ // Transform routes to include full paths for layout component
53
+ const routesWithFullPaths = params.routes.map(route => ({
54
+ ...route,
55
+ fullPath: route.path
56
+ }));
200
57
 
201
- // If all else fails, return a not found component
202
- return () => React.createElement('div', {}, `Route not found: ${currentPath}`)
58
+ return (
59
+ <Route
60
+ key={`${params.path}-${index}`}
61
+ path={routePath}
62
+ element={
63
+ <LayoutComponent
64
+ options={params.options}
65
+ routes={routesWithFullPaths}
66
+ onNavigate={() => {}} // Layout components can use their own navigation logic
67
+ currentPath=""
68
+ />
69
+ }
70
+ >
71
+ {params.routes.map((child, childIndex) => buildRoute(child, childIndex, true))}
72
+ </Route>
73
+ );
203
74
  }
75
+
76
+ return null;
77
+ }
204
78
 
205
- // Transform routes to include full paths for layout component
206
- const routesWithFullPaths = params.routes.map(route => ({
207
- ...route,
208
- fullPath: buildFullPath(parentPath, route.path)
209
- }))
210
-
211
- // Use custom layout component or default
212
- const LayoutComponent = params.layoutComponent || DefaultStackLayout
213
-
214
- return (
215
- <LayoutComponent
216
- options={params.options}
217
- routes={routesWithFullPaths}
218
- ContentComponent={getCurrentRoute()}
219
- onNavigate={navigateToRoute}
220
- currentPath={currentPath}
221
- />
222
- )
223
- }
@@ -102,7 +102,6 @@ export type RouteWithFullPath<T = ScreenOptions> = RouteParam<T> & {
102
102
  export type TabLayoutProps = {
103
103
  options?: NavigatorOptions
104
104
  routes: RouteWithFullPath<TabBarScreenOptions>[]
105
- ContentComponent: React.ComponentType
106
105
  onNavigate: (path: string) => void
107
106
  currentPath: string
108
107
  }
@@ -110,7 +109,6 @@ export type TabLayoutProps = {
110
109
  export type StackLayoutProps = {
111
110
  options?: NavigatorOptions
112
111
  routes: RouteWithFullPath<ScreenOptions>[]
113
- ContentComponent: React.ComponentType
114
112
  onNavigate: (path: string) => void
115
113
  currentPath: string
116
114
  }