@retray-dev/ui-kit 1.7.0 → 2.3.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 +258 -54
- package/README.md +6 -5
- package/dist/index.d.mts +113 -44
- package/dist/index.d.ts +113 -44
- package/dist/index.js +802 -324
- package/dist/index.mjs +794 -323
- package/package.json +6 -2
- package/src/components/Alert/Alert.tsx +24 -12
- package/src/components/AlertBanner/AlertBanner.tsx +83 -0
- package/src/components/AlertBanner/index.ts +2 -0
- package/src/components/Avatar/Avatar.tsx +1 -0
- package/src/components/Badge/Badge.tsx +44 -8
- package/src/components/Button/Button.tsx +12 -5
- package/src/components/Card/Card.tsx +86 -9
- package/src/components/Chip/Chip.tsx +173 -0
- package/src/components/Chip/index.ts +2 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +87 -0
- package/src/components/ConfirmDialog/index.ts +2 -0
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
- package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
- package/src/components/EmptyState/EmptyState.tsx +42 -7
- package/src/components/Input/Input.tsx +102 -21
- package/src/components/LabelValue/LabelValue.tsx +47 -0
- package/src/components/LabelValue/index.ts +2 -0
- package/src/components/ListItem/ListItem.tsx +121 -0
- package/src/components/ListItem/index.ts +2 -0
- package/src/components/MonthPicker/MonthPicker.tsx +92 -0
- package/src/components/MonthPicker/index.ts +2 -0
- package/src/components/Select/Select.tsx +189 -125
- package/src/components/Slider/Slider.tsx +64 -100
- package/src/components/Switch/Switch.tsx +25 -21
- package/src/components/Textarea/Textarea.tsx +12 -2
- package/src/components/Toggle/Toggle.tsx +13 -6
- package/src/index.ts +8 -2
- package/src/theme/ThemeProvider.tsx +11 -8
- package/src/theme/colors.ts +19 -18
- package/src/theme/types.ts +2 -0
- package/src/components/Alert/index.ts +0 -2
- package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +0 -66
- package/src/components/CurrencyInputLarge/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Personal UI Kit for React Native / Expo",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"react-native-reanimated": ">=4.0.0",
|
|
46
46
|
"react-native-gesture-handler": ">=2.0.0",
|
|
47
47
|
"react-native-worklets": ">=0.8.0",
|
|
48
|
-
"react-native-safe-area-context": ">=4.0.0"
|
|
48
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
49
|
+
"@react-native-picker/picker": ">=2.0.0",
|
|
50
|
+
"@react-native-community/slider": ">=4.0.0"
|
|
49
51
|
},
|
|
50
52
|
"pnpm": {
|
|
51
53
|
"overrides": {
|
|
@@ -60,6 +62,8 @@
|
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
65
|
+
"@react-native-picker/picker": "2.11.4",
|
|
66
|
+
"@react-native-community/slider": "^4.5.5",
|
|
63
67
|
"@types/react": "^19.1.0",
|
|
64
68
|
"expo-haptics": "~15.0.8",
|
|
65
69
|
"expo-linear-gradient": "~15.0.8",
|
|
@@ -2,22 +2,36 @@ import React from 'react'
|
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
6
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface AlertBannerProps {
|
|
8
8
|
title?: string
|
|
9
9
|
description?: string
|
|
10
|
-
variant?:
|
|
10
|
+
variant?: AlertBannerVariant
|
|
11
11
|
icon?: React.ReactNode
|
|
12
12
|
style?: ViewStyle
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function
|
|
15
|
+
export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
|
|
16
16
|
const { colors } = useTheme()
|
|
17
17
|
|
|
18
|
-
const borderColor =
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const borderColor =
|
|
19
|
+
variant === 'destructive' ? colors.destructive
|
|
20
|
+
: variant === 'success' ? colors.success
|
|
21
|
+
: colors.border
|
|
22
|
+
|
|
23
|
+
const titleColor =
|
|
24
|
+
variant === 'destructive' ? colors.destructive
|
|
25
|
+
: variant === 'success' ? colors.success
|
|
26
|
+
: colors.foreground
|
|
27
|
+
|
|
28
|
+
const descColor =
|
|
29
|
+
variant === 'destructive' ? colors.destructive
|
|
30
|
+
: variant === 'success' ? colors.success
|
|
31
|
+
: colors.mutedForeground
|
|
32
|
+
|
|
33
|
+
const defaultIcon =
|
|
34
|
+
variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
|
|
21
35
|
|
|
22
36
|
return (
|
|
23
37
|
<View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
|
|
@@ -25,15 +39,13 @@ export function Alert({ title, description, variant = 'default', icon, style }:
|
|
|
25
39
|
<View style={styles.icon}>{icon}</View>
|
|
26
40
|
) : (
|
|
27
41
|
<View style={styles.icon}>
|
|
28
|
-
<Text style={[styles.defaultIcon, { color: titleColor }]}>
|
|
29
|
-
{variant === 'destructive' ? '⚠' : 'ℹ'}
|
|
30
|
-
</Text>
|
|
42
|
+
<Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
|
|
31
43
|
</View>
|
|
32
44
|
)}
|
|
33
45
|
<View style={styles.content}>
|
|
34
|
-
{title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
|
|
46
|
+
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
35
47
|
{description ? (
|
|
36
|
-
<Text style={[styles.description, { color: descColor }]}>{description}</Text>
|
|
48
|
+
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
37
49
|
) : null}
|
|
38
50
|
</View>
|
|
39
51
|
</View>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
|
|
5
|
+
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
6
|
+
|
|
7
|
+
export interface AlertBannerProps {
|
|
8
|
+
title?: string
|
|
9
|
+
description?: string
|
|
10
|
+
variant?: AlertBannerVariant
|
|
11
|
+
icon?: React.ReactNode
|
|
12
|
+
style?: ViewStyle
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
|
|
16
|
+
const { colors } = useTheme()
|
|
17
|
+
|
|
18
|
+
const borderColor =
|
|
19
|
+
variant === 'destructive' ? colors.destructive
|
|
20
|
+
: variant === 'success' ? colors.success
|
|
21
|
+
: colors.border
|
|
22
|
+
|
|
23
|
+
const titleColor =
|
|
24
|
+
variant === 'destructive' ? colors.destructive
|
|
25
|
+
: variant === 'success' ? colors.success
|
|
26
|
+
: colors.foreground
|
|
27
|
+
|
|
28
|
+
const descColor =
|
|
29
|
+
variant === 'destructive' ? colors.destructive
|
|
30
|
+
: variant === 'success' ? colors.success
|
|
31
|
+
: colors.mutedForeground
|
|
32
|
+
|
|
33
|
+
const defaultIcon =
|
|
34
|
+
variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
|
|
38
|
+
{icon ? (
|
|
39
|
+
<View style={styles.icon}>{icon}</View>
|
|
40
|
+
) : (
|
|
41
|
+
<View style={styles.icon}>
|
|
42
|
+
<Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
45
|
+
<View style={styles.content}>
|
|
46
|
+
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
47
|
+
{description ? (
|
|
48
|
+
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
49
|
+
) : null}
|
|
50
|
+
</View>
|
|
51
|
+
</View>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
container: {
|
|
57
|
+
flexDirection: 'row',
|
|
58
|
+
borderWidth: 1,
|
|
59
|
+
borderRadius: 8,
|
|
60
|
+
padding: 16,
|
|
61
|
+
gap: 12,
|
|
62
|
+
},
|
|
63
|
+
icon: {
|
|
64
|
+
marginTop: 2,
|
|
65
|
+
},
|
|
66
|
+
content: {
|
|
67
|
+
flex: 1,
|
|
68
|
+
gap: 4,
|
|
69
|
+
},
|
|
70
|
+
title: {
|
|
71
|
+
fontSize: 14,
|
|
72
|
+
fontWeight: '500',
|
|
73
|
+
lineHeight: 20,
|
|
74
|
+
},
|
|
75
|
+
description: {
|
|
76
|
+
fontSize: 14,
|
|
77
|
+
lineHeight: 20,
|
|
78
|
+
},
|
|
79
|
+
defaultIcon: {
|
|
80
|
+
fontSize: 18,
|
|
81
|
+
fontWeight: '700',
|
|
82
|
+
},
|
|
83
|
+
})
|
|
@@ -52,6 +52,7 @@ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
|
|
|
52
52
|
) : (
|
|
53
53
|
<Text
|
|
54
54
|
style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
|
|
55
|
+
allowFontScaling={true}
|
|
55
56
|
>
|
|
56
57
|
{fallback?.slice(0, 2).toUpperCase() ?? '?'}
|
|
57
58
|
</Text>
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
|
|
5
5
|
export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
|
|
6
|
+
export type BadgeSize = 'sm' | 'md' | 'lg'
|
|
6
7
|
|
|
7
8
|
export interface BadgeProps {
|
|
8
|
-
label
|
|
9
|
+
label?: string
|
|
10
|
+
/** Alternative to \`label\` — allows JSX children. */
|
|
11
|
+
children?: React.ReactNode
|
|
9
12
|
variant?: BadgeVariant
|
|
13
|
+
size?: BadgeSize
|
|
14
|
+
/** Icon rendered before the label/children. */
|
|
15
|
+
icon?: React.ReactNode
|
|
10
16
|
style?: ViewStyle
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
const sizePadding: Record<BadgeSize, ViewStyle> = {
|
|
20
|
+
sm: { paddingHorizontal: 8, paddingVertical: 2 },
|
|
21
|
+
md: { paddingHorizontal: 10, paddingVertical: 4 },
|
|
22
|
+
lg: { paddingHorizontal: 12, paddingVertical: 6 },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sizeFontSize: Record<BadgeSize, TextStyle> = {
|
|
26
|
+
sm: { fontSize: 11 },
|
|
27
|
+
md: { fontSize: 13 },
|
|
28
|
+
lg: { fontSize: 15 },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sizeIconGap: Record<BadgeSize, number> = {
|
|
32
|
+
sm: 4,
|
|
33
|
+
md: 6,
|
|
34
|
+
lg: 6,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Badge({ label, children, variant = 'default', size = 'md', icon, style }: BadgeProps) {
|
|
14
38
|
const { colors } = useTheme()
|
|
15
39
|
|
|
16
40
|
const containerStyle: ViewStyle = {
|
|
@@ -27,9 +51,18 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
|
|
|
27
51
|
outline: colors.foreground,
|
|
28
52
|
}[variant]
|
|
29
53
|
|
|
54
|
+
const content = children ?? label
|
|
55
|
+
|
|
30
56
|
return (
|
|
31
|
-
<View style={[styles.container, containerStyle, style]}>
|
|
32
|
-
<
|
|
57
|
+
<View style={[styles.container, containerStyle, sizePadding[size], { gap: sizeIconGap[size] }, style]}>
|
|
58
|
+
{icon ? <View style={styles.iconContainer}>{icon}</View> : null}
|
|
59
|
+
{typeof content === 'string' ? (
|
|
60
|
+
<Text style={[styles.label, { color: textColor }, sizeFontSize[size]]} allowFontScaling={true}>
|
|
61
|
+
{content}
|
|
62
|
+
</Text>
|
|
63
|
+
) : (
|
|
64
|
+
content
|
|
65
|
+
)}
|
|
33
66
|
</View>
|
|
34
67
|
)
|
|
35
68
|
}
|
|
@@ -37,12 +70,15 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
|
|
|
37
70
|
const styles = StyleSheet.create({
|
|
38
71
|
container: {
|
|
39
72
|
borderRadius: 8,
|
|
40
|
-
paddingHorizontal: 10,
|
|
41
|
-
paddingVertical: 4,
|
|
42
73
|
alignSelf: 'flex-start',
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
},
|
|
77
|
+
iconContainer: {
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
alignItems: 'center',
|
|
43
80
|
},
|
|
44
81
|
label: {
|
|
45
|
-
fontSize: 13,
|
|
46
82
|
fontWeight: '500',
|
|
47
83
|
},
|
|
48
84
|
})
|
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
TouchableOpacityProps,
|
|
9
9
|
ViewStyle,
|
|
10
10
|
TextStyle,
|
|
11
|
+
Platform,
|
|
11
12
|
} from 'react-native'
|
|
13
|
+
|
|
14
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
12
15
|
import * as Haptics from 'expo-haptics'
|
|
13
16
|
import { useTheme } from '../../theme'
|
|
14
17
|
|
|
15
|
-
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
|
|
18
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
|
16
19
|
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
17
20
|
|
|
18
21
|
export interface ButtonProps extends TouchableOpacityProps {
|
|
@@ -67,14 +70,14 @@ export function Button({
|
|
|
67
70
|
if (isDisabled) return
|
|
68
71
|
Animated.spring(scale, {
|
|
69
72
|
toValue: 0.95,
|
|
70
|
-
useNativeDriver:
|
|
73
|
+
useNativeDriver: nativeDriver,
|
|
71
74
|
speed: 40,
|
|
72
75
|
bounciness: 0,
|
|
73
76
|
}).start()
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
const handlePressOut = () => {
|
|
77
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver:
|
|
80
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
@@ -87,6 +90,7 @@ export function Button({
|
|
|
87
90
|
secondary: { backgroundColor: colors.secondary },
|
|
88
91
|
outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
|
|
89
92
|
ghost: { backgroundColor: 'transparent' },
|
|
93
|
+
destructive: { backgroundColor: colors.destructive },
|
|
90
94
|
}[variant]
|
|
91
95
|
|
|
92
96
|
const labelVariantStyle: TextStyle = {
|
|
@@ -94,10 +98,13 @@ export function Button({
|
|
|
94
98
|
secondary: { color: colors.secondaryForeground },
|
|
95
99
|
outline: { color: colors.foreground },
|
|
96
100
|
ghost: { color: colors.foreground },
|
|
101
|
+
destructive: { color: colors.destructiveForeground },
|
|
97
102
|
}[variant]
|
|
98
103
|
|
|
99
104
|
const spinnerColor =
|
|
100
|
-
variant === '
|
|
105
|
+
variant === 'destructive' ? colors.destructiveForeground
|
|
106
|
+
: variant === 'primary' || variant === 'secondary' ? colors.primaryForeground
|
|
107
|
+
: colors.foreground
|
|
101
108
|
|
|
102
109
|
return (
|
|
103
110
|
<Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
|
|
@@ -143,7 +150,7 @@ const styles = StyleSheet.create({
|
|
|
143
150
|
width: '100%',
|
|
144
151
|
},
|
|
145
152
|
disabled: {
|
|
146
|
-
opacity: 0.
|
|
153
|
+
opacity: 0.5,
|
|
147
154
|
},
|
|
148
155
|
label: {
|
|
149
156
|
fontWeight: '600',
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle, TextStyle, Platform } from 'react-native'
|
|
3
|
+
import * as Haptics from 'expo-haptics'
|
|
3
4
|
import { useTheme } from '../../theme'
|
|
4
5
|
|
|
6
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
7
|
+
|
|
8
|
+
export type CardVariant = 'elevated' | 'outlined' | 'filled'
|
|
9
|
+
|
|
5
10
|
export interface CardProps {
|
|
6
11
|
children: React.ReactNode
|
|
12
|
+
/** Visual style variant. `'elevated'` (default) has shadow, `'outlined'` has border only, `'filled'` uses accent background. */
|
|
13
|
+
variant?: CardVariant
|
|
14
|
+
/** Makes the card tappable. Adds press animation and haptic feedback. */
|
|
15
|
+
onPress?: () => void
|
|
7
16
|
style?: ViewStyle
|
|
8
17
|
}
|
|
9
18
|
|
|
@@ -32,15 +41,83 @@ export interface CardFooterProps {
|
|
|
32
41
|
style?: ViewStyle
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
export function Card({ children, style }: CardProps) {
|
|
44
|
+
export function Card({ children, variant = 'elevated', onPress, style }: CardProps) {
|
|
36
45
|
const { colors } = useTheme()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
47
|
+
|
|
48
|
+
const handlePressIn = () => {
|
|
49
|
+
if (!onPress) return
|
|
50
|
+
Animated.spring(scale, {
|
|
51
|
+
toValue: 0.98,
|
|
52
|
+
useNativeDriver: nativeDriver,
|
|
53
|
+
speed: 40,
|
|
54
|
+
bounciness: 0,
|
|
55
|
+
}).start()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handlePressOut = () => {
|
|
59
|
+
if (!onPress) return
|
|
60
|
+
Animated.spring(scale, {
|
|
61
|
+
toValue: 1,
|
|
62
|
+
useNativeDriver: nativeDriver,
|
|
63
|
+
speed: 40,
|
|
64
|
+
bounciness: 4,
|
|
65
|
+
}).start()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handlePress = () => {
|
|
69
|
+
if (!onPress) return
|
|
70
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
|
|
71
|
+
onPress()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const variantStyle: ViewStyle = {
|
|
75
|
+
elevated: {
|
|
76
|
+
backgroundColor: colors.card,
|
|
77
|
+
borderColor: colors.border,
|
|
78
|
+
shadowColor: '#000',
|
|
79
|
+
shadowOffset: { width: 0, height: 1 },
|
|
80
|
+
shadowOpacity: 0.05,
|
|
81
|
+
shadowRadius: 2,
|
|
82
|
+
elevation: 1,
|
|
83
|
+
},
|
|
84
|
+
outlined: {
|
|
85
|
+
backgroundColor: colors.card,
|
|
86
|
+
borderColor: colors.border,
|
|
87
|
+
shadowOpacity: 0,
|
|
88
|
+
elevation: 0,
|
|
89
|
+
},
|
|
90
|
+
filled: {
|
|
91
|
+
backgroundColor: colors.accent,
|
|
92
|
+
borderColor: colors.border,
|
|
93
|
+
shadowOpacity: 0,
|
|
94
|
+
elevation: 0,
|
|
95
|
+
},
|
|
96
|
+
}[variant]
|
|
97
|
+
|
|
98
|
+
const cardContent = (
|
|
99
|
+
<View style={[styles.card, variantStyle, style]}>
|
|
41
100
|
{children}
|
|
42
101
|
</View>
|
|
43
102
|
)
|
|
103
|
+
|
|
104
|
+
if (onPress) {
|
|
105
|
+
return (
|
|
106
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
107
|
+
<TouchableOpacity
|
|
108
|
+
onPress={handlePress}
|
|
109
|
+
onPressIn={handlePressIn}
|
|
110
|
+
onPressOut={handlePressOut}
|
|
111
|
+
activeOpacity={1}
|
|
112
|
+
touchSoundDisabled={true}
|
|
113
|
+
>
|
|
114
|
+
{cardContent}
|
|
115
|
+
</TouchableOpacity>
|
|
116
|
+
</Animated.View>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cardContent
|
|
44
121
|
}
|
|
45
122
|
|
|
46
123
|
export function CardHeader({ children, style }: CardHeaderProps) {
|
|
@@ -49,13 +126,13 @@ export function CardHeader({ children, style }: CardHeaderProps) {
|
|
|
49
126
|
|
|
50
127
|
export function CardTitle({ children, style }: CardTitleProps) {
|
|
51
128
|
const { colors } = useTheme()
|
|
52
|
-
return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
|
|
129
|
+
return <Text style={[styles.title, { color: colors.cardForeground }, style]} allowFontScaling={true}>{children}</Text>
|
|
53
130
|
}
|
|
54
131
|
|
|
55
132
|
export function CardDescription({ children, style }: CardDescriptionProps) {
|
|
56
133
|
const { colors } = useTheme()
|
|
57
134
|
return (
|
|
58
|
-
<Text style={[styles.description, { color: colors.mutedForeground }, style]}>{children}</Text>
|
|
135
|
+
<Text style={[styles.description, { color: colors.mutedForeground }, style]} allowFontScaling={true}>{children}</Text>
|
|
59
136
|
)
|
|
60
137
|
}
|
|
61
138
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Animated,
|
|
5
|
+
View,
|
|
6
|
+
Text,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
Platform,
|
|
10
|
+
Easing,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import * as Haptics from 'expo-haptics'
|
|
13
|
+
import { useTheme } from '../../theme'
|
|
14
|
+
|
|
15
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
16
|
+
|
|
17
|
+
export interface ChipProps {
|
|
18
|
+
label: string
|
|
19
|
+
selected?: boolean
|
|
20
|
+
onPress?: () => void
|
|
21
|
+
style?: ViewStyle
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChipOption {
|
|
25
|
+
label: string
|
|
26
|
+
value: string | number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChipGroupProps {
|
|
30
|
+
options: ChipOption[]
|
|
31
|
+
value?: string | number | (string | number)[]
|
|
32
|
+
onValueChange?: (value: string | number | (string | number)[]) => void
|
|
33
|
+
/** When true, allows selecting multiple chips. `value` and `onValueChange` will use arrays. */
|
|
34
|
+
multiSelect?: boolean
|
|
35
|
+
style?: ViewStyle
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Chip({ label, selected = false, onPress, style }: ChipProps) {
|
|
39
|
+
const { colors } = useTheme()
|
|
40
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
41
|
+
const pressAnim = useRef(new Animated.Value(selected ? 1 : 0)).current
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
Animated.timing(pressAnim, {
|
|
45
|
+
toValue: selected ? 1 : 0,
|
|
46
|
+
duration: 150,
|
|
47
|
+
easing: Easing.out(Easing.ease),
|
|
48
|
+
useNativeDriver: false,
|
|
49
|
+
}).start()
|
|
50
|
+
}, [selected, pressAnim])
|
|
51
|
+
|
|
52
|
+
const handlePressIn = () => {
|
|
53
|
+
Animated.spring(scale, {
|
|
54
|
+
toValue: 0.95,
|
|
55
|
+
useNativeDriver: nativeDriver,
|
|
56
|
+
speed: 40,
|
|
57
|
+
bounciness: 0,
|
|
58
|
+
}).start()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handlePressOut = () => {
|
|
62
|
+
Animated.spring(scale, {
|
|
63
|
+
toValue: 1,
|
|
64
|
+
useNativeDriver: nativeDriver,
|
|
65
|
+
speed: 40,
|
|
66
|
+
bounciness: 4,
|
|
67
|
+
}).start()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handlePress = () => {
|
|
71
|
+
Haptics.selectionAsync()
|
|
72
|
+
onPress?.()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const backgroundColor = pressAnim.interpolate({
|
|
76
|
+
inputRange: [0, 1],
|
|
77
|
+
outputRange: [colors.secondary, colors.primary],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const textColor = pressAnim.interpolate({
|
|
81
|
+
inputRange: [0, 1],
|
|
82
|
+
outputRange: [colors.foreground, colors.primaryForeground],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const borderColor = pressAnim.interpolate({
|
|
86
|
+
inputRange: [0, 1],
|
|
87
|
+
outputRange: [colors.border, colors.primary],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Animated.View style={[styles.wrapper, { transform: [{ scale }] }, style]}>
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
onPress={handlePress}
|
|
94
|
+
onPressIn={handlePressIn}
|
|
95
|
+
onPressOut={handlePressOut}
|
|
96
|
+
activeOpacity={1}
|
|
97
|
+
touchSoundDisabled={true}
|
|
98
|
+
>
|
|
99
|
+
<Animated.View style={[styles.chip, { backgroundColor, borderColor }]}>
|
|
100
|
+
<Animated.Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>
|
|
101
|
+
{label}
|
|
102
|
+
</Animated.Text>
|
|
103
|
+
</Animated.View>
|
|
104
|
+
</TouchableOpacity>
|
|
105
|
+
</Animated.View>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function ChipGroup({ options, value, onValueChange, multiSelect = false, style }: ChipGroupProps) {
|
|
110
|
+
const handlePress = (optionValue: string | number) => {
|
|
111
|
+
if (!multiSelect) {
|
|
112
|
+
onValueChange?.(optionValue)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Multiselect logic
|
|
117
|
+
const currentArray = Array.isArray(value) ? value : value ? [value] : []
|
|
118
|
+
const isSelected = currentArray.includes(optionValue)
|
|
119
|
+
|
|
120
|
+
let newArray: (string | number)[]
|
|
121
|
+
if (isSelected) {
|
|
122
|
+
// Remove from selection
|
|
123
|
+
newArray = currentArray.filter((v) => v !== optionValue)
|
|
124
|
+
} else {
|
|
125
|
+
// Add to selection
|
|
126
|
+
newArray = [...currentArray, optionValue]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onValueChange?.(newArray)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isSelected = (optionValue: string | number): boolean => {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
return value.includes(optionValue)
|
|
135
|
+
}
|
|
136
|
+
return optionValue === value
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View style={[styles.group, style]}>
|
|
141
|
+
{options.map((opt) => (
|
|
142
|
+
<Chip
|
|
143
|
+
key={opt.value}
|
|
144
|
+
label={opt.label}
|
|
145
|
+
selected={isSelected(opt.value)}
|
|
146
|
+
onPress={() => handlePress(opt.value)}
|
|
147
|
+
/>
|
|
148
|
+
))}
|
|
149
|
+
</View>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const styles = StyleSheet.create({
|
|
154
|
+
wrapper: {},
|
|
155
|
+
chip: {
|
|
156
|
+
borderRadius: 999,
|
|
157
|
+
paddingHorizontal: 14,
|
|
158
|
+
paddingVertical: 8,
|
|
159
|
+
borderWidth: 1.5,
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
justifyContent: 'center',
|
|
162
|
+
},
|
|
163
|
+
label: {
|
|
164
|
+
fontSize: 14,
|
|
165
|
+
fontWeight: '500',
|
|
166
|
+
lineHeight: 20,
|
|
167
|
+
},
|
|
168
|
+
group: {
|
|
169
|
+
flexDirection: 'row',
|
|
170
|
+
flexWrap: 'wrap',
|
|
171
|
+
gap: 8,
|
|
172
|
+
},
|
|
173
|
+
})
|