@retray-dev/ui-kit 1.7.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 +181 -34
- package/README.md +6 -5
- package/dist/index.d.mts +73 -8
- package/dist/index.d.ts +73 -8
- package/dist/index.js +385 -16
- package/dist/index.mjs +380 -17
- package/package.json +1 -1
- package/src/components/Alert/Alert.tsx +22 -10
- package/src/components/Alert/index.ts +2 -2
- package/src/components/Button/Button.tsx +11 -4
- 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 +4 -2
- package/src/components/EmptyState/EmptyState.tsx +40 -6
- 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/Switch/Switch.tsx +4 -2
- package/src/index.ts +5 -0
|
@@ -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
|
+
})
|
|
@@ -8,6 +8,8 @@ export interface CurrencyDisplayProps {
|
|
|
8
8
|
prefix?: string
|
|
9
9
|
/** When true, shows two decimal places separated by a comma (e.g. `$25.000,00`). Defaults to `false`. */
|
|
10
10
|
showDecimals?: boolean
|
|
11
|
+
/** Override the color of the formatted text. Defaults to the `foreground` theme token. */
|
|
12
|
+
textColor?: string
|
|
11
13
|
style?: ViewStyle
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -24,13 +26,13 @@ function formatValue(value: number | string, prefix: string, showDecimals: boole
|
|
|
24
26
|
return `${sign}${prefix}${intPart}`
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, style }: CurrencyDisplayProps) {
|
|
29
|
+
export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, textColor, style }: CurrencyDisplayProps) {
|
|
28
30
|
const { colors } = useTheme()
|
|
29
31
|
const formatted = formatValue(value, prefix, showDecimals)
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<View style={[styles.container, style]}>
|
|
33
|
-
<Text style={[styles.amount, { color: colors.foreground }]} allowFontScaling={true}>
|
|
35
|
+
<Text style={[styles.amount, { color: textColor ?? colors.foreground }]} allowFontScaling={true}>
|
|
34
36
|
{formatted}
|
|
35
37
|
</Text>
|
|
36
38
|
</View>
|
|
@@ -7,24 +7,46 @@ export interface EmptyStateProps {
|
|
|
7
7
|
title: string
|
|
8
8
|
description?: string
|
|
9
9
|
action?: React.ReactNode
|
|
10
|
+
/** `compact` hides description/action and uses tighter spacing and a smaller icon. */
|
|
11
|
+
size?: 'default' | 'compact'
|
|
10
12
|
style?: ViewStyle
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function EmptyState({ icon, title, description, action, style }: EmptyStateProps) {
|
|
15
|
+
export function EmptyState({ icon, title, description, action, size = 'default', style }: EmptyStateProps) {
|
|
14
16
|
const { colors } = useTheme()
|
|
17
|
+
const isCompact = size === 'compact'
|
|
15
18
|
|
|
16
19
|
return (
|
|
17
|
-
<View
|
|
20
|
+
<View
|
|
21
|
+
style={[
|
|
22
|
+
styles.container,
|
|
23
|
+
isCompact && styles.containerCompact,
|
|
24
|
+
{ borderColor: colors.border },
|
|
25
|
+
style,
|
|
26
|
+
]}
|
|
27
|
+
>
|
|
18
28
|
{icon ? (
|
|
19
|
-
<View
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
styles.iconWrapper,
|
|
32
|
+
isCompact && styles.iconWrapperCompact,
|
|
33
|
+
{ backgroundColor: colors.muted },
|
|
34
|
+
]}
|
|
35
|
+
>
|
|
36
|
+
{icon}
|
|
37
|
+
</View>
|
|
20
38
|
) : null}
|
|
21
39
|
<View style={styles.textWrapper}>
|
|
22
|
-
<Text
|
|
23
|
-
|
|
40
|
+
<Text
|
|
41
|
+
style={[styles.title, isCompact && styles.titleCompact, { color: colors.foreground }]}
|
|
42
|
+
>
|
|
43
|
+
{title}
|
|
44
|
+
</Text>
|
|
45
|
+
{description && !isCompact ? (
|
|
24
46
|
<Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
|
|
25
47
|
) : null}
|
|
26
48
|
</View>
|
|
27
|
-
{action ? <View style={styles.action}>{action}</View> : null}
|
|
49
|
+
{action && !isCompact ? <View style={styles.action}>{action}</View> : null}
|
|
28
50
|
</View>
|
|
29
51
|
)
|
|
30
52
|
}
|
|
@@ -39,6 +61,10 @@ const styles = StyleSheet.create({
|
|
|
39
61
|
padding: 32,
|
|
40
62
|
gap: 16,
|
|
41
63
|
},
|
|
64
|
+
containerCompact: {
|
|
65
|
+
padding: 20,
|
|
66
|
+
gap: 10,
|
|
67
|
+
},
|
|
42
68
|
iconWrapper: {
|
|
43
69
|
width: 48,
|
|
44
70
|
height: 48,
|
|
@@ -46,6 +72,11 @@ const styles = StyleSheet.create({
|
|
|
46
72
|
alignItems: 'center',
|
|
47
73
|
justifyContent: 'center',
|
|
48
74
|
},
|
|
75
|
+
iconWrapperCompact: {
|
|
76
|
+
width: 36,
|
|
77
|
+
height: 36,
|
|
78
|
+
borderRadius: 8,
|
|
79
|
+
},
|
|
49
80
|
textWrapper: {
|
|
50
81
|
alignItems: 'center',
|
|
51
82
|
gap: 8,
|
|
@@ -56,6 +87,9 @@ const styles = StyleSheet.create({
|
|
|
56
87
|
fontWeight: '500',
|
|
57
88
|
textAlign: 'center',
|
|
58
89
|
},
|
|
90
|
+
titleCompact: {
|
|
91
|
+
fontSize: 15,
|
|
92
|
+
},
|
|
59
93
|
description: {
|
|
60
94
|
fontSize: 14,
|
|
61
95
|
lineHeight: 20,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
|
|
5
|
+
export interface LabelValueProps {
|
|
6
|
+
label: string
|
|
7
|
+
value: string | React.ReactNode
|
|
8
|
+
style?: ViewStyle
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function LabelValue({ label, value, style }: LabelValueProps) {
|
|
12
|
+
const { colors } = useTheme()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<View style={[styles.container, style]}>
|
|
16
|
+
<Text style={[styles.label, { color: colors.mutedForeground }]} allowFontScaling={true}>
|
|
17
|
+
{label}
|
|
18
|
+
</Text>
|
|
19
|
+
{typeof value === 'string' ? (
|
|
20
|
+
<Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
|
|
21
|
+
{value}
|
|
22
|
+
</Text>
|
|
23
|
+
) : (
|
|
24
|
+
value
|
|
25
|
+
)}
|
|
26
|
+
</View>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
flexDirection: 'row',
|
|
33
|
+
justifyContent: 'space-between',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: 12,
|
|
36
|
+
},
|
|
37
|
+
label: {
|
|
38
|
+
fontSize: 13,
|
|
39
|
+
lineHeight: 18,
|
|
40
|
+
},
|
|
41
|
+
value: {
|
|
42
|
+
fontSize: 15,
|
|
43
|
+
fontWeight: '500',
|
|
44
|
+
lineHeight: 22,
|
|
45
|
+
textAlign: 'right',
|
|
46
|
+
},
|
|
47
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Animated,
|
|
5
|
+
View,
|
|
6
|
+
Text,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
Platform,
|
|
10
|
+
} from 'react-native'
|
|
11
|
+
import * as Haptics from 'expo-haptics'
|
|
12
|
+
import { useTheme } from '../../theme'
|
|
13
|
+
|
|
14
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
15
|
+
|
|
16
|
+
export interface ListItemProps {
|
|
17
|
+
icon?: React.ReactNode
|
|
18
|
+
title: string
|
|
19
|
+
subtitle?: string
|
|
20
|
+
trailing?: string | React.ReactNode
|
|
21
|
+
onPress?: () => void
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
style?: ViewStyle
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, style }: ListItemProps) {
|
|
27
|
+
const { colors } = useTheme()
|
|
28
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
29
|
+
|
|
30
|
+
const handlePressIn = () => {
|
|
31
|
+
if (!onPress || disabled) return
|
|
32
|
+
Animated.spring(scale, {
|
|
33
|
+
toValue: 0.97,
|
|
34
|
+
useNativeDriver: nativeDriver,
|
|
35
|
+
speed: 40,
|
|
36
|
+
bounciness: 0,
|
|
37
|
+
}).start()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handlePressOut = () => {
|
|
41
|
+
Animated.spring(scale, {
|
|
42
|
+
toValue: 1,
|
|
43
|
+
useNativeDriver: nativeDriver,
|
|
44
|
+
speed: 40,
|
|
45
|
+
bounciness: 4,
|
|
46
|
+
}).start()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handlePress = () => {
|
|
50
|
+
Haptics.selectionAsync()
|
|
51
|
+
onPress?.()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
|
|
56
|
+
<TouchableOpacity
|
|
57
|
+
style={[styles.container, style]}
|
|
58
|
+
onPress={onPress ? handlePress : undefined}
|
|
59
|
+
onPressIn={handlePressIn}
|
|
60
|
+
onPressOut={handlePressOut}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
activeOpacity={1}
|
|
63
|
+
touchSoundDisabled={true}
|
|
64
|
+
>
|
|
65
|
+
{icon ? <View style={styles.iconWrapper}>{icon}</View> : null}
|
|
66
|
+
<View style={styles.content}>
|
|
67
|
+
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
|
|
68
|
+
{title}
|
|
69
|
+
</Text>
|
|
70
|
+
{subtitle ? (
|
|
71
|
+
<Text style={[styles.subtitle, { color: colors.mutedForeground }]} allowFontScaling={true}>
|
|
72
|
+
{subtitle}
|
|
73
|
+
</Text>
|
|
74
|
+
) : null}
|
|
75
|
+
</View>
|
|
76
|
+
{trailing !== undefined ? (
|
|
77
|
+
typeof trailing === 'string' ? (
|
|
78
|
+
<Text style={[styles.trailing, { color: colors.mutedForeground }]} allowFontScaling={true}>
|
|
79
|
+
{trailing}
|
|
80
|
+
</Text>
|
|
81
|
+
) : (
|
|
82
|
+
trailing
|
|
83
|
+
)
|
|
84
|
+
) : null}
|
|
85
|
+
</TouchableOpacity>
|
|
86
|
+
</Animated.View>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const styles = StyleSheet.create({
|
|
91
|
+
container: {
|
|
92
|
+
flexDirection: 'row',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
paddingHorizontal: 16,
|
|
95
|
+
paddingVertical: 14,
|
|
96
|
+
gap: 12,
|
|
97
|
+
},
|
|
98
|
+
iconWrapper: {
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
justifyContent: 'center',
|
|
101
|
+
},
|
|
102
|
+
content: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
gap: 3,
|
|
105
|
+
},
|
|
106
|
+
title: {
|
|
107
|
+
fontSize: 16,
|
|
108
|
+
fontWeight: '500',
|
|
109
|
+
lineHeight: 22,
|
|
110
|
+
},
|
|
111
|
+
subtitle: {
|
|
112
|
+
fontSize: 13,
|
|
113
|
+
lineHeight: 18,
|
|
114
|
+
},
|
|
115
|
+
trailing: {
|
|
116
|
+
fontSize: 15,
|
|
117
|
+
},
|
|
118
|
+
disabled: {
|
|
119
|
+
opacity: 0.45,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import * as Haptics from 'expo-haptics'
|
|
4
|
+
import { useTheme } from '../../theme'
|
|
5
|
+
|
|
6
|
+
const MONTH_NAMES = [
|
|
7
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
8
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export interface MonthPickerValue {
|
|
12
|
+
/** Month number 1–12 */
|
|
13
|
+
month: number
|
|
14
|
+
year: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MonthPickerProps {
|
|
18
|
+
value: MonthPickerValue
|
|
19
|
+
onChange: (value: MonthPickerValue) => void
|
|
20
|
+
style?: ViewStyle
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
|
|
24
|
+
const { colors } = useTheme()
|
|
25
|
+
|
|
26
|
+
const handlePrev = () => {
|
|
27
|
+
Haptics.selectionAsync()
|
|
28
|
+
if (value.month === 1) {
|
|
29
|
+
onChange({ month: 12, year: value.year - 1 })
|
|
30
|
+
} else {
|
|
31
|
+
onChange({ month: value.month - 1, year: value.year })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleNext = () => {
|
|
36
|
+
Haptics.selectionAsync()
|
|
37
|
+
if (value.month === 12) {
|
|
38
|
+
onChange({ month: 1, year: value.year + 1 })
|
|
39
|
+
} else {
|
|
40
|
+
onChange({ month: value.month + 1, year: value.year })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={[styles.container, style]}>
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
style={styles.arrow}
|
|
48
|
+
onPress={handlePrev}
|
|
49
|
+
activeOpacity={0.6}
|
|
50
|
+
touchSoundDisabled={true}
|
|
51
|
+
>
|
|
52
|
+
<Text style={[styles.arrowText, { color: colors.foreground }]}>‹</Text>
|
|
53
|
+
</TouchableOpacity>
|
|
54
|
+
<Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
|
|
55
|
+
{MONTH_NAMES[value.month - 1]} {value.year}
|
|
56
|
+
</Text>
|
|
57
|
+
<TouchableOpacity
|
|
58
|
+
style={styles.arrow}
|
|
59
|
+
onPress={handleNext}
|
|
60
|
+
activeOpacity={0.6}
|
|
61
|
+
touchSoundDisabled={true}
|
|
62
|
+
>
|
|
63
|
+
<Text style={[styles.arrowText, { color: colors.foreground }]}>›</Text>
|
|
64
|
+
</TouchableOpacity>
|
|
65
|
+
</View>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
container: {
|
|
71
|
+
flexDirection: 'row',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'space-between',
|
|
74
|
+
},
|
|
75
|
+
arrow: {
|
|
76
|
+
width: 44,
|
|
77
|
+
height: 44,
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
justifyContent: 'center',
|
|
80
|
+
},
|
|
81
|
+
arrowText: {
|
|
82
|
+
fontSize: 24,
|
|
83
|
+
lineHeight: 30,
|
|
84
|
+
},
|
|
85
|
+
label: {
|
|
86
|
+
fontSize: 17,
|
|
87
|
+
fontWeight: '500',
|
|
88
|
+
lineHeight: 24,
|
|
89
|
+
textAlign: 'center',
|
|
90
|
+
minWidth: 160,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import { TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
|
|
2
|
+
import { TouchableOpacity, Animated, StyleSheet, ViewStyle, Platform } from 'react-native'
|
|
3
|
+
|
|
4
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
3
5
|
import * as Haptics from 'expo-haptics'
|
|
4
6
|
import { useTheme } from '../../theme'
|
|
5
7
|
|
|
@@ -25,7 +27,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
|
|
|
25
27
|
Animated.parallel([
|
|
26
28
|
Animated.spring(translateX, {
|
|
27
29
|
toValue: checked ? THUMB_TRAVEL : 0,
|
|
28
|
-
useNativeDriver:
|
|
30
|
+
useNativeDriver: nativeDriver,
|
|
29
31
|
bounciness: 4,
|
|
30
32
|
}),
|
|
31
33
|
Animated.timing(trackOpacity, {
|
package/src/index.ts
CHANGED
|
@@ -30,3 +30,8 @@ export * from './components/Toast'
|
|
|
30
30
|
export * from './components/CurrencyInput'
|
|
31
31
|
export * from './components/CurrencyDisplay'
|
|
32
32
|
export * from './components/CurrencyInputLarge'
|
|
33
|
+
export * from './components/ListItem'
|
|
34
|
+
export * from './components/Chip'
|
|
35
|
+
export * from './components/ConfirmDialog'
|
|
36
|
+
export * from './components/LabelValue'
|
|
37
|
+
export * from './components/MonthPicker'
|