@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.
Files changed (39) hide show
  1. package/COMPONENTS.md +264 -15
  2. package/README.md +7 -6
  3. package/dist/index.d.mts +114 -11
  4. package/dist/index.d.ts +114 -11
  5. package/dist/index.js +660 -134
  6. package/dist/index.mjs +656 -138
  7. package/package.json +8 -8
  8. package/src/components/Accordion/Accordion.tsx +4 -4
  9. package/src/components/Alert/Alert.tsx +32 -8
  10. package/src/components/Alert/index.ts +2 -2
  11. package/src/components/Avatar/Avatar.tsx +8 -8
  12. package/src/components/Badge/Badge.tsx +4 -4
  13. package/src/components/Button/Button.tsx +21 -14
  14. package/src/components/Card/Card.tsx +9 -9
  15. package/src/components/Checkbox/Checkbox.tsx +8 -8
  16. package/src/components/Chip/Chip.tsx +142 -0
  17. package/src/components/Chip/index.ts +2 -0
  18. package/src/components/ConfirmDialog/ConfirmDialog.tsx +87 -0
  19. package/src/components/ConfirmDialog/index.ts +2 -0
  20. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +48 -0
  21. package/src/components/CurrencyDisplay/index.ts +1 -0
  22. package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +66 -0
  23. package/src/components/CurrencyInputLarge/index.ts +1 -0
  24. package/src/components/EmptyState/EmptyState.tsx +40 -6
  25. package/src/components/Input/Input.tsx +8 -8
  26. package/src/components/LabelValue/LabelValue.tsx +47 -0
  27. package/src/components/LabelValue/index.ts +2 -0
  28. package/src/components/ListItem/ListItem.tsx +121 -0
  29. package/src/components/ListItem/index.ts +2 -0
  30. package/src/components/MonthPicker/MonthPicker.tsx +92 -0
  31. package/src/components/MonthPicker/index.ts +2 -0
  32. package/src/components/Select/Select.tsx +19 -19
  33. package/src/components/Switch/Switch.tsx +12 -7
  34. package/src/components/Tabs/Tabs.tsx +34 -15
  35. package/src/components/Text/Text.tsx +6 -6
  36. package/src/components/Textarea/Textarea.tsx +9 -9
  37. package/src/components/Toast/Toast.tsx +25 -7
  38. package/src/components/Toggle/Toggle.tsx +93 -24
  39. 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.6.0",
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.8.1"
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": "~14.1.5",
66
- "react": "18.2.0",
67
- "react-native": "0.74.0",
68
- "react-native-gesture-handler": "~2.30.0",
69
- "react-native-reanimated": "~4.3.0",
70
- "react-native-worklets": "~0.8.1",
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: 16,
157
+ paddingVertical: 20,
158
158
  },
159
159
  triggerText: {
160
- fontSize: 15,
160
+ fontSize: 17,
161
161
  fontWeight: '500',
162
162
  flex: 1,
163
163
  },
164
164
  chevron: {
165
- fontSize: 16,
165
+ fontSize: 18,
166
166
  marginLeft: 8,
167
167
  },
168
168
  content: {
169
- paddingBottom: 16,
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 AlertVariant = 'default' | 'destructive'
5
+ export type AlertBannerVariant = 'default' | 'destructive' | 'success'
6
6
 
7
- export interface AlertProps {
7
+ export interface AlertBannerProps {
8
8
  title?: string
9
9
  description?: string
10
- variant?: AlertVariant
10
+ variant?: AlertBannerVariant
11
11
  icon?: React.ReactNode
12
12
  style?: ViewStyle
13
13
  }
14
14
 
15
- export function Alert({ title, description, variant = 'default', icon, style }: AlertProps) {
15
+ export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
16
16
  const { colors } = useTheme()
17
17
 
18
- const borderColor = variant === 'destructive' ? colors.destructive : colors.border
19
- const titleColor = variant === 'destructive' ? colors.destructive : colors.foreground
20
- const descColor = variant === 'destructive' ? colors.destructive : colors.mutedForeground
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 ? <View style={styles.icon}>{icon}</View> : null}
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 { Alert } from './Alert'
2
- export type { AlertProps, AlertVariant } from './Alert'
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: 24,
18
- md: 32,
19
- lg: 48,
20
- xl: 64,
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: 10,
25
- md: 13,
26
- lg: 18,
27
- xl: 24,
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: 6,
40
- paddingHorizontal: 8,
41
- paddingVertical: 2,
39
+ borderRadius: 8,
40
+ paddingHorizontal: 10,
41
+ paddingVertical: 4,
42
42
  alignSelf: 'flex-start',
43
43
  },
44
44
  label: {
45
- fontSize: 12,
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: 16, paddingVertical: 10 },
39
- md: { paddingHorizontal: 20, paddingVertical: 14 },
40
- lg: { paddingHorizontal: 28, paddingVertical: 18 },
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: 14 },
45
- md: { fontSize: 16 },
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: true,
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: true, speed: 40, bounciness: 4 }).start()
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 === 'primary' || variant === 'secondary' ? colors.primaryForeground : colors.foreground
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: 8,
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: 12,
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: 24,
81
+ padding: 28,
82
82
  paddingBottom: 0,
83
- gap: 6,
83
+ gap: 8,
84
84
  },
85
85
  title: {
86
- fontSize: 18,
86
+ fontSize: 20,
87
87
  fontWeight: '600',
88
- lineHeight: 24,
88
+ lineHeight: 28,
89
89
  },
90
90
  description: {
91
- fontSize: 14,
92
- lineHeight: 20,
91
+ fontSize: 15,
92
+ lineHeight: 22,
93
93
  },
94
94
  content: {
95
- padding: 24,
95
+ padding: 28,
96
96
  },
97
97
  footer: {
98
- padding: 24,
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: 10,
77
+ gap: 12,
78
78
  },
79
79
  box: {
80
- width: 24,
81
- height: 24,
82
- borderRadius: 6,
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: 13,
89
- height: 8,
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: 14,
96
- lineHeight: 20,
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,2 @@
1
+ export { Chip, ChipGroup } from './Chip'
2
+ export type { ChipProps, ChipOption, ChipGroupProps } from './Chip'
@@ -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,2 @@
1
+ export { ConfirmDialog } from './ConfirmDialog'
2
+ export type { ConfirmDialogProps } from './ConfirmDialog'
@@ -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'