@idealyst/navigation 1.0.99 → 1.1.1

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.99",
3
+ "version": "1.1.1",
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,8 @@
43
43
  "publish:npm": "npm publish"
44
44
  },
45
45
  "peerDependencies": {
46
- "@idealyst/components": "^1.0.99",
47
- "@idealyst/theme": "^1.0.99",
46
+ "@idealyst/components": "^1.1.1",
47
+ "@idealyst/theme": "^1.1.1",
48
48
  "@react-navigation/bottom-tabs": ">=7.0.0",
49
49
  "@react-navigation/drawer": ">=7.0.0",
50
50
  "@react-navigation/native": ">=7.0.0",
@@ -60,10 +60,10 @@
60
60
  "react-router-dom": ">=6.0.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@idealyst/components": "^1.0.99",
63
+ "@idealyst/components": "^1.1.1",
64
64
  "@idealyst/datagrid": "^1.0.93",
65
65
  "@idealyst/datepicker": "^1.0.93",
66
- "@idealyst/theme": "^1.0.99",
66
+ "@idealyst/theme": "^1.1.1",
67
67
  "@types/react": "^19.1.8",
68
68
  "@types/react-dom": "^19.1.6",
69
69
  "react": "^19.1.0",
@@ -86,4 +86,4 @@
86
86
  "cross-platform",
87
87
  "router"
88
88
  ]
89
- }
89
+ }
@@ -217,10 +217,20 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
217
217
  return memo(buildNavigator(route));
218
218
  }, [route]);
219
219
 
220
+ const canGoBack = () => navigation.canGoBack();
221
+
222
+ const goBack = () => {
223
+ if (navigation.canGoBack()) {
224
+ navigation.goBack();
225
+ }
226
+ };
227
+
220
228
  return (
221
229
  <NavigatorContext.Provider value={{
222
230
  route,
223
231
  navigate,
232
+ canGoBack,
233
+ goBack,
224
234
  }}>
225
235
  <RouteComponent />
226
236
  </NavigatorContext.Provider>
@@ -294,8 +304,16 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
294
304
  }
295
305
  };
296
306
 
307
+ const canGoBack = () => navigation.canGoBack();
308
+
309
+ const goBack = () => {
310
+ if (navigation.canGoBack()) {
311
+ navigation.goBack();
312
+ }
313
+ };
314
+
297
315
  return (
298
- <DrawerNavigatorContext.Provider value={{ navigate, route }}>
316
+ <DrawerNavigatorContext.Provider value={{ navigate, route, canGoBack, goBack }}>
299
317
  {children}
300
318
  </DrawerNavigatorContext.Provider>
301
319
  );
@@ -1,11 +1,13 @@
1
1
  import React, { createContext, memo, useContext, useMemo } from 'react';
2
- import { useNavigate, useParams } from '../router';
2
+ import { useNavigate, useParams, useLocation } from '../router';
3
3
  import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
4
4
  import { buildNavigator, NavigatorParam } from '../routing';
5
5
 
6
6
  const NavigatorContext = createContext<NavigatorContextValue>({
7
7
  navigate: () => {},
8
8
  route: undefined,
9
+ canGoBack: () => false,
10
+ goBack: () => {},
9
11
  });
10
12
 
11
13
  /**
@@ -171,10 +173,37 @@ function findInvalidRouteHandler(
171
173
  return handlers.length > 0 ? handlers[handlers.length - 1] : undefined;
172
174
  }
173
175
 
176
+ /**
177
+ * Get the parent path from a given path.
178
+ * e.g., "/users/123/edit" -> "/users/123"
179
+ * "/users" -> "/"
180
+ * "/" -> null (no parent)
181
+ */
182
+ function getParentPath(path: string): string | null {
183
+ const normalizedPath = path === '' ? '/' : path;
184
+
185
+ if (normalizedPath === '/') {
186
+ return null;
187
+ }
188
+
189
+ const segments = normalizedPath.split('/').filter(Boolean);
190
+
191
+ if (segments.length === 0) {
192
+ return null;
193
+ }
194
+
195
+ if (segments.length === 1) {
196
+ return '/';
197
+ }
198
+
199
+ return '/' + segments.slice(0, -1).join('/');
200
+ }
201
+
174
202
  export const NavigatorProvider = ({
175
203
  route,
176
204
  }: NavigatorProviderProps) => {
177
205
  const reactRouterNavigate = useNavigate();
206
+ const location = useLocation();
178
207
 
179
208
  // Memoize the list of valid route patterns
180
209
  const validPatterns = useMemo(() => buildValidPatterns(route), [route]);
@@ -219,10 +248,27 @@ export const NavigatorProvider = ({
219
248
  return memo(buildNavigator(route));
220
249
  }, [route]);
221
250
 
251
+ const canGoBack = () => {
252
+ const parentPath = getParentPath(location.pathname);
253
+ if (!parentPath) {
254
+ return false;
255
+ }
256
+ return isValidRoute(parentPath, validPatterns);
257
+ };
258
+
259
+ const goBack = () => {
260
+ const parentPath = getParentPath(location.pathname);
261
+ if (parentPath && isValidRoute(parentPath, validPatterns)) {
262
+ reactRouterNavigate(parentPath);
263
+ }
264
+ };
265
+
222
266
  return (
223
267
  <NavigatorContext.Provider value={{
224
268
  route,
225
269
  navigate: navigateFunction,
270
+ canGoBack,
271
+ goBack,
226
272
  }}>
227
273
  <RouteComponent />
228
274
  </NavigatorContext.Provider>
@@ -24,4 +24,16 @@ export type NavigatorProviderProps = {
24
24
  export type NavigatorContextValue = {
25
25
  route: NavigatorParam | undefined;
26
26
  navigate: (params: NavigateParams) => void;
27
+ /**
28
+ * Returns true if there is a parent route in the route hierarchy to navigate back to.
29
+ * On web, this checks for parent routes (not browser history).
30
+ * On native, this uses react-navigation's canGoBack().
31
+ */
32
+ canGoBack: () => boolean;
33
+ /**
34
+ * Navigate back to the parent route in the route hierarchy.
35
+ * On web, this navigates to the parent path (e.g., /users/123 -> /users).
36
+ * On native, this uses react-navigation's goBack().
37
+ */
38
+ goBack: () => void;
27
39
  };
@@ -84,6 +84,66 @@ const SettingsNotFoundScreen = ({ path, params }: NotFoundComponentProps) => {
84
84
  );
85
85
  };
86
86
 
87
+ /**
88
+ * FullScreen Example - demonstrates a screen that renders without parent layout
89
+ * This screen will NOT have the sidebar, header, or any parent navigator chrome
90
+ */
91
+ const FullScreenExample = () => {
92
+ const { navigate } = useNavigator();
93
+
94
+ return (
95
+ <Screen>
96
+ <View
97
+ spacing="lg"
98
+ padding={12}
99
+ style={{
100
+ flex: 1,
101
+ alignItems: 'center',
102
+ justifyContent: 'center',
103
+ backgroundColor: '#1a1a2e',
104
+ }}
105
+ >
106
+ <Icon name="fullscreen" size={80} color="white" />
107
+ <Text size="xxl" weight="bold" style={{ color: 'white', marginTop: 24 }}>
108
+ Full Screen Mode
109
+ </Text>
110
+ <Text size="md" style={{ color: '#aaa', textAlign: 'center', maxWidth: 400, marginTop: 8 }}>
111
+ This screen renders outside of the parent layout. Notice there's no sidebar,
112
+ header, or any navigation chrome from the parent navigator.
113
+ </Text>
114
+ <Card style={{ marginTop: 32, padding: 24, maxWidth: 500 }}>
115
+ <View spacing="md">
116
+ <Text size="lg" weight="semibold">How it works</Text>
117
+ <Text size="sm" color="secondary">
118
+ Add <Text weight="semibold">fullScreen: true</Text> to the screen's options:
119
+ </Text>
120
+ <View style={{ backgroundColor: '#f5f5f5', padding: 12, borderRadius: 8, marginTop: 8 }}>
121
+ <Text size="sm" style={{ fontFamily: 'monospace' }}>
122
+ {`{
123
+ path: "fullscreen-demo",
124
+ type: 'screen',
125
+ component: FullScreenExample,
126
+ options: { fullScreen: true }
127
+ }`}
128
+ </Text>
129
+ </View>
130
+ <Text size="sm" color="secondary" style={{ marginTop: 8 }}>
131
+ Use cases: Onboarding flows, media viewers, immersive experiences, modal-like screens.
132
+ </Text>
133
+ </View>
134
+ </Card>
135
+ <Button
136
+ style={{ marginTop: 32 }}
137
+ intent="primary"
138
+ onPress={() => navigate({ path: '/', replace: false })}
139
+ >
140
+ Back to Home
141
+ </Button>
142
+ </View>
143
+ </Screen>
144
+ );
145
+ };
146
+
87
147
  // Settings section screens
88
148
  const SettingsHomeScreen = () => (
89
149
  <Screen>
@@ -149,6 +209,8 @@ const SettingsNavigator: NavigatorParam = {
149
209
  };
150
210
 
151
211
  const HomeScreen = () => {
212
+ const { navigate } = useNavigator();
213
+
152
214
  return (
153
215
  <Screen>
154
216
  <View spacing='lg' padding={12}>
@@ -194,6 +256,25 @@ const HomeScreen = () => {
194
256
  </Text>
195
257
  </View>
196
258
  </Card>
259
+
260
+ <Card>
261
+ <View spacing="md" style={{ padding: 16 }}>
262
+ <Text size="lg" weight="semibold">
263
+ Full Screen Mode
264
+ </Text>
265
+ <Text>
266
+ Screens can opt-out of parent layout inheritance using the fullScreen option.
267
+ This is useful for onboarding flows, media viewers, or immersive experiences.
268
+ </Text>
269
+ <Button
270
+ style={{ marginTop: 12 }}
271
+ type="outlined"
272
+ onPress={() => navigate({ path: '/fullscreen-demo' })}
273
+ >
274
+ Try Full Screen Demo
275
+ </Button>
276
+ </View>
277
+ </Card>
197
278
  </View>
198
279
  </Screen>
199
280
  )
@@ -211,6 +292,8 @@ const ExampleNavigationRouter: NavigatorParam = {
211
292
  },
212
293
  routes: [
213
294
  { path: "/", type: 'screen', component: HomeScreen, options: { title: "Home" } },
295
+ // FullScreen demo - renders without parent layout (no sidebar/header)
296
+ { path: "fullscreen-demo", type: 'screen', component: FullScreenExample, options: { title: "Full Screen Demo", fullScreen: true } },
214
297
  // Nested settings navigator with its own 404 handling
215
298
  SettingsNavigator,
216
299
  { path: "avatar", type: 'screen', component: AvatarExamples, options: { title: "Avatar" } },
package/src/index.web.ts CHANGED
@@ -3,3 +3,9 @@ export * from './index';
3
3
 
4
4
  // Direct export to fix module resolution
5
5
  export { NavigatorProvider, useNavigator } from './context/NavigatorContext.web';
6
+
7
+ // [DEVCONTAINER FIX] Explicit hook exports to fix Vite re-export resolution
8
+ // When Vite processes `export * from './index'` -> `export * from './hooks'`,
9
+ // it doesn't apply .web extension priority for nested re-exports.
10
+ // This explicit export ensures useParams is available from @idealyst/navigation.
11
+ export { useParams } from './hooks/useParams.web';
@@ -213,12 +213,24 @@ const buildScreen = (params: RouteParam, Navigator: TypedNavigator, parentPath =
213
213
  component = buildNavigator(params, fullPath);
214
214
  }
215
215
 
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
+ }
227
+
216
228
  return (
217
229
  <Navigator.Screen
218
230
  key={`${fullPath}-${index}`}
219
231
  name={fullPath}
220
232
  component={component}
221
- options={params.options}
233
+ options={screenOptions}
222
234
  />
223
235
  )
224
236
  }
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react'
2
2
  import { Routes, Route, useLocation, useParams } from '../router'
3
3
  import { DefaultStackLayout } from '../layouts/DefaultStackLayout'
4
4
  import { DefaultTabLayout } from '../layouts/DefaultTabLayout'
5
- import { NavigatorParam, RouteParam, NotFoundComponentProps } from './types'
5
+ import { NavigatorParam, RouteParam, ScreenParam, NotFoundComponentProps } from './types'
6
6
  import { NavigateParams } from '../context/types'
7
7
 
8
8
  /**
@@ -98,30 +98,55 @@ const NotFoundWrapper = ({
98
98
  export const buildNavigator = (params: NavigatorParam, parentPath = '') => {
99
99
  return () => (
100
100
  <Routes>
101
- {buildRoute(params, 0, false)}
101
+ {buildRoute(params, 0, false, parentPath)}
102
102
  </Routes>
103
103
  )
104
104
  }
105
105
 
106
106
 
107
+ /**
108
+ * Helper to compute full path by joining parent and child paths
109
+ */
110
+ const joinPaths = (parentPath: string, childPath: string): string => {
111
+ // Remove trailing slash from parent
112
+ const normalizedParent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
113
+ // Remove leading slash from child
114
+ const normalizedChild = childPath.startsWith('/') ? childPath.slice(1) : childPath;
115
+
116
+ if (!normalizedParent || normalizedParent === '') {
117
+ return normalizedChild ? `/${normalizedChild}` : '/';
118
+ }
119
+ if (!normalizedChild || normalizedChild === '') {
120
+ return normalizedParent;
121
+ }
122
+ return `${normalizedParent}/${normalizedChild}`;
123
+ };
124
+
125
+ /**
126
+ * Check if a screen has fullScreen option enabled
127
+ */
128
+ const isFullScreenRoute = (route: RouteParam): route is ScreenParam => {
129
+ return route.type === 'screen' && route.options?.fullScreen === true;
130
+ };
131
+
107
132
  /**
108
133
  * Build Route - handles both screens and nested navigators
109
134
  * @param params Route configuration
110
135
  * @param index Route index for key generation
111
136
  * @param isNested Whether this is a nested route (should strip leading slash)
112
- * @returns React Router Route element
137
+ * @param parentPath Full parent path for computing fullScreen route paths
138
+ * @returns React Router Route element or array of elements
113
139
  */
114
- const buildRoute = (params: RouteParam, index: number, isNested = false) => {
140
+ const buildRoute = (params: RouteParam, index: number, isNested = false, parentPath = ''): React.ReactNode => {
115
141
  // For nested routes, strip leading slash to make path relative
116
- const routePath = isNested && params.path.startsWith('/')
117
- ? params.path.slice(1)
142
+ const routePath = isNested && params.path.startsWith('/')
143
+ ? params.path.slice(1)
118
144
  : params.path;
119
-
145
+
120
146
  if (params.type === 'screen') {
121
-
122
147
  // If the route path is empty (root route in navigator), make it an index route
123
148
  const routeProps = routePath === '' ? { index: true } : { path: routePath };
124
-
149
+
125
150
  return (
126
151
  <Route
127
152
  key={`${params.path}-${index}`}
@@ -134,19 +159,28 @@ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
134
159
  const LayoutComponent = params.layoutComponent ||
135
160
  (params.layout === 'tab' ? DefaultTabLayout : DefaultStackLayout);
136
161
 
137
- // Transform routes to include full paths for layout component
138
- const routesWithFullPaths = params.routes.map(route => ({
162
+ // Compute the full path for this navigator
163
+ const navigatorFullPath = joinPaths(parentPath, params.path);
164
+
165
+ // Separate fullScreen routes from regular routes
166
+ const regularRoutes = params.routes.filter(route => !isFullScreenRoute(route));
167
+ const fullScreenRoutes = params.routes.filter(isFullScreenRoute);
168
+
169
+ // Transform routes to include full paths for layout component (only non-fullScreen)
170
+ const routesWithFullPaths = regularRoutes.map(route => ({
139
171
  ...route,
140
172
  fullPath: route.path
141
173
  }));
142
174
 
143
- // Build child routes including catch-all for 404
144
- const childRoutes = params.routes.map((child, childIndex) => buildRoute(child, childIndex, true));
175
+ // Build child routes for regular (non-fullScreen) screens
176
+ const childRoutes = regularRoutes.map((child, childIndex) =>
177
+ buildRoute(child, childIndex, true, navigatorFullPath)
178
+ );
145
179
 
146
180
  // Add catch-all and index routes if notFoundComponent or onInvalidRoute is configured
147
181
  if (params.notFoundComponent || params.onInvalidRoute) {
148
182
  // Check if any route handles the index (empty path or "/" for this navigator)
149
- const hasIndexRoute = params.routes.some(route => {
183
+ const hasIndexRoute = regularRoutes.some(route => {
150
184
  const childPath = route.path.startsWith('/') ? route.path.slice(1) : route.path;
151
185
  return childPath === '' || childPath === '/';
152
186
  });
@@ -183,7 +217,8 @@ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
183
217
  );
184
218
  }
185
219
 
186
- return (
220
+ // Build the main navigator route with layout
221
+ const navigatorRoute = (
187
222
  <Route
188
223
  key={`${params.path}-${index}`}
189
224
  path={routePath}
@@ -198,8 +233,36 @@ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
198
233
  {childRoutes}
199
234
  </Route>
200
235
  );
236
+
237
+ // If there are fullScreen routes, return them as siblings to the navigator route
238
+ if (fullScreenRoutes.length > 0) {
239
+ const fullScreenElements = fullScreenRoutes.map((route, fsIndex) => {
240
+ // Compute full absolute path for the fullScreen route
241
+ const fullPath = joinPaths(navigatorFullPath, route.path);
242
+ // Remove leading slash for React Router path (it will be relative to root)
243
+ const routerPath = fullPath.startsWith('/') ? fullPath.slice(1) : fullPath;
244
+
245
+ return (
246
+ <Route
247
+ key={`fullscreen-${route.path}-${fsIndex}`}
248
+ path={routerPath || '/'}
249
+ element={React.createElement(route.component)}
250
+ />
251
+ );
252
+ });
253
+
254
+ // Return array of routes: navigator + fullScreen siblings
255
+ return (
256
+ <React.Fragment key={`navigator-group-${index}`}>
257
+ {navigatorRoute}
258
+ {fullScreenElements}
259
+ </React.Fragment>
260
+ );
261
+ }
262
+
263
+ return navigatorRoute;
201
264
  }
202
-
265
+
203
266
  return null;
204
267
  }
205
268
 
@@ -61,6 +61,15 @@ export type ScreenOptions = {
61
61
  */
62
62
  title?: string;
63
63
  headerShown?: boolean;
64
+ /**
65
+ * When true, renders the screen outside of parent layout wrappers.
66
+ * Useful for fullscreen modals, onboarding flows, or any screen that
67
+ * should not inherit the parent navigator's layout (header, sidebar, tabs, etc.)
68
+ *
69
+ * Web: Screen renders as a sibling route without the parent LayoutComponent
70
+ * Native: Screen uses fullScreenModal presentation
71
+ */
72
+ fullScreen?: boolean;
64
73
 
65
74
  } & NavigatorOptions;
66
75