rnnovaui 0.9.0 → 0.9.2

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.2",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -3,7 +3,6 @@ import React, { memo, useEffect, useRef, useState } from "react";
3
3
  import { Animated, StyleSheet, View } from "react-native";
4
4
 
5
5
  import { useUIAccordion } from "./UIAccordion";
6
-
7
6
  import { useUIAccordionItem } from "./UIAccordionItem";
8
7
 
9
8
  export const UIAccordionContent = memo(function UIAccordionContent({
@@ -14,88 +13,205 @@ export const UIAccordionContent = memo(function UIAccordionContent({
14
13
  innerStyle,
15
14
 
16
15
  testID,
16
+
17
+ onLayout,
17
18
  }) {
18
19
  const accordion = useUIAccordion();
19
20
 
20
21
  const item = useUIAccordionItem();
21
22
 
22
- const progress = useRef(new Animated.Value(item.open ? 1 : 0)).current;
23
+ const heightAnimation = useRef(new Animated.Value(item.open ? 1 : 0)).current;
24
+
25
+ const opacityAnimation = useRef(
26
+ new Animated.Value(item.open ? 1 : 0),
27
+ ).current;
28
+
29
+ const [contentHeight, setContentHeight] = useState(0);
23
30
 
24
31
  const [mounted, setMounted] = useState(item.open);
25
32
 
33
+ /*
34
+ * ------------------------------------------------
35
+ * MEASURE CONTENT
36
+ * ------------------------------------------------
37
+ */
38
+
39
+ const handleLayout = (event) => {
40
+ const height = event.nativeEvent.layout.height;
41
+
42
+ if (height !== contentHeight) {
43
+ setContentHeight(height);
44
+ }
45
+
46
+ onLayout?.(event);
47
+ };
48
+
49
+ /*
50
+ * ------------------------------------------------
51
+ * OPEN / CLOSE
52
+ * ------------------------------------------------
53
+ */
54
+
26
55
  useEffect(() => {
27
56
  if (item.open) {
57
+ /*
58
+ * Mount before opening.
59
+ */
28
60
  setMounted(true);
29
61
 
30
62
  if (!accordion.animated) {
31
- progress.setValue(1);
63
+ heightAnimation.setValue(1);
64
+
65
+ opacityAnimation.setValue(1);
66
+
32
67
  return;
33
68
  }
34
69
 
35
- Animated.timing(progress, {
36
- toValue: 1,
70
+ /*
71
+ * Smooth opening.
72
+ */
73
+ Animated.parallel([
74
+ Animated.timing(heightAnimation, {
75
+ toValue: 1,
37
76
 
38
- duration: accordion.animationDuration,
77
+ duration: accordion.animationDuration,
39
78
 
40
- useNativeDriver: false,
41
- }).start();
79
+ useNativeDriver: false,
80
+ }),
81
+
82
+ Animated.timing(opacityAnimation, {
83
+ toValue: 1,
84
+
85
+ duration: Math.min(accordion.animationDuration, 180),
86
+
87
+ useNativeDriver: false,
88
+ }),
89
+ ]).start();
42
90
 
43
91
  return;
44
92
  }
45
93
 
94
+ /*
95
+ * ------------------------------------------------
96
+ * CLOSE
97
+ * ------------------------------------------------
98
+ */
99
+
46
100
  if (!accordion.animated) {
47
- progress.setValue(0);
101
+ heightAnimation.setValue(0);
102
+
103
+ opacityAnimation.setValue(0);
104
+
48
105
  setMounted(false);
106
+
49
107
  return;
50
108
  }
51
109
 
52
- Animated.timing(progress, {
53
- toValue: 0,
110
+ /*
111
+ * Animate to zero first.
112
+ */
113
+ Animated.parallel([
114
+ Animated.timing(heightAnimation, {
115
+ toValue: 0,
54
116
 
55
- duration: accordion.animationDuration,
117
+ duration: accordion.animationDuration,
56
118
 
57
- useNativeDriver: false,
58
- }).start(() => {
59
- setMounted(false);
60
- });
61
- }, [item.open, accordion.animated, accordion.animationDuration, progress]);
119
+ useNativeDriver: false,
120
+ }),
121
+
122
+ Animated.timing(opacityAnimation, {
123
+ toValue: 0,
124
+
125
+ duration: Math.min(accordion.animationDuration, 160),
62
126
 
63
- if (!mounted) {
64
- return null;
127
+ useNativeDriver: false,
128
+ }),
129
+ ]).start(({ finished }) => {
130
+ if (finished) {
131
+ setMounted(false);
132
+ }
133
+ });
134
+ }, [
135
+ item.open,
136
+ accordion.animated,
137
+ accordion.animationDuration,
138
+ heightAnimation,
139
+ opacityAnimation,
140
+ ]);
141
+
142
+ /*
143
+ * ------------------------------------------------
144
+ * CLOSED
145
+ * ------------------------------------------------
146
+ */
147
+
148
+ if (!mounted && !item.open) {
149
+ return (
150
+ <View
151
+ style={{
152
+ position: "absolute",
153
+
154
+ opacity: 0,
155
+
156
+ pointerEvents: "none",
157
+ }}
158
+ >
159
+ <View onLayout={handleLayout}>{children}</View>
160
+ </View>
161
+ );
65
162
  }
66
163
 
164
+ /*
165
+ * ------------------------------------------------
166
+ * ANIMATED HEIGHT
167
+ * ------------------------------------------------
168
+ */
169
+
170
+ const animatedHeight =
171
+ contentHeight > 0
172
+ ? heightAnimation.interpolate({
173
+ inputRange: [0, 1],
174
+
175
+ outputRange: [0, contentHeight],
176
+ })
177
+ : undefined;
178
+
179
+ /*
180
+ * ------------------------------------------------
181
+ * RENDER
182
+ * ------------------------------------------------
183
+ */
184
+
67
185
  return (
68
186
  <Animated.View
69
187
  testID={testID}
70
188
  style={[
71
- styles.animatedContainer,
189
+ styles.container,
72
190
 
73
191
  {
74
- opacity: progress,
75
-
76
- maxHeight: progress.interpolate({
77
- inputRange: [0, 1],
192
+ height: animatedHeight,
78
193
 
79
- outputRange: [0, 1000],
80
- }),
194
+ opacity: opacityAnimation,
81
195
  },
82
196
 
83
197
  style,
84
198
  ]}
85
199
  >
86
- <View style={[styles.inner, innerStyle]}>{children}</View>
200
+ <View style={[styles.content, innerStyle]} onLayout={handleLayout}>
201
+ {children}
202
+ </View>
87
203
  </Animated.View>
88
204
  );
89
205
  });
90
206
 
91
207
  const styles = StyleSheet.create({
92
- animatedContainer: {
208
+ container: {
93
209
  width: "100%",
94
210
 
95
211
  overflow: "hidden",
96
212
  },
97
213
 
98
- inner: {
214
+ content: {
99
215
  width: "100%",
100
216
  },
101
217
  });
@@ -0,0 +1,240 @@
1
+ import React, {
2
+ createContext,
3
+ memo,
4
+ useCallback,
5
+ useContext,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
9
+
10
+ import { StyleSheet, View } from "react-native";
11
+
12
+ import { useUITheme } from "../../theme";
13
+
14
+ const UITabsContext = createContext(null);
15
+
16
+ const TAB_SIZES = {
17
+ sm: "sm",
18
+ md: "md",
19
+ lg: "lg",
20
+ };
21
+
22
+ const ANIMATION_PRESETS = {
23
+ none: {
24
+ indicator: "none",
25
+ content: "none",
26
+ press: "none",
27
+ },
28
+
29
+ subtle: {
30
+ indicator: "slide",
31
+ content: "fade",
32
+ press: "scale",
33
+ },
34
+
35
+ smooth: {
36
+ indicator: "slide",
37
+ content: "fadeSlide",
38
+ press: "scale",
39
+ },
40
+
41
+ snappy: {
42
+ indicator: "spring",
43
+ content: "slideFade",
44
+ press: "scale",
45
+ },
46
+
47
+ spring: {
48
+ indicator: "spring",
49
+ content: "slideScale",
50
+ press: "scale",
51
+ },
52
+
53
+ bouncy: {
54
+ indicator: "spring",
55
+ content: "springScale",
56
+ press: "scale",
57
+ },
58
+ };
59
+
60
+ export const UITabs = memo(function UITabs({
61
+ children,
62
+
63
+ value,
64
+ defaultValue = null,
65
+
66
+ onValueChange,
67
+
68
+ size = "md",
69
+
70
+ animated = true,
71
+
72
+ animationPreset = "smooth",
73
+
74
+ indicatorAnimation,
75
+ contentAnimation,
76
+ pressAnimation,
77
+
78
+ animationDuration = 260,
79
+
80
+ spring = {
81
+ damping: 18,
82
+ stiffness: 180,
83
+ mass: 0.8,
84
+ },
85
+
86
+ directionAware = true,
87
+
88
+ variant = "default",
89
+
90
+ activeColor,
91
+ inactiveColor,
92
+
93
+ indicatorColor,
94
+
95
+ indicatorHeight = 2,
96
+
97
+ indicatorWidth = "content",
98
+
99
+ disabled = false,
100
+
101
+ style,
102
+ containerStyle,
103
+
104
+ testID,
105
+ }) {
106
+ const { theme } = useUITheme();
107
+
108
+ const colors = theme?.colors || {};
109
+
110
+ const controlled = value !== undefined;
111
+
112
+ const [internalValue, setInternalValue] = useState(defaultValue);
113
+
114
+ const currentValue = controlled ? value : internalValue;
115
+
116
+ const [previousValue, setPreviousValue] = useState(currentValue);
117
+
118
+ const safeSize = TAB_SIZES[size] ? size : "md";
119
+
120
+ const safePreset =
121
+ ANIMATION_PRESETS[animationPreset] || ANIMATION_PRESETS.smooth;
122
+
123
+ const resolvedIndicatorAnimation = indicatorAnimation ?? safePreset.indicator;
124
+
125
+ const resolvedContentAnimation = contentAnimation ?? safePreset.content;
126
+
127
+ const resolvedPressAnimation = pressAnimation ?? safePreset.press;
128
+
129
+ const changeValue = useCallback(
130
+ (nextValue) => {
131
+ if (disabled || nextValue === currentValue) {
132
+ return;
133
+ }
134
+
135
+ setPreviousValue(currentValue);
136
+
137
+ if (!controlled) {
138
+ setInternalValue(nextValue);
139
+ }
140
+
141
+ onValueChange?.(nextValue);
142
+ },
143
+ [disabled, currentValue, controlled, onValueChange],
144
+ );
145
+
146
+ const contextValue = useMemo(
147
+ () => ({
148
+ colors,
149
+
150
+ value: currentValue,
151
+
152
+ previousValue,
153
+
154
+ setValue: changeValue,
155
+
156
+ size: safeSize,
157
+
158
+ animated,
159
+
160
+ animationDuration,
161
+
162
+ spring,
163
+
164
+ directionAware,
165
+
166
+ indicatorAnimation: animated ? resolvedIndicatorAnimation : "none",
167
+
168
+ contentAnimation: animated ? resolvedContentAnimation : "none",
169
+
170
+ pressAnimation: animated ? resolvedPressAnimation : "none",
171
+
172
+ variant,
173
+
174
+ activeColor: activeColor || colors.primary || "#FF5A1F",
175
+
176
+ inactiveColor: inactiveColor || colors.textSecondary || "#737373",
177
+
178
+ indicatorColor:
179
+ indicatorColor || activeColor || colors.primary || "#FF5A1F",
180
+
181
+ indicatorHeight,
182
+
183
+ indicatorWidth,
184
+
185
+ disabled,
186
+ }),
187
+ [
188
+ colors,
189
+ currentValue,
190
+ previousValue,
191
+ changeValue,
192
+ safeSize,
193
+ animated,
194
+ animationDuration,
195
+ spring,
196
+ directionAware,
197
+ resolvedIndicatorAnimation,
198
+ resolvedContentAnimation,
199
+ resolvedPressAnimation,
200
+ variant,
201
+ activeColor,
202
+ inactiveColor,
203
+ indicatorColor,
204
+ indicatorHeight,
205
+ indicatorWidth,
206
+ disabled,
207
+ ],
208
+ );
209
+
210
+ return (
211
+ <UITabsContext.Provider value={contextValue}>
212
+ <View testID={testID} style={[styles.container, containerStyle, style]}>
213
+ {children}
214
+ </View>
215
+ </UITabsContext.Provider>
216
+ );
217
+ });
218
+
219
+ export function useUITabs() {
220
+ const context = useContext(UITabsContext);
221
+
222
+ if (!context) {
223
+ throw new Error("UITabs components must be used inside <UITabs>.");
224
+ }
225
+
226
+ return context;
227
+ }
228
+
229
+ export {
230
+ TAB_SIZES as UITabsSizes,
231
+ ANIMATION_PRESETS as UITabsAnimationPresets,
232
+ };
233
+
234
+ const styles = StyleSheet.create({
235
+ container: {
236
+ width: "100%",
237
+ },
238
+ });
239
+
240
+ UITabs.displayName = "UITabs";
@@ -0,0 +1,190 @@
1
+ import React, { memo, useEffect, useRef, useState } from "react";
2
+
3
+ import { Animated, StyleSheet } 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
+ animation,
15
+ animationDuration,
16
+
17
+ testID,
18
+ }) {
19
+ const tabs = useUITabs();
20
+
21
+ const active = tabs.value === value;
22
+
23
+ const animationType = animation ?? tabs.contentAnimation;
24
+
25
+ const [mounted, setMounted] = useState(active);
26
+
27
+ const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
28
+
29
+ const direction = getDirection(tabs, value);
30
+
31
+ useEffect(() => {
32
+ const duration = animationDuration ?? tabs.animationDuration;
33
+
34
+ if (active) {
35
+ setMounted(true);
36
+
37
+ progress.setValue(0);
38
+
39
+ if (animationType === "none") {
40
+ progress.setValue(1);
41
+
42
+ return;
43
+ }
44
+
45
+ Animated.timing(progress, {
46
+ toValue: 1,
47
+
48
+ duration,
49
+
50
+ useNativeDriver: true,
51
+ }).start();
52
+
53
+ return;
54
+ }
55
+
56
+ if (animationType === "none") {
57
+ progress.setValue(0);
58
+
59
+ setMounted(false);
60
+
61
+ return;
62
+ }
63
+
64
+ Animated.timing(progress, {
65
+ toValue: 0,
66
+
67
+ duration,
68
+
69
+ useNativeDriver: true,
70
+ }).start(() => {
71
+ setMounted(false);
72
+ });
73
+ }, [
74
+ active,
75
+ animationType,
76
+ animationDuration,
77
+ tabs.animationDuration,
78
+ progress,
79
+ ]);
80
+
81
+ if (!mounted) {
82
+ return null;
83
+ }
84
+
85
+ const transforms = getContentTransforms(animationType, progress, direction);
86
+
87
+ return (
88
+ <Animated.View
89
+ testID={testID}
90
+ accessibilityRole="tabpanel"
91
+ style={[
92
+ styles.container,
93
+
94
+ {
95
+ opacity: getOpacity(animationType, progress),
96
+
97
+ transform: transforms,
98
+ },
99
+
100
+ style,
101
+ ]}
102
+ >
103
+ {children}
104
+ </Animated.View>
105
+ );
106
+ });
107
+
108
+ function getDirection(tabs, value) {
109
+ if (!tabs.directionAware) {
110
+ return 1;
111
+ }
112
+
113
+ if (tabs.previousValue === null || tabs.previousValue === undefined) {
114
+ return 1;
115
+ }
116
+
117
+ return String(value) > String(tabs.previousValue) ? 1 : -1;
118
+ }
119
+
120
+ function getOpacity(type, progress) {
121
+ if (type === "slide" || type === "scale") {
122
+ return 1;
123
+ }
124
+
125
+ return progress;
126
+ }
127
+
128
+ function getContentTransforms(type, progress, direction) {
129
+ switch (type) {
130
+ case "slide":
131
+ return [
132
+ {
133
+ translateX: progress.interpolate({
134
+ inputRange: [0, 1],
135
+ outputRange: [24 * direction, 0],
136
+ }),
137
+ },
138
+ ];
139
+
140
+ case "fadeSlide":
141
+ case "slideFade":
142
+ return [
143
+ {
144
+ translateX: progress.interpolate({
145
+ inputRange: [0, 1],
146
+ outputRange: [20 * direction, 0],
147
+ }),
148
+ },
149
+ ];
150
+
151
+ case "scale":
152
+ return [
153
+ {
154
+ scale: progress.interpolate({
155
+ inputRange: [0, 1],
156
+ outputRange: [0.96, 1],
157
+ }),
158
+ },
159
+ ];
160
+
161
+ case "slideScale":
162
+ case "springScale":
163
+ return [
164
+ {
165
+ translateX: progress.interpolate({
166
+ inputRange: [0, 1],
167
+ outputRange: [18 * direction, 0],
168
+ }),
169
+ },
170
+ {
171
+ scale: progress.interpolate({
172
+ inputRange: [0, 1],
173
+ outputRange: [0.97, 1],
174
+ }),
175
+ },
176
+ ];
177
+
178
+ case "fade":
179
+ default:
180
+ return [];
181
+ }
182
+ }
183
+
184
+ const styles = StyleSheet.create({
185
+ container: {
186
+ width: "100%",
187
+ },
188
+ });
189
+
190
+ UITabsContent.displayName = "UITabs.Content";
@@ -0,0 +1,35 @@
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
+ style,
9
+
10
+ children,
11
+ }) {
12
+ const tabs = useUITabs();
13
+
14
+ if (children) {
15
+ return <View style={style}>{children}</View>;
16
+ }
17
+
18
+ return (
19
+ <View
20
+ style={[
21
+ {
22
+ height: tabs.indicatorHeight,
23
+
24
+ backgroundColor: tabs.indicatorColor,
25
+
26
+ borderRadius: tabs.indicatorHeight / 2,
27
+ },
28
+
29
+ style,
30
+ ]}
31
+ />
32
+ );
33
+ });
34
+
35
+ UITabsIndicator.displayName = "UITabs.Indicator";
@@ -0,0 +1,101 @@
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
+ backgroundColor,
19
+
20
+ borderBottomWidth = 0,
21
+
22
+ borderBottomColor,
23
+
24
+ testID,
25
+ }) {
26
+ const tabs = useUITabs();
27
+
28
+ const horizontalPadding =
29
+ tabs.size === "sm" ? 6 : tabs.size === "lg" ? 14 : 10;
30
+
31
+ const row = (
32
+ <View
33
+ style={[
34
+ styles.row,
35
+ {
36
+ paddingHorizontal: horizontalPadding,
37
+ },
38
+ ]}
39
+ >
40
+ {children}
41
+ </View>
42
+ );
43
+
44
+ const containerStyle = [
45
+ styles.container,
46
+
47
+ {
48
+ backgroundColor: backgroundColor || "transparent",
49
+
50
+ borderBottomWidth,
51
+
52
+ borderBottomColor: borderBottomColor || tabs.colors.border || "#E5E5E5",
53
+ },
54
+
55
+ style,
56
+ ];
57
+
58
+ if (scrollable) {
59
+ return (
60
+ <ScrollView
61
+ testID={testID}
62
+ horizontal
63
+ showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
64
+ keyboardShouldPersistTaps="handled"
65
+ bounces
66
+ contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
67
+ style={containerStyle}
68
+ >
69
+ {row}
70
+ </ScrollView>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <View testID={testID} style={containerStyle}>
76
+ {row}
77
+ </View>
78
+ );
79
+ });
80
+
81
+ const styles = StyleSheet.create({
82
+ container: {
83
+ width: "100%",
84
+ },
85
+
86
+ scrollContent: {
87
+ flexGrow: 1,
88
+ },
89
+
90
+ row: {
91
+ minHeight: 48,
92
+
93
+ flexDirection: "row",
94
+
95
+ alignItems: "stretch",
96
+
97
+ gap: 4,
98
+ },
99
+ });
100
+
101
+ UITabsList.displayName = "UITabs.List";
@@ -0,0 +1,388 @@
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
+ activeStyle,
26
+ inactiveStyle,
27
+
28
+ contentStyle,
29
+
30
+ textStyle,
31
+ activeTextStyle,
32
+ inactiveTextStyle,
33
+
34
+ iconStyle,
35
+
36
+ badgeStyle,
37
+ badgeTextStyle,
38
+
39
+ pressAnimation,
40
+ pressScale = 0.96,
41
+
42
+ indicator = true,
43
+
44
+ testID,
45
+ accessibilityLabel,
46
+ }) {
47
+ const tabs = useUITabs();
48
+
49
+ const active = tabs.value === value;
50
+
51
+ const isDisabled = disabled || tabs.disabled;
52
+
53
+ const scale = useRef(new Animated.Value(1)).current;
54
+
55
+ const resolvedPressAnimation = pressAnimation ?? tabs.pressAnimation;
56
+
57
+ useEffect(() => {
58
+ if (resolvedPressAnimation === "none") {
59
+ scale.setValue(1);
60
+ }
61
+ }, [resolvedPressAnimation, scale]);
62
+
63
+ const animatePressIn = () => {
64
+ if (resolvedPressAnimation === "none") {
65
+ return;
66
+ }
67
+
68
+ if (resolvedPressAnimation === "scale") {
69
+ Animated.spring(scale, {
70
+ toValue: pressScale,
71
+
72
+ ...tabs.spring,
73
+
74
+ useNativeDriver: true,
75
+ }).start();
76
+ }
77
+ };
78
+
79
+ const animatePressOut = () => {
80
+ if (resolvedPressAnimation === "none") {
81
+ return;
82
+ }
83
+
84
+ Animated.spring(scale, {
85
+ toValue: 1,
86
+
87
+ ...tabs.spring,
88
+
89
+ useNativeDriver: true,
90
+ }).start();
91
+ };
92
+
93
+ const handlePress = (event) => {
94
+ if (isDisabled) {
95
+ return;
96
+ }
97
+
98
+ tabs.setValue(value);
99
+
100
+ onPress?.(event);
101
+ };
102
+
103
+ const activeTextColor = activeColor || tabs.activeColor;
104
+
105
+ const inactiveTextColor = inactiveColor || tabs.inactiveColor;
106
+
107
+ const iconElement = icon ? (
108
+ <View
109
+ style={[
110
+ styles.icon,
111
+
112
+ iconPosition === "right" ? styles.iconRight : styles.iconLeft,
113
+
114
+ iconStyle,
115
+ ]}
116
+ >
117
+ {icon}
118
+ </View>
119
+ ) : null;
120
+
121
+ return (
122
+ <Animated.View
123
+ style={[
124
+ styles.animatedWrapper,
125
+
126
+ {
127
+ transform: [
128
+ {
129
+ scale,
130
+ },
131
+ ],
132
+ },
133
+ ]}
134
+ >
135
+ <Pressable
136
+ testID={testID}
137
+ disabled={isDisabled}
138
+ onPress={handlePress}
139
+ onPressIn={animatePressIn}
140
+ onPressOut={animatePressOut}
141
+ accessibilityRole="tab"
142
+ accessibilityState={{
143
+ selected: active,
144
+ disabled: isDisabled,
145
+ }}
146
+ accessibilityLabel={accessibilityLabel}
147
+ style={[
148
+ styles.trigger,
149
+
150
+ {
151
+ minHeight: tabs.size === "sm" ? 42 : tabs.size === "lg" ? 58 : 50,
152
+
153
+ opacity: isDisabled ? 0.45 : 1,
154
+ },
155
+
156
+ inactiveStyle,
157
+
158
+ active ? activeStyle : null,
159
+
160
+ style,
161
+ ]}
162
+ >
163
+ <View style={[styles.content, contentStyle]}>
164
+ {iconPosition === "left" ? iconElement : null}
165
+
166
+ <Text
167
+ numberOfLines={1}
168
+ style={[
169
+ styles.text,
170
+
171
+ {
172
+ fontSize:
173
+ tabs.size === "sm" ? 12 : tabs.size === "lg" ? 16 : 14,
174
+
175
+ color: active ? activeTextColor : inactiveTextColor,
176
+ },
177
+
178
+ textStyle,
179
+
180
+ inactiveTextStyle,
181
+
182
+ active ? activeTextStyle : null,
183
+ ]}
184
+ >
185
+ {children}
186
+ </Text>
187
+
188
+ {iconPosition === "right" ? iconElement : null}
189
+
190
+ {badge !== undefined ? (
191
+ <View
192
+ style={[
193
+ styles.badge,
194
+
195
+ {
196
+ backgroundColor: active
197
+ ? activeTextColor
198
+ : tabs.colors.surfaceSecondary || "#EEEEEE",
199
+ },
200
+
201
+ badgeStyle,
202
+ ]}
203
+ >
204
+ <Text
205
+ style={[
206
+ styles.badgeText,
207
+
208
+ {
209
+ color: active
210
+ ? tabs.colors.onPrimary || "#FFFFFF"
211
+ : tabs.colors.textSecondary || "#525252",
212
+ },
213
+
214
+ badgeTextStyle,
215
+ ]}
216
+ >
217
+ {badge}
218
+ </Text>
219
+ </View>
220
+ ) : null}
221
+ </View>
222
+
223
+ {indicator ? (
224
+ <View style={styles.indicatorContainer}>
225
+ <AnimatedIndicator active={active} />
226
+ </View>
227
+ ) : null}
228
+ </Pressable>
229
+ </Animated.View>
230
+ );
231
+ });
232
+
233
+ function AnimatedIndicator({ active }) {
234
+ const tabs = useUITabs();
235
+
236
+ const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
237
+
238
+ useEffect(() => {
239
+ if (tabs.indicatorAnimation === "none") {
240
+ progress.setValue(active ? 1 : 0);
241
+
242
+ return;
243
+ }
244
+
245
+ if (tabs.indicatorAnimation === "spring") {
246
+ Animated.spring(progress, {
247
+ toValue: active ? 1 : 0,
248
+
249
+ ...tabs.spring,
250
+
251
+ useNativeDriver: true,
252
+ }).start();
253
+
254
+ return;
255
+ }
256
+
257
+ Animated.timing(progress, {
258
+ toValue: active ? 1 : 0,
259
+
260
+ duration: tabs.animationDuration,
261
+
262
+ useNativeDriver: true,
263
+ }).start();
264
+ }, [
265
+ active,
266
+ tabs.indicatorAnimation,
267
+ tabs.animationDuration,
268
+ tabs.spring,
269
+ progress,
270
+ ]);
271
+
272
+ return (
273
+ <Animated.View
274
+ style={[
275
+ styles.indicator,
276
+
277
+ {
278
+ height: tabs.indicatorHeight,
279
+
280
+ backgroundColor: tabs.indicatorColor,
281
+
282
+ opacity: progress,
283
+
284
+ transform: [
285
+ {
286
+ scaleX: progress,
287
+ },
288
+ ],
289
+ },
290
+
291
+ tabs.indicatorWidth === "full"
292
+ ? styles.indicatorFull
293
+ : styles.indicatorContent,
294
+ ]}
295
+ />
296
+ );
297
+ }
298
+
299
+ const styles = StyleSheet.create({
300
+ animatedWrapper: {
301
+ flexShrink: 0,
302
+ },
303
+
304
+ trigger: {
305
+ position: "relative",
306
+
307
+ justifyContent: "center",
308
+
309
+ paddingHorizontal: 10,
310
+ },
311
+
312
+ content: {
313
+ flexDirection: "row",
314
+
315
+ alignItems: "center",
316
+
317
+ justifyContent: "center",
318
+
319
+ gap: 6,
320
+ },
321
+
322
+ text: {
323
+ fontWeight: "600",
324
+
325
+ includeFontPadding: false,
326
+ },
327
+
328
+ icon: {
329
+ alignItems: "center",
330
+
331
+ justifyContent: "center",
332
+ },
333
+
334
+ iconLeft: {
335
+ marginRight: 2,
336
+ },
337
+
338
+ iconRight: {
339
+ marginLeft: 2,
340
+ },
341
+
342
+ badge: {
343
+ minWidth: 18,
344
+ height: 18,
345
+
346
+ paddingHorizontal: 5,
347
+
348
+ borderRadius: 9,
349
+
350
+ alignItems: "center",
351
+
352
+ justifyContent: "center",
353
+ },
354
+
355
+ badgeText: {
356
+ fontSize: 9,
357
+
358
+ fontWeight: "700",
359
+
360
+ includeFontPadding: false,
361
+ },
362
+
363
+ indicatorContainer: {
364
+ position: "absolute",
365
+
366
+ left: 0,
367
+ right: 0,
368
+ bottom: 0,
369
+ },
370
+
371
+ indicator: {
372
+ position: "absolute",
373
+
374
+ bottom: 0,
375
+ },
376
+
377
+ indicatorFull: {
378
+ left: 0,
379
+ right: 0,
380
+ },
381
+
382
+ indicatorContent: {
383
+ left: 8,
384
+ right: 8,
385
+ },
386
+ });
387
+
388
+ UITabsTrigger.displayName = "UITabs.Trigger";
@@ -0,0 +1,16 @@
1
+ import { UITabs } from "./UITabs";
2
+
3
+ import { UITabsList } from "./UITabsList";
4
+ import { UITabsTrigger } from "./UITabsTrigger";
5
+ import { UITabsContent } from "./UITabsContent";
6
+ import { UITabsIndicator } from "./UITabsIndicator";
7
+
8
+ UITabs.List = UITabsList;
9
+
10
+ UITabs.Trigger = UITabsTrigger;
11
+
12
+ UITabs.Content = UITabsContent;
13
+
14
+ UITabs.Indicator = UITabsIndicator;
15
+
16
+ export { UITabs, UITabsList, UITabsTrigger, UITabsContent, UITabsIndicator };
@@ -116,3 +116,10 @@ export {
116
116
  UIAccordionContent,
117
117
  UIAccordionIcon,
118
118
  } from "./Accordion";
119
+ export {
120
+ UITabs,
121
+ UITabsList,
122
+ UITabsTrigger,
123
+ UITabsContent,
124
+ UITabsIndicator,
125
+ } from "./Tabs";
package/src/index.js CHANGED
@@ -125,4 +125,9 @@ export {
125
125
  UIAccordionTrigger,
126
126
  UIAccordionContent,
127
127
  UIAccordionIcon,
128
+ UITabs,
129
+ UITabsList,
130
+ UITabsTrigger,
131
+ UITabsContent,
132
+ UITabsIndicator,
128
133
  } from "./components";