@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
|
@@ -5,10 +5,12 @@ import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
|
5
5
|
import { useTheme } from '../../theme'
|
|
6
6
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
7
7
|
|
|
8
|
-
const MONTH_NAMES =
|
|
9
|
-
'January', 'February', 'March', 'April', 'May', 'June',
|
|
10
|
-
'
|
|
11
|
-
]
|
|
8
|
+
const MONTH_NAMES: Record<string, string[]> = {
|
|
9
|
+
en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
10
|
+
es: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
|
|
11
|
+
pt: ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'],
|
|
12
|
+
fr: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
export interface MonthPickerValue {
|
|
14
16
|
/** Month number 1–12 */
|
|
@@ -19,12 +21,22 @@ export interface MonthPickerValue {
|
|
|
19
21
|
export interface MonthPickerProps {
|
|
20
22
|
value: MonthPickerValue
|
|
21
23
|
onChange: (value: MonthPickerValue) => void
|
|
24
|
+
/** BCP 47 locale tag. Built-in: 'en' | 'es' | 'pt' | 'fr'. For other locales supply formatLabel. */
|
|
25
|
+
locale?: string
|
|
26
|
+
/** Custom label formatter. Takes precedence over locale. */
|
|
27
|
+
formatLabel?: (value: MonthPickerValue) => string
|
|
22
28
|
style?: ViewStyle
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
|
|
31
|
+
export function MonthPicker({ value, onChange, locale = 'en', formatLabel, style }: MonthPickerProps) {
|
|
26
32
|
const { colors } = useTheme()
|
|
27
33
|
|
|
34
|
+
const getLabel = (): string => {
|
|
35
|
+
if (formatLabel) return formatLabel(value)
|
|
36
|
+
const names = MONTH_NAMES[locale] ?? MONTH_NAMES.en
|
|
37
|
+
return `${names[value.month - 1]} ${value.year}`
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
const handlePrev = () => {
|
|
29
41
|
hapticSelection()
|
|
30
42
|
if (value.month === 1) {
|
|
@@ -54,7 +66,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
|
|
|
54
66
|
<Entypo name="chevron-left" size={22} color={colors.foreground} />
|
|
55
67
|
</TouchableOpacity>
|
|
56
68
|
<Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
|
|
57
|
-
{
|
|
69
|
+
{getLabel()}
|
|
58
70
|
</Text>
|
|
59
71
|
<TouchableOpacity
|
|
60
72
|
style={styles.arrow}
|
|
@@ -109,7 +109,7 @@ export function Select({
|
|
|
109
109
|
>
|
|
110
110
|
{selected?.label ?? placeholder}
|
|
111
111
|
</Text>
|
|
112
|
-
<Entypo name="chevron-
|
|
112
|
+
<Entypo name="chevron-down" size={20} color={colors.foregroundMuted} />
|
|
113
113
|
</TouchableOpacity>
|
|
114
114
|
</Animated.View>
|
|
115
115
|
) : null}
|
|
@@ -1,102 +1,214 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform } from 'react-native'
|
|
3
3
|
import {
|
|
4
4
|
BottomSheetModal,
|
|
5
5
|
BottomSheetView,
|
|
6
6
|
BottomSheetScrollView,
|
|
7
7
|
BottomSheetBackdrop,
|
|
8
8
|
BottomSheetModalProvider,
|
|
9
|
+
BottomSheetTextInput,
|
|
10
|
+
BottomSheetFooter,
|
|
9
11
|
type BottomSheetBackdropProps,
|
|
12
|
+
type BottomSheetFooterProps,
|
|
10
13
|
} from '@gorhom/bottom-sheet'
|
|
11
|
-
import {
|
|
14
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
15
|
+
import { AntDesign } from '@expo/vector-icons'
|
|
16
|
+
import { impactMedium } from '../../utils/haptics'
|
|
12
17
|
import { useTheme } from '../../theme'
|
|
13
18
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
14
19
|
|
|
20
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height
|
|
21
|
+
const DEFAULT_MAX_HEIGHT = SCREEN_HEIGHT * 0.85
|
|
22
|
+
const isAndroid = Platform.OS === 'android'
|
|
23
|
+
|
|
15
24
|
export { BottomSheetModalProvider }
|
|
25
|
+
// Re-export BottomSheetTextInput as SheetTextInput for consumer convenience
|
|
26
|
+
export { BottomSheetTextInput as SheetTextInput }
|
|
16
27
|
|
|
17
28
|
export interface SheetProps {
|
|
18
29
|
open: boolean
|
|
19
30
|
onClose: () => void
|
|
20
31
|
title?: string
|
|
32
|
+
/** Secondary text below title. */
|
|
33
|
+
subtitle?: string
|
|
34
|
+
/** @deprecated Use `subtitle` instead. */
|
|
21
35
|
description?: string
|
|
36
|
+
/** Show an X close button in the header. */
|
|
37
|
+
showCloseButton?: boolean
|
|
22
38
|
children?: React.ReactNode
|
|
23
39
|
/** Style for the inner content container. */
|
|
24
40
|
style?: ViewStyle
|
|
25
|
-
/**
|
|
41
|
+
/** Style for the content wrapper (outside the scroll container). */
|
|
42
|
+
contentStyle?: ViewStyle
|
|
43
|
+
/** Render children inside BottomSheetScrollView. */
|
|
26
44
|
scrollable?: boolean
|
|
27
45
|
/** Cap sheet height (dp). Children scroll when content exceeds this value. */
|
|
28
46
|
maxHeight?: number
|
|
47
|
+
/**
|
|
48
|
+
* Keyboard behavior — how the sheet responds to keyboard appearance.
|
|
49
|
+
* - 'interactive': offset sheet by keyboard size (default, works on both platforms)
|
|
50
|
+
* - 'fillParent': extend sheet to fill parent view (can cause restore issues with dynamic sizing)
|
|
51
|
+
* - 'extend': extend sheet to maximum snap point
|
|
52
|
+
*
|
|
53
|
+
* Default: 'interactive' on both platforms.
|
|
54
|
+
*/
|
|
55
|
+
keyboardBehavior?: 'extend' | 'fillParent' | 'interactive'
|
|
56
|
+
/**
|
|
57
|
+
* Keyboard blur behavior — what happens when keyboard dismisses.
|
|
58
|
+
* - 'none': do nothing
|
|
59
|
+
* - 'restore': restore sheet to previous position (default)
|
|
60
|
+
*/
|
|
61
|
+
keyboardBlurBehavior?: 'none' | 'restore'
|
|
62
|
+
/**
|
|
63
|
+
* Blur keyboard when user starts dragging the sheet down.
|
|
64
|
+
* Default: true (recommended for better UX)
|
|
65
|
+
*/
|
|
66
|
+
enableBlurKeyboardOnGesture?: boolean
|
|
67
|
+
/**
|
|
68
|
+
* Android-only: defines keyboard input mode.
|
|
69
|
+
* - 'adjustPan': pan the sheet content (default, fixes restore issues with dynamic sizing)
|
|
70
|
+
* - 'adjustResize': resize the sheet container (can cause transparent gap on dismiss)
|
|
71
|
+
*/
|
|
72
|
+
android_keyboardInputMode?: 'adjustPan' | 'adjustResize'
|
|
73
|
+
/** Sticky footer rendered below the scroll area. */
|
|
74
|
+
footer?: React.ReactNode
|
|
75
|
+
/**
|
|
76
|
+
* Array of snap points for the sheet (e.g., ['50%', '85%'] or [200, 500]).
|
|
77
|
+
* When provided, disables enableDynamicSizing.
|
|
78
|
+
* When omitted, sheet uses dynamic sizing (auto-fits content).
|
|
79
|
+
*/
|
|
80
|
+
snapPoints?: (string | number)[]
|
|
29
81
|
}
|
|
30
82
|
|
|
31
83
|
export function Sheet({
|
|
32
84
|
open,
|
|
33
85
|
onClose,
|
|
34
86
|
title,
|
|
87
|
+
subtitle,
|
|
35
88
|
description,
|
|
89
|
+
showCloseButton = false,
|
|
36
90
|
children,
|
|
37
91
|
style,
|
|
92
|
+
contentStyle,
|
|
38
93
|
scrollable,
|
|
39
94
|
maxHeight,
|
|
95
|
+
keyboardBehavior,
|
|
96
|
+
keyboardBlurBehavior = 'restore',
|
|
97
|
+
enableBlurKeyboardOnGesture = true,
|
|
98
|
+
android_keyboardInputMode = 'adjustPan',
|
|
99
|
+
footer,
|
|
100
|
+
snapPoints,
|
|
40
101
|
}: SheetProps) {
|
|
41
102
|
const { colors } = useTheme()
|
|
103
|
+
const insets = useSafeAreaInsets()
|
|
42
104
|
const ref = useRef<BottomSheetModal>(null)
|
|
105
|
+
|
|
106
|
+
// 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
|
|
107
|
+
// 'fillParent' + 'adjustResize' causes restore issues (transparent gap when keyboard dismisses)
|
|
108
|
+
const effectiveKeyboardBehavior = keyboardBehavior ?? 'interactive'
|
|
43
109
|
|
|
44
110
|
useEffect(() => {
|
|
45
111
|
if (open) {
|
|
46
|
-
|
|
112
|
+
impactMedium()
|
|
47
113
|
ref.current?.present()
|
|
48
114
|
} else {
|
|
49
115
|
ref.current?.dismiss()
|
|
50
116
|
}
|
|
51
117
|
}, [open])
|
|
52
118
|
|
|
53
|
-
const renderBackdrop = (props: BottomSheetBackdropProps) => (
|
|
119
|
+
const renderBackdrop = useCallback((props: BottomSheetBackdropProps) => (
|
|
54
120
|
<BottomSheetBackdrop
|
|
55
121
|
{...props}
|
|
56
122
|
disappearsOnIndex={-1}
|
|
57
123
|
appearsOnIndex={0}
|
|
58
124
|
pressBehavior="close"
|
|
59
125
|
/>
|
|
60
|
-
)
|
|
126
|
+
), [])
|
|
127
|
+
|
|
128
|
+
const renderFooter = useCallback((props: BottomSheetFooterProps) => {
|
|
129
|
+
if (!footer) return null
|
|
130
|
+
return (
|
|
131
|
+
<BottomSheetFooter {...props}>
|
|
132
|
+
{footer}
|
|
133
|
+
</BottomSheetFooter>
|
|
134
|
+
)
|
|
135
|
+
}, [footer])
|
|
136
|
+
|
|
137
|
+
const effectiveSubtitle = subtitle ?? description
|
|
138
|
+
|
|
139
|
+
const showHeader = !!(title || effectiveSubtitle || showCloseButton)
|
|
61
140
|
|
|
62
|
-
const headerNode =
|
|
141
|
+
const headerNode = showHeader ? (
|
|
63
142
|
<View style={styles.header}>
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
143
|
+
<View style={styles.headerRow}>
|
|
144
|
+
{title ? (
|
|
145
|
+
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
|
|
146
|
+
{title}
|
|
147
|
+
</Text>
|
|
148
|
+
) : <View style={{ flex: 1 }} />}
|
|
149
|
+
{showCloseButton ? (
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
onPress={onClose}
|
|
152
|
+
style={styles.closeButton}
|
|
153
|
+
activeOpacity={0.6}
|
|
154
|
+
touchSoundDisabled={true}
|
|
155
|
+
>
|
|
156
|
+
<AntDesign name="close" size={ms(18)} color={colors.foregroundMuted} />
|
|
157
|
+
</TouchableOpacity>
|
|
158
|
+
) : null}
|
|
159
|
+
</View>
|
|
160
|
+
{effectiveSubtitle ? (
|
|
161
|
+
<Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
162
|
+
{effectiveSubtitle}
|
|
70
163
|
</Text>
|
|
71
164
|
) : null}
|
|
72
165
|
</View>
|
|
73
166
|
) : null
|
|
74
167
|
|
|
75
168
|
const useScroll = scrollable || !!maxHeight
|
|
76
|
-
|
|
169
|
+
const effectiveMaxHeight = maxHeight ?? DEFAULT_MAX_HEIGHT
|
|
170
|
+
|
|
171
|
+
// If snapPoints provided, disable dynamic sizing. Otherwise use dynamic sizing.
|
|
172
|
+
const useDynamicSizing = !snapPoints
|
|
173
|
+
|
|
77
174
|
return (
|
|
78
175
|
<BottomSheetModal
|
|
79
176
|
ref={ref}
|
|
80
|
-
enableDynamicSizing
|
|
177
|
+
enableDynamicSizing={useDynamicSizing}
|
|
178
|
+
snapPoints={snapPoints}
|
|
179
|
+
maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
|
|
81
180
|
onDismiss={onClose}
|
|
82
181
|
backdropComponent={renderBackdrop}
|
|
182
|
+
footerComponent={footer ? renderFooter : undefined}
|
|
83
183
|
backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
|
|
84
184
|
handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
|
|
85
185
|
enablePanDownToClose
|
|
186
|
+
topInset={insets.top}
|
|
187
|
+
keyboardBehavior={effectiveKeyboardBehavior}
|
|
188
|
+
keyboardBlurBehavior={keyboardBlurBehavior}
|
|
189
|
+
android_keyboardInputMode={android_keyboardInputMode}
|
|
190
|
+
enableBlurKeyboardOnGesture={enableBlurKeyboardOnGesture}
|
|
86
191
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
{useScroll ? (
|
|
193
|
+
<BottomSheetScrollView
|
|
194
|
+
contentContainerStyle={[
|
|
195
|
+
styles.scrollContent,
|
|
196
|
+
style,
|
|
197
|
+
]}
|
|
198
|
+
style={contentStyle}
|
|
199
|
+
showsVerticalScrollIndicator={true}
|
|
200
|
+
indicatorStyle="black"
|
|
201
|
+
persistentScrollbar={isAndroid}
|
|
202
|
+
>
|
|
203
|
+
{headerNode}
|
|
204
|
+
{children}
|
|
205
|
+
</BottomSheetScrollView>
|
|
206
|
+
) : (
|
|
207
|
+
<BottomSheetView style={[styles.content, contentStyle, style]}>
|
|
208
|
+
{headerNode}
|
|
209
|
+
{children}
|
|
210
|
+
</BottomSheetView>
|
|
211
|
+
)}
|
|
100
212
|
</BottomSheetModal>
|
|
101
213
|
)
|
|
102
214
|
}
|
|
@@ -111,21 +223,38 @@ const styles = StyleSheet.create({
|
|
|
111
223
|
height: vs(4),
|
|
112
224
|
borderRadius: ms(2),
|
|
113
225
|
},
|
|
114
|
-
content: {
|
|
115
|
-
paddingHorizontal: s(24),
|
|
116
|
-
paddingBottom: vs(32),
|
|
117
|
-
},
|
|
118
226
|
header: {
|
|
119
|
-
|
|
120
|
-
|
|
227
|
+
paddingHorizontal: s(16),
|
|
228
|
+
paddingTop: vs(4),
|
|
229
|
+
paddingBottom: vs(12),
|
|
230
|
+
gap: vs(4),
|
|
231
|
+
},
|
|
232
|
+
headerRow: {
|
|
233
|
+
flexDirection: 'row',
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
justifyContent: 'space-between',
|
|
121
236
|
},
|
|
122
237
|
title: {
|
|
123
238
|
fontFamily: 'Poppins-SemiBold',
|
|
124
239
|
fontSize: ms(18),
|
|
240
|
+
flex: 1,
|
|
125
241
|
},
|
|
126
|
-
|
|
242
|
+
subtitle: {
|
|
127
243
|
fontFamily: 'Poppins-Regular',
|
|
128
244
|
fontSize: ms(14),
|
|
129
245
|
lineHeight: mvs(20),
|
|
130
246
|
},
|
|
247
|
+
closeButton: {
|
|
248
|
+
padding: s(4),
|
|
249
|
+
marginLeft: s(8),
|
|
250
|
+
},
|
|
251
|
+
content: {
|
|
252
|
+
paddingHorizontal: s(16),
|
|
253
|
+
paddingBottom: vs(32),
|
|
254
|
+
},
|
|
255
|
+
scrollContent: {
|
|
256
|
+
paddingHorizontal: s(16),
|
|
257
|
+
paddingBottom: vs(32),
|
|
258
|
+
paddingRight: s(16),
|
|
259
|
+
},
|
|
131
260
|
})
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Sheet, BottomSheetModalProvider } from './Sheet'
|
|
1
|
+
export { Sheet, BottomSheetModalProvider, SheetTextInput } from './Sheet'
|
|
2
2
|
export type { SheetProps } from './Sheet'
|
|
@@ -49,11 +49,11 @@ function TabTrigger({
|
|
|
49
49
|
const scale = useRef(new Animated.Value(1)).current
|
|
50
50
|
|
|
51
51
|
const handlePressIn = () => {
|
|
52
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver,
|
|
52
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, stiffness: 600, damping: 35, mass: 0.8 }).start()
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const handlePressOut = () => {
|
|
56
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver,
|
|
56
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, stiffness: 280, damping: 22, mass: 0.8 }).start()
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const isUnderline = variant === 'underline'
|
|
@@ -108,8 +108,8 @@ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, s
|
|
|
108
108
|
if (!layout) return
|
|
109
109
|
if (animate) {
|
|
110
110
|
Animated.parallel([
|
|
111
|
-
Animated.spring(pillX, { toValue: layout.x, useNativeDriver: false,
|
|
112
|
-
Animated.spring(pillWidth, { toValue: layout.width, useNativeDriver: false,
|
|
111
|
+
Animated.spring(pillX, { toValue: layout.x, useNativeDriver: false, stiffness: 380, damping: 38, mass: 1.0 }),
|
|
112
|
+
Animated.spring(pillWidth, { toValue: layout.width, useNativeDriver: false, stiffness: 380, damping: 38, mass: 1.0 }),
|
|
113
113
|
]).start()
|
|
114
114
|
} else {
|
|
115
115
|
pillX.setValue(layout.x)
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|
|
2
2
|
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { s, vs, ms } from '../../utils/scaling'
|
|
5
|
+
import { renderIcon } from '../../utils/icons'
|
|
5
6
|
|
|
6
7
|
const webInputResetStyle: any =
|
|
7
8
|
Platform.OS === 'web'
|
|
@@ -16,6 +17,12 @@ export interface TextareaProps extends TextInputProps {
|
|
|
16
17
|
hint?: string
|
|
17
18
|
/** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
|
|
18
19
|
rows?: number
|
|
20
|
+
/** Icon name from @expo/vector-icons rendered inside top-left corner. */
|
|
21
|
+
prefixIcon?: string
|
|
22
|
+
/** Custom icon node rendered top-left. */
|
|
23
|
+
prefixIconNode?: React.ReactNode
|
|
24
|
+
/** Override prefix icon color. Defaults to foregroundMuted. */
|
|
25
|
+
prefixIconColor?: string
|
|
19
26
|
/** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
|
|
20
27
|
containerStyle?: ViewStyle
|
|
21
28
|
}
|
|
@@ -25,6 +32,9 @@ export function Textarea({
|
|
|
25
32
|
error,
|
|
26
33
|
hint,
|
|
27
34
|
rows = 4,
|
|
35
|
+
prefixIcon,
|
|
36
|
+
prefixIconNode,
|
|
37
|
+
prefixIconColor,
|
|
28
38
|
containerStyle,
|
|
29
39
|
style,
|
|
30
40
|
onFocus,
|
|
@@ -34,40 +44,53 @@ export function Textarea({
|
|
|
34
44
|
const { colors } = useTheme()
|
|
35
45
|
const [focused, setFocused] = useState(false)
|
|
36
46
|
|
|
47
|
+
const resolvedPrefixIcon = prefixIcon
|
|
48
|
+
? renderIcon(prefixIcon, ms(16), prefixIconColor ?? colors.foregroundMuted)
|
|
49
|
+
: prefixIconNode
|
|
50
|
+
|
|
37
51
|
return (
|
|
38
52
|
<View style={[styles.container, containerStyle]}>
|
|
39
53
|
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
40
|
-
<
|
|
41
|
-
multiline
|
|
42
|
-
numberOfLines={rows}
|
|
43
|
-
textAlignVertical="top"
|
|
54
|
+
<View
|
|
44
55
|
style={[
|
|
45
|
-
styles.
|
|
56
|
+
styles.inputWrapper,
|
|
46
57
|
{
|
|
47
58
|
borderColor: error
|
|
48
59
|
? colors.destructive
|
|
49
60
|
: focused
|
|
50
61
|
? (colors.ring ?? colors.primary)
|
|
51
62
|
: colors.border,
|
|
52
|
-
color: colors.foreground,
|
|
53
63
|
backgroundColor: colors.background,
|
|
54
|
-
minHeight: rows * vs(30),
|
|
55
64
|
},
|
|
56
|
-
webInputResetStyle,
|
|
57
|
-
style,
|
|
58
65
|
]}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
>
|
|
67
|
+
{resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
|
|
68
|
+
<TextInput
|
|
69
|
+
multiline
|
|
70
|
+
numberOfLines={rows}
|
|
71
|
+
textAlignVertical="top"
|
|
72
|
+
style={[
|
|
73
|
+
styles.input,
|
|
74
|
+
{
|
|
75
|
+
color: colors.foreground,
|
|
76
|
+
minHeight: rows * vs(30),
|
|
77
|
+
},
|
|
78
|
+
webInputResetStyle,
|
|
79
|
+
style,
|
|
80
|
+
]}
|
|
81
|
+
onFocus={(e) => {
|
|
82
|
+
setFocused(true)
|
|
83
|
+
onFocus?.(e)
|
|
84
|
+
}}
|
|
85
|
+
onBlur={(e) => {
|
|
86
|
+
setFocused(false)
|
|
87
|
+
onBlur?.(e)
|
|
88
|
+
}}
|
|
89
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
90
|
+
allowFontScaling={true}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
71
94
|
{error ? (
|
|
72
95
|
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
73
96
|
) : null}
|
|
@@ -80,23 +103,37 @@ export function Textarea({
|
|
|
80
103
|
|
|
81
104
|
const styles = StyleSheet.create({
|
|
82
105
|
container: {
|
|
83
|
-
gap: vs(
|
|
106
|
+
gap: vs(4),
|
|
84
107
|
},
|
|
85
108
|
label: {
|
|
86
109
|
fontFamily: 'Poppins-Medium',
|
|
87
110
|
fontSize: ms(13),
|
|
111
|
+
lineHeight: vs(18),
|
|
112
|
+
marginBottom: vs(2),
|
|
88
113
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
borderRadius: ms(8),
|
|
114
|
+
inputWrapper: {
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderRadius: 8,
|
|
93
117
|
paddingHorizontal: s(14),
|
|
94
118
|
paddingVertical: vs(11),
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
gap: s(8),
|
|
120
|
+
},
|
|
121
|
+
prefixIcon: {
|
|
122
|
+
alignItems: 'flex-start',
|
|
123
|
+
justifyContent: 'flex-start',
|
|
124
|
+
paddingTop: vs(2),
|
|
125
|
+
},
|
|
126
|
+
input: {
|
|
127
|
+
fontFamily: 'Poppins-Regular',
|
|
128
|
+
fontSize: ms(14),
|
|
129
|
+
lineHeight: vs(22),
|
|
130
|
+
padding: 0,
|
|
131
|
+
margin: 0,
|
|
97
132
|
},
|
|
98
133
|
helperText: {
|
|
99
134
|
fontFamily: 'Poppins-Regular',
|
|
100
|
-
fontSize: ms(
|
|
135
|
+
fontSize: ms(12),
|
|
136
|
+
lineHeight: vs(16),
|
|
137
|
+
marginTop: vs(4),
|
|
101
138
|
},
|
|
102
139
|
})
|