@retray-dev/ui-kit 5.4.0 → 6.0.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 +261 -126
- package/dist/index.d.mts +147 -44
- package/dist/index.d.ts +147 -44
- package/dist/index.js +429 -305
- package/dist/index.mjs +419 -303
- package/package.json +7 -2
- package/src/components/Accordion/Accordion.tsx +6 -2
- package/src/components/AlertBanner/AlertBanner.tsx +16 -33
- package/src/components/Button/Button.tsx +18 -6
- package/src/components/Card/Card.tsx +12 -9
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +4 -4
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +14 -3
- package/src/components/EmptyState/EmptyState.tsx +21 -6
- package/src/components/Input/Input.tsx +21 -10
- package/src/components/ListItem/ListItem.tsx +14 -8
- package/src/components/MediaCard/MediaCard.tsx +1 -0
- package/src/components/MenuItem/MenuItem.tsx +206 -0
- package/src/components/MenuItem/index.ts +2 -0
- package/src/components/Select/Select.tsx +1 -1
- package/src/components/Separator/Separator.tsx +2 -0
- package/src/components/Sheet/Sheet.tsx +164 -51
- package/src/components/Sheet/index.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +4 -4
- package/src/components/Toast/Toast.tsx +41 -267
- package/src/components/Toast/index.ts +1 -2
- package/src/components/Toggle/Toggle.tsx +2 -2
- package/src/index.ts +1 -0
- package/src/theme/colors.ts +3 -0
- package/src/theme/types.ts +11 -0
- package/src/tokens.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.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,9 @@
|
|
|
52
52
|
"react-native-reanimated": ">=4.0.0",
|
|
53
53
|
"react-native-safe-area-context": ">=4.0.0",
|
|
54
54
|
"react-native-size-matters": ">=0.4.0",
|
|
55
|
-
"react-native-worklets": ">=0.5.0"
|
|
55
|
+
"react-native-worklets": ">=0.5.0",
|
|
56
|
+
"react-native-svg": ">=15.0.0",
|
|
57
|
+
"react-native-screens": ">=3.0.0"
|
|
56
58
|
},
|
|
57
59
|
"pnpm": {
|
|
58
60
|
"overrides": {
|
|
@@ -87,6 +89,9 @@
|
|
|
87
89
|
"react-native-reanimated": "~4.1.1",
|
|
88
90
|
"react-native-safe-area-context": "~5.6.2",
|
|
89
91
|
"react-native-worklets": "~0.5.1",
|
|
92
|
+
"sonner-native": "0.23.1",
|
|
93
|
+
"react-native-svg": "15.12.1",
|
|
94
|
+
"react-native-screens": "4.16.0",
|
|
90
95
|
"tsup": "^8.0.0",
|
|
91
96
|
"typescript": "^5.4.0",
|
|
92
97
|
"typescript-eslint": "^8.0.0"
|
|
@@ -12,7 +12,11 @@ import Animated, {
|
|
|
12
12
|
useAnimatedStyle,
|
|
13
13
|
withTiming,
|
|
14
14
|
Easing,
|
|
15
|
+
type EasingFunction,
|
|
15
16
|
} from 'react-native-reanimated'
|
|
17
|
+
|
|
18
|
+
const easingExpand: EasingFunction = Easing.bezier(0.23, 1, 0.32, 1) as unknown as EasingFunction
|
|
19
|
+
const easingCollapse: EasingFunction = Easing.in(Easing.ease)
|
|
16
20
|
import { Entypo } from '@expo/vector-icons'
|
|
17
21
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
18
22
|
import { useTheme } from '../../theme'
|
|
@@ -73,14 +77,14 @@ function AccordionItemComponent({
|
|
|
73
77
|
const derivedHeight = useDerivedValue(() =>
|
|
74
78
|
withTiming(height.value * Number(isExpanded.value), {
|
|
75
79
|
duration: 220,
|
|
76
|
-
easing: isExpanded.value ?
|
|
80
|
+
easing: isExpanded.value ? easingExpand : easingCollapse,
|
|
77
81
|
})
|
|
78
82
|
)
|
|
79
83
|
|
|
80
84
|
const derivedRotation = useDerivedValue(() =>
|
|
81
85
|
withTiming(isExpanded.value ? 1 : 0, {
|
|
82
86
|
duration: 220,
|
|
83
|
-
easing: isExpanded.value ?
|
|
87
|
+
easing: isExpanded.value ? easingExpand : easingCollapse,
|
|
84
88
|
})
|
|
85
89
|
)
|
|
86
90
|
|
|
@@ -4,6 +4,7 @@ import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
|
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms } from '../../utils/scaling'
|
|
6
6
|
import { renderIcon } from '../../utils/icons'
|
|
7
|
+
import { RADIUS } from '../../tokens'
|
|
7
8
|
|
|
8
9
|
export type AlertBannerVariant = 'default' | 'destructive' | 'success' | 'warning'
|
|
9
10
|
|
|
@@ -20,52 +21,36 @@ export interface AlertBannerProps {
|
|
|
20
21
|
export function AlertBanner({ title, description, variant = 'default', icon, iconName, iconColor, style }: AlertBannerProps) {
|
|
21
22
|
const { colors } = useTheme()
|
|
22
23
|
|
|
23
|
-
const bgColor =
|
|
24
|
-
variant === 'destructive' ? colors.destructiveTint
|
|
25
|
-
: variant === 'success' ? colors.successTint
|
|
26
|
-
: variant === 'warning' ? colors.warningTint
|
|
27
|
-
: colors.card
|
|
28
|
-
|
|
29
|
-
const borderColor =
|
|
30
|
-
variant === 'destructive' ? colors.destructiveBorder
|
|
31
|
-
: variant === 'success' ? colors.successBorder
|
|
32
|
-
: variant === 'warning' ? colors.warningBorder
|
|
33
|
-
: colors.border
|
|
34
|
-
|
|
35
24
|
const accentColor =
|
|
36
25
|
variant === 'destructive' ? colors.destructive
|
|
37
26
|
: variant === 'success' ? colors.success
|
|
38
27
|
: variant === 'warning' ? colors.warning
|
|
39
28
|
: colors.primary
|
|
40
29
|
|
|
41
|
-
const titleColor =
|
|
42
|
-
variant === 'default' ? colors.foreground : accentColor
|
|
43
|
-
|
|
44
|
-
const descColor =
|
|
45
|
-
variant === 'default' ? colors.foregroundMuted : accentColor
|
|
46
|
-
|
|
47
30
|
const defaultIcon =
|
|
48
31
|
variant === 'success' ? (
|
|
49
|
-
<FontAwesome5 name="check-circle" size={16} color={accentColor} />
|
|
32
|
+
<FontAwesome5 name="check-circle" size={ms(16)} color={accentColor} />
|
|
50
33
|
) : variant === 'destructive' ? (
|
|
51
|
-
<MaterialIcons name="error-outline" size={17} color={accentColor} />
|
|
34
|
+
<MaterialIcons name="error-outline" size={ms(17)} color={accentColor} />
|
|
52
35
|
) : variant === 'warning' ? (
|
|
53
|
-
<MaterialIcons name="warning-amber" size={17} color={accentColor} />
|
|
36
|
+
<MaterialIcons name="warning-amber" size={ms(17)} color={accentColor} />
|
|
54
37
|
) : (
|
|
55
|
-
<Entypo name="info-with-circle" size={16} color={accentColor} />
|
|
38
|
+
<Entypo name="info-with-circle" size={ms(16)} color={accentColor} />
|
|
56
39
|
)
|
|
57
40
|
|
|
58
41
|
const effectiveIcon: React.ReactNode = iconName
|
|
59
|
-
? renderIcon(iconName, 16, iconColor ?? accentColor)
|
|
42
|
+
? renderIcon(iconName, ms(16), iconColor ?? accentColor)
|
|
60
43
|
: icon ?? defaultIcon
|
|
61
44
|
|
|
62
45
|
return (
|
|
63
|
-
<View style={[styles.container, { backgroundColor:
|
|
46
|
+
<View style={[styles.container, { backgroundColor: colors.card }, style]}>
|
|
47
|
+
{/* Icon */}
|
|
64
48
|
<View style={styles.iconSlot}>{effectiveIcon}</View>
|
|
49
|
+
{/* Text */}
|
|
65
50
|
<View style={styles.content}>
|
|
66
|
-
<Text style={[styles.title, { color:
|
|
51
|
+
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>{title}</Text>
|
|
67
52
|
{description ? (
|
|
68
|
-
<Text style={[styles.description, { color:
|
|
53
|
+
<Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>{description}</Text>
|
|
69
54
|
) : null}
|
|
70
55
|
</View>
|
|
71
56
|
</View>
|
|
@@ -76,11 +61,10 @@ const styles = StyleSheet.create({
|
|
|
76
61
|
container: {
|
|
77
62
|
flexDirection: 'row',
|
|
78
63
|
alignItems: 'flex-start',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
gap: s(10),
|
|
64
|
+
borderRadius: RADIUS.lg,
|
|
65
|
+
gap: s(8),
|
|
66
|
+
paddingVertical: vs(8),
|
|
67
|
+
paddingHorizontal: s(10),
|
|
84
68
|
},
|
|
85
69
|
iconSlot: {
|
|
86
70
|
marginTop: vs(1),
|
|
@@ -92,12 +76,11 @@ const styles = StyleSheet.create({
|
|
|
92
76
|
title: {
|
|
93
77
|
fontFamily: 'Poppins-Medium',
|
|
94
78
|
fontSize: ms(13),
|
|
95
|
-
lineHeight: ms(
|
|
79
|
+
lineHeight: ms(19),
|
|
96
80
|
},
|
|
97
81
|
description: {
|
|
98
82
|
fontFamily: 'Poppins-Regular',
|
|
99
83
|
fontSize: ms(12),
|
|
100
84
|
lineHeight: ms(17),
|
|
101
|
-
opacity: 0.85,
|
|
102
85
|
},
|
|
103
86
|
})
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from 'react-native'
|
|
13
13
|
|
|
14
14
|
const nativeDriver = Platform.OS !== 'web'
|
|
15
|
-
import {
|
|
15
|
+
import { impactMedium } from '../../utils/haptics'
|
|
16
16
|
import { useTheme } from '../../theme'
|
|
17
17
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
18
18
|
import { renderIcon } from '../../utils/icons'
|
|
@@ -73,15 +73,15 @@ export function Button({
|
|
|
73
73
|
|
|
74
74
|
const handlePressIn = () => {
|
|
75
75
|
if (isDisabled) return
|
|
76
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver,
|
|
76
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, stiffness: 600, damping: 35, mass: 0.8 }).start()
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const handlePressOut = () => {
|
|
80
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver,
|
|
80
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, stiffness: 280, damping: 22, mass: 0.8 }).start()
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
84
|
-
|
|
84
|
+
impactMedium()
|
|
85
85
|
onPress?.(e)
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -135,7 +135,16 @@ export function Button({
|
|
|
135
135
|
{...props}
|
|
136
136
|
>
|
|
137
137
|
{loading ? (
|
|
138
|
-
|
|
138
|
+
<>
|
|
139
|
+
<ActivityIndicator size="small" color={spinnerColor} style={{ marginRight: s(6) }} />
|
|
140
|
+
<Text
|
|
141
|
+
style={[styles.label, labelVariantStyle, labelSizeStyles[size], styles.labelLoading]}
|
|
142
|
+
allowFontScaling={true}
|
|
143
|
+
numberOfLines={1}
|
|
144
|
+
>
|
|
145
|
+
{label}
|
|
146
|
+
</Text>
|
|
147
|
+
</>
|
|
139
148
|
) : (
|
|
140
149
|
<>
|
|
141
150
|
{effectiveIcon && iconPosition === 'left' && <>{effectiveIcon}</>}
|
|
@@ -156,7 +165,7 @@ export function Button({
|
|
|
156
165
|
|
|
157
166
|
const styles = StyleSheet.create({
|
|
158
167
|
base: {
|
|
159
|
-
borderRadius: RADIUS.
|
|
168
|
+
borderRadius: RADIUS.md, // 14px — Airbnb-aligned rounded rect (not pill)
|
|
160
169
|
alignItems: 'center',
|
|
161
170
|
justifyContent: 'center',
|
|
162
171
|
flexDirection: 'row',
|
|
@@ -174,4 +183,7 @@ const styles = StyleSheet.create({
|
|
|
174
183
|
labelWithIcon: {
|
|
175
184
|
marginHorizontal: s(6),
|
|
176
185
|
},
|
|
186
|
+
labelLoading: {
|
|
187
|
+
opacity: 0.6,
|
|
188
|
+
},
|
|
177
189
|
})
|
|
@@ -3,6 +3,7 @@ import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle, TextStyl
|
|
|
3
3
|
import { impactLight } from '../../utils/haptics'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
6
|
+
import { RADIUS } from '../../tokens'
|
|
6
7
|
|
|
7
8
|
const nativeDriver = Platform.OS !== 'web'
|
|
8
9
|
|
|
@@ -51,8 +52,9 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
51
52
|
Animated.spring(scale, {
|
|
52
53
|
toValue: 0.98,
|
|
53
54
|
useNativeDriver: nativeDriver,
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
stiffness: 400,
|
|
56
|
+
damping: 30,
|
|
57
|
+
mass: 1.0,
|
|
56
58
|
}).start()
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -61,8 +63,9 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
61
63
|
Animated.spring(scale, {
|
|
62
64
|
toValue: 1,
|
|
63
65
|
useNativeDriver: nativeDriver,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
stiffness: 250,
|
|
67
|
+
damping: 24,
|
|
68
|
+
mass: 1.0,
|
|
66
69
|
}).start()
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -77,10 +80,10 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
77
80
|
backgroundColor: colors.card,
|
|
78
81
|
borderColor: colors.border,
|
|
79
82
|
shadowColor: '#000',
|
|
80
|
-
shadowOffset: { width: 0, height:
|
|
81
|
-
shadowOpacity: 0.
|
|
82
|
-
shadowRadius:
|
|
83
|
-
elevation:
|
|
83
|
+
shadowOffset: { width: 0, height: 6 },
|
|
84
|
+
shadowOpacity: 0.10,
|
|
85
|
+
shadowRadius: 16,
|
|
86
|
+
elevation: 4,
|
|
84
87
|
},
|
|
85
88
|
outlined: {
|
|
86
89
|
backgroundColor: colors.card,
|
|
@@ -147,7 +150,7 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
147
150
|
|
|
148
151
|
const styles = StyleSheet.create({
|
|
149
152
|
card: {
|
|
150
|
-
borderRadius:
|
|
153
|
+
borderRadius: RADIUS.md, // 14px — Airbnb property card spec
|
|
151
154
|
borderWidth: 1,
|
|
152
155
|
},
|
|
153
156
|
header: {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type BottomSheetBackdropProps,
|
|
8
8
|
} from '@gorhom/bottom-sheet'
|
|
9
9
|
import { Feather } from '@expo/vector-icons'
|
|
10
|
-
import {
|
|
10
|
+
import { impactMedium, notificationSuccess, selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
11
11
|
import { useTheme } from '../../theme'
|
|
12
12
|
import { Button } from '../Button'
|
|
13
13
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
@@ -38,7 +38,7 @@ export function ConfirmDialog({
|
|
|
38
38
|
|
|
39
39
|
useEffect(() => {
|
|
40
40
|
if (visible) {
|
|
41
|
-
|
|
41
|
+
impactMedium()
|
|
42
42
|
ref.current?.present()
|
|
43
43
|
} else {
|
|
44
44
|
ref.current?.dismiss()
|
|
@@ -78,7 +78,7 @@ export function ConfirmDialog({
|
|
|
78
78
|
label={confirmLabel}
|
|
79
79
|
variant={confirmVariant}
|
|
80
80
|
fullWidth
|
|
81
|
-
onPress={onConfirm}
|
|
81
|
+
onPress={() => { notificationSuccess(); onConfirm() }}
|
|
82
82
|
icon={
|
|
83
83
|
<Feather
|
|
84
84
|
name={confirmVariant === 'destructive' ? 'trash-2' : 'check'}
|
|
@@ -95,7 +95,7 @@ export function ConfirmDialog({
|
|
|
95
95
|
label={cancelLabel}
|
|
96
96
|
variant="secondary"
|
|
97
97
|
fullWidth
|
|
98
|
-
onPress={onCancel}
|
|
98
|
+
onPress={() => { hapticSelection(); onCancel() }}
|
|
99
99
|
icon={<Feather name="x" size={15} color={colors.foreground} />}
|
|
100
100
|
/>
|
|
101
101
|
</View>
|
|
@@ -12,6 +12,13 @@ const variantFontSize: Record<CurrencyDisplayVariant, number> = {
|
|
|
12
12
|
small: ms(14),
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const variantLetterSpacing: Record<CurrencyDisplayVariant, number> = {
|
|
16
|
+
hero: -2,
|
|
17
|
+
large: -1,
|
|
18
|
+
medium: -0.5,
|
|
19
|
+
small: 0,
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export interface CurrencyDisplayProps {
|
|
16
23
|
value: number | string
|
|
17
24
|
/** Symbol prepended to the formatted value. Defaults to `'$'`. */
|
|
@@ -47,11 +54,12 @@ export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, tex
|
|
|
47
54
|
const formatted = formatValue(value, prefix, showDecimals)
|
|
48
55
|
const baseFontSize = variant ? variantFontSize[variant] : ms(56)
|
|
49
56
|
const fontSize = maxFontSize ?? baseFontSize
|
|
57
|
+
const letterSpacing = variant ? variantLetterSpacing[variant] : -2
|
|
50
58
|
|
|
51
59
|
return (
|
|
52
60
|
<View style={[styles.container, style]}>
|
|
53
61
|
<Text
|
|
54
|
-
style={[styles.amount, { color: textColor ?? colors.foreground, fontSize }]}
|
|
62
|
+
style={[styles.amount, { color: textColor ?? colors.foreground, fontSize, letterSpacing }]}
|
|
55
63
|
allowFontScaling={true}
|
|
56
64
|
numberOfLines={autoScale ? 1 : undefined}
|
|
57
65
|
adjustsFontSizeToFit={autoScale}
|
|
@@ -64,9 +72,12 @@ export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, tex
|
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
const styles = StyleSheet.create({
|
|
67
|
-
container: {
|
|
75
|
+
container: {
|
|
76
|
+
alignSelf: 'flex-start',
|
|
77
|
+
},
|
|
68
78
|
amount: {
|
|
69
79
|
fontFamily: 'Poppins-Bold',
|
|
70
|
-
|
|
80
|
+
includeFontPadding: false,
|
|
81
|
+
textAlignVertical: 'top',
|
|
71
82
|
},
|
|
72
83
|
})
|
|
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
5
|
import { renderIcon } from '../../utils/icons'
|
|
6
|
+
import { Button } from '../Button'
|
|
6
7
|
|
|
7
8
|
export interface EmptyStateProps {
|
|
8
9
|
icon?: React.ReactNode
|
|
@@ -15,13 +16,18 @@ export interface EmptyStateProps {
|
|
|
15
16
|
iconColor?: string
|
|
16
17
|
title: string
|
|
17
18
|
description?: string
|
|
19
|
+
/** Custom action node. Use `actionLabel` + `onAction` for a pre-built primary Button. */
|
|
18
20
|
action?: React.ReactNode
|
|
21
|
+
/** Label for a convenience primary Button rendered below description. Ignored in compact size. */
|
|
22
|
+
actionLabel?: string
|
|
23
|
+
/** Called when the convenience action Button is pressed. Required when `actionLabel` is set. */
|
|
24
|
+
onAction?: () => void
|
|
19
25
|
/** `compact` hides description/action and uses tighter spacing and a smaller icon. */
|
|
20
26
|
size?: 'default' | 'compact'
|
|
21
27
|
style?: ViewStyle
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export function EmptyState({ icon, iconName, iconColor, title, description, action, size = 'default', style }: EmptyStateProps) {
|
|
30
|
+
export function EmptyState({ icon, iconName, iconColor, title, description, action, actionLabel, onAction, size = 'default', style }: EmptyStateProps) {
|
|
25
31
|
const { colors } = useTheme()
|
|
26
32
|
const isCompact = size === 'compact'
|
|
27
33
|
|
|
@@ -60,7 +66,13 @@ export function EmptyState({ icon, iconName, iconColor, title, description, acti
|
|
|
60
66
|
<Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>{description}</Text>
|
|
61
67
|
) : null}
|
|
62
68
|
</View>
|
|
63
|
-
{
|
|
69
|
+
{!isCompact && (action ? (
|
|
70
|
+
<View style={styles.action}>{action}</View>
|
|
71
|
+
) : actionLabel && onAction ? (
|
|
72
|
+
<View style={styles.action}>
|
|
73
|
+
<Button label={actionLabel} variant="primary" onPress={onAction} />
|
|
74
|
+
</View>
|
|
75
|
+
) : null)}
|
|
64
76
|
</View>
|
|
65
77
|
)
|
|
66
78
|
}
|
|
@@ -72,12 +84,8 @@ const styles = StyleSheet.create({
|
|
|
72
84
|
borderWidth: 1,
|
|
73
85
|
borderStyle: 'dashed',
|
|
74
86
|
borderRadius: ms(12),
|
|
75
|
-
padding: s(32),
|
|
76
|
-
gap: vs(16),
|
|
77
87
|
},
|
|
78
88
|
containerCompact: {
|
|
79
|
-
padding: s(20),
|
|
80
|
-
gap: vs(10),
|
|
81
89
|
},
|
|
82
90
|
iconWrapper: {
|
|
83
91
|
width: s(80),
|
|
@@ -85,16 +93,20 @@ const styles = StyleSheet.create({
|
|
|
85
93
|
borderRadius: ms(20),
|
|
86
94
|
alignItems: 'center',
|
|
87
95
|
justifyContent: 'center',
|
|
96
|
+
marginTop: s(32),
|
|
88
97
|
},
|
|
89
98
|
iconWrapperCompact: {
|
|
90
99
|
width: s(56),
|
|
91
100
|
height: s(56),
|
|
92
101
|
borderRadius: ms(14),
|
|
102
|
+
marginTop: s(20),
|
|
93
103
|
},
|
|
94
104
|
textWrapper: {
|
|
95
105
|
alignItems: 'center',
|
|
96
106
|
gap: vs(8),
|
|
97
107
|
maxWidth: s(320),
|
|
108
|
+
paddingHorizontal: s(32),
|
|
109
|
+
marginTop: vs(16),
|
|
98
110
|
},
|
|
99
111
|
title: {
|
|
100
112
|
fontFamily: 'Poppins-Medium',
|
|
@@ -103,6 +115,7 @@ const styles = StyleSheet.create({
|
|
|
103
115
|
},
|
|
104
116
|
titleCompact: {
|
|
105
117
|
fontSize: ms(15),
|
|
118
|
+
marginTop: vs(10),
|
|
106
119
|
},
|
|
107
120
|
description: {
|
|
108
121
|
fontFamily: 'Poppins-Regular',
|
|
@@ -112,5 +125,7 @@ const styles = StyleSheet.create({
|
|
|
112
125
|
},
|
|
113
126
|
action: {
|
|
114
127
|
marginTop: vs(8),
|
|
128
|
+
marginBottom: s(32),
|
|
129
|
+
paddingHorizontal: s(32),
|
|
115
130
|
},
|
|
116
131
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
|
|
1
|
+
import React, { useState, useRef } from 'react'
|
|
2
|
+
import { TextInput, View, Text, Animated, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform, Easing } from 'react-native'
|
|
3
3
|
import { AntDesign } from '@expo/vector-icons'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms } from '../../utils/scaling'
|
|
@@ -16,6 +16,8 @@ export interface InputProps extends TextInputProps {
|
|
|
16
16
|
error?: string
|
|
17
17
|
/** Helper text shown below the input when there is no error. */
|
|
18
18
|
hint?: string
|
|
19
|
+
/** Disabled visual state — dimmed appearance, not editable. Also sets `editable={false}`. */
|
|
20
|
+
disabled?: boolean
|
|
19
21
|
/** Text or component rendered before the input text. */
|
|
20
22
|
prefix?: React.ReactNode
|
|
21
23
|
/** Text or component rendered after the input text. */
|
|
@@ -46,11 +48,13 @@ export interface InputProps extends TextInputProps {
|
|
|
46
48
|
inputWrapperStyle?: ViewStyle
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
|
|
51
|
+
export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, style, onFocus, onBlur, secureTextEntry, editable, ...props }: InputProps) {
|
|
50
52
|
const { colors } = useTheme()
|
|
51
53
|
const [focused, setFocused] = useState(false)
|
|
52
54
|
const [showPassword, setShowPassword] = useState(false)
|
|
55
|
+
const focusAnim = useRef(new Animated.Value(0)).current
|
|
53
56
|
|
|
57
|
+
const isDisabled = disabled || editable === false
|
|
54
58
|
const isPassword = type === 'password'
|
|
55
59
|
const effectiveSecure = isPassword ? !showPassword : secureTextEntry
|
|
56
60
|
|
|
@@ -68,18 +72,19 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
|
|
|
68
72
|
: suffix
|
|
69
73
|
|
|
70
74
|
return (
|
|
71
|
-
<View style={[styles.container, containerStyle]}>
|
|
75
|
+
<View style={[styles.container, isDisabled && styles.containerDisabled, containerStyle]}>
|
|
72
76
|
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
73
|
-
<View
|
|
77
|
+
<Animated.View
|
|
74
78
|
style={[
|
|
75
79
|
styles.inputWrapper,
|
|
76
80
|
{
|
|
77
81
|
borderColor: error
|
|
78
82
|
? colors.destructive
|
|
79
|
-
:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
: focusAnim.interpolate({
|
|
84
|
+
inputRange: [0, 1],
|
|
85
|
+
outputRange: [colors.border, colors.primary],
|
|
86
|
+
}),
|
|
87
|
+
backgroundColor: isDisabled ? colors.surface : colors.background,
|
|
83
88
|
},
|
|
84
89
|
inputWrapperStyle,
|
|
85
90
|
]}
|
|
@@ -104,15 +109,18 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
|
|
|
104
109
|
]}
|
|
105
110
|
onFocus={(e) => {
|
|
106
111
|
setFocused(true)
|
|
112
|
+
Animated.timing(focusAnim, { toValue: 1, duration: 120, easing: Easing.out(Easing.ease), useNativeDriver: false }).start()
|
|
107
113
|
onFocus?.(e)
|
|
108
114
|
}}
|
|
109
115
|
onBlur={(e) => {
|
|
110
116
|
setFocused(false)
|
|
117
|
+
Animated.timing(focusAnim, { toValue: 0, duration: 80, easing: Easing.out(Easing.ease), useNativeDriver: false }).start()
|
|
111
118
|
onBlur?.(e)
|
|
112
119
|
}}
|
|
113
120
|
placeholderTextColor={colors.foregroundMuted}
|
|
114
121
|
allowFontScaling={true}
|
|
115
122
|
secureTextEntry={effectiveSecure}
|
|
123
|
+
editable={isDisabled ? false : editable}
|
|
116
124
|
{...props}
|
|
117
125
|
/>
|
|
118
126
|
{effectiveSuffix ? (
|
|
@@ -124,7 +132,7 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
|
|
|
124
132
|
<View style={styles.suffixContainer}>{effectiveSuffix}</View>
|
|
125
133
|
)
|
|
126
134
|
) : null}
|
|
127
|
-
</View>
|
|
135
|
+
</Animated.View>
|
|
128
136
|
{error ? (
|
|
129
137
|
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
130
138
|
) : null}
|
|
@@ -139,6 +147,9 @@ const styles = StyleSheet.create({
|
|
|
139
147
|
container: {
|
|
140
148
|
gap: vs(8),
|
|
141
149
|
},
|
|
150
|
+
containerDisabled: {
|
|
151
|
+
opacity: 0.6,
|
|
152
|
+
},
|
|
142
153
|
label: {
|
|
143
154
|
fontFamily: 'Poppins-Medium',
|
|
144
155
|
fontSize: ms(14), // caption size for input labels
|
|
@@ -14,6 +14,7 @@ import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
|
14
14
|
import { useTheme } from '../../theme'
|
|
15
15
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
16
16
|
import { renderIcon } from '../../utils/icons'
|
|
17
|
+
import { RADIUS } from '../../tokens'
|
|
17
18
|
|
|
18
19
|
const nativeDriver = Platform.OS !== 'web'
|
|
19
20
|
|
|
@@ -109,8 +110,9 @@ export function ListItem({
|
|
|
109
110
|
Animated.spring(scale, {
|
|
110
111
|
toValue: 0.97,
|
|
111
112
|
useNativeDriver: nativeDriver,
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
stiffness: 350,
|
|
114
|
+
damping: 28,
|
|
115
|
+
mass: 0.9,
|
|
114
116
|
}).start()
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -118,8 +120,9 @@ export function ListItem({
|
|
|
118
120
|
Animated.spring(scale, {
|
|
119
121
|
toValue: 1,
|
|
120
122
|
useNativeDriver: nativeDriver,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
stiffness: 220,
|
|
124
|
+
damping: 20,
|
|
125
|
+
mass: 0.9,
|
|
123
126
|
}).start()
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -140,7 +143,7 @@ export function ListItem({
|
|
|
140
143
|
variant === 'card'
|
|
141
144
|
? {
|
|
142
145
|
backgroundColor: colors.card,
|
|
143
|
-
borderRadius:
|
|
146
|
+
borderRadius: RADIUS.md,
|
|
144
147
|
borderWidth: 1,
|
|
145
148
|
borderColor: colors.border,
|
|
146
149
|
shadowColor: '#000',
|
|
@@ -216,7 +219,10 @@ export function ListItem({
|
|
|
216
219
|
<View
|
|
217
220
|
style={[
|
|
218
221
|
styles.separator,
|
|
219
|
-
{
|
|
222
|
+
{
|
|
223
|
+
backgroundColor: colors.border,
|
|
224
|
+
marginLeft: effectiveLeft ? s(44) + s(12) : 0
|
|
225
|
+
},
|
|
220
226
|
]}
|
|
221
227
|
/>
|
|
222
228
|
) : null}
|
|
@@ -228,7 +234,7 @@ const styles = StyleSheet.create({
|
|
|
228
234
|
container: {
|
|
229
235
|
flexDirection: 'row',
|
|
230
236
|
alignItems: 'center',
|
|
231
|
-
paddingHorizontal:
|
|
237
|
+
paddingHorizontal: 0,
|
|
232
238
|
paddingVertical: vs(10),
|
|
233
239
|
gap: s(12),
|
|
234
240
|
},
|
|
@@ -274,7 +280,7 @@ const styles = StyleSheet.create({
|
|
|
274
280
|
},
|
|
275
281
|
separator: {
|
|
276
282
|
height: StyleSheet.hairlineWidth,
|
|
277
|
-
marginRight:
|
|
283
|
+
marginRight: 0,
|
|
278
284
|
},
|
|
279
285
|
disabled: {
|
|
280
286
|
opacity: 0.45,
|