@retray-dev/ui-kit 1.8.0 → 2.5.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 (34) hide show
  1. package/COMPONENTS.md +150 -43
  2. package/dist/index.d.mts +80 -44
  3. package/dist/index.d.ts +80 -44
  4. package/dist/index.js +627 -457
  5. package/dist/index.mjs +626 -457
  6. package/package.json +8 -2
  7. package/src/components/Accordion/Accordion.tsx +4 -6
  8. package/src/components/Alert/Alert.tsx +3 -3
  9. package/src/components/AlertBanner/AlertBanner.tsx +85 -0
  10. package/src/components/{Alert → AlertBanner}/index.ts +2 -2
  11. package/src/components/Avatar/Avatar.tsx +1 -0
  12. package/src/components/Badge/Badge.tsx +45 -9
  13. package/src/components/Button/Button.tsx +5 -5
  14. package/src/components/Card/Card.tsx +90 -18
  15. package/src/components/Checkbox/Checkbox.tsx +4 -4
  16. package/src/components/Chip/Chip.tsx +36 -5
  17. package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
  18. package/src/components/EmptyState/EmptyState.tsx +2 -1
  19. package/src/components/Input/Input.tsx +107 -26
  20. package/src/components/ListItem/ListItem.tsx +157 -21
  21. package/src/components/MonthPicker/MonthPicker.tsx +3 -6
  22. package/src/components/RadioGroup/RadioGroup.tsx +2 -2
  23. package/src/components/Select/Select.tsx +200 -132
  24. package/src/components/Slider/Slider.tsx +64 -100
  25. package/src/components/Switch/Switch.tsx +22 -20
  26. package/src/components/Textarea/Textarea.tsx +16 -7
  27. package/src/components/Toast/Toast.tsx +23 -18
  28. package/src/components/Toggle/Toggle.tsx +36 -49
  29. package/src/index.ts +3 -2
  30. package/src/theme/ThemeProvider.tsx +11 -8
  31. package/src/theme/colors.ts +19 -18
  32. package/src/theme/types.ts +2 -0
  33. package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +0 -66
  34. package/src/components/CurrencyInputLarge/index.ts +0 -1
@@ -1,46 +1,105 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
3
+ import { AntDesign } from '@expo/vector-icons'
3
4
  import { useTheme } from '../../theme'
4
5
 
6
+ const webInputResetStyle: any =
7
+ Platform.OS === 'web'
8
+ ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
9
+ : {}
10
+
5
11
  export interface InputProps extends TextInputProps {
6
12
  label?: string
7
13
  /** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
8
14
  error?: string
9
15
  /** Helper text shown below the input when there is no error. */
10
16
  hint?: string
11
- /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
17
+ /** Text or component rendered before the input text. */
18
+ prefix?: React.ReactNode
19
+ /** Text or component rendered after the input text. */
20
+ suffix?: React.ReactNode
21
+ /** Style applied to prefix text if prefix is a string. */
22
+ prefixStyle?: TextStyle
23
+ /** Style applied to suffix text if suffix is a string. */
24
+ suffixStyle?: TextStyle
25
+ /** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
26
+ type?: 'text' | 'password'
27
+ /** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
12
28
  containerStyle?: ViewStyle
13
29
  }
14
30
 
15
- export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
31
+ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
16
32
  const { colors } = useTheme()
17
33
  const [focused, setFocused] = useState(false)
34
+ const [showPassword, setShowPassword] = useState(false)
35
+
36
+ const isPassword = type === 'password'
37
+ const effectiveSecure = isPassword ? !showPassword : secureTextEntry
38
+
39
+ // If type is password and no suffix is provided, add the toggle button
40
+ const effectiveSuffix = isPassword && !suffix ? (
41
+ <TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
42
+ <AntDesign name={showPassword ? 'eye' : 'eye-invisible'} size={20} color={colors.mutedForeground} />
43
+ </TouchableOpacity>
44
+ ) : suffix
18
45
 
19
46
  return (
20
47
  <View style={[styles.container, containerStyle]}>
21
48
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
22
- <TextInput
49
+ <View
23
50
  style={[
24
- styles.input,
51
+ styles.inputWrapper,
25
52
  {
26
- borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
27
- color: colors.foreground,
53
+ borderColor: error
54
+ ? colors.destructive
55
+ : focused
56
+ ? (colors.ring ?? colors.primary)
57
+ : colors.border,
28
58
  backgroundColor: colors.background,
29
59
  },
30
- style,
31
60
  ]}
32
- onFocus={(e) => {
33
- setFocused(true)
34
- onFocus?.(e)
35
- }}
36
- onBlur={(e) => {
37
- setFocused(false)
38
- onBlur?.(e)
39
- }}
40
- placeholderTextColor={colors.mutedForeground}
41
- allowFontScaling={true}
42
- {...props}
43
- />
61
+ >
62
+ {prefix ? (
63
+ typeof prefix === 'string' ? (
64
+ <Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
65
+ {prefix}
66
+ </Text>
67
+ ) : (
68
+ <View style={styles.prefixContainer}>{prefix}</View>
69
+ )
70
+ ) : null}
71
+ <TextInput
72
+ style={[
73
+ styles.input,
74
+ {
75
+ color: colors.foreground,
76
+ },
77
+ webInputResetStyle,
78
+ style,
79
+ ]}
80
+ onFocus={(e) => {
81
+ setFocused(true)
82
+ onFocus?.(e)
83
+ }}
84
+ onBlur={(e) => {
85
+ setFocused(false)
86
+ onBlur?.(e)
87
+ }}
88
+ placeholderTextColor={colors.mutedForeground}
89
+ allowFontScaling={true}
90
+ secureTextEntry={effectiveSecure}
91
+ {...props}
92
+ />
93
+ {effectiveSuffix ? (
94
+ typeof effectiveSuffix === 'string' ? (
95
+ <Text style={[styles.suffixText, { color: colors.mutedForeground }, suffixStyle]} allowFontScaling={true}>
96
+ {effectiveSuffix}
97
+ </Text>
98
+ ) : (
99
+ <View style={styles.suffixContainer}>{effectiveSuffix}</View>
100
+ )
101
+ ) : null}
102
+ </View>
44
103
  {error ? (
45
104
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
46
105
  ) : null}
@@ -53,19 +112,41 @@ export function Input({ label, error, hint, containerStyle, style, onFocus, onBl
53
112
 
54
113
  const styles = StyleSheet.create({
55
114
  container: {
56
- gap: 6,
115
+ gap: 8,
57
116
  },
58
117
  label: {
59
118
  fontSize: 15,
60
119
  fontWeight: '500',
61
- marginBottom: 6,
62
120
  },
63
- input: {
121
+ inputWrapper: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
64
124
  borderWidth: 1.5,
65
- borderRadius: 14,
66
- paddingHorizontal: 20,
67
- paddingVertical: 16,
125
+ borderRadius: 8,
126
+ paddingHorizontal: 16,
127
+ paddingVertical: 14,
128
+ },
129
+ input: {
130
+ flex: 1,
68
131
  fontSize: 17,
132
+ paddingVertical: 0,
133
+ },
134
+ prefixContainer: {
135
+ marginRight: 8,
136
+ },
137
+ prefixText: {
138
+ fontSize: 17,
139
+ marginRight: 8,
140
+ },
141
+ suffixContainer: {
142
+ marginLeft: 8,
143
+ },
144
+ suffixText: {
145
+ fontSize: 17,
146
+ marginLeft: 8,
147
+ },
148
+ passwordToggle: {
149
+ padding: 4,
69
150
  },
70
151
  helperText: {
71
152
  fontSize: 13,
@@ -6,24 +6,81 @@ import {
6
6
  Text,
7
7
  StyleSheet,
8
8
  ViewStyle,
9
+ TextStyle,
9
10
  Platform,
10
11
  } from 'react-native'
12
+ import { Entypo } from '@expo/vector-icons'
11
13
  import * as Haptics from 'expo-haptics'
12
14
  import { useTheme } from '../../theme'
13
15
 
14
16
  const nativeDriver = Platform.OS !== 'web'
15
17
 
18
+ export type ListItemVariant = 'plain' | 'card'
19
+
16
20
  export interface ListItemProps {
21
+ /**
22
+ * Arbitrary content rendered on the left (avatar, icon, image, etc.).
23
+ * Rendered inside a 44×44 aligned container.
24
+ */
25
+ leftRender?: React.ReactNode
26
+ /**
27
+ * Arbitrary content rendered on the right (badge, price, chevron, switch, etc.).
28
+ * Replaces the old `trailing` prop (still accepted as an alias).
29
+ */
30
+ rightRender?: React.ReactNode | string
31
+ /** @deprecated Use `rightRender` instead. */
32
+ trailing?: React.ReactNode | string
33
+ /** @deprecated Use `leftRender` instead. */
17
34
  icon?: React.ReactNode
35
+
18
36
  title: string
37
+ /** Secondary line below the title. */
19
38
  subtitle?: string
20
- trailing?: string | React.ReactNode
39
+ /** Tertiary / caption line below the subtitle. */
40
+ caption?: string
41
+
42
+ /**
43
+ * - `plain` (default): no background, no border — designed to sit inside a parent surface (Card, list wrapper, etc.)
44
+ * - `card`: standalone surface with background, border and shadow.
45
+ */
46
+ variant?: ListItemVariant
47
+
48
+ /** Show a right-pointing chevron on the far right. Ignored when `rightRender` / `trailing` is set. */
49
+ showChevron?: boolean
50
+
51
+ /** Visual separator line at the bottom of the item. Useful when rendering multiple plain items in a list. */
52
+ showSeparator?: boolean
53
+
21
54
  onPress?: () => void
22
55
  disabled?: boolean
56
+ /** Style applied to the outer container. */
23
57
  style?: ViewStyle
58
+ /** Style applied to the title Text. */
59
+ titleStyle?: TextStyle
60
+ /** Style applied to the subtitle Text. */
61
+ subtitleStyle?: TextStyle
62
+ /** Style applied to the caption Text. */
63
+ captionStyle?: TextStyle
24
64
  }
25
65
 
26
- export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, style }: ListItemProps) {
66
+ export function ListItem({
67
+ leftRender,
68
+ rightRender,
69
+ trailing,
70
+ icon,
71
+ title,
72
+ subtitle,
73
+ caption,
74
+ variant = 'plain',
75
+ showChevron = false,
76
+ showSeparator = false,
77
+ onPress,
78
+ disabled,
79
+ style,
80
+ titleStyle,
81
+ subtitleStyle,
82
+ captionStyle,
83
+ }: ListItemProps) {
27
84
  const { colors } = useTheme()
28
85
  const scale = useRef(new Animated.Value(1)).current
29
86
 
@@ -51,10 +108,29 @@ export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, s
51
108
  onPress?.()
52
109
  }
53
110
 
111
+ // Support legacy props
112
+ const effectiveLeft = leftRender ?? icon
113
+ const effectiveRight = rightRender ?? trailing
114
+
115
+ const cardStyle: ViewStyle =
116
+ variant === 'card'
117
+ ? {
118
+ backgroundColor: colors.card,
119
+ borderRadius: 12,
120
+ borderWidth: 1,
121
+ borderColor: colors.border,
122
+ shadowColor: '#000',
123
+ shadowOffset: { width: 0, height: 2 },
124
+ shadowOpacity: 0.06,
125
+ shadowRadius: 6,
126
+ elevation: 2,
127
+ }
128
+ : {}
129
+
54
130
  return (
55
131
  <Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
56
132
  <TouchableOpacity
57
- style={[styles.container, style]}
133
+ style={[styles.container, cardStyle, style]}
58
134
  onPress={onPress ? handlePress : undefined}
59
135
  onPressIn={handlePressIn}
60
136
  onPressOut={handlePressOut}
@@ -62,27 +138,64 @@ export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, s
62
138
  activeOpacity={1}
63
139
  touchSoundDisabled={true}
64
140
  >
65
- {icon ? <View style={styles.iconWrapper}>{icon}</View> : null}
141
+ {effectiveLeft ? (
142
+ <View style={styles.leftContainer}>{effectiveLeft}</View>
143
+ ) : null}
144
+
66
145
  <View style={styles.content}>
67
- <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
146
+ <Text
147
+ style={[styles.title, { color: colors.foreground }, titleStyle]}
148
+ numberOfLines={2}
149
+ allowFontScaling={true}
150
+ >
68
151
  {title}
69
152
  </Text>
70
153
  {subtitle ? (
71
- <Text style={[styles.subtitle, { color: colors.mutedForeground }]} allowFontScaling={true}>
154
+ <Text
155
+ style={[styles.subtitle, { color: colors.mutedForeground }, subtitleStyle]}
156
+ numberOfLines={2}
157
+ allowFontScaling={true}
158
+ >
72
159
  {subtitle}
73
160
  </Text>
74
161
  ) : null}
75
- </View>
76
- {trailing !== undefined ? (
77
- typeof trailing === 'string' ? (
78
- <Text style={[styles.trailing, { color: colors.mutedForeground }]} allowFontScaling={true}>
79
- {trailing}
162
+ {caption ? (
163
+ <Text
164
+ style={[styles.caption, { color: colors.mutedForeground }, captionStyle]}
165
+ numberOfLines={1}
166
+ allowFontScaling={true}
167
+ >
168
+ {caption}
80
169
  </Text>
81
- ) : (
82
- trailing
83
- )
170
+ ) : null}
171
+ </View>
172
+
173
+ {effectiveRight !== undefined ? (
174
+ <View style={styles.rightContainer}>
175
+ {typeof effectiveRight === 'string' ? (
176
+ <Text
177
+ style={[styles.rightText, { color: colors.mutedForeground }]}
178
+ allowFontScaling={true}
179
+ >
180
+ {effectiveRight}
181
+ </Text>
182
+ ) : (
183
+ effectiveRight
184
+ )}
185
+ </View>
186
+ ) : showChevron ? (
187
+ <Entypo name="chevron-with-circle-right" size={20} color={colors.mutedForeground} />
84
188
  ) : null}
85
189
  </TouchableOpacity>
190
+
191
+ {showSeparator ? (
192
+ <View
193
+ style={[
194
+ styles.separator,
195
+ { backgroundColor: colors.border, marginLeft: effectiveLeft ? 16 + 44 + 12 : 16 },
196
+ ]}
197
+ />
198
+ ) : null}
86
199
  </Animated.View>
87
200
  )
88
201
  }
@@ -95,26 +208,49 @@ const styles = StyleSheet.create({
95
208
  paddingVertical: 14,
96
209
  gap: 12,
97
210
  },
98
- iconWrapper: {
211
+ leftContainer: {
212
+ width: 44,
213
+ height: 44,
99
214
  alignItems: 'center',
100
215
  justifyContent: 'center',
216
+ flexShrink: 0,
101
217
  },
102
218
  content: {
103
219
  flex: 1,
104
- gap: 3,
220
+ gap: 4,
105
221
  },
106
222
  title: {
107
- fontSize: 16,
223
+ fontSize: 17,
108
224
  fontWeight: '500',
109
- lineHeight: 22,
225
+ lineHeight: 24,
110
226
  },
111
227
  subtitle: {
112
- fontSize: 13,
113
- lineHeight: 18,
228
+ fontSize: 14,
229
+ fontWeight: '400',
230
+ lineHeight: 20,
114
231
  },
115
- trailing: {
232
+ caption: {
233
+ fontSize: 12,
234
+ fontWeight: '400',
235
+ lineHeight: 16,
236
+ opacity: 0.7,
237
+ },
238
+ rightContainer: {
239
+ alignItems: 'flex-end',
240
+ justifyContent: 'center',
241
+ flexShrink: 0,
242
+ maxWidth: 160,
243
+ },
244
+ rightText: {
116
245
  fontSize: 15,
117
246
  },
247
+ chevron: {
248
+ marginLeft: 4,
249
+ },
250
+ separator: {
251
+ height: StyleSheet.hairlineWidth,
252
+ marginRight: 16,
253
+ },
118
254
  disabled: {
119
255
  opacity: 0.45,
120
256
  },
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import { Entypo } from '@expo/vector-icons'
3
4
  import * as Haptics from 'expo-haptics'
4
5
  import { useTheme } from '../../theme'
5
6
 
@@ -49,7 +50,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
49
50
  activeOpacity={0.6}
50
51
  touchSoundDisabled={true}
51
52
  >
52
- <Text style={[styles.arrowText, { color: colors.foreground }]}>‹</Text>
53
+ <Entypo name="chevron-left" size={22} color={colors.foreground} />
53
54
  </TouchableOpacity>
54
55
  <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
55
56
  {MONTH_NAMES[value.month - 1]} {value.year}
@@ -60,7 +61,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
60
61
  activeOpacity={0.6}
61
62
  touchSoundDisabled={true}
62
63
  >
63
- <Text style={[styles.arrowText, { color: colors.foreground }]}>›</Text>
64
+ <Entypo name="chevron-right" size={22} color={colors.foreground} />
64
65
  </TouchableOpacity>
65
66
  </View>
66
67
  )
@@ -78,10 +79,6 @@ const styles = StyleSheet.create({
78
79
  alignItems: 'center',
79
80
  justifyContent: 'center',
80
81
  },
81
- arrowText: {
82
- fontSize: 24,
83
- lineHeight: 30,
84
- },
85
82
  label: {
86
83
  fontSize: 17,
87
84
  fontWeight: '500',
@@ -101,7 +101,7 @@ export function RadioGroup({
101
101
 
102
102
  const styles = StyleSheet.create({
103
103
  container: {
104
- gap: 10,
104
+ gap: 12,
105
105
  },
106
106
  horizontal: {
107
107
  flexDirection: 'row',
@@ -110,7 +110,7 @@ const styles = StyleSheet.create({
110
110
  row: {
111
111
  flexDirection: 'row',
112
112
  alignItems: 'center',
113
- gap: 10,
113
+ gap: 12,
114
114
  },
115
115
  radio: {
116
116
  width: 24,