@retray-dev/ui-kit 1.0.0 → 1.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.
@@ -1,13 +1,11 @@
1
- import React, { useState } from 'react'
1
+ import React, { useRef, useCallback } from 'react'
2
+ import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
2
3
  import {
3
- Modal,
4
- View,
5
- Text,
6
- TouchableOpacity,
7
- FlatList,
8
- StyleSheet,
9
- ViewStyle,
10
- } from 'react-native'
4
+ BottomSheetModal,
5
+ BottomSheetView,
6
+ BottomSheetBackdrop,
7
+ type BottomSheetBackdropProps,
8
+ } from '@gorhom/bottom-sheet'
11
9
  import * as Haptics from 'expo-haptics'
12
10
  import { useTheme } from '../../theme'
13
11
 
@@ -21,8 +19,10 @@ export interface SelectProps {
21
19
  options: SelectOption[]
22
20
  value?: string
23
21
  onValueChange?: (value: string) => void
22
+ /** Text shown when no option is selected. Defaults to `'Select an option'`. */
24
23
  placeholder?: string
25
24
  label?: string
25
+ /** Red helper text; also changes trigger border to `destructive` color. */
26
26
  error?: string
27
27
  disabled?: boolean
28
28
  style?: ViewStyle
@@ -39,27 +39,57 @@ export function Select({
39
39
  style,
40
40
  }: SelectProps) {
41
41
  const { colors } = useTheme()
42
- const [open, setOpen] = useState(false)
42
+ const bottomSheetRef = useRef<BottomSheetModal>(null)
43
+ const scale = useRef(new Animated.Value(1)).current
43
44
 
44
45
  const selected = options.find((o) => o.value === value)
45
46
 
47
+ const handlePressIn = () => {
48
+ if (disabled) return
49
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
50
+ }
51
+
52
+ const handlePressOut = () => {
53
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
54
+ }
55
+
56
+ const handleOpen = () => {
57
+ if (!disabled) {
58
+ Haptics.selectionAsync()
59
+ bottomSheetRef.current?.present()
60
+ }
61
+ }
62
+
63
+ const renderBackdrop = useCallback(
64
+ (props: BottomSheetBackdropProps) => (
65
+ <BottomSheetBackdrop
66
+ {...props}
67
+ disappearsOnIndex={-1}
68
+ appearsOnIndex={0}
69
+ pressBehavior="close"
70
+ />
71
+ ),
72
+ []
73
+ )
74
+
46
75
  return (
47
76
  <View style={[styles.container, style]}>
48
- {label ? (
49
- <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
50
- ) : null}
77
+ {label ? <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text> : null}
51
78
 
79
+ <Animated.View style={{ transform: [{ scale }], opacity: disabled ? 0.45 : 1 }}>
52
80
  <TouchableOpacity
53
81
  style={[
54
82
  styles.trigger,
55
83
  {
56
84
  borderColor: error ? colors.destructive : colors.border,
57
85
  backgroundColor: colors.background,
58
- opacity: disabled ? 0.45 : 1,
59
86
  },
60
87
  ]}
61
- onPress={() => { if (!disabled) { Haptics.selectionAsync(); setOpen(true) } }}
62
- activeOpacity={0.7}
88
+ onPress={handleOpen}
89
+ onPressIn={handlePressIn}
90
+ onPressOut={handlePressOut}
91
+ activeOpacity={1}
92
+ touchSoundDisabled={true}
63
93
  >
64
94
  <Text
65
95
  style={[
@@ -72,54 +102,61 @@ export function Select({
72
102
  </Text>
73
103
  <Text style={[styles.chevron, { color: colors.mutedForeground }]}>▾</Text>
74
104
  </TouchableOpacity>
105
+ </Animated.View>
75
106
 
76
107
  {error ? (
77
108
  <Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
78
109
  ) : null}
79
110
 
80
- <Modal transparent visible={open} onRequestClose={() => setOpen(false)} animationType="fade">
81
- <TouchableOpacity style={styles.overlay} onPress={() => setOpen(false)} activeOpacity={1}>
82
- <View style={[styles.list, { backgroundColor: colors.card, borderColor: colors.border }]}>
83
- <FlatList
84
- data={options}
85
- keyExtractor={(item) => item.value}
86
- renderItem={({ item }) => {
87
- const isSelected = item.value === value
88
- return (
89
- <TouchableOpacity
90
- style={[
91
- styles.option,
92
- isSelected && { backgroundColor: colors.accent },
93
- item.disabled && styles.disabledOption,
94
- ]}
95
- onPress={() => {
96
- if (!item.disabled) {
97
- Haptics.selectionAsync()
98
- onValueChange?.(item.value)
99
- setOpen(false)
100
- }
101
- }}
102
- activeOpacity={0.7}
103
- >
104
- <Text
105
- style={[
106
- styles.optionText,
107
- { color: item.disabled ? colors.mutedForeground : colors.foreground },
108
- isSelected && { fontWeight: '500' },
109
- ]}
110
- >
111
- {item.label}
112
- </Text>
113
- {isSelected ? (
114
- <Text style={[styles.checkmark, { color: colors.primary }]}>✓</Text>
115
- ) : null}
116
- </TouchableOpacity>
117
- )
118
- }}
119
- />
120
- </View>
121
- </TouchableOpacity>
122
- </Modal>
111
+ <BottomSheetModal
112
+ ref={bottomSheetRef}
113
+ enableDynamicSizing
114
+ enablePanDownToClose
115
+ backdropComponent={renderBackdrop}
116
+ backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
117
+ handleIndicatorStyle={[styles.sheetHandle, { backgroundColor: colors.border }]}
118
+ >
119
+ <BottomSheetView style={styles.sheetContent}>
120
+ {label ? (
121
+ <Text style={[styles.sheetTitle, { color: colors.foreground }]}>{label}</Text>
122
+ ) : null}
123
+ {options.map((item) => {
124
+ const isSelected = item.value === value
125
+ return (
126
+ <TouchableOpacity
127
+ key={item.value}
128
+ style={[
129
+ styles.option,
130
+ isSelected && { backgroundColor: colors.accent },
131
+ item.disabled && styles.disabledOption,
132
+ ]}
133
+ onPress={() => {
134
+ if (!item.disabled) {
135
+ Haptics.selectionAsync()
136
+ onValueChange?.(item.value)
137
+ bottomSheetRef.current?.dismiss()
138
+ }
139
+ }}
140
+ activeOpacity={0.7}
141
+ touchSoundDisabled={true}
142
+ >
143
+ <Text
144
+ style={[
145
+ styles.optionText,
146
+ { color: item.disabled ? colors.mutedForeground : colors.foreground },
147
+ isSelected && { fontWeight: '500' },
148
+ ]}
149
+ >
150
+ {item.label}
151
+ </Text>
152
+ {isSelected ? (
153
+ <Text style={[styles.checkmark, { color: colors.primary }]}>✓</Text>
154
+ ) : null}
155
+ </TouchableOpacity>
156
+ )
157
+ })}
158
+ </BottomSheetView>
159
+ </BottomSheetModal>
123
160
  </View>
124
161
  )
125
162
  }
@@ -153,27 +190,36 @@ const styles = StyleSheet.create({
153
190
  helperText: {
154
191
  fontSize: 12,
155
192
  },
156
- overlay: {
157
- flex: 1,
158
- backgroundColor: 'rgba(0,0,0,0.3)',
159
- justifyContent: 'center',
160
- padding: 24,
193
+ sheetBackground: {
194
+ borderTopLeftRadius: 16,
195
+ borderTopRightRadius: 16,
161
196
  },
162
- list: {
163
- borderRadius: 12,
164
- borderWidth: 1,
165
- maxHeight: 300,
166
- overflow: 'hidden',
197
+ sheetHandle: {
198
+ width: 36,
199
+ height: 4,
200
+ borderRadius: 2,
201
+ },
202
+ sheetContent: {
203
+ paddingHorizontal: 16,
204
+ paddingBottom: 32,
205
+ },
206
+ sheetTitle: {
207
+ fontSize: 16,
208
+ fontWeight: '600',
209
+ paddingVertical: 12,
210
+ paddingHorizontal: 4,
167
211
  },
168
212
  option: {
169
213
  flexDirection: 'row',
170
214
  alignItems: 'center',
171
215
  justifyContent: 'space-between',
172
216
  paddingHorizontal: 12,
173
- paddingVertical: 10,
217
+ paddingVertical: 14,
218
+ borderRadius: 8,
174
219
  },
175
220
  optionText: {
176
221
  fontSize: 15,
222
+ flex: 1,
177
223
  },
178
224
  disabledOption: {
179
225
  opacity: 0.45,
@@ -181,5 +227,6 @@ const styles = StyleSheet.create({
181
227
  checkmark: {
182
228
  fontSize: 14,
183
229
  fontWeight: '600',
230
+ marginLeft: 8,
184
231
  },
185
232
  })
@@ -18,7 +18,12 @@ export interface SheetProps {
18
18
  title?: string
19
19
  description?: string
20
20
  children?: React.ReactNode
21
+ /**
22
+ * Heights the sheet can snap to. Accepts percentage strings (`'50%'`) or
23
+ * absolute point values (`300`). Defaults to `['50%']`.
24
+ */
21
25
  snapPoints?: (string | number)[]
26
+ /** Style for the inner `BottomSheetView` content container. */
22
27
  style?: ViewStyle
23
28
  }
24
29
 
@@ -63,13 +68,15 @@ export function Sheet({
63
68
  enablePanDownToClose
64
69
  >
65
70
  <BottomSheetView style={[styles.content, style]}>
66
- {(title || description) ? (
71
+ {title || description ? (
67
72
  <View style={styles.header}>
68
73
  {title ? (
69
74
  <Text style={[styles.title, { color: colors.cardForeground }]}>{title}</Text>
70
75
  ) : null}
71
76
  {description ? (
72
- <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
77
+ <Text style={[styles.description, { color: colors.mutedForeground }]}>
78
+ {description}
79
+ </Text>
73
80
  ) : null}
74
81
  </View>
75
82
  ) : null}
@@ -1,5 +1,6 @@
1
- import React, { useEffect, useRef } from 'react'
2
- import { Animated, StyleSheet, ViewStyle } from 'react-native'
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { Animated, StyleSheet, View, ViewStyle } from 'react-native'
3
+ import { LinearGradient } from 'expo-linear-gradient'
3
4
  import { useTheme } from '../../theme'
4
5
 
5
6
  export interface SkeletonProps {
@@ -10,31 +11,53 @@ export interface SkeletonProps {
10
11
  }
11
12
 
12
13
  export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style }: SkeletonProps) {
13
- const { colors } = useTheme()
14
- const opacity = useRef(new Animated.Value(1)).current
14
+ const { colors, colorScheme } = useTheme()
15
+ const shimmerAnim = useRef(new Animated.Value(0)).current
16
+ const [containerWidth, setContainerWidth] = useState(300)
17
+
18
+ const shimmerHighlight =
19
+ colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'
15
20
 
16
21
  useEffect(() => {
17
22
  const animation = Animated.loop(
18
- Animated.sequence([
19
- Animated.timing(opacity, { toValue: 0.4, duration: 800, useNativeDriver: true }),
20
- Animated.timing(opacity, { toValue: 1, duration: 800, useNativeDriver: true }),
21
- ])
23
+ Animated.timing(shimmerAnim, {
24
+ toValue: 1,
25
+ duration: 1200,
26
+ useNativeDriver: true,
27
+ })
22
28
  )
23
29
  animation.start()
24
30
  return () => animation.stop()
25
- }, [opacity])
31
+ }, [shimmerAnim])
32
+
33
+ const translateX = shimmerAnim.interpolate({
34
+ inputRange: [0, 1],
35
+ outputRange: [-containerWidth, containerWidth],
36
+ })
26
37
 
27
38
  return (
28
- <Animated.View
39
+ <View
29
40
  style={[
30
41
  styles.base,
31
- { width: width as any, height, borderRadius, backgroundColor: colors.muted, opacity },
42
+ { width: width as any, height, borderRadius, backgroundColor: colors.muted },
32
43
  style,
33
44
  ]}
34
- />
45
+ onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
46
+ >
47
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ translateX }] }]}>
48
+ <LinearGradient
49
+ colors={['transparent', shimmerHighlight, 'transparent']}
50
+ start={{ x: 0, y: 0 }}
51
+ end={{ x: 1, y: 0 }}
52
+ style={StyleSheet.absoluteFill}
53
+ />
54
+ </Animated.View>
55
+ </View>
35
56
  )
36
57
  }
37
58
 
38
59
  const styles = StyleSheet.create({
39
- base: {},
60
+ base: {
61
+ overflow: 'hidden',
62
+ },
40
63
  })
@@ -4,11 +4,15 @@ import * as Haptics from 'expo-haptics'
4
4
  import { useTheme } from '../../theme'
5
5
 
6
6
  export interface SliderProps {
7
+ /** Current value. Controlled when provided; falls back to internal state otherwise. */
7
8
  value?: number
8
9
  minimumValue?: number
9
10
  maximumValue?: number
11
+ /** Snap interval. `0` (default) means continuous (no snapping). */
10
12
  step?: number
13
+ /** Called on every move while dragging. */
11
14
  onValueChange?: (value: number) => void
15
+ /** Called once when the user releases the thumb. */
12
16
  onSlidingComplete?: (value: number) => void
13
17
  disabled?: boolean
14
18
  style?: ViewStyle
@@ -86,10 +90,7 @@ export function Slider({
86
90
  >
87
91
  <View style={[styles.track, { backgroundColor: colors.muted }]}>
88
92
  <View
89
- style={[
90
- styles.range,
91
- { width: `${percent}%` as any, backgroundColor: colors.primary },
92
- ]}
93
+ style={[styles.range, { width: `${percent}%` as any, backgroundColor: colors.primary }]}
93
94
  />
94
95
  </View>
95
96
  <View
@@ -17,11 +17,5 @@ const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
17
17
 
18
18
  export function Spinner({ size = 'md', color, ...props }: SpinnerProps) {
19
19
  const { colors } = useTheme()
20
- return (
21
- <ActivityIndicator
22
- size={sizeMap[size]}
23
- color={color ?? colors.primary}
24
- {...props}
25
- />
26
- )
20
+ return <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
27
21
  }
@@ -43,9 +43,13 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
43
43
 
44
44
  return (
45
45
  <TouchableOpacity
46
- onPress={() => { Haptics.selectionAsync(); onCheckedChange?.(!checked) }}
46
+ onPress={() => {
47
+ Haptics.selectionAsync()
48
+ onCheckedChange?.(!checked)
49
+ }}
47
50
  disabled={disabled}
48
51
  activeOpacity={0.8}
52
+ touchSoundDisabled={true}
49
53
  style={[styles.wrapper, { opacity: disabled ? 0.45 : 1 }, style]}
50
54
  >
51
55
  <Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useRef, useEffect } from 'react'
2
2
  import { View, TouchableOpacity, Text, Animated, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
3
4
  import { useTheme } from '../../theme'
4
5
 
5
6
  export interface TabItem {
@@ -9,6 +10,10 @@ export interface TabItem {
9
10
 
10
11
  export interface TabsProps {
11
12
  tabs: TabItem[]
13
+ /**
14
+ * Controlled active tab value. When omitted the component manages state internally
15
+ * (uncontrolled), defaulting to the first tab.
16
+ */
12
17
  value?: string
13
18
  onValueChange?: (value: string) => void
14
19
  children?: React.ReactNode
@@ -22,6 +27,53 @@ export interface TabsContentProps {
22
27
  style?: ViewStyle
23
28
  }
24
29
 
30
+ function TabTrigger({
31
+ tab,
32
+ isActive,
33
+ onPress,
34
+ onLayout,
35
+ }: {
36
+ tab: TabItem
37
+ isActive: boolean
38
+ onPress: () => void
39
+ onLayout: (e: any) => void
40
+ }) {
41
+ const { colors } = useTheme()
42
+ const scale = useRef(new Animated.Value(1)).current
43
+
44
+ const handlePressIn = () => {
45
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
46
+ }
47
+
48
+ const handlePressOut = () => {
49
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
50
+ }
51
+
52
+ return (
53
+ <TouchableOpacity
54
+ style={styles.trigger}
55
+ onPress={onPress}
56
+ onPressIn={handlePressIn}
57
+ onPressOut={handlePressOut}
58
+ onLayout={onLayout}
59
+ activeOpacity={1}
60
+ touchSoundDisabled={true}
61
+ >
62
+ <Animated.View style={{ transform: [{ scale }] }}>
63
+ <Text
64
+ style={[
65
+ styles.triggerLabel,
66
+ { color: isActive ? colors.foreground : colors.mutedForeground },
67
+ isActive && styles.activeTriggerLabel,
68
+ ]}
69
+ >
70
+ {tab.label}
71
+ </Text>
72
+ </Animated.View>
73
+ </TouchableOpacity>
74
+ )
75
+ }
76
+
25
77
  export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps) {
26
78
  const [internal, setInternal] = useState(tabs[0]?.value ?? '')
27
79
  const { colors } = useTheme()
@@ -37,8 +89,18 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
37
89
  if (!layout) return
38
90
  if (animate) {
39
91
  Animated.parallel([
40
- Animated.spring(pillX, { toValue: layout.x, useNativeDriver: false, speed: 20, bounciness: 0 }),
41
- Animated.spring(pillWidth, { toValue: layout.width, useNativeDriver: false, speed: 20, bounciness: 0 }),
92
+ Animated.spring(pillX, {
93
+ toValue: layout.x,
94
+ useNativeDriver: false,
95
+ speed: 20,
96
+ bounciness: 0,
97
+ }),
98
+ Animated.spring(pillWidth, {
99
+ toValue: layout.width,
100
+ useNativeDriver: false,
101
+ speed: 20,
102
+ bounciness: 0,
103
+ }),
42
104
  ]).start()
43
105
  } else {
44
106
  pillX.setValue(layout.x)
@@ -53,6 +115,7 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
53
115
  }, [active])
54
116
 
55
117
  const handlePress = (v: string) => {
118
+ Haptics.selectionAsync()
56
119
  if (!value) setInternal(v)
57
120
  onValueChange?.(v)
58
121
  }
@@ -79,35 +142,22 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
79
142
  },
80
143
  ]}
81
144
  />
82
- {tabs.map((tab) => {
83
- const isActive = tab.value === active
84
- return (
85
- <TouchableOpacity
86
- key={tab.value}
87
- style={styles.trigger}
88
- onPress={() => handlePress(tab.value)}
89
- activeOpacity={0.7}
90
- onLayout={(e) => {
91
- const { x, width } = e.nativeEvent.layout
92
- tabLayouts.current[tab.value] = { x, width }
93
- if (tab.value === active) {
94
- animatePill(tab.value, false)
95
- initialised.current = true
96
- }
97
- }}
98
- >
99
- <Text
100
- style={[
101
- styles.triggerLabel,
102
- { color: isActive ? colors.foreground : colors.mutedForeground },
103
- isActive && styles.activeTriggerLabel,
104
- ]}
105
- >
106
- {tab.label}
107
- </Text>
108
- </TouchableOpacity>
109
- )
110
- })}
145
+ {tabs.map((tab) => (
146
+ <TabTrigger
147
+ key={tab.value}
148
+ tab={tab}
149
+ isActive={tab.value === active}
150
+ onPress={() => handlePress(tab.value)}
151
+ onLayout={(e) => {
152
+ const { x, width } = e.nativeEvent.layout
153
+ tabLayouts.current[tab.value] = { x, width }
154
+ if (tab.value === active) {
155
+ animatePill(tab.value, false)
156
+ initialised.current = true
157
+ }
158
+ }}
159
+ />
160
+ ))}
111
161
  </View>
112
162
  {children}
113
163
  </View>
@@ -133,6 +183,7 @@ const styles = StyleSheet.create({
133
183
  paddingHorizontal: 12,
134
184
  borderRadius: 6,
135
185
  alignItems: 'center',
186
+ justifyContent: 'center',
136
187
  zIndex: 1,
137
188
  },
138
189
  triggerLabel: {
@@ -1,23 +1,36 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
5
  export interface TextareaProps extends TextInputProps {
6
6
  label?: string
7
+ /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
7
8
  error?: string
9
+ /** Helper text shown below the textarea when there is no error. */
8
10
  hint?: string
11
+ /** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
9
12
  rows?: number
13
+ /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
14
+ containerStyle?: ViewStyle
10
15
  }
11
16
 
12
- export function Textarea({ label, error, hint, rows = 4, style, onFocus, onBlur, ...props }: TextareaProps) {
17
+ export function Textarea({
18
+ label,
19
+ error,
20
+ hint,
21
+ rows = 4,
22
+ containerStyle,
23
+ style,
24
+ onFocus,
25
+ onBlur,
26
+ ...props
27
+ }: TextareaProps) {
13
28
  const { colors } = useTheme()
14
29
  const [focused, setFocused] = useState(false)
15
30
 
16
31
  return (
17
- <View style={styles.container}>
18
- {label ? (
19
- <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
20
- ) : null}
32
+ <View style={[styles.container, containerStyle]}>
33
+ {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
21
34
  <TextInput
22
35
  multiline
23
36
  numberOfLines={rows}
@@ -32,17 +45,23 @@ export function Textarea({ label, error, hint, rows = 4, style, onFocus, onBlur,
32
45
  },
33
46
  style,
34
47
  ]}
35
- onFocus={(e) => { setFocused(true); onFocus?.(e) }}
36
- onBlur={(e) => { setFocused(false); onBlur?.(e) }}
48
+ onFocus={(e) => {
49
+ setFocused(true)
50
+ onFocus?.(e)
51
+ }}
52
+ onBlur={(e) => {
53
+ setFocused(false)
54
+ onBlur?.(e)
55
+ }}
37
56
  placeholderTextColor={colors.mutedForeground}
38
57
  allowFontScaling={true}
39
58
  {...props}
40
59
  />
41
60
  {error ? (
42
- <Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
61
+ <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
43
62
  ) : null}
44
63
  {!error && hint ? (
45
- <Text style={[styles.helperText, { color: colors.mutedForeground }]}>{hint}</Text>
64
+ <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
46
65
  ) : null}
47
66
  </View>
48
67
  )