@retray-dev/ui-kit 1.8.0 → 2.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 +150 -43
- package/dist/index.d.mts +80 -44
- package/dist/index.d.ts +80 -44
- package/dist/index.js +627 -457
- package/dist/index.mjs +626 -457
- package/package.json +8 -2
- package/src/components/Accordion/Accordion.tsx +4 -6
- package/src/components/Alert/Alert.tsx +3 -3
- package/src/components/AlertBanner/AlertBanner.tsx +85 -0
- package/src/components/{Alert → AlertBanner}/index.ts +2 -2
- package/src/components/Avatar/Avatar.tsx +1 -0
- package/src/components/Badge/Badge.tsx +45 -9
- package/src/components/Button/Button.tsx +5 -5
- package/src/components/Card/Card.tsx +90 -18
- package/src/components/Checkbox/Checkbox.tsx +4 -4
- package/src/components/Chip/Chip.tsx +36 -5
- package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
- package/src/components/EmptyState/EmptyState.tsx +2 -1
- package/src/components/Input/Input.tsx +107 -26
- package/src/components/ListItem/ListItem.tsx +157 -21
- package/src/components/MonthPicker/MonthPicker.tsx +3 -6
- package/src/components/RadioGroup/RadioGroup.tsx +2 -2
- package/src/components/Select/Select.tsx +200 -132
- package/src/components/Slider/Slider.tsx +64 -100
- package/src/components/Switch/Switch.tsx +22 -20
- package/src/components/Textarea/Textarea.tsx +16 -7
- package/src/components/Toast/Toast.tsx +23 -18
- package/src/components/Toggle/Toggle.tsx +36 -49
- package/src/index.ts +3 -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/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.5.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,10 @@
|
|
|
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",
|
|
51
|
+
"@expo/vector-icons": ">=14.0.0"
|
|
49
52
|
},
|
|
50
53
|
"pnpm": {
|
|
51
54
|
"overrides": {
|
|
@@ -60,7 +63,10 @@
|
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|
|
62
65
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
66
|
+
"@react-native-picker/picker": "2.11.4",
|
|
67
|
+
"@react-native-community/slider": "^4.5.5",
|
|
63
68
|
"@types/react": "^19.1.0",
|
|
69
|
+
"@expo/vector-icons": "^15.1.1",
|
|
64
70
|
"expo-haptics": "~15.0.8",
|
|
65
71
|
"expo-linear-gradient": "~15.0.8",
|
|
66
72
|
"react": "19.1.0",
|
|
@@ -14,6 +14,7 @@ import ReanimatedAnimated, {
|
|
|
14
14
|
withTiming,
|
|
15
15
|
Easing,
|
|
16
16
|
} from 'react-native-reanimated'
|
|
17
|
+
import { Entypo } from '@expo/vector-icons'
|
|
17
18
|
import * as Haptics from 'expo-haptics'
|
|
18
19
|
import { useTheme } from '../../theme'
|
|
19
20
|
|
|
@@ -99,11 +100,9 @@ function AccordionItemComponent({
|
|
|
99
100
|
touchSoundDisabled={true}
|
|
100
101
|
>
|
|
101
102
|
<Text style={[styles.triggerText, { color: colors.foreground }]}>{item.trigger}</Text>
|
|
102
|
-
<ReanimatedAnimated.
|
|
103
|
-
|
|
104
|
-
>
|
|
105
|
-
▾
|
|
106
|
-
</ReanimatedAnimated.Text>
|
|
103
|
+
<ReanimatedAnimated.View style={[styles.chevron, rotationStyle]}>
|
|
104
|
+
<Entypo name="chevron-down" size={20} color={colors.foreground} />
|
|
105
|
+
</ReanimatedAnimated.View>
|
|
107
106
|
</TouchableOpacity>
|
|
108
107
|
</Animated.View>
|
|
109
108
|
|
|
@@ -162,7 +161,6 @@ const styles = StyleSheet.create({
|
|
|
162
161
|
flex: 1,
|
|
163
162
|
},
|
|
164
163
|
chevron: {
|
|
165
|
-
fontSize: 18,
|
|
166
164
|
marginLeft: 8,
|
|
167
165
|
},
|
|
168
166
|
content: {
|
|
@@ -39,13 +39,13 @@ export function AlertBanner({ title, description, variant = 'default', icon, sty
|
|
|
39
39
|
<View style={styles.icon}>{icon}</View>
|
|
40
40
|
) : (
|
|
41
41
|
<View style={styles.icon}>
|
|
42
|
-
<Text style={[styles.defaultIcon, { color: titleColor }]}>{defaultIcon}</Text>
|
|
42
|
+
<Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
|
|
43
43
|
</View>
|
|
44
44
|
)}
|
|
45
45
|
<View style={styles.content}>
|
|
46
|
-
{title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
|
|
46
|
+
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
47
47
|
{description ? (
|
|
48
|
-
<Text style={[styles.description, { color: descColor }]}>{description}</Text>
|
|
48
|
+
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
49
49
|
) : null}
|
|
50
50
|
</View>
|
|
51
51
|
</View>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
|
|
4
|
+
import { useTheme } from '../../theme'
|
|
5
|
+
|
|
6
|
+
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
7
|
+
|
|
8
|
+
export interface AlertBannerProps {
|
|
9
|
+
title?: string
|
|
10
|
+
description?: string
|
|
11
|
+
variant?: AlertBannerVariant
|
|
12
|
+
icon?: React.ReactNode
|
|
13
|
+
style?: ViewStyle
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
|
|
17
|
+
const { colors } = useTheme()
|
|
18
|
+
|
|
19
|
+
const borderColor =
|
|
20
|
+
variant === 'destructive' ? colors.destructive
|
|
21
|
+
: variant === 'success' ? colors.success
|
|
22
|
+
: colors.border
|
|
23
|
+
|
|
24
|
+
const titleColor =
|
|
25
|
+
variant === 'destructive' ? colors.destructive
|
|
26
|
+
: variant === 'success' ? colors.success
|
|
27
|
+
: colors.foreground
|
|
28
|
+
|
|
29
|
+
const descColor =
|
|
30
|
+
variant === 'destructive' ? colors.destructive
|
|
31
|
+
: variant === 'success' ? colors.success
|
|
32
|
+
: colors.mutedForeground
|
|
33
|
+
|
|
34
|
+
const defaultIcon =
|
|
35
|
+
variant === 'success' ? (
|
|
36
|
+
<FontAwesome5 name="check-circle" size={18} color={titleColor} />
|
|
37
|
+
) : variant === 'destructive' ? (
|
|
38
|
+
<MaterialIcons name="error-outline" size={20} color={titleColor} />
|
|
39
|
+
) : (
|
|
40
|
+
<Entypo name="info-with-circle" size={18} color={titleColor} />
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
|
|
45
|
+
<View style={styles.icon}>{icon ?? defaultIcon}</View>
|
|
46
|
+
<View style={styles.content}>
|
|
47
|
+
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
48
|
+
{description ? (
|
|
49
|
+
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
50
|
+
) : null}
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
container: {
|
|
58
|
+
flexDirection: 'row',
|
|
59
|
+
borderWidth: 1,
|
|
60
|
+
borderRadius: 12,
|
|
61
|
+
padding: 16,
|
|
62
|
+
gap: 12,
|
|
63
|
+
shadowColor: '#000',
|
|
64
|
+
shadowOffset: { width: 0, height: 4 },
|
|
65
|
+
shadowOpacity: 0.06,
|
|
66
|
+
shadowRadius: 12,
|
|
67
|
+
elevation: 3,
|
|
68
|
+
},
|
|
69
|
+
icon: {
|
|
70
|
+
marginTop: 0,
|
|
71
|
+
},
|
|
72
|
+
content: {
|
|
73
|
+
flex: 1,
|
|
74
|
+
gap: 4,
|
|
75
|
+
},
|
|
76
|
+
title: {
|
|
77
|
+
fontSize: 14,
|
|
78
|
+
fontWeight: '500',
|
|
79
|
+
lineHeight: 20,
|
|
80
|
+
},
|
|
81
|
+
description: {
|
|
82
|
+
fontSize: 14,
|
|
83
|
+
lineHeight: 20,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AlertBanner } from './
|
|
2
|
-
export type { AlertBannerProps, AlertBannerVariant } from './
|
|
1
|
+
export { AlertBanner } from './AlertBanner'
|
|
2
|
+
export type { AlertBannerProps, AlertBannerVariant } from './AlertBanner'
|
|
@@ -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,22 +51,34 @@ 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
|
}
|
|
36
69
|
|
|
37
70
|
const styles = StyleSheet.create({
|
|
38
71
|
container: {
|
|
39
|
-
borderRadius:
|
|
40
|
-
paddingHorizontal: 10,
|
|
41
|
-
paddingVertical: 4,
|
|
72
|
+
borderRadius: 6,
|
|
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
|
})
|
|
@@ -38,9 +38,9 @@ export interface ButtonProps extends TouchableOpacityProps {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
41
|
-
sm: { paddingHorizontal: 20, paddingVertical:
|
|
42
|
-
md: { paddingHorizontal: 24, paddingVertical:
|
|
43
|
-
lg: { paddingHorizontal: 32, paddingVertical:
|
|
41
|
+
sm: { paddingHorizontal: 20, paddingVertical: 10 },
|
|
42
|
+
md: { paddingHorizontal: 24, paddingVertical: 14 },
|
|
43
|
+
lg: { paddingHorizontal: 32, paddingVertical: 18 },
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
@@ -150,12 +150,12 @@ const styles = StyleSheet.create({
|
|
|
150
150
|
width: '100%',
|
|
151
151
|
},
|
|
152
152
|
disabled: {
|
|
153
|
-
opacity: 0.
|
|
153
|
+
opacity: 0.5,
|
|
154
154
|
},
|
|
155
155
|
label: {
|
|
156
156
|
fontWeight: '600',
|
|
157
157
|
},
|
|
158
158
|
labelWithIcon: {
|
|
159
|
-
marginHorizontal:
|
|
159
|
+
marginHorizontal: 8,
|
|
160
160
|
},
|
|
161
161
|
})
|
|
@@ -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: 4 },
|
|
80
|
+
shadowOpacity: 0.06,
|
|
81
|
+
shadowRadius: 12,
|
|
82
|
+
elevation: 3,
|
|
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
|
|
|
@@ -69,16 +146,11 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
69
146
|
|
|
70
147
|
const styles = StyleSheet.create({
|
|
71
148
|
card: {
|
|
72
|
-
borderRadius:
|
|
149
|
+
borderRadius: 12,
|
|
73
150
|
borderWidth: 1,
|
|
74
|
-
shadowColor: '#000',
|
|
75
|
-
shadowOffset: { width: 0, height: 1 },
|
|
76
|
-
shadowOpacity: 0.05,
|
|
77
|
-
shadowRadius: 2,
|
|
78
|
-
elevation: 1,
|
|
79
151
|
},
|
|
80
152
|
header: {
|
|
81
|
-
padding:
|
|
153
|
+
padding: 24,
|
|
82
154
|
paddingBottom: 0,
|
|
83
155
|
gap: 8,
|
|
84
156
|
},
|
|
@@ -92,10 +164,10 @@ const styles = StyleSheet.create({
|
|
|
92
164
|
lineHeight: 22,
|
|
93
165
|
},
|
|
94
166
|
content: {
|
|
95
|
-
padding:
|
|
167
|
+
padding: 24,
|
|
96
168
|
},
|
|
97
169
|
footer: {
|
|
98
|
-
padding:
|
|
170
|
+
padding: 24,
|
|
99
171
|
paddingTop: 0,
|
|
100
172
|
flexDirection: 'row',
|
|
101
173
|
alignItems: 'center',
|
|
@@ -77,16 +77,16 @@ const styles = StyleSheet.create({
|
|
|
77
77
|
gap: 12,
|
|
78
78
|
},
|
|
79
79
|
box: {
|
|
80
|
-
width:
|
|
81
|
-
height:
|
|
80
|
+
width: 24,
|
|
81
|
+
height: 24,
|
|
82
82
|
borderRadius: 8,
|
|
83
83
|
borderWidth: 1.5,
|
|
84
84
|
alignItems: 'center',
|
|
85
85
|
justifyContent: 'center',
|
|
86
86
|
},
|
|
87
87
|
checkmark: {
|
|
88
|
-
width:
|
|
89
|
-
height:
|
|
88
|
+
width: 12,
|
|
89
|
+
height: 7,
|
|
90
90
|
borderLeftWidth: 2,
|
|
91
91
|
borderBottomWidth: 2,
|
|
92
92
|
transform: [{ rotate: '-45deg' }, { translateY: -1 }],
|
|
@@ -28,8 +28,10 @@ export interface ChipOption {
|
|
|
28
28
|
|
|
29
29
|
export interface ChipGroupProps {
|
|
30
30
|
options: ChipOption[]
|
|
31
|
-
value?: string | number
|
|
32
|
-
onValueChange?: (value: string | number) => void
|
|
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
|
|
33
35
|
style?: ViewStyle
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -104,15 +106,44 @@ export function Chip({ label, selected = false, onPress, style }: ChipProps) {
|
|
|
104
106
|
)
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
export function ChipGroup({ options, value, onValueChange, style }: ChipGroupProps) {
|
|
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
|
+
|
|
108
139
|
return (
|
|
109
140
|
<View style={[styles.group, style]}>
|
|
110
141
|
{options.map((opt) => (
|
|
111
142
|
<Chip
|
|
112
143
|
key={opt.value}
|
|
113
144
|
label={opt.label}
|
|
114
|
-
selected={opt.value
|
|
115
|
-
onPress={() =>
|
|
145
|
+
selected={isSelected(opt.value)}
|
|
146
|
+
onPress={() => handlePress(opt.value)}
|
|
116
147
|
/>
|
|
117
148
|
))}
|
|
118
149
|
</View>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { ViewStyle } from 'react-native'
|
|
2
|
+
import { ViewStyle, TextStyle } from 'react-native'
|
|
3
3
|
import { Input } from '../Input'
|
|
4
4
|
|
|
5
5
|
export interface CurrencyInputProps {
|
|
@@ -11,6 +11,8 @@ export interface CurrencyInputProps {
|
|
|
11
11
|
prefix?: string
|
|
12
12
|
/** Character used to separate groups of three digits. Defaults to `'.'`. */
|
|
13
13
|
thousandsSeparator?: '.' | ','
|
|
14
|
+
/** Font size variant. `'large'` renders at 36pt, `'default'` at 17pt. */
|
|
15
|
+
size?: 'default' | 'large'
|
|
14
16
|
label?: string
|
|
15
17
|
/** Red helper text; also changes input border to destructive color. */
|
|
16
18
|
error?: string
|
|
@@ -18,6 +20,7 @@ export interface CurrencyInputProps {
|
|
|
18
20
|
placeholder?: string
|
|
19
21
|
editable?: boolean
|
|
20
22
|
containerStyle?: ViewStyle
|
|
23
|
+
style?: TextStyle
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function formatCurrency(raw: string, separator: '.' | ','): string {
|
|
@@ -32,12 +35,14 @@ export function CurrencyInput({
|
|
|
32
35
|
onChangeValue,
|
|
33
36
|
prefix = '$',
|
|
34
37
|
thousandsSeparator = '.',
|
|
38
|
+
size = 'default',
|
|
35
39
|
label,
|
|
36
40
|
error,
|
|
37
41
|
hint,
|
|
38
42
|
placeholder,
|
|
39
43
|
editable,
|
|
40
44
|
containerStyle,
|
|
45
|
+
style,
|
|
41
46
|
}: CurrencyInputProps) {
|
|
42
47
|
const handleChange = (text: string) => {
|
|
43
48
|
const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
|
|
@@ -49,6 +54,8 @@ export function CurrencyInput({
|
|
|
49
54
|
onChangeValue?.(isNaN(raw) ? 0 : raw)
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
const inputStyle: TextStyle = size === 'large' ? { fontSize: 36 } : {}
|
|
58
|
+
|
|
52
59
|
return (
|
|
53
60
|
<Input
|
|
54
61
|
value={value}
|
|
@@ -60,6 +67,7 @@ export function CurrencyInput({
|
|
|
60
67
|
placeholder={placeholder ?? `${prefix}0`}
|
|
61
68
|
editable={editable}
|
|
62
69
|
containerStyle={containerStyle}
|
|
70
|
+
style={[inputStyle, style]}
|
|
63
71
|
/>
|
|
64
72
|
)
|
|
65
73
|
}
|
|
@@ -39,11 +39,12 @@ export function EmptyState({ icon, title, description, action, size = 'default',
|
|
|
39
39
|
<View style={styles.textWrapper}>
|
|
40
40
|
<Text
|
|
41
41
|
style={[styles.title, isCompact && styles.titleCompact, { color: colors.foreground }]}
|
|
42
|
+
allowFontScaling={true}
|
|
42
43
|
>
|
|
43
44
|
{title}
|
|
44
45
|
</Text>
|
|
45
46
|
{description && !isCompact ? (
|
|
46
|
-
<Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
|
|
47
|
+
<Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>{description}</Text>
|
|
47
48
|
) : null}
|
|
48
49
|
</View>
|
|
49
50
|
{action && !isCompact ? <View style={styles.action}>{action}</View> : null}
|