@retray-dev/ui-kit 1.6.0 → 1.8.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 +264 -15
- package/README.md +7 -6
- package/dist/index.d.mts +114 -11
- package/dist/index.d.ts +114 -11
- package/dist/index.js +660 -134
- package/dist/index.mjs +656 -138
- package/package.json +8 -8
- package/src/components/Accordion/Accordion.tsx +4 -4
- package/src/components/Alert/Alert.tsx +32 -8
- package/src/components/Alert/index.ts +2 -2
- package/src/components/Avatar/Avatar.tsx +8 -8
- package/src/components/Badge/Badge.tsx +4 -4
- package/src/components/Button/Button.tsx +21 -14
- package/src/components/Card/Card.tsx +9 -9
- package/src/components/Checkbox/Checkbox.tsx +8 -8
- package/src/components/Chip/Chip.tsx +142 -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 +48 -0
- package/src/components/CurrencyDisplay/index.ts +1 -0
- package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +66 -0
- package/src/components/CurrencyInputLarge/index.ts +1 -0
- package/src/components/EmptyState/EmptyState.tsx +40 -6
- package/src/components/Input/Input.tsx +8 -8
- 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 +19 -19
- package/src/components/Switch/Switch.tsx +12 -7
- package/src/components/Tabs/Tabs.tsx +34 -15
- package/src/components/Text/Text.tsx +6 -6
- package/src/components/Textarea/Textarea.tsx +9 -9
- package/src/components/Toast/Toast.tsx +25 -7
- package/src/components/Toggle/Toggle.tsx +93 -24
- package/src/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Personal UI Kit for React Native / Expo",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"fast-xml-parser": "^5.5.7",
|
|
53
53
|
"react": "19.1.0",
|
|
54
54
|
"react-native": "0.81.5",
|
|
55
|
-
"react-native-worklets": "0.
|
|
55
|
+
"react-native-worklets": "0.5.1"
|
|
56
56
|
},
|
|
57
57
|
"onlyBuiltDependencies": [
|
|
58
58
|
"esbuild"
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"expo-haptics": "~15.0.8",
|
|
65
|
-
"expo-linear-gradient": "~
|
|
66
|
-
"react": "
|
|
67
|
-
"react-native": "0.
|
|
68
|
-
"react-native-gesture-handler": "~2.
|
|
69
|
-
"react-native-reanimated": "~4.
|
|
70
|
-
"react-native-worklets": "~0.
|
|
65
|
+
"expo-linear-gradient": "~15.0.8",
|
|
66
|
+
"react": "19.1.0",
|
|
67
|
+
"react-native": "0.81.5",
|
|
68
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
69
|
+
"react-native-reanimated": "~4.1.1",
|
|
70
|
+
"react-native-worklets": "~0.5.1",
|
|
71
71
|
"react-native-safe-area-context": "~5.6.2",
|
|
72
72
|
"eslint": "^9.0.0",
|
|
73
73
|
"@eslint/js": "^9.0.0",
|
|
@@ -154,19 +154,19 @@ const styles = StyleSheet.create({
|
|
|
154
154
|
flexDirection: 'row',
|
|
155
155
|
justifyContent: 'space-between',
|
|
156
156
|
alignItems: 'center',
|
|
157
|
-
paddingVertical:
|
|
157
|
+
paddingVertical: 20,
|
|
158
158
|
},
|
|
159
159
|
triggerText: {
|
|
160
|
-
fontSize:
|
|
160
|
+
fontSize: 17,
|
|
161
161
|
fontWeight: '500',
|
|
162
162
|
flex: 1,
|
|
163
163
|
},
|
|
164
164
|
chevron: {
|
|
165
|
-
fontSize:
|
|
165
|
+
fontSize: 18,
|
|
166
166
|
marginLeft: 8,
|
|
167
167
|
},
|
|
168
168
|
content: {
|
|
169
|
-
paddingBottom:
|
|
169
|
+
paddingBottom: 20,
|
|
170
170
|
position: 'absolute',
|
|
171
171
|
width: '100%',
|
|
172
172
|
},
|
|
@@ -2,26 +2,46 @@ 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]}>
|
|
24
|
-
{icon ?
|
|
38
|
+
{icon ? (
|
|
39
|
+
<View style={styles.icon}>{icon}</View>
|
|
40
|
+
) : (
|
|
41
|
+
<View style={styles.icon}>
|
|
42
|
+
<Text style={[styles.defaultIcon, { color: titleColor }]}>{defaultIcon}</Text>
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
25
45
|
<View style={styles.content}>
|
|
26
46
|
{title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
|
|
27
47
|
{description ? (
|
|
@@ -56,4 +76,8 @@ const styles = StyleSheet.create({
|
|
|
56
76
|
fontSize: 14,
|
|
57
77
|
lineHeight: 20,
|
|
58
78
|
},
|
|
79
|
+
defaultIcon: {
|
|
80
|
+
fontSize: 18,
|
|
81
|
+
fontWeight: '700',
|
|
82
|
+
},
|
|
59
83
|
})
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { AlertBanner } from './Alert'
|
|
2
|
+
export type { AlertBannerProps, AlertBannerVariant } from './Alert'
|
|
@@ -14,17 +14,17 @@ export interface AvatarProps {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const sizeMap: Record<AvatarSize, number> = {
|
|
17
|
-
sm:
|
|
18
|
-
md:
|
|
19
|
-
lg:
|
|
20
|
-
xl:
|
|
17
|
+
sm: 28,
|
|
18
|
+
md: 40,
|
|
19
|
+
lg: 56,
|
|
20
|
+
xl: 72,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const fontSizeMap: Record<AvatarSize, number> = {
|
|
24
|
-
sm:
|
|
25
|
-
md:
|
|
26
|
-
lg:
|
|
27
|
-
xl:
|
|
24
|
+
sm: 12,
|
|
25
|
+
md: 16,
|
|
26
|
+
lg: 22,
|
|
27
|
+
xl: 28,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
|
|
@@ -36,13 +36,13 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
|
|
|
36
36
|
|
|
37
37
|
const styles = StyleSheet.create({
|
|
38
38
|
container: {
|
|
39
|
-
borderRadius:
|
|
40
|
-
paddingHorizontal:
|
|
41
|
-
paddingVertical:
|
|
39
|
+
borderRadius: 8,
|
|
40
|
+
paddingHorizontal: 10,
|
|
41
|
+
paddingVertical: 4,
|
|
42
42
|
alignSelf: 'flex-start',
|
|
43
43
|
},
|
|
44
44
|
label: {
|
|
45
|
-
fontSize:
|
|
45
|
+
fontSize: 13,
|
|
46
46
|
fontWeight: '500',
|
|
47
47
|
},
|
|
48
48
|
})
|
|
@@ -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 {
|
|
@@ -28,21 +31,21 @@ export interface ButtonProps extends TouchableOpacityProps {
|
|
|
28
31
|
/** Replaces the label with a spinner and forces `disabled`. */
|
|
29
32
|
loading?: boolean
|
|
30
33
|
fullWidth?: boolean
|
|
31
|
-
/** Icon rendered alongside the label. */
|
|
32
|
-
icon?: React.ReactNode
|
|
34
|
+
/** Icon rendered alongside the label. Can be a ReactNode or a render function `(props) => ReactNode`. */
|
|
35
|
+
icon?: React.ReactNode | ((props: { label: string; size: ButtonSize; variant: ButtonVariant }) => React.ReactNode)
|
|
33
36
|
/** Side the icon appears on. Defaults to `'left'`. */
|
|
34
37
|
iconPosition?: 'left' | 'right'
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
38
|
-
sm: { paddingHorizontal:
|
|
39
|
-
md: { paddingHorizontal:
|
|
40
|
-
lg: { paddingHorizontal:
|
|
41
|
+
sm: { paddingHorizontal: 20, paddingVertical: 12 },
|
|
42
|
+
md: { paddingHorizontal: 24, paddingVertical: 16 },
|
|
43
|
+
lg: { paddingHorizontal: 32, paddingVertical: 20 },
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
44
|
-
sm: { fontSize:
|
|
45
|
-
md: { fontSize:
|
|
47
|
+
sm: { fontSize: 15 },
|
|
48
|
+
md: { fontSize: 17 },
|
|
46
49
|
lg: { fontSize: 18 },
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -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 }] }]}>
|
|
@@ -122,9 +129,9 @@ export function Button({
|
|
|
122
129
|
<ActivityIndicator size="small" color={spinnerColor} />
|
|
123
130
|
) : (
|
|
124
131
|
<>
|
|
125
|
-
{icon && iconPosition === 'left' && <>{icon}</>}
|
|
132
|
+
{icon && iconPosition === 'left' && <>{typeof icon === 'function' ? icon({ label, size, variant }) : icon}</>}
|
|
126
133
|
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], icon ? styles.labelWithIcon : undefined]}>{label}</Text>
|
|
127
|
-
{icon && iconPosition === 'right' && <>{icon}</>}
|
|
134
|
+
{icon && iconPosition === 'right' && <>{typeof icon === 'function' ? icon({ label, size, variant }) : icon}</>}
|
|
128
135
|
</>
|
|
129
136
|
)}
|
|
130
137
|
</TouchableOpacity>
|
|
@@ -134,7 +141,7 @@ export function Button({
|
|
|
134
141
|
|
|
135
142
|
const styles = StyleSheet.create({
|
|
136
143
|
base: {
|
|
137
|
-
borderRadius:
|
|
144
|
+
borderRadius: 999,
|
|
138
145
|
alignItems: 'center',
|
|
139
146
|
justifyContent: 'center',
|
|
140
147
|
flexDirection: 'row',
|
|
@@ -69,7 +69,7 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
69
69
|
|
|
70
70
|
const styles = StyleSheet.create({
|
|
71
71
|
card: {
|
|
72
|
-
borderRadius:
|
|
72
|
+
borderRadius: 20,
|
|
73
73
|
borderWidth: 1,
|
|
74
74
|
shadowColor: '#000',
|
|
75
75
|
shadowOffset: { width: 0, height: 1 },
|
|
@@ -78,24 +78,24 @@ const styles = StyleSheet.create({
|
|
|
78
78
|
elevation: 1,
|
|
79
79
|
},
|
|
80
80
|
header: {
|
|
81
|
-
padding:
|
|
81
|
+
padding: 28,
|
|
82
82
|
paddingBottom: 0,
|
|
83
|
-
gap:
|
|
83
|
+
gap: 8,
|
|
84
84
|
},
|
|
85
85
|
title: {
|
|
86
|
-
fontSize:
|
|
86
|
+
fontSize: 20,
|
|
87
87
|
fontWeight: '600',
|
|
88
|
-
lineHeight:
|
|
88
|
+
lineHeight: 28,
|
|
89
89
|
},
|
|
90
90
|
description: {
|
|
91
|
-
fontSize:
|
|
92
|
-
lineHeight:
|
|
91
|
+
fontSize: 15,
|
|
92
|
+
lineHeight: 22,
|
|
93
93
|
},
|
|
94
94
|
content: {
|
|
95
|
-
padding:
|
|
95
|
+
padding: 28,
|
|
96
96
|
},
|
|
97
97
|
footer: {
|
|
98
|
-
padding:
|
|
98
|
+
padding: 28,
|
|
99
99
|
paddingTop: 0,
|
|
100
100
|
flexDirection: 'row',
|
|
101
101
|
alignItems: 'center',
|
|
@@ -74,25 +74,25 @@ const styles = StyleSheet.create({
|
|
|
74
74
|
row: {
|
|
75
75
|
flexDirection: 'row',
|
|
76
76
|
alignItems: 'center',
|
|
77
|
-
gap:
|
|
77
|
+
gap: 12,
|
|
78
78
|
},
|
|
79
79
|
box: {
|
|
80
|
-
width:
|
|
81
|
-
height:
|
|
82
|
-
borderRadius:
|
|
80
|
+
width: 28,
|
|
81
|
+
height: 28,
|
|
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: 15,
|
|
89
|
+
height: 9,
|
|
90
90
|
borderLeftWidth: 2,
|
|
91
91
|
borderBottomWidth: 2,
|
|
92
92
|
transform: [{ rotate: '-45deg' }, { translateY: -1 }],
|
|
93
93
|
},
|
|
94
94
|
label: {
|
|
95
|
-
fontSize:
|
|
96
|
-
lineHeight:
|
|
95
|
+
fontSize: 15,
|
|
96
|
+
lineHeight: 22,
|
|
97
97
|
},
|
|
98
98
|
})
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
|
32
|
+
onValueChange?: (value: string | number) => void
|
|
33
|
+
style?: ViewStyle
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Chip({ label, selected = false, onPress, style }: ChipProps) {
|
|
37
|
+
const { colors } = useTheme()
|
|
38
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
39
|
+
const pressAnim = useRef(new Animated.Value(selected ? 1 : 0)).current
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
Animated.timing(pressAnim, {
|
|
43
|
+
toValue: selected ? 1 : 0,
|
|
44
|
+
duration: 150,
|
|
45
|
+
easing: Easing.out(Easing.ease),
|
|
46
|
+
useNativeDriver: false,
|
|
47
|
+
}).start()
|
|
48
|
+
}, [selected, pressAnim])
|
|
49
|
+
|
|
50
|
+
const handlePressIn = () => {
|
|
51
|
+
Animated.spring(scale, {
|
|
52
|
+
toValue: 0.95,
|
|
53
|
+
useNativeDriver: nativeDriver,
|
|
54
|
+
speed: 40,
|
|
55
|
+
bounciness: 0,
|
|
56
|
+
}).start()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handlePressOut = () => {
|
|
60
|
+
Animated.spring(scale, {
|
|
61
|
+
toValue: 1,
|
|
62
|
+
useNativeDriver: nativeDriver,
|
|
63
|
+
speed: 40,
|
|
64
|
+
bounciness: 4,
|
|
65
|
+
}).start()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handlePress = () => {
|
|
69
|
+
Haptics.selectionAsync()
|
|
70
|
+
onPress?.()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const backgroundColor = pressAnim.interpolate({
|
|
74
|
+
inputRange: [0, 1],
|
|
75
|
+
outputRange: [colors.secondary, colors.primary],
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const textColor = pressAnim.interpolate({
|
|
79
|
+
inputRange: [0, 1],
|
|
80
|
+
outputRange: [colors.foreground, colors.primaryForeground],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const borderColor = pressAnim.interpolate({
|
|
84
|
+
inputRange: [0, 1],
|
|
85
|
+
outputRange: [colors.border, colors.primary],
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Animated.View style={[styles.wrapper, { transform: [{ scale }] }, style]}>
|
|
90
|
+
<TouchableOpacity
|
|
91
|
+
onPress={handlePress}
|
|
92
|
+
onPressIn={handlePressIn}
|
|
93
|
+
onPressOut={handlePressOut}
|
|
94
|
+
activeOpacity={1}
|
|
95
|
+
touchSoundDisabled={true}
|
|
96
|
+
>
|
|
97
|
+
<Animated.View style={[styles.chip, { backgroundColor, borderColor }]}>
|
|
98
|
+
<Animated.Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>
|
|
99
|
+
{label}
|
|
100
|
+
</Animated.Text>
|
|
101
|
+
</Animated.View>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
</Animated.View>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ChipGroup({ options, value, onValueChange, style }: ChipGroupProps) {
|
|
108
|
+
return (
|
|
109
|
+
<View style={[styles.group, style]}>
|
|
110
|
+
{options.map((opt) => (
|
|
111
|
+
<Chip
|
|
112
|
+
key={opt.value}
|
|
113
|
+
label={opt.label}
|
|
114
|
+
selected={opt.value === value}
|
|
115
|
+
onPress={() => onValueChange?.(opt.value)}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
</View>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const styles = StyleSheet.create({
|
|
123
|
+
wrapper: {},
|
|
124
|
+
chip: {
|
|
125
|
+
borderRadius: 999,
|
|
126
|
+
paddingHorizontal: 14,
|
|
127
|
+
paddingVertical: 8,
|
|
128
|
+
borderWidth: 1.5,
|
|
129
|
+
alignItems: 'center',
|
|
130
|
+
justifyContent: 'center',
|
|
131
|
+
},
|
|
132
|
+
label: {
|
|
133
|
+
fontSize: 14,
|
|
134
|
+
fontWeight: '500',
|
|
135
|
+
lineHeight: 20,
|
|
136
|
+
},
|
|
137
|
+
group: {
|
|
138
|
+
flexDirection: 'row',
|
|
139
|
+
flexWrap: 'wrap',
|
|
140
|
+
gap: 8,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Modal, View, Text, StyleSheet, TouchableOpacity } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
import { Button } from '../Button'
|
|
5
|
+
|
|
6
|
+
export interface ConfirmDialogProps {
|
|
7
|
+
visible: boolean
|
|
8
|
+
title: string
|
|
9
|
+
description?: string
|
|
10
|
+
confirmLabel?: string
|
|
11
|
+
cancelLabel?: string
|
|
12
|
+
confirmVariant?: 'primary' | 'destructive'
|
|
13
|
+
onConfirm: () => void
|
|
14
|
+
onCancel: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ConfirmDialog({
|
|
18
|
+
visible,
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
confirmLabel = 'Confirm',
|
|
22
|
+
cancelLabel = 'Cancel',
|
|
23
|
+
confirmVariant = 'primary',
|
|
24
|
+
onConfirm,
|
|
25
|
+
onCancel,
|
|
26
|
+
}: ConfirmDialogProps) {
|
|
27
|
+
const { colors } = useTheme()
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
|
|
31
|
+
<TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={onCancel}>
|
|
32
|
+
<View
|
|
33
|
+
style={[styles.dialog, { backgroundColor: colors.card }]}
|
|
34
|
+
onStartShouldSetResponder={() => true}
|
|
35
|
+
>
|
|
36
|
+
<Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>
|
|
37
|
+
{title}
|
|
38
|
+
</Text>
|
|
39
|
+
{description ? (
|
|
40
|
+
<Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
|
|
41
|
+
{description}
|
|
42
|
+
</Text>
|
|
43
|
+
) : null}
|
|
44
|
+
<View style={styles.actions}>
|
|
45
|
+
<Button label={cancelLabel} variant="outline" fullWidth onPress={onCancel} />
|
|
46
|
+
<Button label={confirmLabel} variant={confirmVariant} fullWidth onPress={onConfirm} />
|
|
47
|
+
</View>
|
|
48
|
+
</View>
|
|
49
|
+
</TouchableOpacity>
|
|
50
|
+
</Modal>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
overlay: {
|
|
56
|
+
flex: 1,
|
|
57
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
padding: 24,
|
|
61
|
+
},
|
|
62
|
+
dialog: {
|
|
63
|
+
width: '100%',
|
|
64
|
+
maxWidth: 400,
|
|
65
|
+
borderRadius: 16,
|
|
66
|
+
padding: 24,
|
|
67
|
+
gap: 12,
|
|
68
|
+
shadowColor: '#000',
|
|
69
|
+
shadowOffset: { width: 0, height: 8 },
|
|
70
|
+
shadowOpacity: 0.15,
|
|
71
|
+
shadowRadius: 16,
|
|
72
|
+
elevation: 8,
|
|
73
|
+
},
|
|
74
|
+
title: {
|
|
75
|
+
fontSize: 18,
|
|
76
|
+
fontWeight: '600',
|
|
77
|
+
lineHeight: 26,
|
|
78
|
+
},
|
|
79
|
+
description: {
|
|
80
|
+
fontSize: 15,
|
|
81
|
+
lineHeight: 22,
|
|
82
|
+
},
|
|
83
|
+
actions: {
|
|
84
|
+
gap: 10,
|
|
85
|
+
marginTop: 8,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
|
|
5
|
+
export interface CurrencyDisplayProps {
|
|
6
|
+
value: number | string
|
|
7
|
+
/** Symbol prepended to the formatted value. Defaults to `'$'`. */
|
|
8
|
+
prefix?: string
|
|
9
|
+
/** When true, shows two decimal places separated by a comma (e.g. `$25.000,00`). Defaults to `false`. */
|
|
10
|
+
showDecimals?: boolean
|
|
11
|
+
/** Override the color of the formatted text. Defaults to the `foreground` theme token. */
|
|
12
|
+
textColor?: string
|
|
13
|
+
style?: ViewStyle
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatValue(value: number | string, prefix: string, showDecimals: boolean): string {
|
|
17
|
+
const num = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.-]/g, '')) : value
|
|
18
|
+
if (isNaN(num)) return `${prefix}0`
|
|
19
|
+
const abs = Math.abs(num)
|
|
20
|
+
const sign = num < 0 ? '-' : ''
|
|
21
|
+
const intPart = Math.floor(abs).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.')
|
|
22
|
+
if (showDecimals) {
|
|
23
|
+
const decStr = (abs % 1).toFixed(2).slice(2)
|
|
24
|
+
return `${sign}${prefix}${intPart},${decStr}`
|
|
25
|
+
}
|
|
26
|
+
return `${sign}${prefix}${intPart}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, textColor, style }: CurrencyDisplayProps) {
|
|
30
|
+
const { colors } = useTheme()
|
|
31
|
+
const formatted = formatValue(value, prefix, showDecimals)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View style={[styles.container, style]}>
|
|
35
|
+
<Text style={[styles.amount, { color: textColor ?? colors.foreground }]} allowFontScaling={true}>
|
|
36
|
+
{formatted}
|
|
37
|
+
</Text>
|
|
38
|
+
</View>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
container: {},
|
|
44
|
+
amount: {
|
|
45
|
+
fontSize: 56,
|
|
46
|
+
fontWeight: '700',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CurrencyDisplay'
|