@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Personal UI Kit for React Native / Expo",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -21,7 +21,13 @@
|
|
|
21
21
|
"build": "tsup",
|
|
22
22
|
"dev": "tsup --watch",
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
|
-
"
|
|
24
|
+
"lint": "eslint src",
|
|
25
|
+
"lint:fix": "eslint src --fix",
|
|
26
|
+
"format": "prettier --write src",
|
|
27
|
+
"format:check": "prettier --check src",
|
|
28
|
+
"lint:all": "pnpm lint && pnpm --filter retray-ui-kit-example lint",
|
|
29
|
+
"format:all": "pnpm format && pnpm --filter retray-ui-kit-example format",
|
|
30
|
+
"deploy": "pnpm typecheck && pnpm build && npm publish --access public"
|
|
25
31
|
},
|
|
26
32
|
"keywords": [
|
|
27
33
|
"react-native",
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
"react": ">=17",
|
|
35
41
|
"react-native": ">=0.70",
|
|
36
42
|
"expo-haptics": ">=14.0.0",
|
|
43
|
+
"expo-linear-gradient": ">=13.0.0",
|
|
37
44
|
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
38
45
|
"react-native-reanimated": ">=4.0.0",
|
|
39
46
|
"react-native-gesture-handler": ">=2.0.0",
|
|
@@ -47,18 +54,28 @@
|
|
|
47
54
|
"react-native": "0.81.5",
|
|
48
55
|
"react-native-worklets": "0.5.1"
|
|
49
56
|
},
|
|
50
|
-
"onlyBuiltDependencies": [
|
|
57
|
+
"onlyBuiltDependencies": [
|
|
58
|
+
"esbuild"
|
|
59
|
+
]
|
|
51
60
|
},
|
|
52
61
|
"devDependencies": {
|
|
53
62
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
54
63
|
"@types/react": "^19.1.0",
|
|
55
64
|
"expo-haptics": "~15.0.8",
|
|
65
|
+
"expo-linear-gradient": "~14.1.5",
|
|
56
66
|
"react": "18.2.0",
|
|
57
67
|
"react-native": "0.74.0",
|
|
58
68
|
"react-native-gesture-handler": "~2.28.0",
|
|
59
69
|
"react-native-reanimated": "~4.1.1",
|
|
60
70
|
"react-native-worklets": "~0.5.0",
|
|
61
71
|
"react-native-safe-area-context": "~5.6.2",
|
|
72
|
+
"eslint": "^9.0.0",
|
|
73
|
+
"@eslint/js": "^9.0.0",
|
|
74
|
+
"typescript-eslint": "^8.0.0",
|
|
75
|
+
"eslint-plugin-react": "^7.37.0",
|
|
76
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
77
|
+
"eslint-config-prettier": "^10.0.0",
|
|
78
|
+
"prettier": "^3.0.0",
|
|
62
79
|
"tsup": "^8.0.0",
|
|
63
80
|
"typescript": "^5.4.0"
|
|
64
81
|
}
|
|
@@ -4,11 +4,16 @@ import {
|
|
|
4
4
|
Text,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
Animated,
|
|
7
|
-
Easing,
|
|
8
7
|
StyleSheet,
|
|
9
8
|
LayoutChangeEvent,
|
|
10
9
|
ViewStyle,
|
|
11
10
|
} from 'react-native'
|
|
11
|
+
import ReanimatedAnimated, {
|
|
12
|
+
useSharedValue,
|
|
13
|
+
useAnimatedStyle,
|
|
14
|
+
withTiming,
|
|
15
|
+
Easing,
|
|
16
|
+
} from 'react-native-reanimated'
|
|
12
17
|
import * as Haptics from 'expo-haptics'
|
|
13
18
|
import { useTheme } from '../../theme'
|
|
14
19
|
|
|
@@ -40,25 +45,15 @@ function AccordionItemComponent({
|
|
|
40
45
|
onToggle: () => void
|
|
41
46
|
}) {
|
|
42
47
|
const { colors } = useTheme()
|
|
43
|
-
const animatedHeight =
|
|
44
|
-
const animatedRotation =
|
|
48
|
+
const animatedHeight = useSharedValue(0)
|
|
49
|
+
const animatedRotation = useSharedValue(0)
|
|
45
50
|
const contentHeight = useRef(0)
|
|
51
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
46
52
|
|
|
47
53
|
const toggle = (open: boolean) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
duration: 220,
|
|
52
|
-
easing: open ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
|
|
53
|
-
useNativeDriver: false,
|
|
54
|
-
}),
|
|
55
|
-
Animated.timing(animatedRotation, {
|
|
56
|
-
toValue: open ? 1 : 0,
|
|
57
|
-
duration: 220,
|
|
58
|
-
easing: open ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
|
|
59
|
-
useNativeDriver: true,
|
|
60
|
-
}),
|
|
61
|
-
]).start()
|
|
54
|
+
const easing = open ? Easing.out(Easing.ease) : Easing.in(Easing.ease)
|
|
55
|
+
animatedHeight.value = withTiming(open ? contentHeight.current : 0, { duration: 220, easing })
|
|
56
|
+
animatedRotation.value = withTiming(open ? 1 : 0, { duration: 220, easing })
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
React.useEffect(() => {
|
|
@@ -68,37 +63,55 @@ function AccordionItemComponent({
|
|
|
68
63
|
const onLayout = (e: LayoutChangeEvent) => {
|
|
69
64
|
if (contentHeight.current === 0) {
|
|
70
65
|
contentHeight.current = e.nativeEvent.layout.height
|
|
71
|
-
if (isOpen) animatedHeight.
|
|
66
|
+
if (isOpen) animatedHeight.value = contentHeight.current
|
|
72
67
|
}
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
70
|
+
const heightStyle = useAnimatedStyle(() => ({
|
|
71
|
+
height: animatedHeight.value,
|
|
72
|
+
overflow: 'hidden',
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
const rotationStyle = useAnimatedStyle(() => ({
|
|
76
|
+
transform: [{ rotate: `${animatedRotation.value * 180}deg` }],
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
const handlePressIn = () => {
|
|
80
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handlePressOut = () => {
|
|
84
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
85
|
+
}
|
|
79
86
|
|
|
80
87
|
return (
|
|
81
88
|
<View style={[styles.item, { borderBottomColor: colors.border }]}>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
90
|
+
<TouchableOpacity
|
|
91
|
+
style={styles.trigger}
|
|
92
|
+
onPress={() => {
|
|
93
|
+
Haptics.selectionAsync()
|
|
94
|
+
onToggle()
|
|
95
|
+
}}
|
|
96
|
+
onPressIn={handlePressIn}
|
|
97
|
+
onPressOut={handlePressOut}
|
|
98
|
+
activeOpacity={1}
|
|
99
|
+
touchSoundDisabled={true}
|
|
92
100
|
>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
<Text style={[styles.triggerText, { color: colors.foreground }]}>{item.trigger}</Text>
|
|
102
|
+
<ReanimatedAnimated.Text
|
|
103
|
+
style={[styles.chevron, { color: colors.foreground }, rotationStyle]}
|
|
104
|
+
>
|
|
105
|
+
▾
|
|
106
|
+
</ReanimatedAnimated.Text>
|
|
107
|
+
</TouchableOpacity>
|
|
108
|
+
</Animated.View>
|
|
96
109
|
|
|
97
|
-
<
|
|
110
|
+
<ReanimatedAnimated.View style={heightStyle}>
|
|
98
111
|
<View style={styles.content} onLayout={onLayout}>
|
|
99
112
|
{item.content}
|
|
100
113
|
</View>
|
|
101
|
-
</
|
|
114
|
+
</ReanimatedAnimated.View>
|
|
102
115
|
</View>
|
|
103
116
|
)
|
|
104
117
|
}
|
|
@@ -152,7 +165,6 @@ const styles = StyleSheet.create({
|
|
|
152
165
|
fontSize: 16,
|
|
153
166
|
marginLeft: 8,
|
|
154
167
|
},
|
|
155
|
-
contentWrapper: {},
|
|
156
168
|
content: {
|
|
157
169
|
paddingBottom: 16,
|
|
158
170
|
position: 'absolute',
|
|
@@ -24,7 +24,9 @@ export function Alert({ title, description, variant = 'default', icon, style }:
|
|
|
24
24
|
{icon ? <View style={styles.icon}>{icon}</View> : null}
|
|
25
25
|
<View style={styles.content}>
|
|
26
26
|
{title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
|
|
27
|
-
{description ?
|
|
27
|
+
{description ? (
|
|
28
|
+
<Text style={[styles.description, { color: descColor }]}>{description}</Text>
|
|
29
|
+
) : null}
|
|
28
30
|
</View>
|
|
29
31
|
</View>
|
|
30
32
|
)
|
|
@@ -5,7 +5,9 @@ import { useTheme } from '../../theme'
|
|
|
5
5
|
export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
6
6
|
|
|
7
7
|
export interface AvatarProps {
|
|
8
|
+
/** Remote image URI. Falls back to `fallback` initials on error or when omitted. */
|
|
8
9
|
src?: string
|
|
10
|
+
/** Up to 2 characters shown when the image is unavailable. Auto-uppercased. Defaults to `'?'`. */
|
|
9
11
|
fallback?: string
|
|
10
12
|
size?: AvatarSize
|
|
11
13
|
style?: ViewStyle
|
|
@@ -48,7 +50,9 @@ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
|
|
|
48
50
|
onError={() => setImageError(true)}
|
|
49
51
|
/>
|
|
50
52
|
) : (
|
|
51
|
-
<Text
|
|
53
|
+
<Text
|
|
54
|
+
style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
|
|
55
|
+
>
|
|
52
56
|
{fallback?.slice(0, 2).toUpperCase() ?? '?'}
|
|
53
57
|
</Text>
|
|
54
58
|
)}
|
|
@@ -29,7 +29,7 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
|
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
31
|
<View style={[styles.container, containerStyle, style]}>
|
|
32
|
-
<Text style={[styles.label, { color: textColor }]}>{label}</Text>
|
|
32
|
+
<Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Text>
|
|
33
33
|
</View>
|
|
34
34
|
)
|
|
35
35
|
}
|
|
@@ -28,6 +28,10 @@ export interface ButtonProps extends TouchableOpacityProps {
|
|
|
28
28
|
/** Replaces the label with a spinner and forces `disabled`. */
|
|
29
29
|
loading?: boolean
|
|
30
30
|
fullWidth?: boolean
|
|
31
|
+
/** Icon rendered alongside the label. */
|
|
32
|
+
icon?: React.ReactNode
|
|
33
|
+
/** Side the icon appears on. Defaults to `'left'`. */
|
|
34
|
+
iconPosition?: 'left' | 'right'
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
@@ -48,6 +52,8 @@ export function Button({
|
|
|
48
52
|
size = 'md',
|
|
49
53
|
loading = false,
|
|
50
54
|
fullWidth = false,
|
|
55
|
+
icon,
|
|
56
|
+
iconPosition = 'left',
|
|
51
57
|
disabled,
|
|
52
58
|
style,
|
|
53
59
|
onPress,
|
|
@@ -59,11 +65,16 @@ export function Button({
|
|
|
59
65
|
|
|
60
66
|
const handlePressIn = () => {
|
|
61
67
|
if (isDisabled) return
|
|
62
|
-
Animated.spring(scale, {
|
|
68
|
+
Animated.spring(scale, {
|
|
69
|
+
toValue: 0.95,
|
|
70
|
+
useNativeDriver: true,
|
|
71
|
+
speed: 40,
|
|
72
|
+
bounciness: 0,
|
|
73
|
+
}).start()
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
const handlePressOut = () => {
|
|
66
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness:
|
|
77
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
@@ -85,9 +96,8 @@ export function Button({
|
|
|
85
96
|
ghost: { color: colors.foreground },
|
|
86
97
|
}[variant]
|
|
87
98
|
|
|
88
|
-
const spinnerColor =
|
|
89
|
-
? colors.primaryForeground
|
|
90
|
-
: colors.foreground
|
|
99
|
+
const spinnerColor =
|
|
100
|
+
variant === 'primary' || variant === 'secondary' ? colors.primaryForeground : colors.foreground
|
|
91
101
|
|
|
92
102
|
return (
|
|
93
103
|
<Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
|
|
@@ -102,6 +112,7 @@ export function Button({
|
|
|
102
112
|
]}
|
|
103
113
|
disabled={isDisabled}
|
|
104
114
|
activeOpacity={1}
|
|
115
|
+
touchSoundDisabled={true}
|
|
105
116
|
onPress={handlePress}
|
|
106
117
|
onPressIn={handlePressIn}
|
|
107
118
|
onPressOut={handlePressOut}
|
|
@@ -110,9 +121,11 @@ export function Button({
|
|
|
110
121
|
{loading ? (
|
|
111
122
|
<ActivityIndicator size="small" color={spinnerColor} />
|
|
112
123
|
) : (
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
124
|
+
<>
|
|
125
|
+
{icon && iconPosition === 'left' && <>{icon}</>}
|
|
126
|
+
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], icon ? styles.labelWithIcon : undefined]}>{label}</Text>
|
|
127
|
+
{icon && iconPosition === 'right' && <>{icon}</>}
|
|
128
|
+
</>
|
|
116
129
|
)}
|
|
117
130
|
</TouchableOpacity>
|
|
118
131
|
</Animated.View>
|
|
@@ -135,4 +148,7 @@ const styles = StyleSheet.create({
|
|
|
135
148
|
label: {
|
|
136
149
|
fontWeight: '600',
|
|
137
150
|
},
|
|
151
|
+
labelWithIcon: {
|
|
152
|
+
marginHorizontal: 6,
|
|
153
|
+
},
|
|
138
154
|
})
|
|
@@ -36,11 +36,7 @@ export function Card({ children, style }: CardProps) {
|
|
|
36
36
|
const { colors } = useTheme()
|
|
37
37
|
return (
|
|
38
38
|
<View
|
|
39
|
-
style={[
|
|
40
|
-
styles.card,
|
|
41
|
-
{ backgroundColor: colors.card, borderColor: colors.border },
|
|
42
|
-
style,
|
|
43
|
-
]}
|
|
39
|
+
style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }, style]}
|
|
44
40
|
>
|
|
45
41
|
{children}
|
|
46
42
|
</View>
|
|
@@ -53,9 +49,7 @@ export function CardHeader({ children, style }: CardHeaderProps) {
|
|
|
53
49
|
|
|
54
50
|
export function CardTitle({ children, style }: CardTitleProps) {
|
|
55
51
|
const { colors } = useTheme()
|
|
56
|
-
return
|
|
57
|
-
<Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
|
|
58
|
-
)
|
|
52
|
+
return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
export function CardDescription({ children, style }: CardDescriptionProps) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import * as Haptics from 'expo-haptics'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
|
|
@@ -11,15 +11,38 @@ export interface CheckboxProps {
|
|
|
11
11
|
style?: ViewStyle
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function Checkbox({
|
|
14
|
+
export function Checkbox({
|
|
15
|
+
checked = false,
|
|
16
|
+
onCheckedChange,
|
|
17
|
+
label,
|
|
18
|
+
disabled,
|
|
19
|
+
style,
|
|
20
|
+
}: CheckboxProps) {
|
|
15
21
|
const { colors } = useTheme()
|
|
22
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
23
|
+
|
|
24
|
+
const handlePressIn = () => {
|
|
25
|
+
if (disabled) return
|
|
26
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handlePressOut = () => {
|
|
30
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
31
|
+
}
|
|
16
32
|
|
|
17
33
|
return (
|
|
34
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
18
35
|
<TouchableOpacity
|
|
19
36
|
style={[styles.row, style]}
|
|
20
|
-
onPress={() => {
|
|
37
|
+
onPress={() => {
|
|
38
|
+
Haptics.selectionAsync()
|
|
39
|
+
onCheckedChange?.(!checked)
|
|
40
|
+
}}
|
|
41
|
+
onPressIn={handlePressIn}
|
|
42
|
+
onPressOut={handlePressOut}
|
|
21
43
|
disabled={disabled}
|
|
22
|
-
activeOpacity={
|
|
44
|
+
activeOpacity={1}
|
|
45
|
+
touchSoundDisabled={true}
|
|
23
46
|
>
|
|
24
47
|
<View
|
|
25
48
|
style={[
|
|
@@ -31,14 +54,19 @@ export function Checkbox({ checked = false, onCheckedChange, label, disabled, st
|
|
|
31
54
|
},
|
|
32
55
|
]}
|
|
33
56
|
>
|
|
34
|
-
{checked ?
|
|
57
|
+
{checked ? (
|
|
58
|
+
<View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
|
|
59
|
+
) : null}
|
|
35
60
|
</View>
|
|
36
61
|
{label ? (
|
|
37
|
-
<Text
|
|
62
|
+
<Text
|
|
63
|
+
style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}
|
|
64
|
+
>
|
|
38
65
|
{label}
|
|
39
66
|
</Text>
|
|
40
67
|
) : null}
|
|
41
68
|
</TouchableOpacity>
|
|
69
|
+
</Animated.View>
|
|
42
70
|
)
|
|
43
71
|
}
|
|
44
72
|
|
|
@@ -16,9 +16,7 @@ export function EmptyState({ icon, title, description, action, style }: EmptySta
|
|
|
16
16
|
return (
|
|
17
17
|
<View style={[styles.container, { borderColor: colors.border }, style]}>
|
|
18
18
|
{icon ? (
|
|
19
|
-
<View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>
|
|
20
|
-
{icon}
|
|
21
|
-
</View>
|
|
19
|
+
<View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>{icon}</View>
|
|
22
20
|
) : null}
|
|
23
21
|
<View style={styles.textWrapper}>
|
|
24
22
|
<Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
|
|
@@ -1,22 +1,24 @@
|
|
|
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 InputProps extends TextInputProps {
|
|
6
6
|
label?: string
|
|
7
|
+
/** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
|
|
7
8
|
error?: string
|
|
9
|
+
/** Helper text shown below the input when there is no error. */
|
|
8
10
|
hint?: string
|
|
11
|
+
/** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
|
|
12
|
+
containerStyle?: ViewStyle
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
export function Input({ label, error, hint, style, onFocus, onBlur, ...props }: InputProps) {
|
|
15
|
+
export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
|
|
12
16
|
const { colors } = useTheme()
|
|
13
17
|
const [focused, setFocused] = useState(false)
|
|
14
18
|
|
|
15
19
|
return (
|
|
16
|
-
<View style={styles.container}>
|
|
17
|
-
{label ?
|
|
18
|
-
<Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
|
|
19
|
-
) : null}
|
|
20
|
+
<View style={[styles.container, containerStyle]}>
|
|
21
|
+
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
20
22
|
<TextInput
|
|
21
23
|
style={[
|
|
22
24
|
styles.input,
|
|
@@ -27,17 +29,23 @@ export function Input({ label, error, hint, style, onFocus, onBlur, ...props }:
|
|
|
27
29
|
},
|
|
28
30
|
style,
|
|
29
31
|
]}
|
|
30
|
-
onFocus={(e) => {
|
|
31
|
-
|
|
32
|
+
onFocus={(e) => {
|
|
33
|
+
setFocused(true)
|
|
34
|
+
onFocus?.(e)
|
|
35
|
+
}}
|
|
36
|
+
onBlur={(e) => {
|
|
37
|
+
setFocused(false)
|
|
38
|
+
onBlur?.(e)
|
|
39
|
+
}}
|
|
32
40
|
placeholderTextColor={colors.mutedForeground}
|
|
33
41
|
allowFontScaling={true}
|
|
34
42
|
{...props}
|
|
35
43
|
/>
|
|
36
44
|
{error ? (
|
|
37
|
-
<Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
|
|
45
|
+
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
38
46
|
) : null}
|
|
39
47
|
{!error && hint ? (
|
|
40
|
-
<Text style={[styles.helperText, { color: colors.mutedForeground }]}>{hint}</Text>
|
|
48
|
+
<Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
|
|
41
49
|
) : null}
|
|
42
50
|
</View>
|
|
43
51
|
)
|
|
@@ -3,7 +3,9 @@ import { View, Animated, StyleSheet, ViewStyle } from 'react-native'
|
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
|
|
5
5
|
export interface ProgressProps {
|
|
6
|
+
/** Current progress value. Clamped to `[0, max]`. Defaults to `0`. */
|
|
6
7
|
value?: number
|
|
8
|
+
/** Maximum value. Defaults to `100`. */
|
|
7
9
|
max?: number
|
|
8
10
|
style?: ViewStyle
|
|
9
11
|
}
|
|
@@ -30,10 +32,7 @@ export function Progress({ value = 0, max = 100, style }: ProgressProps) {
|
|
|
30
32
|
onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
|
|
31
33
|
>
|
|
32
34
|
<Animated.View
|
|
33
|
-
style={[
|
|
34
|
-
styles.indicator,
|
|
35
|
-
{ width: animatedWidth, backgroundColor: colors.primary },
|
|
36
|
-
]}
|
|
35
|
+
style={[styles.indicator, { width: animatedWidth, backgroundColor: colors.primary }]}
|
|
37
36
|
/>
|
|
38
37
|
</View>
|
|
39
38
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import * as Haptics from 'expo-haptics'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
|
|
@@ -17,6 +17,67 @@ export interface RadioGroupProps {
|
|
|
17
17
|
style?: ViewStyle
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function RadioItem({
|
|
21
|
+
option,
|
|
22
|
+
selected,
|
|
23
|
+
onSelect,
|
|
24
|
+
}: {
|
|
25
|
+
option: RadioOption
|
|
26
|
+
selected: boolean
|
|
27
|
+
onSelect: () => void
|
|
28
|
+
}) {
|
|
29
|
+
const { colors } = useTheme()
|
|
30
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
31
|
+
|
|
32
|
+
const handlePressIn = () => {
|
|
33
|
+
if (option.disabled) return
|
|
34
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handlePressOut = () => {
|
|
38
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
43
|
+
<TouchableOpacity
|
|
44
|
+
style={styles.row}
|
|
45
|
+
onPress={() => {
|
|
46
|
+
if (!option.disabled) {
|
|
47
|
+
Haptics.selectionAsync()
|
|
48
|
+
onSelect()
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
onPressIn={handlePressIn}
|
|
52
|
+
onPressOut={handlePressOut}
|
|
53
|
+
activeOpacity={1}
|
|
54
|
+
touchSoundDisabled={true}
|
|
55
|
+
disabled={option.disabled}
|
|
56
|
+
>
|
|
57
|
+
<View
|
|
58
|
+
style={[
|
|
59
|
+
styles.radio,
|
|
60
|
+
{
|
|
61
|
+
borderColor: selected ? colors.primary : colors.border,
|
|
62
|
+
opacity: option.disabled ? 0.45 : 1,
|
|
63
|
+
},
|
|
64
|
+
]}
|
|
65
|
+
>
|
|
66
|
+
{selected ? <View style={[styles.dot, { backgroundColor: colors.primary }]} /> : null}
|
|
67
|
+
</View>
|
|
68
|
+
<Text
|
|
69
|
+
style={[
|
|
70
|
+
styles.label,
|
|
71
|
+
{ color: option.disabled ? colors.mutedForeground : colors.foreground },
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{option.label}
|
|
75
|
+
</Text>
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
</Animated.View>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
20
81
|
export function RadioGroup({
|
|
21
82
|
options,
|
|
22
83
|
value,
|
|
@@ -24,50 +85,16 @@ export function RadioGroup({
|
|
|
24
85
|
orientation = 'vertical',
|
|
25
86
|
style,
|
|
26
87
|
}: RadioGroupProps) {
|
|
27
|
-
const { colors } = useTheme()
|
|
28
|
-
|
|
29
88
|
return (
|
|
30
|
-
<View
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<TouchableOpacity
|
|
41
|
-
key={option.value}
|
|
42
|
-
style={styles.row}
|
|
43
|
-
onPress={() => { if (!option.disabled) { Haptics.selectionAsync(); onValueChange?.(option.value) } }}
|
|
44
|
-
activeOpacity={0.7}
|
|
45
|
-
disabled={option.disabled}
|
|
46
|
-
>
|
|
47
|
-
<View
|
|
48
|
-
style={[
|
|
49
|
-
styles.radio,
|
|
50
|
-
{
|
|
51
|
-
borderColor: selected ? colors.primary : colors.border,
|
|
52
|
-
opacity: option.disabled ? 0.45 : 1,
|
|
53
|
-
},
|
|
54
|
-
]}
|
|
55
|
-
>
|
|
56
|
-
{selected ? (
|
|
57
|
-
<View style={[styles.dot, { backgroundColor: colors.primary }]} />
|
|
58
|
-
) : null}
|
|
59
|
-
</View>
|
|
60
|
-
<Text
|
|
61
|
-
style={[
|
|
62
|
-
styles.label,
|
|
63
|
-
{ color: option.disabled ? colors.mutedForeground : colors.foreground },
|
|
64
|
-
]}
|
|
65
|
-
>
|
|
66
|
-
{option.label}
|
|
67
|
-
</Text>
|
|
68
|
-
</TouchableOpacity>
|
|
69
|
-
)
|
|
70
|
-
})}
|
|
89
|
+
<View style={[styles.container, orientation === 'horizontal' && styles.horizontal, style]}>
|
|
90
|
+
{options.map((option) => (
|
|
91
|
+
<RadioItem
|
|
92
|
+
key={option.value}
|
|
93
|
+
option={option}
|
|
94
|
+
selected={option.value === value}
|
|
95
|
+
onSelect={() => onValueChange?.(option.value)}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
71
98
|
</View>
|
|
72
99
|
)
|
|
73
100
|
}
|