@idealyst/navigation 1.0.49 → 1.0.51

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.
@@ -0,0 +1,142 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ export const tabBarLayoutStyles = StyleSheet.create(theme => ({
4
+ container: {
5
+ flex: 1,
6
+ backgroundColor: theme.colors.background,
7
+ },
8
+
9
+ headerContainer: {
10
+ flexDirection: 'row',
11
+ alignItems: 'center',
12
+ justifyContent: 'space-between',
13
+ backgroundColor: theme.colors.surface,
14
+ borderBottomWidth: 1,
15
+ borderBottomColor: theme.colors.border,
16
+ paddingHorizontal: theme.spacing?.md,
17
+ zIndex: 10,
18
+ web: {
19
+ backgroundColor: '#ffffff', // Light background for web
20
+ borderBottomColor: '#e0e0e0', // Light border for web
21
+ },
22
+ },
23
+
24
+ headerContent: {
25
+ flex: 1,
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ },
29
+
30
+ headerRightContent: {
31
+ flexDirection: 'row',
32
+ alignItems: 'center',
33
+ gap: theme.spacing?.sm,
34
+ },
35
+
36
+ headerTabs: {
37
+ flexDirection: 'row',
38
+ alignItems: 'center',
39
+ gap: theme.spacing?.xs,
40
+ },
41
+
42
+ bodyContainer: {
43
+ flex: 1,
44
+ position: 'relative',
45
+ },
46
+
47
+ mainContent: {
48
+ flex: 1,
49
+ },
50
+
51
+ contentArea: {
52
+ flex: 1,
53
+ },
54
+
55
+ tabBarBottom: {
56
+ flexDirection: 'row',
57
+ backgroundColor: theme.colors.surface,
58
+ borderTopWidth: 1,
59
+ borderTopColor: theme.colors.border,
60
+ paddingTop: 6,
61
+ paddingHorizontal: 0,
62
+ web: {
63
+ backgroundColor: '#ffffff', // Light background for web
64
+ borderTopColor: '#e0e0e0', // Light border for web
65
+ },
66
+ },
67
+
68
+ tabButton: {
69
+ flex: 1,
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ paddingVertical: 6,
73
+ paddingHorizontal: 4,
74
+ minHeight: 49,
75
+ },
76
+
77
+ tabButtonHeader: {
78
+ flex: 0,
79
+ flexDirection: 'row',
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ paddingVertical: theme.spacing?.xs,
83
+ paddingHorizontal: theme.spacing?.md,
84
+ borderRadius: theme.radius?.md,
85
+ minHeight: 36,
86
+ },
87
+
88
+ tabButtonActive: {
89
+ // Let native platform handle active state styling
90
+ },
91
+
92
+ tabButtonDisabled: {
93
+ opacity: 0.5,
94
+ },
95
+
96
+ tabIconContainer: {
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ },
100
+
101
+ tabIcon: {
102
+ width: 24,
103
+ height: 24,
104
+ },
105
+
106
+ tabLabel: {
107
+ fontSize: 10,
108
+ marginTop: 2,
109
+ color: theme.colors.onSurface,
110
+ textAlign: 'center',
111
+ },
112
+
113
+ tabLabelHeader: {
114
+ fontSize: 14,
115
+ marginLeft: theme.spacing?.xs,
116
+ color: theme.colors.onSurface,
117
+ },
118
+
119
+ tabLabelActive: {
120
+ color: theme.colors.primary,
121
+ fontWeight: '600',
122
+ },
123
+
124
+ tabBadge: {
125
+ position: 'absolute',
126
+ top: -4,
127
+ right: -8,
128
+ backgroundColor: theme.colors.error,
129
+ borderRadius: 10,
130
+ minWidth: 16,
131
+ height: 16,
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ paddingHorizontal: 4,
135
+ },
136
+
137
+ tabBadgeText: {
138
+ color: theme.colors.onError,
139
+ fontSize: 10,
140
+ fontWeight: 'bold',
141
+ },
142
+ }));
@@ -0,0 +1,286 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { View, Text, Button, Icon, Pressable } from '@idealyst/components';
3
+ import { TabBarLayoutProps, TabBarConfig, TabButtonProps } from './types';
4
+ import { tabBarLayoutStyles } from './TabBarLayout.styles';
5
+
6
+ const DEFAULT_AUTO_BREAKPOINT = 768;
7
+
8
+ // Web doesn't need safe area insets
9
+ const useSafeAreaInsets = () => ({ top: 0, bottom: 0, left: 0, right: 0 });
10
+
11
+ const WebHeader: React.FC<{
12
+ config: any;
13
+ styles: any;
14
+ showTabsInHeader: boolean;
15
+ tabBarConfig: TabBarConfig;
16
+ insets: any;
17
+ }> = ({ config, styles, showTabsInHeader, tabBarConfig, insets }) => {
18
+ const handleBackPress = () => {
19
+ if (config.onBackPress) {
20
+ config.onBackPress();
21
+ } else {
22
+ // Web history back
23
+ window.history.back();
24
+ }
25
+ };
26
+
27
+ const headerDynamicStyles = {
28
+ height: config.height,
29
+ minHeight: config.height,
30
+ paddingTop: config.enabled ? insets.top : 0,
31
+ };
32
+
33
+ return (
34
+ <View
35
+ style={[
36
+ styles.headerContainer,
37
+ headerDynamicStyles,
38
+ config.style,
39
+ ]}
40
+ >
41
+ <View style={styles.headerContent}>
42
+ {/* Custom content overrides native elements */}
43
+ {config.content ? (
44
+ config.content
45
+ ) : (
46
+ <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
47
+ {/* Back Button */}
48
+ {config.showBackButton && (
49
+ <Button
50
+ variant="text"
51
+ onPress={handleBackPress}
52
+ style={{ marginRight: 8, minWidth: 'auto' }}
53
+ >
54
+ <Icon name="arrow-left" size="lg" color="primary" />
55
+ </Button>
56
+ )}
57
+
58
+ {/* Title */}
59
+ {config.title && (
60
+ <Text
61
+ size="large"
62
+ weight="semibold"
63
+ style={{ flex: 1 }}
64
+ >
65
+ {config.title}
66
+ </Text>
67
+ )}
68
+ </View>
69
+ )}
70
+ </View>
71
+
72
+ <View style={styles.headerRightContent}>
73
+ {/* Tabs in header for wide screens */}
74
+ {showTabsInHeader && tabBarConfig.items.length > 0 && (
75
+ <TabBar config={tabBarConfig} position="header" />
76
+ )}
77
+
78
+ {/* Additional right content */}
79
+ {config.rightContent}
80
+ </View>
81
+ </View>
82
+ );
83
+ };
84
+
85
+ const TabButton: React.FC<TabButtonProps> = ({ item, isActive, onPress, position }) => {
86
+ const styles = tabBarLayoutStyles;
87
+ const isHeader = position === 'header';
88
+
89
+ return (
90
+ <Pressable
91
+ onPress={onPress}
92
+ disabled={item.disabled}
93
+ style={[
94
+ isHeader ? styles.tabButtonHeader : styles.tabButton,
95
+ isActive && styles.tabButtonActive,
96
+ item.disabled && styles.tabButtonDisabled,
97
+ ]}
98
+ >
99
+ <View style={styles.tabIconContainer}>
100
+ {item.icon && (
101
+ typeof item.icon === 'string' ? (
102
+ <Icon name={item.icon as any} size={isHeader ? "lg" : "md"} color={isActive ? 'primary' : 'secondary'} />
103
+ ) : (
104
+ item.icon
105
+ )
106
+ )}
107
+ {item.badge !== undefined && (
108
+ <View style={styles.tabBadge}>
109
+ <Text style={styles.tabBadgeText}>
110
+ {typeof item.badge === 'number' && item.badge > 99 ? '99+' : item.badge}
111
+ </Text>
112
+ </View>
113
+ )}
114
+ </View>
115
+ {(!isHeader || (isHeader && item.label)) && (
116
+ <Text
117
+ size={isHeader ? "medium" : "small"}
118
+ color={isActive ? 'primary' : 'secondary'}
119
+ style={isHeader ? { marginLeft: 8 } : { marginTop: 2, textAlign: 'center', fontSize: 10 }}
120
+ >
121
+ {item.label}
122
+ </Text>
123
+ )}
124
+ </Pressable>
125
+ );
126
+ };
127
+
128
+ const TabBar: React.FC<{
129
+ config: TabBarConfig;
130
+ position: 'bottom' | 'header';
131
+ }> = ({ config, position }) => {
132
+ const styles = tabBarLayoutStyles;
133
+
134
+ if (config.renderTabBar) {
135
+ return (
136
+ <>
137
+ {config.renderTabBar({
138
+ items: config.items,
139
+ activeTab: config.activeTab,
140
+ onTabSelect: config.onTabSelect,
141
+ position,
142
+ })}
143
+ </>
144
+ );
145
+ }
146
+
147
+ const containerStyle = position === 'header'
148
+ ? styles.headerTabs
149
+ : [styles.tabBarBottom, config.style];
150
+
151
+ return (
152
+ <View style={containerStyle}>
153
+ {config.items.map((item) => {
154
+ if (item.renderTab) {
155
+ return (
156
+ <React.Fragment key={item.id}>
157
+ {item.renderTab({
158
+ item,
159
+ isActive: item.id === config.activeTab,
160
+ onPress: () => config.onTabSelect?.(item.id),
161
+ position,
162
+ })}
163
+ </React.Fragment>
164
+ );
165
+ }
166
+
167
+ return (
168
+ <TabButton
169
+ key={item.id}
170
+ item={item}
171
+ isActive={item.id === config.activeTab}
172
+ onPress={() => config.onTabSelect?.(item.id)}
173
+ position={position}
174
+ />
175
+ );
176
+ })}
177
+ </View>
178
+ );
179
+ };
180
+
181
+ const TabBarLayout: React.FC<TabBarLayoutProps> = ({
182
+ children,
183
+ tabBar = {},
184
+ header = {},
185
+ style,
186
+ testID,
187
+ }) => {
188
+ const styles = tabBarLayoutStyles;
189
+ const insets = useSafeAreaInsets();
190
+ const [screenWidth, setScreenWidth] = useState(() => window.innerWidth);
191
+
192
+ // Default tab bar configuration
193
+ const tabBarConfig = {
194
+ items: [],
195
+ activeTab: undefined,
196
+ onTabSelect: undefined,
197
+ position: 'auto' as const,
198
+ autoBreakpoint: DEFAULT_AUTO_BREAKPOINT,
199
+ style: undefined,
200
+ tabStyle: undefined,
201
+ showLabels: true,
202
+ renderTabBar: undefined,
203
+ ...tabBar,
204
+ };
205
+
206
+ // Default header configuration
207
+ const headerConfig = {
208
+ enabled: true,
209
+ height: 64,
210
+ title: undefined,
211
+ showBackButton: false,
212
+ onBackPress: undefined,
213
+ content: null,
214
+ rightContent: null,
215
+ style: undefined,
216
+ showTabs: true,
217
+ native: true,
218
+ ...header,
219
+ };
220
+
221
+ // Update screen width on window resize
222
+ useEffect(() => {
223
+ const updateDimensions = () => {
224
+ setScreenWidth(window.innerWidth);
225
+ };
226
+
227
+ window.addEventListener('resize', updateDimensions);
228
+ return () => window.removeEventListener('resize', updateDimensions);
229
+ }, []);
230
+
231
+ // Determine actual tab position
232
+ const actualTabPosition = (() => {
233
+ if (tabBarConfig.position === 'auto') {
234
+ // On web, use header tabs for wide screens
235
+ return screenWidth >= tabBarConfig.autoBreakpoint ? 'header' : 'bottom';
236
+ }
237
+ return tabBarConfig.position;
238
+ })();
239
+
240
+ const showTabsInHeader = actualTabPosition === 'header' && headerConfig.showTabs;
241
+ const showTabsAtBottom = actualTabPosition === 'bottom';
242
+
243
+ // Create dynamic styles for bottom tab bar (no safe area needed on web)
244
+ const tabBarBottomStyles = {
245
+ paddingBottom: 0,
246
+ };
247
+
248
+ return (
249
+ <View
250
+ style={[
251
+ styles.container,
252
+ style,
253
+ ]}
254
+ testID={testID}
255
+ >
256
+ {/* Header */}
257
+ {headerConfig.enabled && (
258
+ <WebHeader
259
+ config={headerConfig}
260
+ styles={styles}
261
+ showTabsInHeader={showTabsInHeader}
262
+ tabBarConfig={tabBarConfig}
263
+ insets={insets}
264
+ />
265
+ )}
266
+
267
+ {/* Main Content Area */}
268
+ <View style={styles.bodyContainer}>
269
+ <View style={styles.mainContent}>
270
+ <View style={styles.contentArea}>
271
+ {children}
272
+ </View>
273
+ </View>
274
+ </View>
275
+
276
+ {/* Bottom Tab Bar */}
277
+ {showTabsAtBottom && tabBarConfig.items.length > 0 && (
278
+ <View style={tabBarBottomStyles}>
279
+ <TabBar config={tabBarConfig} position="bottom" />
280
+ </View>
281
+ )}
282
+ </View>
283
+ );
284
+ };
285
+
286
+ export default TabBarLayout;
@@ -0,0 +1,2 @@
1
+ export { default as TabBarLayout } from './TabBarLayout.native';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default as TabBarLayout } from './TabBarLayout';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default as TabBarLayout } from './TabBarLayout';
2
+ export * from './types';
@@ -0,0 +1,176 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface TabBarItem {
4
+ /**
5
+ * Unique identifier for the tab
6
+ */
7
+ id: string;
8
+
9
+ /**
10
+ * Display label for the tab
11
+ */
12
+ label: string;
13
+
14
+ /**
15
+ * Icon for the tab (can be a component or icon name)
16
+ */
17
+ icon?: ReactNode | string;
18
+
19
+ /**
20
+ * Badge content to display on the tab
21
+ */
22
+ badge?: string | number;
23
+
24
+ /**
25
+ * Whether the tab is disabled
26
+ */
27
+ disabled?: boolean;
28
+
29
+ /**
30
+ * Custom render function for the tab button
31
+ */
32
+ renderTab?: (props: TabButtonProps) => ReactNode;
33
+ }
34
+
35
+ export interface TabButtonProps {
36
+ item: TabBarItem;
37
+ isActive: boolean;
38
+ onPress: () => void;
39
+ position: 'bottom' | 'header';
40
+ }
41
+
42
+ export interface TabBarConfig {
43
+ /**
44
+ * Array of tab items
45
+ */
46
+ items: TabBarItem[];
47
+
48
+ /**
49
+ * Currently active tab ID
50
+ */
51
+ activeTab?: string;
52
+
53
+ /**
54
+ * Callback when tab is selected
55
+ */
56
+ onTabSelect?: (tabId: string) => void;
57
+
58
+ /**
59
+ * Position of tab bar
60
+ * - 'bottom': Traditional mobile tab bar at bottom
61
+ * - 'header': Tabs integrated into header (for wider screens)
62
+ * - 'auto': Automatically choose based on screen width
63
+ */
64
+ position?: 'bottom' | 'header' | 'auto';
65
+
66
+ /**
67
+ * Breakpoint for auto position switching (default: 768px)
68
+ */
69
+ autoBreakpoint?: number;
70
+
71
+ /**
72
+ * Custom styles for tab bar container
73
+ */
74
+ style?: any;
75
+
76
+ /**
77
+ * Custom styles for tab buttons
78
+ */
79
+ tabStyle?: any;
80
+
81
+ /**
82
+ * Whether to show labels with icons
83
+ */
84
+ showLabels?: boolean;
85
+
86
+ /**
87
+ * Custom component for rendering tab bar
88
+ */
89
+ renderTabBar?: (props: TabBarRenderProps) => ReactNode;
90
+ }
91
+
92
+ export interface TabBarRenderProps {
93
+ items: TabBarItem[];
94
+ activeTab?: string;
95
+ onTabSelect?: (tabId: string) => void;
96
+ position: 'bottom' | 'header';
97
+ }
98
+
99
+ export interface TabBarHeaderConfig {
100
+ /**
101
+ * Whether the header is enabled
102
+ */
103
+ enabled?: boolean;
104
+
105
+ /**
106
+ * Height of the header
107
+ */
108
+ height?: number;
109
+
110
+ /**
111
+ * Header title (native-style)
112
+ */
113
+ title?: string;
114
+
115
+ /**
116
+ * Whether to show back button (auto-detected from navigation stack)
117
+ */
118
+ showBackButton?: boolean;
119
+
120
+ /**
121
+ * Custom back button handler (overrides native navigation)
122
+ */
123
+ onBackPress?: () => void;
124
+
125
+ /**
126
+ * Content to display in the header (left side) - overrides native elements
127
+ */
128
+ content?: ReactNode;
129
+
130
+ /**
131
+ * Content to display on the right side of header
132
+ */
133
+ rightContent?: ReactNode;
134
+
135
+ /**
136
+ * Custom styles for the header
137
+ */
138
+ style?: any;
139
+
140
+ /**
141
+ * Whether to show tabs in header (when position is 'header')
142
+ */
143
+ showTabs?: boolean;
144
+
145
+ /**
146
+ * Use native header styling (platform-specific)
147
+ */
148
+ native?: boolean;
149
+ }
150
+
151
+ export interface TabBarLayoutProps {
152
+ /**
153
+ * The main content to display
154
+ */
155
+ children?: ReactNode;
156
+
157
+ /**
158
+ * Tab bar configuration
159
+ */
160
+ tabBar?: TabBarConfig;
161
+
162
+ /**
163
+ * Header configuration
164
+ */
165
+ header?: TabBarHeaderConfig;
166
+
167
+ /**
168
+ * Additional styles for the layout container
169
+ */
170
+ style?: any;
171
+
172
+ /**
173
+ * Test ID for testing
174
+ */
175
+ testID?: string;
176
+ }
@@ -1 +1,2 @@
1
- export * from './GeneralLayout';
1
+ export * from './GeneralLayout';
2
+ export * from './TabBarLayout';