@idealyst/navigation 1.2.115 → 1.2.116

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.2.115",
3
+ "version": "1.2.116",
4
4
  "description": "Cross-platform navigation library for React and React Native",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@idealyst/camera": "^1.2.30",
48
- "@idealyst/components": "^1.2.115",
48
+ "@idealyst/components": "^1.2.116",
49
49
  "@idealyst/microphone": "^1.2.30",
50
- "@idealyst/theme": "^1.2.115",
50
+ "@idealyst/theme": "^1.2.116",
51
51
  "@react-navigation/bottom-tabs": ">=7.0.0",
52
52
  "@react-navigation/drawer": ">=7.0.0",
53
53
  "@react-navigation/native": ">=7.0.0",
@@ -74,13 +74,13 @@
74
74
  "@idealyst/animate": "^1.2.38",
75
75
  "@idealyst/blur": "^1.2.40",
76
76
  "@idealyst/camera": "^1.2.30",
77
- "@idealyst/components": "^1.2.115",
77
+ "@idealyst/components": "^1.2.116",
78
78
  "@idealyst/datagrid": "^1.2.30",
79
79
  "@idealyst/datepicker": "^1.2.30",
80
80
  "@idealyst/lottie": "^1.2.38",
81
- "@idealyst/markdown": "^1.2.115",
81
+ "@idealyst/markdown": "^1.2.116",
82
82
  "@idealyst/microphone": "^1.2.30",
83
- "@idealyst/theme": "^1.2.115",
83
+ "@idealyst/theme": "^1.2.116",
84
84
  "@types/react": "^19.1.8",
85
85
  "@types/react-dom": "^19.1.6",
86
86
  "react": "^19.1.0",
@@ -207,6 +207,7 @@ const parseParameterizedPath = (path: string, rootRoute: any): { routeName: stri
207
207
  const UnwrappedNavigatorProvider = ({ route, floatingComponent }: NavigatorProviderProps) => {
208
208
 
209
209
  const navigation = useNavigation();
210
+ const navState = navigation.getState?.();
210
211
 
211
212
  const navigate = (params: NavigateParams, _redirectCount = 0) => {
212
213
  // Validate params - catch common mistake of passing string directly
@@ -326,6 +327,7 @@ const UnwrappedNavigatorProvider = ({ route, floatingComponent }: NavigatorProvi
326
327
  return (
327
328
  <NavigatorContext.Provider value={{
328
329
  route,
330
+ currentPath: navState?.routes?.[navState.index]?.name ?? '/',
329
331
  navigate,
330
332
  replace,
331
333
  canGoBack,
@@ -461,7 +463,7 @@ const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation:
461
463
  };
462
464
 
463
465
  return (
464
- <DrawerNavigatorContext.Provider value={{ navigate, replace, route, canGoBack, goBack }}>
466
+ <DrawerNavigatorContext.Provider value={{ navigate, replace, route, currentPath: '/', canGoBack, goBack }}>
465
467
  {children}
466
468
  </DrawerNavigatorContext.Provider>
467
469
  );
@@ -3,13 +3,19 @@ import { useNavigate, useParams, useLocation } from '../router';
3
3
  import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
4
4
  import { buildNavigator, NavigatorParam } from '../routing';
5
5
 
6
- const NavigatorContext = createContext<NavigatorContextValue>({
7
- navigate: () => {},
8
- replace: () => {},
9
- route: undefined,
10
- canGoBack: () => false,
11
- goBack: () => {},
12
- });
6
+ // Use Symbol.for() to ensure a single context instance even if this module
7
+ // is loaded multiple times by the bundler (e.g. Vite with symlinked packages).
8
+ const CONTEXT_KEY = Symbol.for('idealyst.navigator.context');
9
+ const _global = globalThis as any;
10
+ const NavigatorContext: React.Context<NavigatorContextValue> = _global[CONTEXT_KEY] ||
11
+ (_global[CONTEXT_KEY] = createContext<NavigatorContextValue>({
12
+ navigate: () => {},
13
+ replace: () => {},
14
+ route: undefined,
15
+ currentPath: '/',
16
+ canGoBack: () => false,
17
+ goBack: () => {},
18
+ }));
13
19
 
14
20
  /**
15
21
  * Normalize a path and substitute variables
@@ -313,6 +319,7 @@ export const NavigatorProvider = ({
313
319
  return (
314
320
  <NavigatorContext.Provider value={{
315
321
  route,
322
+ currentPath: location.pathname,
316
323
  navigate: navigateFunction,
317
324
  replace,
318
325
  canGoBack,
@@ -41,6 +41,11 @@ export type NavigatorProviderProps = {
41
41
  */
42
42
  export type NavigatorContextValue = {
43
43
  route: NavigatorParam | undefined;
44
+ /**
45
+ * The current URL path (web) or active route path (native).
46
+ * Useful for highlighting active navigation items in sidebars/drawers.
47
+ */
48
+ currentPath: string;
44
49
  navigate: (params: NavigateParams) => void;
45
50
  /**
46
51
  * Replace the current screen with a new one. The current screen unmounts
@@ -56,7 +56,9 @@ function TabButton({route, onNavigate, currentPath}: TabButtonProps) {
56
56
  onPress={() => onNavigate(route.path)}
57
57
  style={{ margin: 4 }}
58
58
  >
59
- {route.options?.tabBarIcon?.({ size: 20, color: currentPath === route.path ? 'blue' : 'black' })}
59
+ {typeof route.options?.tabBarIcon === 'string'
60
+ ? null /* string icons handled by default layout */
61
+ : route.options?.tabBarIcon?.({ focused: currentPath === route.path, size: 'sm', color: currentPath === route.path ? 'blue' : 'black' })}
60
62
  <Text style={{ color: currentPath === route.fullPath ? 'blue' : 'black' }}>
61
63
  {route.fullPath === '/' ? 'Home' : route.fullPath}
62
64
  </Text>
@@ -0,0 +1,134 @@
1
+ import React from 'react'
2
+ import { View, Text, Pressable, Icon } from '@idealyst/components'
3
+ import { TabLayoutProps, DrawerSidebarProps } from '../routing/types'
4
+ import { Outlet } from '../router'
5
+ import { useNavigator } from '../context'
6
+
7
+ export interface DrawerLayoutProps extends TabLayoutProps {
8
+ /**
9
+ * Optional custom sidebar component.
10
+ * When provided, replaces the default sidebar navigation.
11
+ */
12
+ sidebarComponent?: React.ComponentType<DrawerSidebarProps>
13
+ }
14
+
15
+ /**
16
+ * Default Drawer Layout Component for Web
17
+ * Provides a sidebar + content navigation interface using @idealyst/components.
18
+ * The sidebar displays navigation items from routes with active state highlighting.
19
+ * If a custom sidebarComponent is provided, it renders that instead of the default sidebar.
20
+ */
21
+ export const DefaultDrawerLayout: React.FC<DrawerLayoutProps> = ({
22
+ options,
23
+ routes,
24
+ currentPath,
25
+ sidebarComponent: SidebarComponent
26
+ }) => {
27
+ const navigator = useNavigator()
28
+
29
+ return (
30
+ <View style={{ height: '100vh', flexDirection: 'column' }}>
31
+ {/* Header */}
32
+ {(options?.headerTitle || options?.headerLeft || options?.headerRight) && (
33
+ <View style={{
34
+ padding: 16,
35
+ borderBottomWidth: 1,
36
+ borderBottomColor: '#e0e0e0',
37
+ backgroundColor: '#f8f9fa'
38
+ }}>
39
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
40
+ <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
41
+ {options?.headerLeft && React.createElement(options.headerLeft as any)}
42
+
43
+ {options?.headerTitle && (
44
+ typeof options.headerTitle === 'string' ? (
45
+ <Text typography="h4" style={{ marginLeft: options.headerLeft ? 12 : 0 }}>
46
+ {options.headerTitle}
47
+ </Text>
48
+ ) : (
49
+ React.createElement(options.headerTitle as any)
50
+ )
51
+ )}
52
+ </View>
53
+
54
+ {options?.headerRight && React.createElement(options.headerRight as any)}
55
+ </View>
56
+ </View>
57
+ )}
58
+
59
+ {/* Sidebar + Content */}
60
+ <View style={{ flex: 1, flexDirection: 'row' }}>
61
+ {/* Sidebar */}
62
+ {SidebarComponent ? (
63
+ <View style={{
64
+ width: 240,
65
+ borderRightWidth: 1,
66
+ borderRightColor: '#e0e0e0',
67
+ backgroundColor: '#f8f9fa'
68
+ }}>
69
+ <SidebarComponent currentPath={currentPath} />
70
+ </View>
71
+ ) : (
72
+ <View style={{
73
+ width: 240,
74
+ borderRightWidth: 1,
75
+ borderRightColor: '#e0e0e0',
76
+ backgroundColor: '#f8f9fa',
77
+ paddingTop: 8
78
+ }}>
79
+ {routes.map((route) => {
80
+ if (route.type !== 'screen') return null
81
+
82
+ const isActive = currentPath === route.fullPath
83
+ || (route.fullPath !== '/' && currentPath.startsWith(route.fullPath + '/'))
84
+ const screenRoute = route as any
85
+ const label = screenRoute.options?.tabBarLabel
86
+ || screenRoute.options?.title
87
+ || (route.fullPath === '/' ? 'Home' : route.fullPath)
88
+ const icon = screenRoute.options?.tabBarIcon
89
+
90
+ return (
91
+ <Pressable
92
+ key={route.path}
93
+ onPress={() => {
94
+ navigator.navigate({ path: route.fullPath })
95
+ }}
96
+ style={{
97
+ flexDirection: 'row',
98
+ alignItems: 'center',
99
+ paddingVertical: 10,
100
+ paddingHorizontal: 16,
101
+ gap: 10,
102
+ backgroundColor: isActive ? '#e8f0fe' : 'transparent',
103
+ borderRightWidth: isActive ? 3 : 0,
104
+ borderRightColor: isActive ? '#1a73e8' : 'transparent'
105
+ }}
106
+ >
107
+ {icon && typeof icon === 'string' && (
108
+ <Icon
109
+ name={icon as any}
110
+ size="sm"
111
+ color={isActive ? 'blue' : 'gray'}
112
+ />
113
+ )}
114
+ <Text
115
+ typography="body2"
116
+ weight={isActive ? 'semibold' : 'normal'}
117
+ color={isActive ? 'primary' : undefined}
118
+ >
119
+ {label}
120
+ </Text>
121
+ </Pressable>
122
+ )
123
+ })}
124
+ </View>
125
+ )}
126
+
127
+ {/* Content Area */}
128
+ <View style={{ flex: 1, padding: 20 }}>
129
+ <Outlet />
130
+ </View>
131
+ </View>
132
+ </View>
133
+ )
134
+ }
@@ -1,3 +1,5 @@
1
1
  export { DefaultTabLayout } from './DefaultTabLayout'
2
2
  export { DefaultStackLayout } from './DefaultStackLayout'
3
- export type { TabLayoutProps, StackLayoutProps, TabLayoutComponent, StackLayoutComponent } from '../routing/types'
3
+ export { DefaultDrawerLayout } from './DefaultDrawerLayout'
4
+ export type { DrawerLayoutProps } from './DefaultDrawerLayout'
5
+ export type { TabLayoutProps, StackLayoutProps, TabLayoutComponent, StackLayoutComponent } from '../routing/types'
@@ -17,9 +17,14 @@ export const DrawerContentWrapper: React.FC<{
17
17
  // Get safe area insets from React Native Safe Area Context
18
18
  const insets = useSafeAreaInsets();
19
19
 
20
+ // Compute current path from navigation state
21
+ const state = drawerProps.state;
22
+ const currentRoute = state.routes[state.index];
23
+ const currentPath = currentRoute?.path ?? `/${currentRoute?.name ?? ''}`;
24
+
20
25
  return (
21
26
  <DrawerNavigatorProvider navigation={drawerProps.navigation} route={route}>
22
- <Content insets={insets} />
27
+ <Content currentPath={currentPath} insets={insets} />
23
28
  </DrawerNavigatorProvider>
24
29
  );
25
30
  };
@@ -2,7 +2,9 @@ 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, ScreenParam, NotFoundComponentProps, StackLayoutProps, TabLayoutProps, RouteWithFullPath } from './types'
5
+ import { DefaultDrawerLayout } from '../layouts/DefaultDrawerLayout'
6
+ import { NavigatorParam, RouteParam, ScreenParam, NotFoundComponentProps, StackLayoutProps, TabLayoutProps, DrawerSidebarProps, RouteWithFullPath } from './types'
7
+ import { DrawerLayoutProps } from '../layouts/DefaultDrawerLayout'
6
8
  import { NavigateParams } from '../context/types'
7
9
 
8
10
  /**
@@ -90,19 +92,22 @@ const NotFoundWrapper = ({
90
92
  }
91
93
 
92
94
  /**
93
- * Wrapper component that provides currentPath to layout components
95
+ * Wrapper component that provides currentPath to layout components.
96
+ * For drawer layouts, also passes through the optional sidebarComponent.
94
97
  */
95
98
  const LayoutWrapper: React.FC<{
96
- LayoutComponent: React.ComponentType<StackLayoutProps | TabLayoutProps>
99
+ LayoutComponent: React.ComponentType<StackLayoutProps | TabLayoutProps | DrawerLayoutProps>
97
100
  options?: any
98
101
  routes: RouteWithFullPath[]
99
- }> = ({ LayoutComponent, options, routes }) => {
102
+ sidebarComponent?: React.ComponentType<DrawerSidebarProps>
103
+ }> = ({ LayoutComponent, options, routes, sidebarComponent }) => {
100
104
  const location = useLocation()
101
105
  return (
102
106
  <LayoutComponent
103
107
  options={options}
104
108
  routes={routes}
105
109
  currentPath={location.pathname}
110
+ {...(sidebarComponent ? { sidebarComponent } : {})}
106
111
  />
107
112
  )
108
113
  }
@@ -173,9 +178,11 @@ const buildRoute = (params: RouteParam, index: number, isNested = false, parentP
173
178
  />
174
179
  );
175
180
  } else if (params.type === 'navigator') {
176
- // Get the layout component directly
181
+ // Get the layout component based on navigator layout type
177
182
  const LayoutComponent = params.layoutComponent ||
178
- (params.layout === 'tab' ? DefaultTabLayout : DefaultStackLayout);
183
+ (params.layout === 'tab' ? DefaultTabLayout :
184
+ params.layout === 'drawer' ? DefaultDrawerLayout :
185
+ DefaultStackLayout);
179
186
 
180
187
  // Compute the full path for this navigator
181
188
  const navigatorFullPath = joinPaths(parentPath, params.path);
@@ -235,6 +242,9 @@ const buildRoute = (params: RouteParam, index: number, isNested = false, parentP
235
242
  );
236
243
  }
237
244
 
245
+ // Extract sidebarComponent for drawer navigators
246
+ const sidebarComponent = params.layout === 'drawer' ? params.sidebarComponent : undefined;
247
+
238
248
  // Build the main navigator route with layout
239
249
  // Use LayoutWrapper to provide reactive currentPath
240
250
  const navigatorRoute = (
@@ -246,6 +256,7 @@ const buildRoute = (params: RouteParam, index: number, isNested = false, parentP
246
256
  LayoutComponent={LayoutComponent}
247
257
  options={params.options}
248
258
  routes={routesWithFullPaths}
259
+ sidebarComponent={sidebarComponent}
249
260
  />
250
261
  }
251
262
  >
@@ -11,10 +11,17 @@ export const NOT_FOUND_SCREEN_NAME = '__notFound__';
11
11
  */
12
12
  export type TabBarScreenOptions = {
13
13
  /**
14
- * Icon component for tab/drawer navigation (React.ComponentType, React.ReactElement, function, or string)
14
+ * Icon for tab/drawer navigation.
15
+ *
16
+ * Can be:
17
+ * - A **string** (icon name) — e.g. `"home"`, `"cog"`. The default layout renders
18
+ * `<Icon name={tabBarIcon} size="sm" />` automatically.
19
+ * - A **render function** — receives `{ focused, color, size }`. The `size` param is
20
+ * a number (from native tab bars); **ignore it** and use a Size token instead:
21
+ * `tabBarIcon: ({ focused }) => <Icon name={focused ? 'home' : 'home-outline'} size="sm" />`
15
22
  */
16
- tabBarIcon?: ((props: { focused: boolean; color: string; size: string | number }) => React.ReactElement)
17
-
23
+ tabBarIcon?: string | ((props: { focused: boolean; color: string; size: string | number }) => React.ReactElement)
24
+
18
25
  /**
19
26
  * Label for tab/drawer navigation
20
27
  */
@@ -130,10 +137,12 @@ export type StackNavigatorParam = {
130
137
  } & BaseNavigatorParam
131
138
 
132
139
  /**
133
- * Props passed to drawer sidebar components on mobile
134
- * Includes safe area insets for proper layout
140
+ * Props passed to drawer sidebar components
141
+ * Includes the current path and safe area insets for proper layout
135
142
  */
136
143
  export type DrawerSidebarProps = {
144
+ /** Current URL path, useful for highlighting active nav items */
145
+ currentPath: string;
137
146
  /**
138
147
  * Safe area insets (mobile only)
139
148
  * Use these to add padding to avoid notches, status bars, etc.