@retray-dev/ui-kit 1.0.0 → 1.5.0
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/COMPONENTS.md +120 -64
- package/README.md +18 -19
- package/dist/index.d.mts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +431 -265
- package/dist/index.mjs +430 -265
- package/package.json +20 -3
- package/src/components/Accordion/Accordion.tsx +50 -38
- package/src/components/Alert/Alert.tsx +3 -1
- package/src/components/Avatar/Avatar.tsx +5 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Button/Button.tsx +24 -8
- package/src/components/Card/Card.tsx +2 -8
- package/src/components/Checkbox/Checkbox.tsx +35 -7
- package/src/components/EmptyState/EmptyState.tsx +1 -3
- package/src/components/Input/Input.tsx +18 -10
- package/src/components/Progress/Progress.tsx +3 -4
- package/src/components/RadioGroup/RadioGroup.tsx +72 -45
- package/src/components/Select/Select.tsx +117 -70
- package/src/components/Sheet/Sheet.tsx +9 -2
- package/src/components/Skeleton/Skeleton.tsx +36 -13
- package/src/components/Slider/Slider.tsx +5 -4
- package/src/components/Spinner/Spinner.tsx +1 -7
- package/src/components/Switch/Switch.tsx +5 -1
- package/src/components/Tabs/Tabs.tsx +82 -31
- package/src/components/Textarea/Textarea.tsx +29 -10
- package/src/components/Toast/Toast.tsx +69 -33
- package/src/components/Toggle/Toggle.tsx +32 -20
- package/src/theme/colors.ts +4 -0
- package/src/theme/types.ts +2 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useRef, useCallback } from 'react'
|
|
2
|
+
import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
StyleSheet,
|
|
9
|
-
ViewStyle,
|
|
10
|
-
} from 'react-native'
|
|
4
|
+
BottomSheetModal,
|
|
5
|
+
BottomSheetView,
|
|
6
|
+
BottomSheetBackdrop,
|
|
7
|
+
type BottomSheetBackdropProps,
|
|
8
|
+
} from '@gorhom/bottom-sheet'
|
|
11
9
|
import * as Haptics from 'expo-haptics'
|
|
12
10
|
import { useTheme } from '../../theme'
|
|
13
11
|
|
|
@@ -21,8 +19,10 @@ export interface SelectProps {
|
|
|
21
19
|
options: SelectOption[]
|
|
22
20
|
value?: string
|
|
23
21
|
onValueChange?: (value: string) => void
|
|
22
|
+
/** Text shown when no option is selected. Defaults to `'Select an option'`. */
|
|
24
23
|
placeholder?: string
|
|
25
24
|
label?: string
|
|
25
|
+
/** Red helper text; also changes trigger border to `destructive` color. */
|
|
26
26
|
error?: string
|
|
27
27
|
disabled?: boolean
|
|
28
28
|
style?: ViewStyle
|
|
@@ -39,27 +39,57 @@ export function Select({
|
|
|
39
39
|
style,
|
|
40
40
|
}: SelectProps) {
|
|
41
41
|
const { colors } = useTheme()
|
|
42
|
-
const
|
|
42
|
+
const bottomSheetRef = useRef<BottomSheetModal>(null)
|
|
43
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
43
44
|
|
|
44
45
|
const selected = options.find((o) => o.value === value)
|
|
45
46
|
|
|
47
|
+
const handlePressIn = () => {
|
|
48
|
+
if (disabled) return
|
|
49
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handlePressOut = () => {
|
|
53
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleOpen = () => {
|
|
57
|
+
if (!disabled) {
|
|
58
|
+
Haptics.selectionAsync()
|
|
59
|
+
bottomSheetRef.current?.present()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const renderBackdrop = useCallback(
|
|
64
|
+
(props: BottomSheetBackdropProps) => (
|
|
65
|
+
<BottomSheetBackdrop
|
|
66
|
+
{...props}
|
|
67
|
+
disappearsOnIndex={-1}
|
|
68
|
+
appearsOnIndex={0}
|
|
69
|
+
pressBehavior="close"
|
|
70
|
+
/>
|
|
71
|
+
),
|
|
72
|
+
[]
|
|
73
|
+
)
|
|
74
|
+
|
|
46
75
|
return (
|
|
47
76
|
<View style={[styles.container, style]}>
|
|
48
|
-
{label ?
|
|
49
|
-
<Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
|
|
50
|
-
) : null}
|
|
77
|
+
{label ? <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text> : null}
|
|
51
78
|
|
|
79
|
+
<Animated.View style={{ transform: [{ scale }], opacity: disabled ? 0.45 : 1 }}>
|
|
52
80
|
<TouchableOpacity
|
|
53
81
|
style={[
|
|
54
82
|
styles.trigger,
|
|
55
83
|
{
|
|
56
84
|
borderColor: error ? colors.destructive : colors.border,
|
|
57
85
|
backgroundColor: colors.background,
|
|
58
|
-
opacity: disabled ? 0.45 : 1,
|
|
59
86
|
},
|
|
60
87
|
]}
|
|
61
|
-
onPress={
|
|
62
|
-
|
|
88
|
+
onPress={handleOpen}
|
|
89
|
+
onPressIn={handlePressIn}
|
|
90
|
+
onPressOut={handlePressOut}
|
|
91
|
+
activeOpacity={1}
|
|
92
|
+
touchSoundDisabled={true}
|
|
63
93
|
>
|
|
64
94
|
<Text
|
|
65
95
|
style={[
|
|
@@ -72,54 +102,61 @@ export function Select({
|
|
|
72
102
|
</Text>
|
|
73
103
|
<Text style={[styles.chevron, { color: colors.mutedForeground }]}>▾</Text>
|
|
74
104
|
</TouchableOpacity>
|
|
105
|
+
</Animated.View>
|
|
75
106
|
|
|
76
107
|
{error ? (
|
|
77
108
|
<Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
|
|
78
109
|
) : null}
|
|
79
110
|
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
<BottomSheetModal
|
|
112
|
+
ref={bottomSheetRef}
|
|
113
|
+
enableDynamicSizing
|
|
114
|
+
enablePanDownToClose
|
|
115
|
+
backdropComponent={renderBackdrop}
|
|
116
|
+
backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
|
|
117
|
+
handleIndicatorStyle={[styles.sheetHandle, { backgroundColor: colors.border }]}
|
|
118
|
+
>
|
|
119
|
+
<BottomSheetView style={styles.sheetContent}>
|
|
120
|
+
{label ? (
|
|
121
|
+
<Text style={[styles.sheetTitle, { color: colors.foreground }]}>{label}</Text>
|
|
122
|
+
) : null}
|
|
123
|
+
{options.map((item) => {
|
|
124
|
+
const isSelected = item.value === value
|
|
125
|
+
return (
|
|
126
|
+
<TouchableOpacity
|
|
127
|
+
key={item.value}
|
|
128
|
+
style={[
|
|
129
|
+
styles.option,
|
|
130
|
+
isSelected && { backgroundColor: colors.accent },
|
|
131
|
+
item.disabled && styles.disabledOption,
|
|
132
|
+
]}
|
|
133
|
+
onPress={() => {
|
|
134
|
+
if (!item.disabled) {
|
|
135
|
+
Haptics.selectionAsync()
|
|
136
|
+
onValueChange?.(item.value)
|
|
137
|
+
bottomSheetRef.current?.dismiss()
|
|
138
|
+
}
|
|
139
|
+
}}
|
|
140
|
+
activeOpacity={0.7}
|
|
141
|
+
touchSoundDisabled={true}
|
|
142
|
+
>
|
|
143
|
+
<Text
|
|
144
|
+
style={[
|
|
145
|
+
styles.optionText,
|
|
146
|
+
{ color: item.disabled ? colors.mutedForeground : colors.foreground },
|
|
147
|
+
isSelected && { fontWeight: '500' },
|
|
148
|
+
]}
|
|
149
|
+
>
|
|
150
|
+
{item.label}
|
|
151
|
+
</Text>
|
|
152
|
+
{isSelected ? (
|
|
153
|
+
<Text style={[styles.checkmark, { color: colors.primary }]}>✓</Text>
|
|
154
|
+
) : null}
|
|
155
|
+
</TouchableOpacity>
|
|
156
|
+
)
|
|
157
|
+
})}
|
|
158
|
+
</BottomSheetView>
|
|
159
|
+
</BottomSheetModal>
|
|
123
160
|
</View>
|
|
124
161
|
)
|
|
125
162
|
}
|
|
@@ -153,27 +190,36 @@ const styles = StyleSheet.create({
|
|
|
153
190
|
helperText: {
|
|
154
191
|
fontSize: 12,
|
|
155
192
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
justifyContent: 'center',
|
|
160
|
-
padding: 24,
|
|
193
|
+
sheetBackground: {
|
|
194
|
+
borderTopLeftRadius: 16,
|
|
195
|
+
borderTopRightRadius: 16,
|
|
161
196
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
sheetHandle: {
|
|
198
|
+
width: 36,
|
|
199
|
+
height: 4,
|
|
200
|
+
borderRadius: 2,
|
|
201
|
+
},
|
|
202
|
+
sheetContent: {
|
|
203
|
+
paddingHorizontal: 16,
|
|
204
|
+
paddingBottom: 32,
|
|
205
|
+
},
|
|
206
|
+
sheetTitle: {
|
|
207
|
+
fontSize: 16,
|
|
208
|
+
fontWeight: '600',
|
|
209
|
+
paddingVertical: 12,
|
|
210
|
+
paddingHorizontal: 4,
|
|
167
211
|
},
|
|
168
212
|
option: {
|
|
169
213
|
flexDirection: 'row',
|
|
170
214
|
alignItems: 'center',
|
|
171
215
|
justifyContent: 'space-between',
|
|
172
216
|
paddingHorizontal: 12,
|
|
173
|
-
paddingVertical:
|
|
217
|
+
paddingVertical: 14,
|
|
218
|
+
borderRadius: 8,
|
|
174
219
|
},
|
|
175
220
|
optionText: {
|
|
176
221
|
fontSize: 15,
|
|
222
|
+
flex: 1,
|
|
177
223
|
},
|
|
178
224
|
disabledOption: {
|
|
179
225
|
opacity: 0.45,
|
|
@@ -181,5 +227,6 @@ const styles = StyleSheet.create({
|
|
|
181
227
|
checkmark: {
|
|
182
228
|
fontSize: 14,
|
|
183
229
|
fontWeight: '600',
|
|
230
|
+
marginLeft: 8,
|
|
184
231
|
},
|
|
185
232
|
})
|
|
@@ -18,7 +18,12 @@ export interface SheetProps {
|
|
|
18
18
|
title?: string
|
|
19
19
|
description?: string
|
|
20
20
|
children?: React.ReactNode
|
|
21
|
+
/**
|
|
22
|
+
* Heights the sheet can snap to. Accepts percentage strings (`'50%'`) or
|
|
23
|
+
* absolute point values (`300`). Defaults to `['50%']`.
|
|
24
|
+
*/
|
|
21
25
|
snapPoints?: (string | number)[]
|
|
26
|
+
/** Style for the inner `BottomSheetView` content container. */
|
|
22
27
|
style?: ViewStyle
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -63,13 +68,15 @@ export function Sheet({
|
|
|
63
68
|
enablePanDownToClose
|
|
64
69
|
>
|
|
65
70
|
<BottomSheetView style={[styles.content, style]}>
|
|
66
|
-
{
|
|
71
|
+
{title || description ? (
|
|
67
72
|
<View style={styles.header}>
|
|
68
73
|
{title ? (
|
|
69
74
|
<Text style={[styles.title, { color: colors.cardForeground }]}>{title}</Text>
|
|
70
75
|
) : null}
|
|
71
76
|
{description ? (
|
|
72
|
-
<Text style={[styles.description, { color: colors.mutedForeground }]}>
|
|
77
|
+
<Text style={[styles.description, { color: colors.mutedForeground }]}>
|
|
78
|
+
{description}
|
|
79
|
+
</Text>
|
|
73
80
|
) : null}
|
|
74
81
|
</View>
|
|
75
82
|
) : null}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import { Animated, StyleSheet, ViewStyle } from 'react-native'
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Animated, StyleSheet, View, ViewStyle } from 'react-native'
|
|
3
|
+
import { LinearGradient } from 'expo-linear-gradient'
|
|
3
4
|
import { useTheme } from '../../theme'
|
|
4
5
|
|
|
5
6
|
export interface SkeletonProps {
|
|
@@ -10,31 +11,53 @@ export interface SkeletonProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style }: SkeletonProps) {
|
|
13
|
-
const { colors } = useTheme()
|
|
14
|
-
const
|
|
14
|
+
const { colors, colorScheme } = useTheme()
|
|
15
|
+
const shimmerAnim = useRef(new Animated.Value(0)).current
|
|
16
|
+
const [containerWidth, setContainerWidth] = useState(300)
|
|
17
|
+
|
|
18
|
+
const shimmerHighlight =
|
|
19
|
+
colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'
|
|
15
20
|
|
|
16
21
|
useEffect(() => {
|
|
17
22
|
const animation = Animated.loop(
|
|
18
|
-
Animated.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
Animated.timing(shimmerAnim, {
|
|
24
|
+
toValue: 1,
|
|
25
|
+
duration: 1200,
|
|
26
|
+
useNativeDriver: true,
|
|
27
|
+
})
|
|
22
28
|
)
|
|
23
29
|
animation.start()
|
|
24
30
|
return () => animation.stop()
|
|
25
|
-
}, [
|
|
31
|
+
}, [shimmerAnim])
|
|
32
|
+
|
|
33
|
+
const translateX = shimmerAnim.interpolate({
|
|
34
|
+
inputRange: [0, 1],
|
|
35
|
+
outputRange: [-containerWidth, containerWidth],
|
|
36
|
+
})
|
|
26
37
|
|
|
27
38
|
return (
|
|
28
|
-
<
|
|
39
|
+
<View
|
|
29
40
|
style={[
|
|
30
41
|
styles.base,
|
|
31
|
-
{ width: width as any, height, borderRadius, backgroundColor: colors.muted
|
|
42
|
+
{ width: width as any, height, borderRadius, backgroundColor: colors.muted },
|
|
32
43
|
style,
|
|
33
44
|
]}
|
|
34
|
-
|
|
45
|
+
onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
|
|
46
|
+
>
|
|
47
|
+
<Animated.View style={[StyleSheet.absoluteFill, { transform: [{ translateX }] }]}>
|
|
48
|
+
<LinearGradient
|
|
49
|
+
colors={['transparent', shimmerHighlight, 'transparent']}
|
|
50
|
+
start={{ x: 0, y: 0 }}
|
|
51
|
+
end={{ x: 1, y: 0 }}
|
|
52
|
+
style={StyleSheet.absoluteFill}
|
|
53
|
+
/>
|
|
54
|
+
</Animated.View>
|
|
55
|
+
</View>
|
|
35
56
|
)
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
const styles = StyleSheet.create({
|
|
39
|
-
base: {
|
|
60
|
+
base: {
|
|
61
|
+
overflow: 'hidden',
|
|
62
|
+
},
|
|
40
63
|
})
|
|
@@ -4,11 +4,15 @@ import * as Haptics from 'expo-haptics'
|
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
|
|
6
6
|
export interface SliderProps {
|
|
7
|
+
/** Current value. Controlled when provided; falls back to internal state otherwise. */
|
|
7
8
|
value?: number
|
|
8
9
|
minimumValue?: number
|
|
9
10
|
maximumValue?: number
|
|
11
|
+
/** Snap interval. `0` (default) means continuous (no snapping). */
|
|
10
12
|
step?: number
|
|
13
|
+
/** Called on every move while dragging. */
|
|
11
14
|
onValueChange?: (value: number) => void
|
|
15
|
+
/** Called once when the user releases the thumb. */
|
|
12
16
|
onSlidingComplete?: (value: number) => void
|
|
13
17
|
disabled?: boolean
|
|
14
18
|
style?: ViewStyle
|
|
@@ -86,10 +90,7 @@ export function Slider({
|
|
|
86
90
|
>
|
|
87
91
|
<View style={[styles.track, { backgroundColor: colors.muted }]}>
|
|
88
92
|
<View
|
|
89
|
-
style={[
|
|
90
|
-
styles.range,
|
|
91
|
-
{ width: `${percent}%` as any, backgroundColor: colors.primary },
|
|
92
|
-
]}
|
|
93
|
+
style={[styles.range, { width: `${percent}%` as any, backgroundColor: colors.primary }]}
|
|
93
94
|
/>
|
|
94
95
|
</View>
|
|
95
96
|
<View
|
|
@@ -17,11 +17,5 @@ const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
|
|
|
17
17
|
|
|
18
18
|
export function Spinner({ size = 'md', color, ...props }: SpinnerProps) {
|
|
19
19
|
const { colors } = useTheme()
|
|
20
|
-
return
|
|
21
|
-
<ActivityIndicator
|
|
22
|
-
size={sizeMap[size]}
|
|
23
|
-
color={color ?? colors.primary}
|
|
24
|
-
{...props}
|
|
25
|
-
/>
|
|
26
|
-
)
|
|
20
|
+
return <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
|
|
27
21
|
}
|
|
@@ -43,9 +43,13 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<TouchableOpacity
|
|
46
|
-
onPress={() => {
|
|
46
|
+
onPress={() => {
|
|
47
|
+
Haptics.selectionAsync()
|
|
48
|
+
onCheckedChange?.(!checked)
|
|
49
|
+
}}
|
|
47
50
|
disabled={disabled}
|
|
48
51
|
activeOpacity={0.8}
|
|
52
|
+
touchSoundDisabled={true}
|
|
49
53
|
style={[styles.wrapper, { opacity: disabled ? 0.45 : 1 }, style]}
|
|
50
54
|
>
|
|
51
55
|
<Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react'
|
|
2
2
|
import { View, TouchableOpacity, Text, Animated, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import * as Haptics from 'expo-haptics'
|
|
3
4
|
import { useTheme } from '../../theme'
|
|
4
5
|
|
|
5
6
|
export interface TabItem {
|
|
@@ -9,6 +10,10 @@ export interface TabItem {
|
|
|
9
10
|
|
|
10
11
|
export interface TabsProps {
|
|
11
12
|
tabs: TabItem[]
|
|
13
|
+
/**
|
|
14
|
+
* Controlled active tab value. When omitted the component manages state internally
|
|
15
|
+
* (uncontrolled), defaulting to the first tab.
|
|
16
|
+
*/
|
|
12
17
|
value?: string
|
|
13
18
|
onValueChange?: (value: string) => void
|
|
14
19
|
children?: React.ReactNode
|
|
@@ -22,6 +27,53 @@ export interface TabsContentProps {
|
|
|
22
27
|
style?: ViewStyle
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function TabTrigger({
|
|
31
|
+
tab,
|
|
32
|
+
isActive,
|
|
33
|
+
onPress,
|
|
34
|
+
onLayout,
|
|
35
|
+
}: {
|
|
36
|
+
tab: TabItem
|
|
37
|
+
isActive: boolean
|
|
38
|
+
onPress: () => void
|
|
39
|
+
onLayout: (e: any) => void
|
|
40
|
+
}) {
|
|
41
|
+
const { colors } = useTheme()
|
|
42
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
43
|
+
|
|
44
|
+
const handlePressIn = () => {
|
|
45
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handlePressOut = () => {
|
|
49
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<TouchableOpacity
|
|
54
|
+
style={styles.trigger}
|
|
55
|
+
onPress={onPress}
|
|
56
|
+
onPressIn={handlePressIn}
|
|
57
|
+
onPressOut={handlePressOut}
|
|
58
|
+
onLayout={onLayout}
|
|
59
|
+
activeOpacity={1}
|
|
60
|
+
touchSoundDisabled={true}
|
|
61
|
+
>
|
|
62
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
63
|
+
<Text
|
|
64
|
+
style={[
|
|
65
|
+
styles.triggerLabel,
|
|
66
|
+
{ color: isActive ? colors.foreground : colors.mutedForeground },
|
|
67
|
+
isActive && styles.activeTriggerLabel,
|
|
68
|
+
]}
|
|
69
|
+
>
|
|
70
|
+
{tab.label}
|
|
71
|
+
</Text>
|
|
72
|
+
</Animated.View>
|
|
73
|
+
</TouchableOpacity>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
25
77
|
export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps) {
|
|
26
78
|
const [internal, setInternal] = useState(tabs[0]?.value ?? '')
|
|
27
79
|
const { colors } = useTheme()
|
|
@@ -37,8 +89,18 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
|
|
|
37
89
|
if (!layout) return
|
|
38
90
|
if (animate) {
|
|
39
91
|
Animated.parallel([
|
|
40
|
-
Animated.spring(pillX, {
|
|
41
|
-
|
|
92
|
+
Animated.spring(pillX, {
|
|
93
|
+
toValue: layout.x,
|
|
94
|
+
useNativeDriver: false,
|
|
95
|
+
speed: 20,
|
|
96
|
+
bounciness: 0,
|
|
97
|
+
}),
|
|
98
|
+
Animated.spring(pillWidth, {
|
|
99
|
+
toValue: layout.width,
|
|
100
|
+
useNativeDriver: false,
|
|
101
|
+
speed: 20,
|
|
102
|
+
bounciness: 0,
|
|
103
|
+
}),
|
|
42
104
|
]).start()
|
|
43
105
|
} else {
|
|
44
106
|
pillX.setValue(layout.x)
|
|
@@ -53,6 +115,7 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
|
|
|
53
115
|
}, [active])
|
|
54
116
|
|
|
55
117
|
const handlePress = (v: string) => {
|
|
118
|
+
Haptics.selectionAsync()
|
|
56
119
|
if (!value) setInternal(v)
|
|
57
120
|
onValueChange?.(v)
|
|
58
121
|
}
|
|
@@ -79,35 +142,22 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
|
|
|
79
142
|
},
|
|
80
143
|
]}
|
|
81
144
|
/>
|
|
82
|
-
{tabs.map((tab) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
>
|
|
99
|
-
<Text
|
|
100
|
-
style={[
|
|
101
|
-
styles.triggerLabel,
|
|
102
|
-
{ color: isActive ? colors.foreground : colors.mutedForeground },
|
|
103
|
-
isActive && styles.activeTriggerLabel,
|
|
104
|
-
]}
|
|
105
|
-
>
|
|
106
|
-
{tab.label}
|
|
107
|
-
</Text>
|
|
108
|
-
</TouchableOpacity>
|
|
109
|
-
)
|
|
110
|
-
})}
|
|
145
|
+
{tabs.map((tab) => (
|
|
146
|
+
<TabTrigger
|
|
147
|
+
key={tab.value}
|
|
148
|
+
tab={tab}
|
|
149
|
+
isActive={tab.value === active}
|
|
150
|
+
onPress={() => handlePress(tab.value)}
|
|
151
|
+
onLayout={(e) => {
|
|
152
|
+
const { x, width } = e.nativeEvent.layout
|
|
153
|
+
tabLayouts.current[tab.value] = { x, width }
|
|
154
|
+
if (tab.value === active) {
|
|
155
|
+
animatePill(tab.value, false)
|
|
156
|
+
initialised.current = true
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
))}
|
|
111
161
|
</View>
|
|
112
162
|
{children}
|
|
113
163
|
</View>
|
|
@@ -133,6 +183,7 @@ const styles = StyleSheet.create({
|
|
|
133
183
|
paddingHorizontal: 12,
|
|
134
184
|
borderRadius: 6,
|
|
135
185
|
alignItems: 'center',
|
|
186
|
+
justifyContent: 'center',
|
|
136
187
|
zIndex: 1,
|
|
137
188
|
},
|
|
138
189
|
triggerLabel: {
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native'
|
|
2
|
+
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
|
|
5
5
|
export interface TextareaProps extends TextInputProps {
|
|
6
6
|
label?: string
|
|
7
|
+
/** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
|
|
7
8
|
error?: string
|
|
9
|
+
/** Helper text shown below the textarea when there is no error. */
|
|
8
10
|
hint?: string
|
|
11
|
+
/** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
|
|
9
12
|
rows?: number
|
|
13
|
+
/** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
|
|
14
|
+
containerStyle?: ViewStyle
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export function Textarea({
|
|
17
|
+
export function Textarea({
|
|
18
|
+
label,
|
|
19
|
+
error,
|
|
20
|
+
hint,
|
|
21
|
+
rows = 4,
|
|
22
|
+
containerStyle,
|
|
23
|
+
style,
|
|
24
|
+
onFocus,
|
|
25
|
+
onBlur,
|
|
26
|
+
...props
|
|
27
|
+
}: TextareaProps) {
|
|
13
28
|
const { colors } = useTheme()
|
|
14
29
|
const [focused, setFocused] = useState(false)
|
|
15
30
|
|
|
16
31
|
return (
|
|
17
|
-
<View style={styles.container}>
|
|
18
|
-
{label ?
|
|
19
|
-
<Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
|
|
20
|
-
) : null}
|
|
32
|
+
<View style={[styles.container, containerStyle]}>
|
|
33
|
+
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
21
34
|
<TextInput
|
|
22
35
|
multiline
|
|
23
36
|
numberOfLines={rows}
|
|
@@ -32,17 +45,23 @@ export function Textarea({ label, error, hint, rows = 4, style, onFocus, onBlur,
|
|
|
32
45
|
},
|
|
33
46
|
style,
|
|
34
47
|
]}
|
|
35
|
-
onFocus={(e) => {
|
|
36
|
-
|
|
48
|
+
onFocus={(e) => {
|
|
49
|
+
setFocused(true)
|
|
50
|
+
onFocus?.(e)
|
|
51
|
+
}}
|
|
52
|
+
onBlur={(e) => {
|
|
53
|
+
setFocused(false)
|
|
54
|
+
onBlur?.(e)
|
|
55
|
+
}}
|
|
37
56
|
placeholderTextColor={colors.mutedForeground}
|
|
38
57
|
allowFontScaling={true}
|
|
39
58
|
{...props}
|
|
40
59
|
/>
|
|
41
60
|
{error ? (
|
|
42
|
-
<Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
|
|
61
|
+
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
43
62
|
) : null}
|
|
44
63
|
{!error && hint ? (
|
|
45
|
-
<Text style={[styles.helperText, { color: colors.mutedForeground }]}>{hint}</Text>
|
|
64
|
+
<Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
|
|
46
65
|
) : null}
|
|
47
66
|
</View>
|
|
48
67
|
)
|