rnnovaui 0.9.0 → 0.9.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": "rnnovaui",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -0,0 +1,170 @@
1
+ import React, {
2
+ Children,
3
+ createContext,
4
+ isValidElement,
5
+ memo,
6
+ useCallback,
7
+ useContext,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+
12
+ import { StyleSheet, View } from "react-native";
13
+
14
+ import { useUITheme } from "../../theme";
15
+ import { UITabsList } from "./UITabsList";
16
+ import { UITabsTrigger } from "./UITabsTrigger";
17
+ import { UITabsContent } from "./UITabsContent";
18
+ import { UITabsIndicator } from "./UITabsIndicator";
19
+
20
+ const TabsContext = createContext(null);
21
+
22
+ const UI_TABS_SIZES = {
23
+ sm: "sm",
24
+ md: "md",
25
+ lg: "lg",
26
+ };
27
+
28
+ export const UITabs = memo(function UITabs({
29
+ children,
30
+
31
+ value,
32
+ defaultValue,
33
+
34
+ onValueChange,
35
+
36
+ size = "md",
37
+
38
+ animated = true,
39
+ animationDuration = 220,
40
+
41
+ variant = "default",
42
+
43
+ activeColor,
44
+ inactiveColor,
45
+
46
+ indicatorColor,
47
+ indicatorHeight = 2,
48
+
49
+ indicatorWidth = "content",
50
+
51
+ disabled = false,
52
+
53
+ containerStyle,
54
+
55
+ testID,
56
+ }) {
57
+ const { theme } = useUITheme();
58
+
59
+ const colors = theme?.colors || {};
60
+
61
+ const controlled = value !== undefined;
62
+
63
+ const [internalValue, setInternalValue] = useState(defaultValue ?? null);
64
+
65
+ const currentValue = controlled ? value : internalValue;
66
+
67
+ const safeSize = UI_TABS_SIZES[size] ? size : "md";
68
+
69
+ const changeValue = useCallback(
70
+ (nextValue) => {
71
+ if (disabled) {
72
+ return;
73
+ }
74
+
75
+ if (!controlled) {
76
+ setInternalValue(nextValue);
77
+ }
78
+
79
+ onValueChange?.(nextValue);
80
+ },
81
+ [disabled, controlled, onValueChange],
82
+ );
83
+
84
+ const contextValue = useMemo(
85
+ () => ({
86
+ colors,
87
+
88
+ value: currentValue,
89
+
90
+ setValue: changeValue,
91
+
92
+ size: safeSize,
93
+
94
+ animated,
95
+
96
+ animationDuration,
97
+
98
+ variant,
99
+
100
+ activeColor: activeColor || colors.primary || "#FF5A1F",
101
+
102
+ inactiveColor: inactiveColor || colors.textSecondary || "#525252",
103
+
104
+ indicatorColor:
105
+ indicatorColor || activeColor || colors.primary || "#FF5A1F",
106
+
107
+ indicatorHeight,
108
+
109
+ indicatorWidth,
110
+
111
+ disabled,
112
+ }),
113
+ [
114
+ colors,
115
+ currentValue,
116
+ changeValue,
117
+ safeSize,
118
+ animated,
119
+ animationDuration,
120
+ variant,
121
+ activeColor,
122
+ inactiveColor,
123
+ indicatorColor,
124
+ indicatorHeight,
125
+ indicatorWidth,
126
+ disabled,
127
+ ],
128
+ );
129
+
130
+ return (
131
+ <TabsContext.Provider value={contextValue}>
132
+ <View testID={testID} style={[styles.container, containerStyle]}>
133
+ {Children.map(children, (child) => {
134
+ if (!isValidElement(child)) {
135
+ return child;
136
+ }
137
+
138
+ return child;
139
+ })}
140
+ </View>
141
+ </TabsContext.Provider>
142
+ );
143
+ });
144
+
145
+ export function useUITabs() {
146
+ const context = useContext(TabsContext);
147
+
148
+ if (!context) {
149
+ throw new Error("UITabs components must be used inside UITabs.");
150
+ }
151
+
152
+ return context;
153
+ }
154
+
155
+ export const UITabsSizes = UI_TABS_SIZES;
156
+
157
+ const styles = StyleSheet.create({
158
+ container: {
159
+ width: "100%",
160
+ },
161
+ });
162
+
163
+ UITabs.displayName = "UITabs";
164
+ UITabs.List = UITabsList;
165
+
166
+ UITabs.Trigger = UITabsTrigger;
167
+
168
+ UITabs.Content = UITabsContent;
169
+
170
+ UITabs.Indicator = UITabsIndicator;
@@ -0,0 +1,122 @@
1
+ import React, { memo, useEffect, useRef, useState } from "react";
2
+
3
+ import { Animated, View } from "react-native";
4
+
5
+ import { useUITabs } from "./UITabs";
6
+
7
+ export const UITabsContent = memo(function UITabsContent({
8
+ children,
9
+
10
+ value,
11
+
12
+ style,
13
+
14
+ animationDuration,
15
+
16
+ testID,
17
+ }) {
18
+ const tabs = useUITabs();
19
+
20
+ const active = tabs.value === value;
21
+
22
+ const [mounted, setMounted] = useState(active);
23
+
24
+ const opacity = useRef(new Animated.Value(active ? 1 : 0)).current;
25
+
26
+ const translateY = useRef(new Animated.Value(active ? 0 : 5)).current;
27
+
28
+ useEffect(() => {
29
+ if (active) {
30
+ setMounted(true);
31
+
32
+ if (!tabs.animated) {
33
+ opacity.setValue(1);
34
+ translateY.setValue(0);
35
+
36
+ return;
37
+ }
38
+
39
+ Animated.parallel([
40
+ Animated.timing(opacity, {
41
+ toValue: 1,
42
+
43
+ duration: animationDuration ?? tabs.animationDuration,
44
+
45
+ useNativeDriver: true,
46
+ }),
47
+
48
+ Animated.timing(translateY, {
49
+ toValue: 0,
50
+
51
+ duration: animationDuration ?? tabs.animationDuration,
52
+
53
+ useNativeDriver: true,
54
+ }),
55
+ ]).start();
56
+
57
+ return;
58
+ }
59
+
60
+ if (!tabs.animated) {
61
+ opacity.setValue(0);
62
+ setMounted(false);
63
+
64
+ return;
65
+ }
66
+
67
+ Animated.parallel([
68
+ Animated.timing(opacity, {
69
+ toValue: 0,
70
+
71
+ duration: animationDuration ?? tabs.animationDuration,
72
+
73
+ useNativeDriver: true,
74
+ }),
75
+
76
+ Animated.timing(translateY, {
77
+ toValue: 5,
78
+
79
+ duration: animationDuration ?? tabs.animationDuration,
80
+
81
+ useNativeDriver: true,
82
+ }),
83
+ ]).start(() => {
84
+ setMounted(false);
85
+ });
86
+ }, [
87
+ active,
88
+ tabs.animated,
89
+ tabs.animationDuration,
90
+ animationDuration,
91
+ opacity,
92
+ translateY,
93
+ ]);
94
+
95
+ if (!mounted) {
96
+ return null;
97
+ }
98
+
99
+ return (
100
+ <Animated.View
101
+ testID={testID}
102
+ accessibilityRole="tabpanel"
103
+ style={[
104
+ {
105
+ opacity,
106
+
107
+ transform: [
108
+ {
109
+ translateY,
110
+ },
111
+ ],
112
+ },
113
+
114
+ style,
115
+ ]}
116
+ >
117
+ {children}
118
+ </Animated.View>
119
+ );
120
+ });
121
+
122
+ UITabsContent.displayName = "UITabs.Content";
@@ -0,0 +1,34 @@
1
+ import React, { memo } from "react";
2
+
3
+ import { View } from "react-native";
4
+
5
+ import { useUITabs } from "./UITabs";
6
+
7
+ export const UITabsIndicator = memo(function UITabsIndicator({
8
+ children,
9
+ style,
10
+ }) {
11
+ const tabs = useUITabs();
12
+
13
+ if (children) {
14
+ return <View style={style}>{children}</View>;
15
+ }
16
+
17
+ return (
18
+ <View
19
+ style={[
20
+ {
21
+ height: tabs.indicatorHeight,
22
+
23
+ backgroundColor: tabs.indicatorColor,
24
+
25
+ borderRadius: tabs.indicatorHeight / 2,
26
+ },
27
+
28
+ style,
29
+ ]}
30
+ />
31
+ );
32
+ });
33
+
34
+ UITabsIndicator.displayName = "UITabs.Indicator";
@@ -0,0 +1,82 @@
1
+ import React, { memo } from "react";
2
+
3
+ import { ScrollView, StyleSheet, View } from "react-native";
4
+
5
+ import { useUITabs } from "./UITabs";
6
+
7
+ export const UITabsList = memo(function UITabsList({
8
+ children,
9
+
10
+ scrollable = false,
11
+
12
+ showsHorizontalScrollIndicator = false,
13
+
14
+ contentContainerStyle,
15
+
16
+ style,
17
+
18
+ testID,
19
+ }) {
20
+ const tabs = useUITabs();
21
+
22
+ const sizePadding = tabs.size === "sm" ? 8 : tabs.size === "lg" ? 16 : 12;
23
+
24
+ const content = (
25
+ <View
26
+ style={[
27
+ styles.row,
28
+
29
+ {
30
+ paddingHorizontal: sizePadding,
31
+ },
32
+ ]}
33
+ >
34
+ {children}
35
+ </View>
36
+ );
37
+
38
+ if (scrollable) {
39
+ return (
40
+ <ScrollView
41
+ testID={testID}
42
+ horizontal
43
+ showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
44
+ bounces
45
+ keyboardShouldPersistTaps="handled"
46
+ contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
47
+ style={style}
48
+ >
49
+ {content}
50
+ </ScrollView>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <View testID={testID} style={[styles.container, style]}>
56
+ {content}
57
+ </View>
58
+ );
59
+ });
60
+
61
+ const styles = StyleSheet.create({
62
+ container: {
63
+ width: "100%",
64
+ overflow: "hidden",
65
+ },
66
+
67
+ scrollContent: {
68
+ flexGrow: 1,
69
+ },
70
+
71
+ row: {
72
+ minHeight: 48,
73
+
74
+ flexDirection: "row",
75
+
76
+ alignItems: "stretch",
77
+
78
+ gap: 4,
79
+ },
80
+ });
81
+
82
+ UITabsList.displayName = "UITabs.List";
@@ -0,0 +1,288 @@
1
+ import React, { memo, useEffect, useRef } from "react";
2
+
3
+ import { Animated, Pressable, StyleSheet, Text, View } from "react-native";
4
+
5
+ import { useUITabs } from "./UITabs";
6
+
7
+ export const UITabsTrigger = memo(function UITabsTrigger({
8
+ children,
9
+
10
+ value,
11
+
12
+ icon,
13
+ iconPosition = "left",
14
+
15
+ badge,
16
+
17
+ disabled = false,
18
+
19
+ onPress,
20
+
21
+ activeColor,
22
+ inactiveColor,
23
+
24
+ style,
25
+ contentStyle,
26
+ textStyle,
27
+ iconStyle,
28
+ badgeStyle,
29
+
30
+ testID,
31
+
32
+ accessibilityLabel,
33
+ }) {
34
+ const tabs = useUITabs();
35
+
36
+ const active = tabs.value === value;
37
+
38
+ const isDisabled = disabled || tabs.disabled;
39
+
40
+ const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
41
+
42
+ useEffect(() => {
43
+ if (!tabs.animated) {
44
+ progress.setValue(active ? 1 : 0);
45
+
46
+ return;
47
+ }
48
+
49
+ Animated.timing(progress, {
50
+ toValue: active ? 1 : 0,
51
+
52
+ duration: tabs.animationDuration,
53
+
54
+ useNativeDriver: true,
55
+ }).start();
56
+ }, [active, tabs.animated, tabs.animationDuration, progress]);
57
+
58
+ const handlePress = (event) => {
59
+ if (isDisabled) {
60
+ return;
61
+ }
62
+
63
+ tabs.setValue(value);
64
+
65
+ onPress?.(event);
66
+ };
67
+
68
+ const resolvedActiveColor = activeColor || tabs.activeColor;
69
+
70
+ const resolvedInactiveColor = inactiveColor || tabs.inactiveColor;
71
+
72
+ const iconElement = icon ? (
73
+ <View
74
+ style={[
75
+ styles.icon,
76
+
77
+ iconPosition === "right" ? styles.iconRight : styles.iconLeft,
78
+
79
+ iconStyle,
80
+ ]}
81
+ >
82
+ {icon}
83
+ </View>
84
+ ) : null;
85
+
86
+ const textElement = (
87
+ <Text
88
+ numberOfLines={1}
89
+ style={[
90
+ styles.text,
91
+
92
+ {
93
+ fontSize: tabs.size === "sm" ? 12 : tabs.size === "lg" ? 16 : 14,
94
+
95
+ color: active ? resolvedActiveColor : resolvedInactiveColor,
96
+
97
+ opacity: isDisabled ? 0.45 : 1,
98
+ },
99
+
100
+ textStyle,
101
+ ]}
102
+ >
103
+ {children}
104
+ </Text>
105
+ );
106
+
107
+ return (
108
+ <Pressable
109
+ testID={testID}
110
+ disabled={isDisabled}
111
+ onPress={handlePress}
112
+ accessibilityRole="tab"
113
+ accessibilityState={{
114
+ selected: active,
115
+ disabled: isDisabled,
116
+ }}
117
+ accessibilityLabel={accessibilityLabel}
118
+ style={({ pressed }) => [
119
+ styles.trigger,
120
+
121
+ {
122
+ minHeight: tabs.size === "sm" ? 42 : tabs.size === "lg" ? 56 : 48,
123
+
124
+ opacity: pressed ? 0.7 : 1,
125
+ },
126
+
127
+ style,
128
+ ]}
129
+ >
130
+ <View
131
+ style={[
132
+ styles.content,
133
+
134
+ iconPosition === "right" ? styles.contentRight : null,
135
+
136
+ contentStyle,
137
+ ]}
138
+ >
139
+ {iconPosition === "left" ? iconElement : null}
140
+
141
+ {textElement}
142
+
143
+ {iconPosition === "right" ? iconElement : null}
144
+
145
+ {badge !== undefined ? (
146
+ <View
147
+ style={[
148
+ styles.badge,
149
+
150
+ {
151
+ backgroundColor: active
152
+ ? resolvedActiveColor
153
+ : tabs.colors.surfaceSecondary || "#F1F1F1",
154
+ },
155
+
156
+ badgeStyle,
157
+ ]}
158
+ >
159
+ <Text
160
+ style={[
161
+ styles.badgeText,
162
+
163
+ {
164
+ color: active
165
+ ? tabs.colors.onPrimary || "#FFFFFF"
166
+ : tabs.colors.textSecondary || "#525252",
167
+ },
168
+ ]}
169
+ >
170
+ {badge}
171
+ </Text>
172
+ </View>
173
+ ) : null}
174
+ </View>
175
+
176
+ <Animated.View
177
+ style={[
178
+ styles.indicator,
179
+
180
+ {
181
+ backgroundColor: tabs.indicatorColor,
182
+
183
+ height: tabs.indicatorHeight,
184
+
185
+ opacity: progress,
186
+
187
+ transform: [
188
+ {
189
+ scaleX: progress,
190
+ },
191
+ ],
192
+ },
193
+
194
+ tabs.indicatorWidth === "full"
195
+ ? styles.indicatorFull
196
+ : styles.indicatorContent,
197
+ ]}
198
+ />
199
+ </Pressable>
200
+ );
201
+ });
202
+
203
+ const styles = StyleSheet.create({
204
+ trigger: {
205
+ position: "relative",
206
+
207
+ justifyContent: "center",
208
+
209
+ paddingHorizontal: 10,
210
+ },
211
+
212
+ content: {
213
+ flexDirection: "row",
214
+
215
+ alignItems: "center",
216
+
217
+ justifyContent: "center",
218
+
219
+ gap: 6,
220
+ },
221
+
222
+ contentRight: {
223
+ flexDirection: "row",
224
+ },
225
+
226
+ icon: {
227
+ alignItems: "center",
228
+
229
+ justifyContent: "center",
230
+ },
231
+
232
+ iconLeft: {
233
+ marginRight: 2,
234
+ },
235
+
236
+ iconRight: {
237
+ marginLeft: 2,
238
+ },
239
+
240
+ text: {
241
+ fontWeight: "600",
242
+
243
+ includeFontPadding: false,
244
+ },
245
+
246
+ badge: {
247
+ minWidth: 18,
248
+ height: 18,
249
+
250
+ paddingHorizontal: 5,
251
+
252
+ borderRadius: 9,
253
+
254
+ alignItems: "center",
255
+
256
+ justifyContent: "center",
257
+ },
258
+
259
+ badgeText: {
260
+ fontSize: 9,
261
+
262
+ fontWeight: "700",
263
+
264
+ includeFontPadding: false,
265
+ },
266
+
267
+ indicator: {
268
+ position: "absolute",
269
+
270
+ bottom: 0,
271
+
272
+ left: 0,
273
+
274
+ right: 0,
275
+ },
276
+
277
+ indicatorFull: {
278
+ left: 0,
279
+ right: 0,
280
+ },
281
+
282
+ indicatorContent: {
283
+ left: 8,
284
+ right: 8,
285
+ },
286
+ });
287
+
288
+ UITabsTrigger.displayName = "UITabs.Trigger";
@@ -0,0 +1,9 @@
1
+ export { UITabs, UITabsSizes, useUITabs } from "./UITabs";
2
+
3
+ export { UITabsList } from "./UITabsList";
4
+
5
+ export { UITabsTrigger } from "./UITabsTrigger";
6
+
7
+ export { UITabsContent } from "./UITabsContent";
8
+
9
+ export { UITabsIndicator } from "./UITabsIndicator";
@@ -116,3 +116,12 @@ export {
116
116
  UIAccordionContent,
117
117
  UIAccordionIcon,
118
118
  } from "./Accordion";
119
+ export {
120
+ UITabs,
121
+ UITabsSizes,
122
+ useUITabs,
123
+ UITabsList,
124
+ UITabsTrigger,
125
+ UITabsContent,
126
+ UITabsIndicator,
127
+ } from "./Tabs";
package/src/index.js CHANGED
@@ -125,4 +125,11 @@ export {
125
125
  UIAccordionTrigger,
126
126
  UIAccordionContent,
127
127
  UIAccordionIcon,
128
+ UITabs,
129
+ UITabsSizes,
130
+ useUITabs,
131
+ UITabsList,
132
+ UITabsTrigger,
133
+ UITabsContent,
134
+ UITabsIndicator,
128
135
  } from "./components";