@retray-dev/ui-kit 2.5.1 → 2.5.2
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 +2 -2
- package/dist/index.js +374 -362
- package/dist/index.mjs +362 -331
- package/package.json +23 -21
- package/src/components/Accordion/Accordion.tsx +61 -57
- package/src/components/Alert/Alert.tsx +11 -10
- package/src/components/AlertBanner/AlertBanner.tsx +9 -8
- package/src/components/Avatar/Avatar.tsx +9 -8
- package/src/components/Badge/Badge.tsx +11 -10
- package/src/components/Button/Button.tsx +10 -9
- package/src/components/Card/Card.tsx +12 -11
- package/src/components/Checkbox/Checkbox.tsx +16 -13
- package/src/components/Chip/Chip.tsx +8 -7
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +12 -11
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +2 -1
- package/src/components/CurrencyInput/CurrencyInput.tsx +2 -1
- package/src/components/EmptyState/EmptyState.tsx +19 -18
- package/src/components/Input/Input.tsx +15 -14
- package/src/components/LabelValue/LabelValue.tsx +6 -5
- package/src/components/ListItem/ListItem.tsx +20 -19
- package/src/components/MonthPicker/MonthPicker.tsx +9 -8
- package/src/components/Progress/Progress.tsx +2 -1
- package/src/components/RadioGroup/RadioGroup.tsx +18 -15
- package/src/components/Select/Select.tsx +25 -24
- package/src/components/Sheet/Sheet.tsx +15 -14
- package/src/components/Slider/Slider.tsx +7 -6
- package/src/components/Switch/Switch.tsx +7 -6
- package/src/components/Tabs/Tabs.tsx +17 -14
- package/src/components/Text/Text.tsx +7 -6
- package/src/components/Textarea/Textarea.tsx +9 -8
- package/src/components/Toast/Toast.tsx +19 -18
- package/src/components/Toggle/Toggle.tsx +9 -8
- package/src/utils/haptics.ts +32 -0
- package/src/utils/scaling.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "Personal UI Kit for React Native / Expo",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -37,18 +37,19 @@
|
|
|
37
37
|
],
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"@expo/vector-icons": ">=14.0.0",
|
|
41
|
+
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
42
|
+
"@react-native-community/slider": ">=4.0.0",
|
|
43
|
+
"@react-native-picker/picker": ">=2.0.0",
|
|
42
44
|
"expo-haptics": ">=14.0.0",
|
|
43
45
|
"expo-linear-gradient": ">=13.0.0",
|
|
44
|
-
"
|
|
45
|
-
"react-native
|
|
46
|
+
"react": ">=17",
|
|
47
|
+
"react-native": ">=0.70",
|
|
46
48
|
"react-native-gesture-handler": ">=2.0.0",
|
|
47
|
-
"react-native-
|
|
49
|
+
"react-native-reanimated": ">=4.0.0",
|
|
48
50
|
"react-native-safe-area-context": ">=4.0.0",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"@expo/vector-icons": ">=14.0.0"
|
|
51
|
+
"react-native-size-matters": ">=0.4.0",
|
|
52
|
+
"react-native-worklets": ">=0.8.0"
|
|
52
53
|
},
|
|
53
54
|
"pnpm": {
|
|
54
55
|
"overrides": {
|
|
@@ -62,27 +63,28 @@
|
|
|
62
63
|
]
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
66
|
+
"@eslint/js": "^9.0.0",
|
|
67
|
+
"react-native-size-matters": "^0.4.2",
|
|
68
|
+
"@expo/vector-icons": "^15.1.1",
|
|
65
69
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
66
|
-
"@react-native-
|
|
67
|
-
"@react-native-
|
|
70
|
+
"@react-native-community/slider": "5.0.1",
|
|
71
|
+
"@react-native-picker/picker": "2.11.1",
|
|
68
72
|
"@types/react": "^19.1.0",
|
|
69
|
-
"
|
|
73
|
+
"eslint": "^9.0.0",
|
|
74
|
+
"eslint-config-prettier": "^10.0.0",
|
|
75
|
+
"eslint-plugin-react": "^7.37.0",
|
|
76
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
70
77
|
"expo-haptics": "~15.0.8",
|
|
71
78
|
"expo-linear-gradient": "~15.0.8",
|
|
79
|
+
"prettier": "^3.0.0",
|
|
72
80
|
"react": "19.1.0",
|
|
73
81
|
"react-native": "0.81.5",
|
|
74
82
|
"react-native-gesture-handler": "~2.28.0",
|
|
75
83
|
"react-native-reanimated": "~4.1.1",
|
|
76
|
-
"react-native-worklets": "~0.5.1",
|
|
77
84
|
"react-native-safe-area-context": "~5.6.2",
|
|
78
|
-
"
|
|
79
|
-
"@eslint/js": "^9.0.0",
|
|
80
|
-
"typescript-eslint": "^8.0.0",
|
|
81
|
-
"eslint-plugin-react": "^7.37.0",
|
|
82
|
-
"eslint-plugin-react-hooks": "^5.0.0",
|
|
83
|
-
"eslint-config-prettier": "^10.0.0",
|
|
84
|
-
"prettier": "^3.0.0",
|
|
85
|
+
"react-native-worklets": "~0.5.1",
|
|
85
86
|
"tsup": "^8.0.0",
|
|
86
|
-
"typescript": "^5.4.0"
|
|
87
|
+
"typescript": "^5.4.0",
|
|
88
|
+
"typescript-eslint": "^8.0.0"
|
|
87
89
|
}
|
|
88
90
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import React, { useState
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
-
|
|
6
|
-
Animated,
|
|
5
|
+
Pressable,
|
|
7
6
|
StyleSheet,
|
|
8
|
-
LayoutChangeEvent,
|
|
9
7
|
ViewStyle,
|
|
10
8
|
} from 'react-native'
|
|
11
|
-
import
|
|
9
|
+
import Animated, {
|
|
12
10
|
useSharedValue,
|
|
11
|
+
useDerivedValue,
|
|
13
12
|
useAnimatedStyle,
|
|
14
13
|
withTiming,
|
|
15
14
|
Easing,
|
|
16
15
|
} from 'react-native-reanimated'
|
|
17
16
|
import { Entypo } from '@expo/vector-icons'
|
|
18
|
-
import
|
|
17
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
19
18
|
import { useTheme } from '../../theme'
|
|
19
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
20
20
|
|
|
21
21
|
export interface AccordionItem {
|
|
22
22
|
value: string
|
|
@@ -46,71 +46,73 @@ function AccordionItemComponent({
|
|
|
46
46
|
onToggle: () => void
|
|
47
47
|
}) {
|
|
48
48
|
const { colors } = useTheme()
|
|
49
|
-
const animatedHeight = useSharedValue(0)
|
|
50
|
-
const animatedRotation = useSharedValue(0)
|
|
51
|
-
const contentHeight = useRef(0)
|
|
52
|
-
const scale = useRef(new Animated.Value(1)).current
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
animatedRotation.value = withTiming(open ? 1 : 0, { duration: 220, easing })
|
|
58
|
-
}
|
|
50
|
+
// Shared values — all animation lives on the UI thread
|
|
51
|
+
const isExpanded = useSharedValue(isOpen)
|
|
52
|
+
const height = useSharedValue(0)
|
|
59
53
|
|
|
54
|
+
// Keep isExpanded in sync with the parent-driven isOpen prop
|
|
60
55
|
React.useEffect(() => {
|
|
61
|
-
|
|
56
|
+
isExpanded.value = isOpen
|
|
62
57
|
}, [isOpen])
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
// Derived animated height — pattern from Reanimated docs:
|
|
60
|
+
// height * Number(isExpanded) gives 0 when closed and the measured height when open.
|
|
61
|
+
// withTiming wraps it so every change animates automatically.
|
|
62
|
+
const derivedHeight = useDerivedValue(() =>
|
|
63
|
+
withTiming(height.value * Number(isExpanded.value), {
|
|
64
|
+
duration: 220,
|
|
65
|
+
easing: isExpanded.value ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
|
|
66
|
+
})
|
|
67
|
+
)
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
|
|
69
|
+
const derivedRotation = useDerivedValue(() =>
|
|
70
|
+
withTiming(isExpanded.value ? 1 : 0, {
|
|
71
|
+
duration: 220,
|
|
72
|
+
easing: isExpanded.value ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const bodyStyle = useAnimatedStyle(() => ({
|
|
77
|
+
height: derivedHeight.value,
|
|
73
78
|
overflow: 'hidden',
|
|
74
79
|
}))
|
|
75
80
|
|
|
76
81
|
const rotationStyle = useAnimatedStyle(() => ({
|
|
77
|
-
transform: [{ rotate: `${
|
|
82
|
+
transform: [{ rotate: `${derivedRotation.value * 180}deg` }],
|
|
78
83
|
}))
|
|
79
84
|
|
|
80
|
-
const handlePressIn = () => {
|
|
81
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const handlePressOut = () => {
|
|
85
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
85
|
return (
|
|
89
86
|
<View style={[styles.item, { borderBottomColor: colors.border }]}>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
<Pressable
|
|
88
|
+
style={({ pressed }) => [styles.trigger, { opacity: pressed ? 0.6 : 1 }]}
|
|
89
|
+
onPress={() => {
|
|
90
|
+
hapticSelection()
|
|
91
|
+
onToggle()
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<Text style={[styles.triggerText, { color: colors.foreground }]}>{item.trigger}</Text>
|
|
95
|
+
<Animated.View style={[styles.chevron, rotationStyle]}>
|
|
96
|
+
<Entypo name="chevron-down" size={20} color={colors.foreground} />
|
|
97
|
+
</Animated.View>
|
|
98
|
+
</Pressable>
|
|
99
|
+
|
|
100
|
+
{/*
|
|
101
|
+
The Animated.View height is driven by derivedHeight (0 when closed, full height when open).
|
|
102
|
+
The inner View uses position:'absolute' so onLayout always measures the natural content
|
|
103
|
+
height regardless of the animated wrapper's current height — this is the key pattern
|
|
104
|
+
from the Reanimated docs that prevents the jump.
|
|
105
|
+
*/}
|
|
106
|
+
<Animated.View style={bodyStyle}>
|
|
107
|
+
<View
|
|
108
|
+
style={styles.content}
|
|
109
|
+
onLayout={(e) => {
|
|
110
|
+
height.value = e.nativeEvent.layout.height
|
|
96
111
|
}}
|
|
97
|
-
onPressIn={handlePressIn}
|
|
98
|
-
onPressOut={handlePressOut}
|
|
99
|
-
activeOpacity={1}
|
|
100
|
-
touchSoundDisabled={true}
|
|
101
112
|
>
|
|
102
|
-
<Text style={[styles.triggerText, { color: colors.foreground }]}>{item.trigger}</Text>
|
|
103
|
-
<ReanimatedAnimated.View style={[styles.chevron, rotationStyle]}>
|
|
104
|
-
<Entypo name="chevron-down" size={20} color={colors.foreground} />
|
|
105
|
-
</ReanimatedAnimated.View>
|
|
106
|
-
</TouchableOpacity>
|
|
107
|
-
</Animated.View>
|
|
108
|
-
|
|
109
|
-
<ReanimatedAnimated.View style={heightStyle}>
|
|
110
|
-
<View style={styles.content} onLayout={onLayout}>
|
|
111
113
|
{item.content}
|
|
112
114
|
</View>
|
|
113
|
-
</
|
|
115
|
+
</Animated.View>
|
|
114
116
|
</View>
|
|
115
117
|
)
|
|
116
118
|
}
|
|
@@ -153,18 +155,20 @@ const styles = StyleSheet.create({
|
|
|
153
155
|
flexDirection: 'row',
|
|
154
156
|
justifyContent: 'space-between',
|
|
155
157
|
alignItems: 'center',
|
|
156
|
-
paddingVertical: 20,
|
|
158
|
+
paddingVertical: vs(20),
|
|
157
159
|
},
|
|
158
160
|
triggerText: {
|
|
159
|
-
fontSize: 17,
|
|
161
|
+
fontSize: ms(17),
|
|
160
162
|
fontWeight: '500',
|
|
161
163
|
flex: 1,
|
|
162
164
|
},
|
|
163
165
|
chevron: {
|
|
164
|
-
marginLeft: 8,
|
|
166
|
+
marginLeft: s(8),
|
|
165
167
|
},
|
|
168
|
+
// position:'absolute' is the key — the inner View escapes the animated wrapper's
|
|
169
|
+
// clipped height so onLayout always reports the true content height.
|
|
166
170
|
content: {
|
|
167
|
-
paddingBottom: 20,
|
|
171
|
+
paddingBottom: vs(20),
|
|
168
172
|
position: 'absolute',
|
|
169
173
|
width: '100%',
|
|
170
174
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
4
5
|
|
|
5
6
|
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
6
7
|
|
|
@@ -56,28 +57,28 @@ const styles = StyleSheet.create({
|
|
|
56
57
|
container: {
|
|
57
58
|
flexDirection: 'row',
|
|
58
59
|
borderWidth: 1,
|
|
59
|
-
borderRadius: 8,
|
|
60
|
-
padding: 16,
|
|
61
|
-
gap: 12,
|
|
60
|
+
borderRadius: ms(8),
|
|
61
|
+
padding: s(16),
|
|
62
|
+
gap: s(12),
|
|
62
63
|
},
|
|
63
64
|
icon: {
|
|
64
|
-
marginTop: 2,
|
|
65
|
+
marginTop: vs(2),
|
|
65
66
|
},
|
|
66
67
|
content: {
|
|
67
68
|
flex: 1,
|
|
68
|
-
gap: 4,
|
|
69
|
+
gap: vs(4),
|
|
69
70
|
},
|
|
70
71
|
title: {
|
|
71
|
-
fontSize: 14,
|
|
72
|
+
fontSize: ms(14),
|
|
72
73
|
fontWeight: '500',
|
|
73
|
-
lineHeight: 20,
|
|
74
|
+
lineHeight: mvs(20),
|
|
74
75
|
},
|
|
75
76
|
description: {
|
|
76
|
-
fontSize: 14,
|
|
77
|
-
lineHeight: 20,
|
|
77
|
+
fontSize: ms(14),
|
|
78
|
+
lineHeight: mvs(20),
|
|
78
79
|
},
|
|
79
80
|
defaultIcon: {
|
|
80
|
-
fontSize: 18,
|
|
81
|
+
fontSize: ms(18),
|
|
81
82
|
fontWeight: '700',
|
|
82
83
|
},
|
|
83
84
|
})
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
6
|
|
|
6
7
|
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
7
8
|
|
|
@@ -57,9 +58,9 @@ const styles = StyleSheet.create({
|
|
|
57
58
|
container: {
|
|
58
59
|
flexDirection: 'row',
|
|
59
60
|
borderWidth: 1,
|
|
60
|
-
borderRadius: 12,
|
|
61
|
-
padding: 16,
|
|
62
|
-
gap: 12,
|
|
61
|
+
borderRadius: ms(12),
|
|
62
|
+
padding: s(16),
|
|
63
|
+
gap: s(12),
|
|
63
64
|
shadowColor: '#000',
|
|
64
65
|
shadowOffset: { width: 0, height: 4 },
|
|
65
66
|
shadowOpacity: 0.06,
|
|
@@ -71,15 +72,15 @@ const styles = StyleSheet.create({
|
|
|
71
72
|
},
|
|
72
73
|
content: {
|
|
73
74
|
flex: 1,
|
|
74
|
-
gap: 4,
|
|
75
|
+
gap: vs(4),
|
|
75
76
|
},
|
|
76
77
|
title: {
|
|
77
|
-
fontSize: 14,
|
|
78
|
+
fontSize: ms(14),
|
|
78
79
|
fontWeight: '500',
|
|
79
|
-
lineHeight: 20,
|
|
80
|
+
lineHeight: mvs(20),
|
|
80
81
|
},
|
|
81
82
|
description: {
|
|
82
|
-
fontSize: 14,
|
|
83
|
-
lineHeight: 20,
|
|
83
|
+
fontSize: ms(14),
|
|
84
|
+
lineHeight: mvs(20),
|
|
84
85
|
},
|
|
85
86
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { View, Text, Image, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
|
+
import { s, ms } from '../../utils/scaling'
|
|
4
5
|
|
|
5
6
|
export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
6
7
|
|
|
@@ -14,17 +15,17 @@ export interface AvatarProps {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const sizeMap: Record<AvatarSize, number> = {
|
|
17
|
-
sm: 28,
|
|
18
|
-
md: 40,
|
|
19
|
-
lg: 56,
|
|
20
|
-
xl: 72,
|
|
18
|
+
sm: s(28),
|
|
19
|
+
md: s(40),
|
|
20
|
+
lg: s(56),
|
|
21
|
+
xl: s(72),
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const fontSizeMap: Record<AvatarSize, number> = {
|
|
24
|
-
sm: 12,
|
|
25
|
-
md: 16,
|
|
26
|
-
lg: 22,
|
|
27
|
-
xl: 28,
|
|
25
|
+
sm: ms(12),
|
|
26
|
+
md: ms(16),
|
|
27
|
+
lg: ms(22),
|
|
28
|
+
xl: ms(28),
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
4
5
|
|
|
5
6
|
export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
|
|
6
7
|
export type BadgeSize = 'sm' | 'md' | 'lg'
|
|
@@ -17,21 +18,21 @@ export interface BadgeProps {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const sizePadding: Record<BadgeSize, ViewStyle> = {
|
|
20
|
-
sm: { paddingHorizontal: 8, paddingVertical: 2 },
|
|
21
|
-
md: { paddingHorizontal: 10, paddingVertical: 4 },
|
|
22
|
-
lg: { paddingHorizontal: 12, paddingVertical: 6 },
|
|
21
|
+
sm: { paddingHorizontal: s(8), paddingVertical: vs(2) },
|
|
22
|
+
md: { paddingHorizontal: s(10), paddingVertical: vs(4) },
|
|
23
|
+
lg: { paddingHorizontal: s(12), paddingVertical: vs(6) },
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const sizeFontSize: Record<BadgeSize, TextStyle> = {
|
|
26
|
-
sm: { fontSize: 11 },
|
|
27
|
-
md: { fontSize: 13 },
|
|
28
|
-
lg: { fontSize: 15 },
|
|
27
|
+
sm: { fontSize: ms(11) },
|
|
28
|
+
md: { fontSize: ms(13) },
|
|
29
|
+
lg: { fontSize: ms(15) },
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const sizeIconGap: Record<BadgeSize, number> = {
|
|
32
|
-
sm: 4,
|
|
33
|
-
md: 6,
|
|
34
|
-
lg: 6,
|
|
33
|
+
sm: s(4),
|
|
34
|
+
md: s(6),
|
|
35
|
+
lg: s(6),
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function Badge({ label, children, variant = 'default', size = 'md', icon, style }: BadgeProps) {
|
|
@@ -69,7 +70,7 @@ export function Badge({ label, children, variant = 'default', size = 'md', icon,
|
|
|
69
70
|
|
|
70
71
|
const styles = StyleSheet.create({
|
|
71
72
|
container: {
|
|
72
|
-
borderRadius: 6,
|
|
73
|
+
borderRadius: ms(6),
|
|
73
74
|
alignSelf: 'flex-start',
|
|
74
75
|
flexDirection: 'row',
|
|
75
76
|
alignItems: 'center',
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
} from 'react-native'
|
|
13
13
|
|
|
14
14
|
const nativeDriver = Platform.OS !== 'web'
|
|
15
|
-
import
|
|
15
|
+
import { impactLight } from '../../utils/haptics'
|
|
16
16
|
import { useTheme } from '../../theme'
|
|
17
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
17
18
|
|
|
18
19
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
|
19
20
|
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
@@ -38,15 +39,15 @@ export interface ButtonProps extends TouchableOpacityProps {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
41
|
-
sm: { paddingHorizontal: 20, paddingVertical: 10 },
|
|
42
|
-
md: { paddingHorizontal: 24, paddingVertical: 14 },
|
|
43
|
-
lg: { paddingHorizontal: 32, paddingVertical: 18 },
|
|
42
|
+
sm: { paddingHorizontal: s(20), paddingVertical: vs(10) },
|
|
43
|
+
md: { paddingHorizontal: s(24), paddingVertical: vs(14) },
|
|
44
|
+
lg: { paddingHorizontal: s(32), paddingVertical: vs(18) },
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
47
|
-
sm: { fontSize: 15 },
|
|
48
|
-
md: { fontSize: 17 },
|
|
49
|
-
lg: { fontSize: 18 },
|
|
48
|
+
sm: { fontSize: ms(15) },
|
|
49
|
+
md: { fontSize: ms(17) },
|
|
50
|
+
lg: { fontSize: ms(18) },
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function Button({
|
|
@@ -81,7 +82,7 @@ export function Button({
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
84
|
-
|
|
85
|
+
impactLight()
|
|
85
86
|
onPress?.(e)
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -156,6 +157,6 @@ const styles = StyleSheet.create({
|
|
|
156
157
|
fontWeight: '600',
|
|
157
158
|
},
|
|
158
159
|
labelWithIcon: {
|
|
159
|
-
marginHorizontal: 8,
|
|
160
|
+
marginHorizontal: s(8),
|
|
160
161
|
},
|
|
161
162
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useRef } from 'react'
|
|
2
2
|
import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle, TextStyle, Platform } from 'react-native'
|
|
3
|
-
import
|
|
3
|
+
import { impactLight } from '../../utils/haptics'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
6
|
|
|
6
7
|
const nativeDriver = Platform.OS !== 'web'
|
|
7
8
|
|
|
@@ -67,7 +68,7 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
67
68
|
|
|
68
69
|
const handlePress = () => {
|
|
69
70
|
if (!onPress) return
|
|
70
|
-
|
|
71
|
+
impactLight()
|
|
71
72
|
onPress()
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -146,28 +147,28 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
146
147
|
|
|
147
148
|
const styles = StyleSheet.create({
|
|
148
149
|
card: {
|
|
149
|
-
borderRadius: 12,
|
|
150
|
+
borderRadius: ms(12),
|
|
150
151
|
borderWidth: 1,
|
|
151
152
|
},
|
|
152
153
|
header: {
|
|
153
|
-
padding: 24,
|
|
154
|
+
padding: s(24),
|
|
154
155
|
paddingBottom: 0,
|
|
155
|
-
gap: 8,
|
|
156
|
+
gap: vs(8),
|
|
156
157
|
},
|
|
157
158
|
title: {
|
|
158
|
-
fontSize: 20,
|
|
159
|
+
fontSize: ms(20),
|
|
159
160
|
fontWeight: '600',
|
|
160
|
-
lineHeight: 28,
|
|
161
|
+
lineHeight: mvs(28),
|
|
161
162
|
},
|
|
162
163
|
description: {
|
|
163
|
-
fontSize: 15,
|
|
164
|
-
lineHeight: 22,
|
|
164
|
+
fontSize: ms(15),
|
|
165
|
+
lineHeight: mvs(22),
|
|
165
166
|
},
|
|
166
167
|
content: {
|
|
167
|
-
padding: 24,
|
|
168
|
+
padding: s(24),
|
|
168
169
|
},
|
|
169
170
|
footer: {
|
|
170
|
-
padding: 24,
|
|
171
|
+
padding: s(24),
|
|
171
172
|
paddingTop: 0,
|
|
172
173
|
flexDirection: 'row',
|
|
173
174
|
alignItems: 'center',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React, { useRef } from 'react'
|
|
2
|
-
import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
-
import
|
|
2
|
+
import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle, Platform } from 'react-native'
|
|
3
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
4
|
+
|
|
5
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
4
6
|
import { useTheme } from '../../theme'
|
|
7
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
8
|
|
|
6
9
|
export interface CheckboxProps {
|
|
7
10
|
checked?: boolean
|
|
@@ -23,11 +26,11 @@ export function Checkbox({
|
|
|
23
26
|
|
|
24
27
|
const handlePressIn = () => {
|
|
25
28
|
if (disabled) return
|
|
26
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver:
|
|
29
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const handlePressOut = () => {
|
|
30
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver:
|
|
33
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
return (
|
|
@@ -35,7 +38,7 @@ export function Checkbox({
|
|
|
35
38
|
<TouchableOpacity
|
|
36
39
|
style={[styles.row, style]}
|
|
37
40
|
onPress={() => {
|
|
38
|
-
|
|
41
|
+
hapticSelection()
|
|
39
42
|
onCheckedChange?.(!checked)
|
|
40
43
|
}}
|
|
41
44
|
onPressIn={handlePressIn}
|
|
@@ -74,25 +77,25 @@ const styles = StyleSheet.create({
|
|
|
74
77
|
row: {
|
|
75
78
|
flexDirection: 'row',
|
|
76
79
|
alignItems: 'center',
|
|
77
|
-
gap: 12,
|
|
80
|
+
gap: s(12),
|
|
78
81
|
},
|
|
79
82
|
box: {
|
|
80
|
-
width: 24,
|
|
81
|
-
height: 24,
|
|
82
|
-
borderRadius: 8,
|
|
83
|
+
width: s(24),
|
|
84
|
+
height: s(24),
|
|
85
|
+
borderRadius: ms(8),
|
|
83
86
|
borderWidth: 1.5,
|
|
84
87
|
alignItems: 'center',
|
|
85
88
|
justifyContent: 'center',
|
|
86
89
|
},
|
|
87
90
|
checkmark: {
|
|
88
|
-
width: 12,
|
|
89
|
-
height: 7,
|
|
91
|
+
width: s(12),
|
|
92
|
+
height: vs(7),
|
|
90
93
|
borderLeftWidth: 2,
|
|
91
94
|
borderBottomWidth: 2,
|
|
92
95
|
transform: [{ rotate: '-45deg' }, { translateY: -1 }],
|
|
93
96
|
},
|
|
94
97
|
label: {
|
|
95
|
-
fontSize: 15,
|
|
96
|
-
lineHeight: 22,
|
|
98
|
+
fontSize: ms(15),
|
|
99
|
+
lineHeight: mvs(22),
|
|
97
100
|
},
|
|
98
101
|
})
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
Platform,
|
|
10
10
|
Easing,
|
|
11
11
|
} from 'react-native'
|
|
12
|
-
import
|
|
12
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
13
13
|
import { useTheme } from '../../theme'
|
|
14
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
14
15
|
|
|
15
16
|
const nativeDriver = Platform.OS !== 'web'
|
|
16
17
|
|
|
@@ -68,7 +69,7 @@ export function Chip({ label, selected = false, onPress, style }: ChipProps) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const handlePress = () => {
|
|
71
|
-
|
|
72
|
+
hapticSelection()
|
|
72
73
|
onPress?.()
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -154,20 +155,20 @@ const styles = StyleSheet.create({
|
|
|
154
155
|
wrapper: {},
|
|
155
156
|
chip: {
|
|
156
157
|
borderRadius: 999,
|
|
157
|
-
paddingHorizontal: 14,
|
|
158
|
-
paddingVertical: 8,
|
|
158
|
+
paddingHorizontal: s(14),
|
|
159
|
+
paddingVertical: vs(8),
|
|
159
160
|
borderWidth: 1.5,
|
|
160
161
|
alignItems: 'center',
|
|
161
162
|
justifyContent: 'center',
|
|
162
163
|
},
|
|
163
164
|
label: {
|
|
164
|
-
fontSize: 14,
|
|
165
|
+
fontSize: ms(14),
|
|
165
166
|
fontWeight: '500',
|
|
166
|
-
lineHeight: 20,
|
|
167
|
+
lineHeight: mvs(20),
|
|
167
168
|
},
|
|
168
169
|
group: {
|
|
169
170
|
flexDirection: 'row',
|
|
170
171
|
flexWrap: 'wrap',
|
|
171
|
-
gap: 8,
|
|
172
|
+
gap: s(8),
|
|
172
173
|
},
|
|
173
174
|
})
|