@retray-dev/ui-kit 1.8.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 +80 -23
- package/dist/index.d.mts +46 -42
- package/dist/index.d.ts +46 -42
- package/dist/index.js +442 -333
- package/dist/index.mjs +441 -333
- package/package.json +6 -2
- package/src/components/Alert/Alert.tsx +3 -3
- package/src/components/AlertBanner/AlertBanner.tsx +83 -0
- package/src/components/{Alert → AlertBanner}/index.ts +2 -2
- package/src/components/Avatar/Avatar.tsx +1 -0
- package/src/components/Badge/Badge.tsx +44 -8
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.tsx +86 -9
- 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 +102 -21
- package/src/components/MonthPicker/MonthPicker.tsx +2 -2
- package/src/components/Select/Select.tsx +189 -125
- package/src/components/Slider/Slider.tsx +64 -100
- package/src/components/Switch/Switch.tsx +22 -20
- package/src/components/Textarea/Textarea.tsx +12 -2
- package/src/components/Toggle/Toggle.tsx +13 -6
- 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.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",
|
|
@@ -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,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
|
+
})
|
|
@@ -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,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
|
})
|
|
@@ -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
|
|
|
@@ -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}
|
|
@@ -1,46 +1,104 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
|
|
2
|
+
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
|
|
5
|
+
const webInputResetStyle: any =
|
|
6
|
+
Platform.OS === 'web'
|
|
7
|
+
? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
|
|
8
|
+
: {}
|
|
9
|
+
|
|
5
10
|
export interface InputProps extends TextInputProps {
|
|
6
11
|
label?: string
|
|
7
12
|
/** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
|
|
8
13
|
error?: string
|
|
9
14
|
/** Helper text shown below the input when there is no error. */
|
|
10
15
|
hint?: string
|
|
11
|
-
/**
|
|
16
|
+
/** Text or component rendered before the input text. */
|
|
17
|
+
prefix?: React.ReactNode
|
|
18
|
+
/** Text or component rendered after the input text. */
|
|
19
|
+
suffix?: React.ReactNode
|
|
20
|
+
/** Style applied to prefix text if prefix is a string. */
|
|
21
|
+
prefixStyle?: TextStyle
|
|
22
|
+
/** Style applied to suffix text if suffix is a string. */
|
|
23
|
+
suffixStyle?: TextStyle
|
|
24
|
+
/** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
|
|
25
|
+
type?: 'text' | 'password'
|
|
26
|
+
/** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
|
|
12
27
|
containerStyle?: ViewStyle
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
|
|
30
|
+
export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
|
|
16
31
|
const { colors } = useTheme()
|
|
17
32
|
const [focused, setFocused] = useState(false)
|
|
33
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
34
|
+
|
|
35
|
+
const isPassword = type === 'password'
|
|
36
|
+
const effectiveSecure = isPassword ? !showPassword : secureTextEntry
|
|
37
|
+
|
|
38
|
+
// If type is password and no suffix is provided, add the toggle button
|
|
39
|
+
const effectiveSuffix = isPassword && !suffix ? (
|
|
40
|
+
<TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
|
|
41
|
+
<Text style={[styles.suffixText, { color: colors.mutedForeground }]}>{showPassword ? '👁' : '👁🗨'}</Text>
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
) : suffix
|
|
18
44
|
|
|
19
45
|
return (
|
|
20
46
|
<View style={[styles.container, containerStyle]}>
|
|
21
47
|
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
22
|
-
<
|
|
48
|
+
<View
|
|
23
49
|
style={[
|
|
24
|
-
styles.
|
|
50
|
+
styles.inputWrapper,
|
|
25
51
|
{
|
|
26
|
-
borderColor: error
|
|
27
|
-
|
|
52
|
+
borderColor: error
|
|
53
|
+
? colors.destructive
|
|
54
|
+
: focused
|
|
55
|
+
? (colors.ring ?? colors.primary)
|
|
56
|
+
: colors.border,
|
|
28
57
|
backgroundColor: colors.background,
|
|
29
58
|
},
|
|
30
|
-
style,
|
|
31
59
|
]}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
>
|
|
61
|
+
{prefix ? (
|
|
62
|
+
typeof prefix === 'string' ? (
|
|
63
|
+
<Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
|
|
64
|
+
{prefix}
|
|
65
|
+
</Text>
|
|
66
|
+
) : (
|
|
67
|
+
<View style={styles.prefixContainer}>{prefix}</View>
|
|
68
|
+
)
|
|
69
|
+
) : null}
|
|
70
|
+
<TextInput
|
|
71
|
+
style={[
|
|
72
|
+
styles.input,
|
|
73
|
+
{
|
|
74
|
+
color: colors.foreground,
|
|
75
|
+
},
|
|
76
|
+
webInputResetStyle,
|
|
77
|
+
style,
|
|
78
|
+
]}
|
|
79
|
+
onFocus={(e) => {
|
|
80
|
+
setFocused(true)
|
|
81
|
+
onFocus?.(e)
|
|
82
|
+
}}
|
|
83
|
+
onBlur={(e) => {
|
|
84
|
+
setFocused(false)
|
|
85
|
+
onBlur?.(e)
|
|
86
|
+
}}
|
|
87
|
+
placeholderTextColor={colors.mutedForeground}
|
|
88
|
+
allowFontScaling={true}
|
|
89
|
+
secureTextEntry={effectiveSecure}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
{effectiveSuffix ? (
|
|
93
|
+
typeof effectiveSuffix === 'string' ? (
|
|
94
|
+
<Text style={[styles.suffixText, { color: colors.mutedForeground }, suffixStyle]} allowFontScaling={true}>
|
|
95
|
+
{effectiveSuffix}
|
|
96
|
+
</Text>
|
|
97
|
+
) : (
|
|
98
|
+
<View style={styles.suffixContainer}>{effectiveSuffix}</View>
|
|
99
|
+
)
|
|
100
|
+
) : null}
|
|
101
|
+
</View>
|
|
44
102
|
{error ? (
|
|
45
103
|
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
46
104
|
) : null}
|
|
@@ -60,12 +118,35 @@ const styles = StyleSheet.create({
|
|
|
60
118
|
fontWeight: '500',
|
|
61
119
|
marginBottom: 6,
|
|
62
120
|
},
|
|
63
|
-
|
|
121
|
+
inputWrapper: {
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
alignItems: 'center',
|
|
64
124
|
borderWidth: 1.5,
|
|
65
125
|
borderRadius: 14,
|
|
66
126
|
paddingHorizontal: 20,
|
|
67
127
|
paddingVertical: 16,
|
|
128
|
+
},
|
|
129
|
+
input: {
|
|
130
|
+
flex: 1,
|
|
68
131
|
fontSize: 17,
|
|
132
|
+
paddingVertical: 0,
|
|
133
|
+
},
|
|
134
|
+
prefixContainer: {
|
|
135
|
+
marginRight: 8,
|
|
136
|
+
},
|
|
137
|
+
prefixText: {
|
|
138
|
+
fontSize: 17,
|
|
139
|
+
marginRight: 8,
|
|
140
|
+
},
|
|
141
|
+
suffixContainer: {
|
|
142
|
+
marginLeft: 8,
|
|
143
|
+
},
|
|
144
|
+
suffixText: {
|
|
145
|
+
fontSize: 17,
|
|
146
|
+
marginLeft: 8,
|
|
147
|
+
},
|
|
148
|
+
passwordToggle: {
|
|
149
|
+
padding: 4,
|
|
69
150
|
},
|
|
70
151
|
helperText: {
|
|
71
152
|
fontSize: 13,
|
|
@@ -49,7 +49,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
|
|
|
49
49
|
activeOpacity={0.6}
|
|
50
50
|
touchSoundDisabled={true}
|
|
51
51
|
>
|
|
52
|
-
<Text style={[styles.arrowText, { color: colors.foreground }]}>‹</Text>
|
|
52
|
+
<Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>‹</Text>
|
|
53
53
|
</TouchableOpacity>
|
|
54
54
|
<Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
|
|
55
55
|
{MONTH_NAMES[value.month - 1]} {value.year}
|
|
@@ -60,7 +60,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
|
|
|
60
60
|
activeOpacity={0.6}
|
|
61
61
|
touchSoundDisabled={true}
|
|
62
62
|
>
|
|
63
|
-
<Text style={[styles.arrowText, { color: colors.foreground }]}>›</Text>
|
|
63
|
+
<Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>›</Text>
|
|
64
64
|
</TouchableOpacity>
|
|
65
65
|
</View>
|
|
66
66
|
)
|