@idealyst/navigation 1.2.114 → 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 +6 -6
- package/src/context/NavigatorContext.native.tsx +3 -1
- package/src/context/NavigatorContext.web.tsx +14 -7
- package/src/context/types.ts +5 -0
- package/src/examples/CustomTabLayout.tsx +3 -1
- package/src/layouts/DefaultDrawerLayout.tsx +134 -0
- package/src/layouts/index.ts +3 -1
- package/src/routing/DrawerContentWrapper.native.tsx +6 -1
- package/src/routing/router.web.tsx +17 -6
- package/src/routing/types.ts +14 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/navigation",
|
|
3
|
-
"version": "1.2.
|
|
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.
|
|
48
|
+
"@idealyst/components": "^1.2.116",
|
|
49
49
|
"@idealyst/microphone": "^1.2.30",
|
|
50
|
-
"@idealyst/theme": "^1.2.
|
|
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.
|
|
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.
|
|
81
|
+
"@idealyst/markdown": "^1.2.116",
|
|
82
82
|
"@idealyst/microphone": "^1.2.30",
|
|
83
|
-
"@idealyst/theme": "^1.2.
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
package/src/context/types.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/layouts/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { DefaultTabLayout } from './DefaultTabLayout'
|
|
2
2
|
export { DefaultStackLayout } from './DefaultStackLayout'
|
|
3
|
-
export
|
|
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 {
|
|
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
|
-
|
|
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
|
|
181
|
+
// Get the layout component based on navigator layout type
|
|
177
182
|
const LayoutComponent = params.layoutComponent ||
|
|
178
|
-
(params.layout === 'tab' ? DefaultTabLayout :
|
|
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
|
>
|
package/src/routing/types.ts
CHANGED
|
@@ -11,10 +11,17 @@ export const NOT_FOUND_SCREEN_NAME = '__notFound__';
|
|
|
11
11
|
*/
|
|
12
12
|
export type TabBarScreenOptions = {
|
|
13
13
|
/**
|
|
14
|
-
* Icon
|
|
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
|
|
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.
|