@sigmela/router 0.2.8 → 0.3.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.
Files changed (39) hide show
  1. package/README.md +107 -1
  2. package/lib/module/Drawer/Drawer.js +250 -0
  3. package/lib/module/Drawer/DrawerContext.js +4 -0
  4. package/lib/module/Drawer/DrawerIcon.web.js +47 -0
  5. package/lib/module/Drawer/RenderDrawer.native.js +244 -0
  6. package/lib/module/Drawer/RenderDrawer.web.js +197 -0
  7. package/lib/module/Drawer/useDrawer.js +11 -0
  8. package/lib/module/Navigation.js +4 -2
  9. package/lib/module/NavigationStack.js +14 -4
  10. package/lib/module/Router.js +214 -60
  11. package/lib/module/RouterContext.js +1 -1
  12. package/lib/module/ScreenStack/ScreenStack.web.js +78 -12
  13. package/lib/module/ScreenStackItem/ScreenStackItem.js +7 -7
  14. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +65 -6
  15. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +4 -5
  16. package/lib/module/SplitView/RenderSplitView.web.js +5 -7
  17. package/lib/module/SplitView/SplitView.js +10 -2
  18. package/lib/module/StackRenderer.js +10 -4
  19. package/lib/module/TabBar/RenderTabBar.native.js +55 -24
  20. package/lib/module/TabBar/RenderTabBar.web.js +25 -2
  21. package/lib/module/TabBar/TabBar.js +8 -1
  22. package/lib/module/TabBar/TabIcon.web.js +12 -7
  23. package/lib/module/index.js +2 -0
  24. package/lib/module/styles.css +255 -91
  25. package/lib/typescript/src/Drawer/Drawer.d.ts +100 -0
  26. package/lib/typescript/src/Drawer/DrawerContext.d.ts +3 -0
  27. package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +7 -0
  28. package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +8 -0
  29. package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +8 -0
  30. package/lib/typescript/src/Drawer/useDrawer.d.ts +2 -0
  31. package/lib/typescript/src/NavigationStack.d.ts +1 -0
  32. package/lib/typescript/src/Router.d.ts +13 -0
  33. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +1 -1
  34. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +1 -1
  35. package/lib/typescript/src/SplitView/SplitView.d.ts +2 -0
  36. package/lib/typescript/src/TabBar/TabBar.d.ts +10 -1
  37. package/lib/typescript/src/index.d.ts +5 -0
  38. package/lib/typescript/src/types.d.ts +12 -3
  39. package/package.json +28 -12
package/README.md CHANGED
@@ -8,9 +8,11 @@ This library is **URL-first**: you navigate by **paths** (`/users/42?tab=posts`)
8
8
 
9
9
  - **Stacks**: predictable stack-based navigation
10
10
  - **Tabs**: `TabBar` with native + web renderers (or custom tab bar)
11
+ - **Drawer**: side-panel navigation (`Drawer`)
11
12
  - **Split view**: master/details navigation (`SplitView`)
12
13
  - **Modals & sheets**: via `stackPresentation` (`modal`, `sheet`, …)
13
14
  - **Controllers**: async/guarded navigation (only present when ready)
15
+ - **Appearance**: global styling via `NavigationAppearance` (tab bar colors, fonts, blur effects, etc.)
14
16
  - **Web History integration**: keeps Router state in sync with `pushState`, `replaceState`, `popstate`
15
17
  - **Dynamic root**: swap root navigation tree at runtime (`router.setRoot`)
16
18
  - **Type-safe hooks**: `useParams`, `useQueryParams`, `useRoute`, `useCurrentRoute`
@@ -27,7 +29,7 @@ yarn add @sigmela/native-sheet
27
29
 
28
30
  - `react`
29
31
  - `react-native`
30
- - `react-native-screens` (>= `4.18.0`)
32
+ - `react-native-screens` (>= `4.24.0`)
31
33
  - `@sigmela/native-sheet` (>= `0.0.1`) — only if you use sheets
32
34
 
33
35
  ### Web CSS
@@ -292,10 +294,45 @@ Key methods:
292
294
  Notes:
293
295
  - Exactly one of `stack`, `node`, `screen` must be provided.
294
296
  - Use `prefix` to mount a tab's routes under a base path (e.g. `/mail`).
297
+ - All `TabsScreenProps` from `react-native-screens` are forwarded to native. This includes lifecycle events (`onWillAppear`, `onDidAppear`, `onWillDisappear`, `onDidDisappear`), accessibility props (`testID`, `accessibilityLabel`, `tabBarItemTestID`, `tabBarItemAccessibilityLabel`), `orientation`, `systemItem`, `freezeContents`, `placeholder`, `scrollEdgeEffects`, badge styling, and more.
298
+
299
+ #### `setTabBarConfig()`
300
+
301
+ Runtime tab bar configuration:
302
+
303
+ ```tsx
304
+ tabBar.setTabBarConfig({
305
+ bottomAccessory: (environment) => <MiniPlayer layout={environment} />, // iOS 26+
306
+ experimentalControlNavigationStateInJS: true,
307
+ });
308
+ ```
295
309
 
296
310
  Web behavior note:
297
311
  - The built-in **web** tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using `router.reset(firstRoutePath)`.
298
312
 
313
+ ### `Drawer`
314
+
315
+ `Drawer` provides side-panel navigation, similar to `TabBar` but with a slide-out panel.
316
+
317
+ ```tsx
318
+ import { Drawer, NavigationStack } from '@sigmela/router';
319
+
320
+ const homeStack = new NavigationStack().addScreen('/', HomeScreen);
321
+ const settingsStack = new NavigationStack().addScreen('/settings', SettingsScreen);
322
+
323
+ const drawer = new Drawer({ width: 280 })
324
+ .addTab({ key: 'home', stack: homeStack, title: 'Home' })
325
+ .addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
326
+ ```
327
+
328
+ Key methods:
329
+ - `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, ... })`
330
+ - `open()`, `close()`, `toggle()` — manage drawer state
331
+ - `getIsOpen()` — current open state
332
+ - `subscribeOpenState(listener)` — subscribe to open/close changes
333
+ - `onIndexChange(index)` — switch active tab
334
+ - `setBadge(index, badge | null)`
335
+
299
336
  ### `SplitView`
300
337
 
301
338
  `SplitView` renders **two stacks**: `primary` and `secondary`.
@@ -350,6 +387,54 @@ stack.addScreen('/users/:userId', UserDetails);
350
387
 
351
388
  If you never call `present()`, the screen is not pushed/replaced.
352
389
 
390
+ ## Appearance
391
+
392
+ Pass `NavigationAppearance` to `<Navigation>` to customize styling globally:
393
+
394
+ ```tsx
395
+ import { Navigation, type NavigationAppearance } from '@sigmela/router';
396
+
397
+ const appearance: NavigationAppearance = {
398
+ tabBar: {
399
+ backgroundColor: '#ffffff',
400
+ iconColor: '#999999',
401
+ iconColorActive: '#007AFF',
402
+ badgeBackgroundColor: '#FF3B30',
403
+ iOSShadowColor: '#00000020',
404
+ title: {
405
+ fontFamily: 'Inter',
406
+ fontSize: 10,
407
+ color: '#999999',
408
+ activeColor: '#007AFF',
409
+ activeFontSize: 12, // Android: active tab title font size
410
+ },
411
+
412
+ // Android-specific
413
+ androidActiveIndicatorEnabled: true,
414
+ androidActiveIndicatorColor: '#007AFF20',
415
+ androidRippleColor: '#007AFF10',
416
+ labelVisibilityMode: 'labeled',
417
+
418
+ // Tab bar behavior
419
+ hidden: false, // hide/show the tab bar
420
+ tintColor: '#007AFF', // iOS: selected tab tint + glow color
421
+ controllerMode: 'automatic', // iOS 18+: 'automatic' | 'tabBar' | 'tabSidebar'
422
+ minimizeBehavior: 'automatic', // iOS 26+: 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'
423
+ nativeContainerBackgroundColor: '#fff', // native container background
424
+ iOSBlurEffect: 'systemDefault', // iOS: tab bar blur effect
425
+ },
426
+ header: { /* ScreenStackHeaderConfigProps */ },
427
+ sheet: {
428
+ cornerRadius: 16,
429
+ backgroundColor: '#ffffff',
430
+ },
431
+ };
432
+
433
+ export default function App() {
434
+ return <Navigation router={router} appearance={appearance} />;
435
+ }
436
+ ```
437
+
353
438
  ## Hooks
354
439
 
355
440
  ### `useRouter()`
@@ -410,6 +495,27 @@ function ScreenInsideTabs() {
410
495
  }
411
496
  ```
412
497
 
498
+ ### `useTabBarHeight()`
499
+
500
+ Returns the tab bar height constant (`57`). Useful for bottom padding.
501
+
502
+ ### `useDrawer()`
503
+
504
+ Returns the nearest `Drawer` from context (only inside drawer screens).
505
+
506
+ ```tsx
507
+ import { useDrawer } from '@sigmela/router';
508
+
509
+ function ScreenInsideDrawer() {
510
+ const drawer = useDrawer();
511
+ return <Button title="Open menu" onPress={() => drawer.open()} />;
512
+ }
513
+ ```
514
+
515
+ ### `useSplitView()`
516
+
517
+ Returns the nearest `SplitView` from context (only inside split view screens).
518
+
413
519
  ## Web integration
414
520
 
415
521
  ### History API syncing
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { RenderDrawer } from './RenderDrawer';
5
+ export class Drawer {
6
+ screens = {};
7
+ stacks = {};
8
+ nodes = {};
9
+ listeners = new Set();
10
+ openListeners = new Set();
11
+ constructor(options = {}) {
12
+ this.drawerId = `drawer-${Math.random().toString(36).slice(2)}`;
13
+ this.width = options.width ?? 280;
14
+ this.state = {
15
+ tabs: [],
16
+ index: options.initialIndex ?? 0,
17
+ config: {
18
+ component: options.component
19
+ },
20
+ isOpen: false
21
+ };
22
+ }
23
+ getId() {
24
+ return this.drawerId;
25
+ }
26
+ addTab(tab) {
27
+ const sourcesCount = (tab.stack ? 1 : 0) + (tab.node ? 1 : 0) + (tab.screen ? 1 : 0);
28
+ if (sourcesCount !== 1) {
29
+ throw new Error(`Drawer.addTab: exactly one of { stack, node, screen } must be provided (got ${sourcesCount})`);
30
+ }
31
+ const {
32
+ key,
33
+ ...rest
34
+ } = tab;
35
+ const nextIndex = this.state.tabs.length;
36
+ const tabKey = key ?? `drawer-item-${nextIndex}`;
37
+ const nextTabs = [...this.state.tabs, {
38
+ tabKey,
39
+ tabPrefix: tab.prefix,
40
+ title: rest.title,
41
+ icon: rest.icon,
42
+ selectedIcon: rest.selectedIcon,
43
+ badgeValue: rest.badgeValue
44
+ }];
45
+ this.setState({
46
+ tabs: nextTabs
47
+ });
48
+ if (tab.stack) {
49
+ this.stacks[tabKey] = tab.stack;
50
+ } else if (tab.node) {
51
+ this.nodes[tabKey] = tab.node;
52
+ } else if (tab.screen) {
53
+ this.screens[tabKey] = tab.screen;
54
+ }
55
+ return this;
56
+ }
57
+ setBadge(tabIndex, badge) {
58
+ this.setState({
59
+ tabs: this.state.tabs.map((item, index) => index === tabIndex ? {
60
+ ...item,
61
+ badgeValue: badge ?? undefined
62
+ } : item)
63
+ });
64
+ }
65
+ setTabBarConfig(config) {
66
+ this.setState({
67
+ config: {
68
+ ...this.state.config,
69
+ ...config
70
+ }
71
+ });
72
+ }
73
+ onIndexChange(index) {
74
+ this.setState({
75
+ index
76
+ });
77
+ }
78
+
79
+ // ---- Open/Close state ----
80
+
81
+ open() {
82
+ if (!this.state.isOpen) {
83
+ this.setState({
84
+ isOpen: true
85
+ });
86
+ this.notifyOpenListeners(true);
87
+ }
88
+ }
89
+ close() {
90
+ if (this.state.isOpen) {
91
+ this.setState({
92
+ isOpen: false
93
+ });
94
+ this.notifyOpenListeners(false);
95
+ }
96
+ }
97
+ toggle() {
98
+ if (this.state.isOpen) {
99
+ this.close();
100
+ } else {
101
+ this.open();
102
+ }
103
+ }
104
+ getIsOpen() {
105
+ return this.state.isOpen;
106
+ }
107
+ subscribeOpenState(listener) {
108
+ this.openListeners.add(listener);
109
+ return () => {
110
+ this.openListeners.delete(listener);
111
+ };
112
+ }
113
+ notifyOpenListeners(isOpen) {
114
+ for (const listener of Array.from(this.openListeners)) {
115
+ try {
116
+ listener(isOpen);
117
+ } catch (e) {
118
+ if (__DEV__) {
119
+ console.error('[Drawer] openListener error', e);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // ---- Standard TabBar-like methods ----
126
+
127
+ getState = () => {
128
+ return this.state;
129
+ };
130
+ setState(state) {
131
+ this.state = {
132
+ ...this.state,
133
+ ...state
134
+ };
135
+ this.notifyListeners();
136
+ }
137
+ notifyListeners() {
138
+ for (const listener of Array.from(this.listeners)) {
139
+ try {
140
+ listener();
141
+ } catch (e) {
142
+ if (__DEV__) {
143
+ console.error('[Drawer] listener error', e);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ subscribe(listener) {
149
+ this.listeners.add(listener);
150
+ return () => {
151
+ this.listeners.delete(listener);
152
+ };
153
+ }
154
+ getTabs() {
155
+ return this.state.tabs.slice();
156
+ }
157
+ getInitialIndex() {
158
+ return this.state.index ?? 0;
159
+ }
160
+ getActiveChildId() {
161
+ const activeTab = this.state.tabs[this.state.index];
162
+ if (!activeTab) return undefined;
163
+ const node = this.nodes[activeTab.tabKey] ?? this.stacks[activeTab.tabKey];
164
+ return node?.getId();
165
+ }
166
+ switchToRoute(routeId) {
167
+ const idx = this.findTabIndexByRoute(routeId);
168
+ if (idx === -1) return;
169
+ if (idx === this.state.index) return;
170
+ this.setState({
171
+ index: idx
172
+ });
173
+ }
174
+ hasRoute(routeId) {
175
+ return this.findTabIndexByRoute(routeId) !== -1;
176
+ }
177
+ setActiveChildByRoute(routeId) {
178
+ this.switchToRoute(routeId);
179
+ }
180
+ getNodeRoutes() {
181
+ return [];
182
+ }
183
+ getNodeChildren() {
184
+ const children = [];
185
+ for (let idx = 0; idx < this.state.tabs.length; idx++) {
186
+ const tab = this.state.tabs[idx];
187
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
188
+ if (node) {
189
+ children.push({
190
+ prefix: tab?.tabPrefix ?? '',
191
+ node,
192
+ onMatch: () => this.onIndexChange(idx)
193
+ });
194
+ }
195
+ }
196
+ return children;
197
+ }
198
+ getRenderer() {
199
+ const drawerInstance = this;
200
+ return function DrawerScreen(props) {
201
+ return /*#__PURE__*/React.createElement(RenderDrawer, {
202
+ drawer: drawerInstance,
203
+ appearance: props?.appearance
204
+ });
205
+ };
206
+ }
207
+ seed() {
208
+ const activeTab = this.state.tabs[this.state.index];
209
+ if (!activeTab) return null;
210
+ const node = this.nodes[activeTab.tabKey];
211
+ if (node) {
212
+ return node.seed?.() ?? null;
213
+ }
214
+ const stack = this.stacks[activeTab.tabKey];
215
+ if (!stack) return null;
216
+ const firstRoute = stack.getFirstRoute();
217
+ if (!firstRoute) return null;
218
+ return {
219
+ routeId: firstRoute.routeId,
220
+ path: firstRoute.path,
221
+ stackId: stack.getId()
222
+ };
223
+ }
224
+ findTabIndexByRoute(routeId) {
225
+ for (let i = 0; i < this.state.tabs.length; i++) {
226
+ const tab = this.state.tabs[i];
227
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
228
+ if (!node) continue;
229
+ const hasRoute = this.nodeHasRoute(node, routeId);
230
+ if (hasRoute) {
231
+ return i;
232
+ }
233
+ }
234
+ return -1;
235
+ }
236
+ nodeHasRoute(node, routeId) {
237
+ const routes = node.getNodeRoutes();
238
+ for (const r of routes) {
239
+ if (r.routeId === routeId) return true;
240
+ if (r.childNode) {
241
+ if (this.nodeHasRoute(r.childNode, routeId)) return true;
242
+ }
243
+ }
244
+ const children = node.getNodeChildren();
245
+ for (const child of children) {
246
+ if (this.nodeHasRoute(child.node, routeId)) return true;
247
+ }
248
+ return false;
249
+ }
250
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ import { createContext } from 'react';
4
+ export const DrawerContext = /*#__PURE__*/createContext(null);
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ import { memo, useMemo } from 'react';
4
+ import { Image } from 'react-native';
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ const resolveImageUri = source => {
7
+ if (!source) return undefined;
8
+ const resolved = typeof Image.resolveAssetSource === 'function' ? Image.resolveAssetSource(source) : undefined;
9
+ if (resolved?.uri) return resolved.uri;
10
+ if (Array.isArray(source)) {
11
+ const first = source[0];
12
+ if (first && typeof first === 'object' && 'uri' in first) return first.uri;
13
+ }
14
+ if (typeof source === 'object' && source && 'uri' in source) return source.uri;
15
+ return undefined;
16
+ };
17
+ export const DrawerIcon = /*#__PURE__*/memo(({
18
+ source,
19
+ tintColor
20
+ }) => {
21
+ const iconUri = resolveImageUri(source);
22
+ const useMask = Boolean(tintColor && iconUri);
23
+ const maskStyle = useMemo(() => {
24
+ if (!useMask || !iconUri) return undefined;
25
+ return {
26
+ backgroundColor: tintColor,
27
+ WebkitMaskImage: `url(${iconUri})`,
28
+ WebkitMaskSize: 'contain',
29
+ WebkitMaskRepeat: 'no-repeat',
30
+ WebkitMaskPosition: 'center',
31
+ maskImage: `url(${iconUri})`,
32
+ maskSize: 'contain',
33
+ maskRepeat: 'no-repeat',
34
+ maskPosition: 'center',
35
+ width: '100%',
36
+ height: '100%'
37
+ };
38
+ }, [tintColor, iconUri, useMask]);
39
+ if (maskStyle) {
40
+ return /*#__PURE__*/_jsx("div", {
41
+ style: maskStyle
42
+ });
43
+ }
44
+ return /*#__PURE__*/_jsx(Image, {
45
+ source: source
46
+ });
47
+ });
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+
3
+ import { StackRenderer } from "../StackRenderer.js";
4
+ import { DrawerContext } from "./DrawerContext.js";
5
+ import { useRouter } from "../RouterContext.js";
6
+ import { ScreenStackItem } from 'react-native-screens';
7
+ import { Pressable, StyleSheet, View, Text } from 'react-native';
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
+ import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
10
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ const TIMING_CONFIG = {
13
+ duration: 300,
14
+ easing: Easing.bezier(0.22, 0.61, 0.36, 1)
15
+ };
16
+ const DrawerStackRenderer = /*#__PURE__*/memo(({
17
+ stack,
18
+ appearance
19
+ }) => {
20
+ const router = useRouter();
21
+ const stackId = stack.getId();
22
+ const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
23
+ const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
24
+ const history = useSyncExternalStore(subscribe, get, get);
25
+ return /*#__PURE__*/_jsx(StackRenderer, {
26
+ appearance: appearance,
27
+ stackId: stackId,
28
+ history: history
29
+ });
30
+ });
31
+ DrawerStackRenderer.displayName = 'DrawerStackRenderer';
32
+ const DrawerNodeRenderer = /*#__PURE__*/memo(({
33
+ node,
34
+ appearance
35
+ }) => {
36
+ const Renderer = useMemo(() => node.getRenderer(), [node]);
37
+ return /*#__PURE__*/_jsx(Renderer, {
38
+ appearance: appearance
39
+ });
40
+ });
41
+ DrawerNodeRenderer.displayName = 'DrawerNodeRenderer';
42
+ export const RenderDrawer = /*#__PURE__*/memo(({
43
+ drawer,
44
+ appearance = {}
45
+ }) => {
46
+ const router = useRouter();
47
+ const insets = useSafeAreaInsets();
48
+ const drawerWidth = drawer.width;
49
+ const subscribe = useCallback(cb => drawer.subscribe(cb), [drawer]);
50
+ const snapshot = useSyncExternalStore(subscribe, drawer.getState, drawer.getState);
51
+ const {
52
+ tabs,
53
+ index,
54
+ config,
55
+ isOpen
56
+ } = snapshot;
57
+
58
+ // Reanimated shared value for drawer position
59
+ const translateX = useSharedValue(0);
60
+ const overlayOpacity = useSharedValue(0);
61
+ useEffect(() => {
62
+ translateX.value = withTiming(isOpen ? drawerWidth : 0, TIMING_CONFIG);
63
+ overlayOpacity.value = withTiming(isOpen ? 0.3 : 0, TIMING_CONFIG);
64
+ }, [isOpen, drawerWidth, translateX, overlayOpacity]);
65
+ const drawerStyle = useAnimatedStyle(() => ({
66
+ transform: [{
67
+ translateX: translateX.value - drawerWidth
68
+ }]
69
+ }));
70
+ const mainStyle = useAnimatedStyle(() => ({
71
+ transform: [{
72
+ translateX: translateX.value
73
+ }]
74
+ }));
75
+ const overlayStyle = useAnimatedStyle(() => ({
76
+ opacity: overlayOpacity.value,
77
+ pointerEvents: isOpen ? 'auto' : 'none'
78
+ }));
79
+ const onItemPress = useCallback(nextIndex => {
80
+ const targetTab = tabs[nextIndex];
81
+ if (!targetTab) return;
82
+ const targetStack = drawer.stacks[targetTab.tabKey];
83
+ const targetNode = drawer.nodes[targetTab.tabKey];
84
+ if (nextIndex !== index) {
85
+ drawer.onIndexChange(nextIndex);
86
+ }
87
+ if (targetStack) {
88
+ const stackId = targetStack.getId();
89
+ const stackHistory = router.getStackHistory(stackId);
90
+ if (stackHistory.length === 0) {
91
+ const firstRoute = targetStack.getFirstRoute();
92
+ if (firstRoute?.path) {
93
+ router.navigate(firstRoute.path);
94
+ }
95
+ }
96
+ } else if (targetNode) {
97
+ const nodeId = targetNode.getId?.();
98
+ if (nodeId) {
99
+ const nodeHistory = router.getStackHistory(nodeId);
100
+ if (nodeHistory.length === 0) {
101
+ const seed = targetNode.seed?.();
102
+ if (seed?.path) {
103
+ const prefix = targetTab.tabPrefix ?? '';
104
+ const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
105
+ router.navigate(fullPath);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ drawer.close();
111
+ }, [tabs, drawer, index, router]);
112
+ const handleOverlayPress = useCallback(() => {
113
+ drawer.close();
114
+ }, [drawer]);
115
+ const [visited, setVisited] = useState({});
116
+ useEffect(() => {
117
+ const key = tabs[index]?.tabKey;
118
+ if (key) {
119
+ setVisited(prev => prev[key] ? prev : {
120
+ ...prev,
121
+ [key]: true
122
+ });
123
+ }
124
+ }, [tabs, index]);
125
+ const CustomDrawer = config.component;
126
+ return /*#__PURE__*/_jsx(ScreenStackItem, {
127
+ screenId: "root-drawer",
128
+ headerConfig: {
129
+ hidden: true
130
+ },
131
+ style: StyleSheet.absoluteFill,
132
+ stackAnimation: "slide_from_right",
133
+ children: /*#__PURE__*/_jsx(DrawerContext.Provider, {
134
+ value: drawer,
135
+ children: /*#__PURE__*/_jsxs(View, {
136
+ style: styles.container,
137
+ children: [/*#__PURE__*/_jsx(Animated.View, {
138
+ style: [styles.sidebar, {
139
+ width: drawerWidth
140
+ }, drawerStyle],
141
+ children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
142
+ onItemPress: onItemPress,
143
+ activeIndex: index,
144
+ items: tabs,
145
+ isOpen: isOpen,
146
+ onClose: handleOverlayPress
147
+ }) : /*#__PURE__*/_jsx(View, {
148
+ style: [styles.sidebarContent, {
149
+ paddingTop: insets.top + 12
150
+ }],
151
+ children: tabs.map((tab, i) => {
152
+ const isActive = i === index;
153
+ return /*#__PURE__*/_jsx(Pressable, {
154
+ onPress: () => onItemPress(i),
155
+ style: [styles.item, isActive && styles.itemActive],
156
+ children: /*#__PURE__*/_jsx(Text, {
157
+ style: [styles.itemText, isActive && styles.itemTextActive],
158
+ children: tab.title
159
+ })
160
+ }, tab.tabKey);
161
+ })
162
+ })
163
+ }), /*#__PURE__*/_jsx(Animated.View, {
164
+ style: [styles.overlay, overlayStyle],
165
+ children: /*#__PURE__*/_jsx(Pressable, {
166
+ style: StyleSheet.absoluteFill,
167
+ onPress: handleOverlayPress
168
+ })
169
+ }), /*#__PURE__*/_jsx(Animated.View, {
170
+ style: [styles.main, mainStyle],
171
+ children: tabs.filter(t => visited[t.tabKey]).map(tab => {
172
+ const isActive = tab.tabKey === tabs[index]?.tabKey;
173
+ const stackForTab = drawer.stacks[tab.tabKey];
174
+ const nodeForTab = drawer.nodes[tab.tabKey];
175
+ const ScreenForTab = drawer.screens[tab.tabKey];
176
+ return /*#__PURE__*/_jsx(View, {
177
+ style: [styles.flex, !isActive && styles.hidden],
178
+ children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
179
+ appearance: appearance,
180
+ stack: stackForTab
181
+ }) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
182
+ appearance: appearance,
183
+ node: nodeForTab
184
+ }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
185
+ }, `drawer-content-${tab.tabKey}`);
186
+ })
187
+ })]
188
+ })
189
+ })
190
+ });
191
+ });
192
+ const styles = StyleSheet.create({
193
+ container: {
194
+ flex: 1,
195
+ flexDirection: 'row',
196
+ overflow: 'hidden'
197
+ },
198
+ sidebar: {
199
+ position: 'absolute',
200
+ top: 0,
201
+ bottom: 0,
202
+ left: 0,
203
+ zIndex: 10,
204
+ backgroundColor: '#ffffff'
205
+ },
206
+ sidebarContent: {
207
+ flex: 1
208
+ },
209
+ main: {
210
+ flex: 1,
211
+ width: '100%'
212
+ },
213
+ overlay: {
214
+ ...StyleSheet.absoluteFillObject,
215
+ zIndex: 5,
216
+ backgroundColor: '#000000'
217
+ },
218
+ item: {
219
+ height: 44,
220
+ flexDirection: 'row',
221
+ alignItems: 'center',
222
+ paddingHorizontal: 12,
223
+ marginHorizontal: 12,
224
+ marginBottom: 4,
225
+ borderRadius: 10
226
+ },
227
+ itemActive: {
228
+ backgroundColor: 'rgba(201, 204, 209, 0.32)'
229
+ },
230
+ itemText: {
231
+ flex: 1,
232
+ fontSize: 14,
233
+ fontWeight: '600'
234
+ },
235
+ itemTextActive: {
236
+ fontWeight: '700'
237
+ },
238
+ flex: {
239
+ flex: 1
240
+ },
241
+ hidden: {
242
+ display: 'none'
243
+ }
244
+ });