@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.
Files changed (42) hide show
  1. package/COMPONENTS.md +500 -140
  2. package/EXAMPLES.md +666 -0
  3. package/README.md +3 -3
  4. package/dist/index.d.mts +253 -49
  5. package/dist/index.d.ts +253 -49
  6. package/dist/index.js +955 -610
  7. package/dist/index.mjs +886 -552
  8. package/package.json +9 -3
  9. package/src/components/Accordion/Accordion.tsx +31 -4
  10. package/src/components/AlertBanner/AlertBanner.tsx +16 -33
  11. package/src/components/Avatar/Avatar.tsx +21 -7
  12. package/src/components/Button/Button.tsx +34 -13
  13. package/src/components/ButtonGroup/ButtonGroup.tsx +60 -0
  14. package/src/components/ButtonGroup/index.ts +1 -0
  15. package/src/components/Card/Card.tsx +12 -9
  16. package/src/components/Chip/Chip.tsx +8 -1
  17. package/src/components/ConfirmDialog/ConfirmDialog.tsx +4 -4
  18. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +38 -5
  19. package/src/components/DetailRow/DetailRow.tsx +140 -0
  20. package/src/components/DetailRow/index.ts +1 -0
  21. package/src/components/EmptyState/EmptyState.tsx +21 -6
  22. package/src/components/Input/Input.tsx +21 -10
  23. package/src/components/LabelValue/LabelValue.tsx +25 -4
  24. package/src/components/ListItem/ListItem.tsx +14 -8
  25. package/src/components/MediaCard/MediaCard.tsx +1 -0
  26. package/src/components/MenuItem/MenuItem.tsx +206 -0
  27. package/src/components/MenuItem/index.ts +2 -0
  28. package/src/components/MonthPicker/MonthPicker.tsx +18 -6
  29. package/src/components/Select/Select.tsx +1 -1
  30. package/src/components/Separator/Separator.tsx +2 -0
  31. package/src/components/Sheet/Sheet.tsx +165 -36
  32. package/src/components/Sheet/index.ts +1 -1
  33. package/src/components/Tabs/Tabs.tsx +4 -4
  34. package/src/components/Textarea/Textarea.tsx +66 -29
  35. package/src/components/Toast/Toast.tsx +41 -267
  36. package/src/components/Toast/index.ts +1 -2
  37. package/src/components/Toggle/Toggle.tsx +2 -2
  38. package/src/index.ts +6 -0
  39. package/src/theme/colors.ts +3 -0
  40. package/src/theme/types.ts +11 -0
  41. package/src/tokens.ts +4 -4
  42. 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
- {action && !isCompact ? <View style={styles.action}>{action}</View> : null}
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
- : focused
80
- ? colors.primary
81
- : colors.border,
82
- backgroundColor: colors.background,
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
- <Text style={[styles.label, { color: colors.foregroundMuted }]} allowFontScaling={true}>
18
- {label}
19
- </Text>
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
- speed: 40,
113
- bounciness: 0,
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
- speed: 40,
122
- bounciness: 4,
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: 12,
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
- { backgroundColor: colors.border, marginLeft: effectiveLeft ? s(16) + s(44) + s(12) : s(16) },
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: s(16),
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: s(16),
283
+ marginRight: 0,
278
284
  },
279
285
  disabled: {
280
286
  opacity: 0.45,
@@ -229,6 +229,7 @@ const styles = StyleSheet.create({
229
229
  },
230
230
  meta: {
231
231
  paddingTop: vs(8),
232
+ paddingBottom: vs(4),
232
233
  gap: vs(2),
233
234
  },
234
235
  title: {
@@ -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
+ })
@@ -0,0 +1,2 @@
1
+ export { MenuItem } from './MenuItem'
2
+ export type { MenuItemProps, MenuItemVariant } from './MenuItem'