@retray-dev/ui-kit 5.2.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 +500 -140
- package/EXAMPLES.md +666 -0
- package/README.md +3 -3
- package/dist/index.d.mts +253 -49
- package/dist/index.d.ts +253 -49
- package/dist/index.js +955 -610
- package/dist/index.mjs +886 -552
- package/package.json +9 -3
- package/src/components/Accordion/Accordion.tsx +31 -4
- package/src/components/AlertBanner/AlertBanner.tsx +16 -33
- package/src/components/Avatar/Avatar.tsx +21 -7
- package/src/components/Button/Button.tsx +34 -13
- package/src/components/ButtonGroup/ButtonGroup.tsx +60 -0
- package/src/components/ButtonGroup/index.ts +1 -0
- package/src/components/Card/Card.tsx +12 -9
- package/src/components/Chip/Chip.tsx +8 -1
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +4 -4
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +38 -5
- package/src/components/DetailRow/DetailRow.tsx +140 -0
- package/src/components/DetailRow/index.ts +1 -0
- package/src/components/EmptyState/EmptyState.tsx +21 -6
- package/src/components/Input/Input.tsx +21 -10
- package/src/components/LabelValue/LabelValue.tsx +25 -4
- 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/MonthPicker/MonthPicker.tsx +18 -6
- package/src/components/Select/Select.tsx +1 -1
- package/src/components/Separator/Separator.tsx +2 -0
- package/src/components/Sheet/Sheet.tsx +165 -36
- package/src/components/Sheet/index.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +4 -4
- package/src/components/Textarea/Textarea.tsx +66 -29
- 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 +6 -0
- package/src/theme/colors.ts +3 -0
- package/src/theme/types.ts +11 -0
- package/src/tokens.ts +4 -4
- package/src/utils/typography.ts +24 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
|
+
import { renderIcon } from '../../utils/icons'
|
|
6
|
+
|
|
7
|
+
export type DetailRowSeparator = 'dotted' | 'solid' | 'dashed' | 'none'
|
|
8
|
+
export type DetailRowLabelWeight = 'normal' | 'medium' | 'semibold' | 'bold'
|
|
9
|
+
|
|
10
|
+
const weightMap: Record<DetailRowLabelWeight, string> = {
|
|
11
|
+
normal: 'Poppins-Regular',
|
|
12
|
+
medium: 'Poppins-Medium',
|
|
13
|
+
semibold: 'Poppins-SemiBold',
|
|
14
|
+
bold: 'Poppins-Bold',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DetailRowProps {
|
|
18
|
+
label: React.ReactNode
|
|
19
|
+
value: string
|
|
20
|
+
/** Dotted/dashed/solid line between label and value. Defaults to 'dotted'. */
|
|
21
|
+
separator?: DetailRowSeparator
|
|
22
|
+
labelWeight?: DetailRowLabelWeight
|
|
23
|
+
/** Semantic color key or hex string for value text. */
|
|
24
|
+
valueColor?: string
|
|
25
|
+
/** Node rendered left of the label (e.g. Avatar, Icon). */
|
|
26
|
+
leftIcon?: React.ReactNode
|
|
27
|
+
/** Icon name from @expo/vector-icons rendered left of label. Takes precedence over leftIcon. */
|
|
28
|
+
leftIconName?: string
|
|
29
|
+
/** Override left icon color. Defaults to foregroundMuted. */
|
|
30
|
+
leftIconColor?: string
|
|
31
|
+
/** Icon name from @expo/vector-icons rendered right of value. */
|
|
32
|
+
rightIconName?: string
|
|
33
|
+
/** Override right icon color. Defaults to foregroundMuted. */
|
|
34
|
+
rightIconColor?: string
|
|
35
|
+
style?: ViewStyle
|
|
36
|
+
labelStyle?: TextStyle
|
|
37
|
+
valueStyle?: TextStyle
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DetailRow({
|
|
41
|
+
label,
|
|
42
|
+
value,
|
|
43
|
+
separator = 'dotted',
|
|
44
|
+
labelWeight = 'normal',
|
|
45
|
+
valueColor,
|
|
46
|
+
leftIcon,
|
|
47
|
+
leftIconName,
|
|
48
|
+
leftIconColor,
|
|
49
|
+
rightIconName,
|
|
50
|
+
rightIconColor,
|
|
51
|
+
style,
|
|
52
|
+
labelStyle,
|
|
53
|
+
valueStyle,
|
|
54
|
+
}: DetailRowProps) {
|
|
55
|
+
const { colors } = useTheme()
|
|
56
|
+
|
|
57
|
+
const resolvedLeftIcon = leftIconName
|
|
58
|
+
? renderIcon(leftIconName, ms(14), leftIconColor ?? colors.foregroundMuted)
|
|
59
|
+
: leftIcon
|
|
60
|
+
|
|
61
|
+
const resolvedRightIcon = rightIconName
|
|
62
|
+
? renderIcon(rightIconName, ms(14), rightIconColor ?? colors.foregroundMuted)
|
|
63
|
+
: null
|
|
64
|
+
|
|
65
|
+
const separatorStyle: ViewStyle | null =
|
|
66
|
+
separator === 'none'
|
|
67
|
+
? null
|
|
68
|
+
: {
|
|
69
|
+
flex: 1,
|
|
70
|
+
height: 1,
|
|
71
|
+
borderBottomWidth: 1,
|
|
72
|
+
borderStyle: separator,
|
|
73
|
+
borderColor: 'rgba(128,128,128,0.3)',
|
|
74
|
+
marginHorizontal: s(4),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<View style={[styles.row, style]}>
|
|
79
|
+
<View style={styles.labelSide}>
|
|
80
|
+
{resolvedLeftIcon ? <View style={styles.icon}>{resolvedLeftIcon}</View> : null}
|
|
81
|
+
{typeof label === 'string' ? (
|
|
82
|
+
<Text
|
|
83
|
+
style={[styles.labelText, { color: colors.foregroundMuted, fontFamily: weightMap[labelWeight] }, labelStyle]}
|
|
84
|
+
allowFontScaling={true}
|
|
85
|
+
>
|
|
86
|
+
{label}
|
|
87
|
+
</Text>
|
|
88
|
+
) : (
|
|
89
|
+
label
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
{separatorStyle ? <View style={separatorStyle} /> : <View style={styles.spacer} />}
|
|
93
|
+
<View style={styles.valueSide}>
|
|
94
|
+
<Text
|
|
95
|
+
style={[styles.valueText, { color: valueColor ?? colors.foreground }, valueStyle]}
|
|
96
|
+
allowFontScaling={true}
|
|
97
|
+
>
|
|
98
|
+
{value}
|
|
99
|
+
</Text>
|
|
100
|
+
{resolvedRightIcon ? <View style={styles.icon}>{resolvedRightIcon}</View> : null}
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
row: {
|
|
108
|
+
flexDirection: 'row',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
gap: s(4),
|
|
111
|
+
},
|
|
112
|
+
labelSide: {
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: s(4),
|
|
116
|
+
flexShrink: 0,
|
|
117
|
+
},
|
|
118
|
+
icon: {
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
justifyContent: 'center',
|
|
121
|
+
},
|
|
122
|
+
spacer: {
|
|
123
|
+
flex: 1,
|
|
124
|
+
},
|
|
125
|
+
labelText: {
|
|
126
|
+
fontSize: ms(13),
|
|
127
|
+
lineHeight: mvs(18),
|
|
128
|
+
},
|
|
129
|
+
valueSide: {
|
|
130
|
+
flexDirection: 'row',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
gap: s(4),
|
|
133
|
+
flexShrink: 0,
|
|
134
|
+
},
|
|
135
|
+
valueText: {
|
|
136
|
+
fontFamily: 'Poppins-SemiBold',
|
|
137
|
+
fontSize: ms(13),
|
|
138
|
+
lineHeight: mvs(18),
|
|
139
|
+
},
|
|
140
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DetailRow'
|
|
@@ -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
|
|
@@ -2,21 +2,33 @@ import React from 'react'
|
|
|
2
2
|
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
|
+
import { renderIcon } from '../../utils/icons'
|
|
5
6
|
|
|
6
7
|
export interface LabelValueProps {
|
|
7
8
|
label: string
|
|
8
9
|
value: string | React.ReactNode
|
|
10
|
+
/** Icon name from @expo/vector-icons rendered left of label. */
|
|
11
|
+
iconName?: string
|
|
12
|
+
/** Override icon color. Defaults to foregroundMuted. */
|
|
13
|
+
iconColor?: string
|
|
9
14
|
style?: ViewStyle
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export function LabelValue({ label, value, style }: LabelValueProps) {
|
|
17
|
+
export function LabelValue({ label, value, iconName, iconColor, style }: LabelValueProps) {
|
|
13
18
|
const { colors } = useTheme()
|
|
14
19
|
|
|
20
|
+
const resolvedIcon = iconName
|
|
21
|
+
? renderIcon(iconName, ms(14), iconColor ?? colors.foregroundMuted)
|
|
22
|
+
: null
|
|
23
|
+
|
|
15
24
|
return (
|
|
16
25
|
<View style={[styles.container, style]}>
|
|
17
|
-
<
|
|
18
|
-
{
|
|
19
|
-
|
|
26
|
+
<View style={styles.labelSide}>
|
|
27
|
+
{resolvedIcon ? <View style={styles.icon}>{resolvedIcon}</View> : null}
|
|
28
|
+
<Text style={[styles.label, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
29
|
+
{label}
|
|
30
|
+
</Text>
|
|
31
|
+
</View>
|
|
20
32
|
{typeof value === 'string' ? (
|
|
21
33
|
<Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
|
|
22
34
|
{value}
|
|
@@ -35,6 +47,15 @@ const styles = StyleSheet.create({
|
|
|
35
47
|
alignItems: 'center',
|
|
36
48
|
gap: s(12),
|
|
37
49
|
},
|
|
50
|
+
labelSide: {
|
|
51
|
+
flexDirection: 'row',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
gap: s(4),
|
|
54
|
+
},
|
|
55
|
+
icon: {
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
justifyContent: 'center',
|
|
58
|
+
},
|
|
38
59
|
label: {
|
|
39
60
|
fontFamily: 'Poppins-Regular',
|
|
40
61
|
fontSize: ms(13),
|
|
@@ -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,
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Animated,
|
|
5
|
+
View,
|
|
6
|
+
Text,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
TextStyle,
|
|
10
|
+
Platform,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { Entypo } from '@expo/vector-icons'
|
|
13
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
14
|
+
import { useTheme } from '../../theme'
|
|
15
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
16
|
+
import { renderIcon } from '../../utils/icons'
|
|
17
|
+
import { RADIUS } from '../../tokens'
|
|
18
|
+
|
|
19
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
20
|
+
|
|
21
|
+
export type MenuItemVariant = 'plain' | 'card'
|
|
22
|
+
|
|
23
|
+
export interface MenuItemProps {
|
|
24
|
+
label: string
|
|
25
|
+
/**
|
|
26
|
+
* Icon name from `@expo/vector-icons` rendered on the left.
|
|
27
|
+
* See https://icons.expo.fyi.
|
|
28
|
+
*/
|
|
29
|
+
iconName?: string
|
|
30
|
+
/** Custom icon node rendered on the left. */
|
|
31
|
+
icon?: React.ReactNode
|
|
32
|
+
/** Override icon color. Defaults to `foreground`. */
|
|
33
|
+
iconColor?: string
|
|
34
|
+
/**
|
|
35
|
+
* Custom content rendered on the right.
|
|
36
|
+
* When provided, replaces the default chevron.
|
|
37
|
+
* Use for checkboxes, switches, badges, or other controls.
|
|
38
|
+
*/
|
|
39
|
+
rightRender?: React.ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* Show chevron on the right. Defaults to `true`.
|
|
42
|
+
* Ignored when `rightRender` is provided.
|
|
43
|
+
*/
|
|
44
|
+
showChevron?: boolean
|
|
45
|
+
onPress: () => void
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* - `plain` (default): no background — sits inside a parent surface.
|
|
49
|
+
* - `card`: standalone surface with background + border.
|
|
50
|
+
*/
|
|
51
|
+
variant?: MenuItemVariant
|
|
52
|
+
/** Visual separator line at the bottom. */
|
|
53
|
+
showSeparator?: boolean
|
|
54
|
+
/** Style applied to the outer container. */
|
|
55
|
+
style?: ViewStyle
|
|
56
|
+
/** Style applied to the label Text. */
|
|
57
|
+
labelStyle?: TextStyle
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function MenuItem({
|
|
61
|
+
label,
|
|
62
|
+
iconName,
|
|
63
|
+
icon,
|
|
64
|
+
iconColor,
|
|
65
|
+
rightRender,
|
|
66
|
+
showChevron = true,
|
|
67
|
+
onPress,
|
|
68
|
+
disabled = false,
|
|
69
|
+
variant = 'plain',
|
|
70
|
+
showSeparator = false,
|
|
71
|
+
style,
|
|
72
|
+
labelStyle,
|
|
73
|
+
}: MenuItemProps) {
|
|
74
|
+
const { colors } = useTheme()
|
|
75
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
76
|
+
|
|
77
|
+
const handlePressIn = () => {
|
|
78
|
+
if (disabled) return
|
|
79
|
+
Animated.spring(scale, {
|
|
80
|
+
toValue: 0.97,
|
|
81
|
+
useNativeDriver: nativeDriver,
|
|
82
|
+
stiffness: 350,
|
|
83
|
+
damping: 28,
|
|
84
|
+
mass: 0.9,
|
|
85
|
+
}).start()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handlePressOut = () => {
|
|
89
|
+
Animated.spring(scale, {
|
|
90
|
+
toValue: 1,
|
|
91
|
+
useNativeDriver: nativeDriver,
|
|
92
|
+
stiffness: 220,
|
|
93
|
+
damping: 20,
|
|
94
|
+
mass: 0.9,
|
|
95
|
+
}).start()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handlePress = () => {
|
|
99
|
+
hapticSelection()
|
|
100
|
+
onPress()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const resolvedIcon: React.ReactNode = iconName
|
|
104
|
+
? renderIcon(iconName, 22, iconColor ?? colors.foreground)
|
|
105
|
+
: icon
|
|
106
|
+
|
|
107
|
+
const cardStyle: ViewStyle =
|
|
108
|
+
variant === 'card'
|
|
109
|
+
? {
|
|
110
|
+
backgroundColor: colors.card,
|
|
111
|
+
borderRadius: RADIUS.md,
|
|
112
|
+
borderWidth: 1,
|
|
113
|
+
borderColor: colors.border,
|
|
114
|
+
shadowColor: '#000',
|
|
115
|
+
shadowOffset: { width: 0, height: 2 },
|
|
116
|
+
shadowOpacity: 0.06,
|
|
117
|
+
shadowRadius: 6,
|
|
118
|
+
elevation: 2,
|
|
119
|
+
}
|
|
120
|
+
: {}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={[styles.container, cardStyle, style]}
|
|
126
|
+
onPress={handlePress}
|
|
127
|
+
onPressIn={handlePressIn}
|
|
128
|
+
onPressOut={handlePressOut}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
activeOpacity={1}
|
|
131
|
+
touchSoundDisabled={true}
|
|
132
|
+
>
|
|
133
|
+
{resolvedIcon ? (
|
|
134
|
+
<View style={styles.iconContainer}>{resolvedIcon}</View>
|
|
135
|
+
) : null}
|
|
136
|
+
|
|
137
|
+
<Text
|
|
138
|
+
style={[styles.label, { color: colors.foreground }, labelStyle]}
|
|
139
|
+
numberOfLines={1}
|
|
140
|
+
allowFontScaling={true}
|
|
141
|
+
>
|
|
142
|
+
{label}
|
|
143
|
+
</Text>
|
|
144
|
+
|
|
145
|
+
{rightRender !== undefined ? (
|
|
146
|
+
<View
|
|
147
|
+
style={styles.rightContainer}
|
|
148
|
+
onStartShouldSetResponder={() => true}
|
|
149
|
+
onResponderRelease={() => {}}
|
|
150
|
+
>
|
|
151
|
+
{rightRender}
|
|
152
|
+
</View>
|
|
153
|
+
) : showChevron ? (
|
|
154
|
+
<Entypo name="chevron-right" size={18} color={colors.foregroundMuted} />
|
|
155
|
+
) : null}
|
|
156
|
+
</TouchableOpacity>
|
|
157
|
+
|
|
158
|
+
{showSeparator ? (
|
|
159
|
+
<View
|
|
160
|
+
style={[
|
|
161
|
+
styles.separator,
|
|
162
|
+
{
|
|
163
|
+
backgroundColor: colors.border,
|
|
164
|
+
marginLeft: resolvedIcon ? s(22) + s(12) : 0,
|
|
165
|
+
opacity: 0.6,
|
|
166
|
+
},
|
|
167
|
+
]}
|
|
168
|
+
/>
|
|
169
|
+
) : null}
|
|
170
|
+
</Animated.View>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const styles = StyleSheet.create({
|
|
175
|
+
container: {
|
|
176
|
+
flexDirection: 'row',
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
paddingHorizontal: 0,
|
|
179
|
+
paddingVertical: vs(16),
|
|
180
|
+
minHeight: vs(54),
|
|
181
|
+
gap: s(12),
|
|
182
|
+
},
|
|
183
|
+
iconContainer: {
|
|
184
|
+
width: s(22),
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
justifyContent: 'center',
|
|
187
|
+
flexShrink: 0,
|
|
188
|
+
},
|
|
189
|
+
label: {
|
|
190
|
+
fontFamily: 'Poppins-Medium',
|
|
191
|
+
fontSize: ms(15),
|
|
192
|
+
flex: 1,
|
|
193
|
+
},
|
|
194
|
+
rightContainer: {
|
|
195
|
+
alignItems: 'flex-end',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
flexShrink: 0,
|
|
198
|
+
},
|
|
199
|
+
separator: {
|
|
200
|
+
height: StyleSheet.hairlineWidth,
|
|
201
|
+
marginRight: 0,
|
|
202
|
+
},
|
|
203
|
+
disabled: {
|
|
204
|
+
opacity: 0.45,
|
|
205
|
+
},
|
|
206
|
+
})
|