@retray-dev/ui-kit 1.8.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "1.8.0",
3
+ "version": "2.3.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -45,7 +45,9 @@
45
45
  "react-native-reanimated": ">=4.0.0",
46
46
  "react-native-gesture-handler": ">=2.0.0",
47
47
  "react-native-worklets": ">=0.8.0",
48
- "react-native-safe-area-context": ">=4.0.0"
48
+ "react-native-safe-area-context": ">=4.0.0",
49
+ "@react-native-picker/picker": ">=2.0.0",
50
+ "@react-native-community/slider": ">=4.0.0"
49
51
  },
50
52
  "pnpm": {
51
53
  "overrides": {
@@ -60,6 +62,8 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "@gorhom/bottom-sheet": "^5.0.0",
65
+ "@react-native-picker/picker": "2.11.4",
66
+ "@react-native-community/slider": "^4.5.5",
63
67
  "@types/react": "^19.1.0",
64
68
  "expo-haptics": "~15.0.8",
65
69
  "expo-linear-gradient": "~15.0.8",
@@ -39,13 +39,13 @@ export function AlertBanner({ title, description, variant = 'default', icon, sty
39
39
  <View style={styles.icon}>{icon}</View>
40
40
  ) : (
41
41
  <View style={styles.icon}>
42
- <Text style={[styles.defaultIcon, { color: titleColor }]}>{defaultIcon}</Text>
42
+ <Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
43
43
  </View>
44
44
  )}
45
45
  <View style={styles.content}>
46
- {title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
46
+ {title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
47
47
  {description ? (
48
- <Text style={[styles.description, { color: descColor }]}>{description}</Text>
48
+ <Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
49
49
  ) : null}
50
50
  </View>
51
51
  </View>
@@ -0,0 +1,83 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type AlertBannerVariant = 'default' | 'destructive' | 'success'
6
+
7
+ export interface AlertBannerProps {
8
+ title?: string
9
+ description?: string
10
+ variant?: AlertBannerVariant
11
+ icon?: React.ReactNode
12
+ style?: ViewStyle
13
+ }
14
+
15
+ export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
16
+ const { colors } = useTheme()
17
+
18
+ const borderColor =
19
+ variant === 'destructive' ? colors.destructive
20
+ : variant === 'success' ? colors.success
21
+ : colors.border
22
+
23
+ const titleColor =
24
+ variant === 'destructive' ? colors.destructive
25
+ : variant === 'success' ? colors.success
26
+ : colors.foreground
27
+
28
+ const descColor =
29
+ variant === 'destructive' ? colors.destructive
30
+ : variant === 'success' ? colors.success
31
+ : colors.mutedForeground
32
+
33
+ const defaultIcon =
34
+ variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
35
+
36
+ return (
37
+ <View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
38
+ {icon ? (
39
+ <View style={styles.icon}>{icon}</View>
40
+ ) : (
41
+ <View style={styles.icon}>
42
+ <Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
43
+ </View>
44
+ )}
45
+ <View style={styles.content}>
46
+ {title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
47
+ {description ? (
48
+ <Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
49
+ ) : null}
50
+ </View>
51
+ </View>
52
+ )
53
+ }
54
+
55
+ const styles = StyleSheet.create({
56
+ container: {
57
+ flexDirection: 'row',
58
+ borderWidth: 1,
59
+ borderRadius: 8,
60
+ padding: 16,
61
+ gap: 12,
62
+ },
63
+ icon: {
64
+ marginTop: 2,
65
+ },
66
+ content: {
67
+ flex: 1,
68
+ gap: 4,
69
+ },
70
+ title: {
71
+ fontSize: 14,
72
+ fontWeight: '500',
73
+ lineHeight: 20,
74
+ },
75
+ description: {
76
+ fontSize: 14,
77
+ lineHeight: 20,
78
+ },
79
+ defaultIcon: {
80
+ fontSize: 18,
81
+ fontWeight: '700',
82
+ },
83
+ })
@@ -1,2 +1,2 @@
1
- export { AlertBanner } from './Alert'
2
- export type { AlertBannerProps, AlertBannerVariant } from './Alert'
1
+ export { AlertBanner } from './AlertBanner'
2
+ export type { AlertBannerProps, AlertBannerVariant } from './AlertBanner'
@@ -52,6 +52,7 @@ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
52
52
  ) : (
53
53
  <Text
54
54
  style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
55
+ allowFontScaling={true}
55
56
  >
56
57
  {fallback?.slice(0, 2).toUpperCase() ?? '?'}
57
58
  </Text>
@@ -1,16 +1,40 @@
1
1
  import React from 'react'
2
- import { View, Text, StyleSheet, ViewStyle } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
5
  export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
6
+ export type BadgeSize = 'sm' | 'md' | 'lg'
6
7
 
7
8
  export interface BadgeProps {
8
- label: string
9
+ label?: string
10
+ /** Alternative to \`label\` — allows JSX children. */
11
+ children?: React.ReactNode
9
12
  variant?: BadgeVariant
13
+ size?: BadgeSize
14
+ /** Icon rendered before the label/children. */
15
+ icon?: React.ReactNode
10
16
  style?: ViewStyle
11
17
  }
12
18
 
13
- export function Badge({ label, variant = 'default', style }: BadgeProps) {
19
+ const sizePadding: Record<BadgeSize, ViewStyle> = {
20
+ sm: { paddingHorizontal: 8, paddingVertical: 2 },
21
+ md: { paddingHorizontal: 10, paddingVertical: 4 },
22
+ lg: { paddingHorizontal: 12, paddingVertical: 6 },
23
+ }
24
+
25
+ const sizeFontSize: Record<BadgeSize, TextStyle> = {
26
+ sm: { fontSize: 11 },
27
+ md: { fontSize: 13 },
28
+ lg: { fontSize: 15 },
29
+ }
30
+
31
+ const sizeIconGap: Record<BadgeSize, number> = {
32
+ sm: 4,
33
+ md: 6,
34
+ lg: 6,
35
+ }
36
+
37
+ export function Badge({ label, children, variant = 'default', size = 'md', icon, style }: BadgeProps) {
14
38
  const { colors } = useTheme()
15
39
 
16
40
  const containerStyle: ViewStyle = {
@@ -27,9 +51,18 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
27
51
  outline: colors.foreground,
28
52
  }[variant]
29
53
 
54
+ const content = children ?? label
55
+
30
56
  return (
31
- <View style={[styles.container, containerStyle, style]}>
32
- <Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Text>
57
+ <View style={[styles.container, containerStyle, sizePadding[size], { gap: sizeIconGap[size] }, style]}>
58
+ {icon ? <View style={styles.iconContainer}>{icon}</View> : null}
59
+ {typeof content === 'string' ? (
60
+ <Text style={[styles.label, { color: textColor }, sizeFontSize[size]]} allowFontScaling={true}>
61
+ {content}
62
+ </Text>
63
+ ) : (
64
+ content
65
+ )}
33
66
  </View>
34
67
  )
35
68
  }
@@ -37,12 +70,15 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
37
70
  const styles = StyleSheet.create({
38
71
  container: {
39
72
  borderRadius: 8,
40
- paddingHorizontal: 10,
41
- paddingVertical: 4,
42
73
  alignSelf: 'flex-start',
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ },
77
+ iconContainer: {
78
+ justifyContent: 'center',
79
+ alignItems: 'center',
43
80
  },
44
81
  label: {
45
- fontSize: 13,
46
82
  fontWeight: '500',
47
83
  },
48
84
  })
@@ -150,7 +150,7 @@ const styles = StyleSheet.create({
150
150
  width: '100%',
151
151
  },
152
152
  disabled: {
153
- opacity: 0.45,
153
+ opacity: 0.5,
154
154
  },
155
155
  label: {
156
156
  fontWeight: '600',
@@ -1,9 +1,18 @@
1
- import React from 'react'
2
- import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle, TextStyle, Platform } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
3
4
  import { useTheme } from '../../theme'
4
5
 
6
+ const nativeDriver = Platform.OS !== 'web'
7
+
8
+ export type CardVariant = 'elevated' | 'outlined' | 'filled'
9
+
5
10
  export interface CardProps {
6
11
  children: React.ReactNode
12
+ /** Visual style variant. `'elevated'` (default) has shadow, `'outlined'` has border only, `'filled'` uses accent background. */
13
+ variant?: CardVariant
14
+ /** Makes the card tappable. Adds press animation and haptic feedback. */
15
+ onPress?: () => void
7
16
  style?: ViewStyle
8
17
  }
9
18
 
@@ -32,15 +41,83 @@ export interface CardFooterProps {
32
41
  style?: ViewStyle
33
42
  }
34
43
 
35
- export function Card({ children, style }: CardProps) {
44
+ export function Card({ children, variant = 'elevated', onPress, style }: CardProps) {
36
45
  const { colors } = useTheme()
37
- return (
38
- <View
39
- style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }, style]}
40
- >
46
+ const scale = useRef(new Animated.Value(1)).current
47
+
48
+ const handlePressIn = () => {
49
+ if (!onPress) return
50
+ Animated.spring(scale, {
51
+ toValue: 0.98,
52
+ useNativeDriver: nativeDriver,
53
+ speed: 40,
54
+ bounciness: 0,
55
+ }).start()
56
+ }
57
+
58
+ const handlePressOut = () => {
59
+ if (!onPress) return
60
+ Animated.spring(scale, {
61
+ toValue: 1,
62
+ useNativeDriver: nativeDriver,
63
+ speed: 40,
64
+ bounciness: 4,
65
+ }).start()
66
+ }
67
+
68
+ const handlePress = () => {
69
+ if (!onPress) return
70
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
71
+ onPress()
72
+ }
73
+
74
+ const variantStyle: ViewStyle = {
75
+ elevated: {
76
+ backgroundColor: colors.card,
77
+ borderColor: colors.border,
78
+ shadowColor: '#000',
79
+ shadowOffset: { width: 0, height: 1 },
80
+ shadowOpacity: 0.05,
81
+ shadowRadius: 2,
82
+ elevation: 1,
83
+ },
84
+ outlined: {
85
+ backgroundColor: colors.card,
86
+ borderColor: colors.border,
87
+ shadowOpacity: 0,
88
+ elevation: 0,
89
+ },
90
+ filled: {
91
+ backgroundColor: colors.accent,
92
+ borderColor: colors.border,
93
+ shadowOpacity: 0,
94
+ elevation: 0,
95
+ },
96
+ }[variant]
97
+
98
+ const cardContent = (
99
+ <View style={[styles.card, variantStyle, style]}>
41
100
  {children}
42
101
  </View>
43
102
  )
103
+
104
+ if (onPress) {
105
+ return (
106
+ <Animated.View style={{ transform: [{ scale }] }}>
107
+ <TouchableOpacity
108
+ onPress={handlePress}
109
+ onPressIn={handlePressIn}
110
+ onPressOut={handlePressOut}
111
+ activeOpacity={1}
112
+ touchSoundDisabled={true}
113
+ >
114
+ {cardContent}
115
+ </TouchableOpacity>
116
+ </Animated.View>
117
+ )
118
+ }
119
+
120
+ return cardContent
44
121
  }
45
122
 
46
123
  export function CardHeader({ children, style }: CardHeaderProps) {
@@ -49,13 +126,13 @@ export function CardHeader({ children, style }: CardHeaderProps) {
49
126
 
50
127
  export function CardTitle({ children, style }: CardTitleProps) {
51
128
  const { colors } = useTheme()
52
- return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
129
+ return <Text style={[styles.title, { color: colors.cardForeground }, style]} allowFontScaling={true}>{children}</Text>
53
130
  }
54
131
 
55
132
  export function CardDescription({ children, style }: CardDescriptionProps) {
56
133
  const { colors } = useTheme()
57
134
  return (
58
- <Text style={[styles.description, { color: colors.mutedForeground }, style]}>{children}</Text>
135
+ <Text style={[styles.description, { color: colors.mutedForeground }, style]} allowFontScaling={true}>{children}</Text>
59
136
  )
60
137
  }
61
138
 
@@ -28,8 +28,10 @@ export interface ChipOption {
28
28
 
29
29
  export interface ChipGroupProps {
30
30
  options: ChipOption[]
31
- value?: string | number
32
- onValueChange?: (value: string | number) => void
31
+ value?: string | number | (string | number)[]
32
+ onValueChange?: (value: string | number | (string | number)[]) => void
33
+ /** When true, allows selecting multiple chips. `value` and `onValueChange` will use arrays. */
34
+ multiSelect?: boolean
33
35
  style?: ViewStyle
34
36
  }
35
37
 
@@ -104,15 +106,44 @@ export function Chip({ label, selected = false, onPress, style }: ChipProps) {
104
106
  )
105
107
  }
106
108
 
107
- export function ChipGroup({ options, value, onValueChange, style }: ChipGroupProps) {
109
+ export function ChipGroup({ options, value, onValueChange, multiSelect = false, style }: ChipGroupProps) {
110
+ const handlePress = (optionValue: string | number) => {
111
+ if (!multiSelect) {
112
+ onValueChange?.(optionValue)
113
+ return
114
+ }
115
+
116
+ // Multiselect logic
117
+ const currentArray = Array.isArray(value) ? value : value ? [value] : []
118
+ const isSelected = currentArray.includes(optionValue)
119
+
120
+ let newArray: (string | number)[]
121
+ if (isSelected) {
122
+ // Remove from selection
123
+ newArray = currentArray.filter((v) => v !== optionValue)
124
+ } else {
125
+ // Add to selection
126
+ newArray = [...currentArray, optionValue]
127
+ }
128
+
129
+ onValueChange?.(newArray)
130
+ }
131
+
132
+ const isSelected = (optionValue: string | number): boolean => {
133
+ if (Array.isArray(value)) {
134
+ return value.includes(optionValue)
135
+ }
136
+ return optionValue === value
137
+ }
138
+
108
139
  return (
109
140
  <View style={[styles.group, style]}>
110
141
  {options.map((opt) => (
111
142
  <Chip
112
143
  key={opt.value}
113
144
  label={opt.label}
114
- selected={opt.value === value}
115
- onPress={() => onValueChange?.(opt.value)}
145
+ selected={isSelected(opt.value)}
146
+ onPress={() => handlePress(opt.value)}
116
147
  />
117
148
  ))}
118
149
  </View>
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { ViewStyle } from 'react-native'
2
+ import { ViewStyle, TextStyle } from 'react-native'
3
3
  import { Input } from '../Input'
4
4
 
5
5
  export interface CurrencyInputProps {
@@ -11,6 +11,8 @@ export interface CurrencyInputProps {
11
11
  prefix?: string
12
12
  /** Character used to separate groups of three digits. Defaults to `'.'`. */
13
13
  thousandsSeparator?: '.' | ','
14
+ /** Font size variant. `'large'` renders at 36pt, `'default'` at 17pt. */
15
+ size?: 'default' | 'large'
14
16
  label?: string
15
17
  /** Red helper text; also changes input border to destructive color. */
16
18
  error?: string
@@ -18,6 +20,7 @@ export interface CurrencyInputProps {
18
20
  placeholder?: string
19
21
  editable?: boolean
20
22
  containerStyle?: ViewStyle
23
+ style?: TextStyle
21
24
  }
22
25
 
23
26
  function formatCurrency(raw: string, separator: '.' | ','): string {
@@ -32,12 +35,14 @@ export function CurrencyInput({
32
35
  onChangeValue,
33
36
  prefix = '$',
34
37
  thousandsSeparator = '.',
38
+ size = 'default',
35
39
  label,
36
40
  error,
37
41
  hint,
38
42
  placeholder,
39
43
  editable,
40
44
  containerStyle,
45
+ style,
41
46
  }: CurrencyInputProps) {
42
47
  const handleChange = (text: string) => {
43
48
  const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
@@ -49,6 +54,8 @@ export function CurrencyInput({
49
54
  onChangeValue?.(isNaN(raw) ? 0 : raw)
50
55
  }
51
56
 
57
+ const inputStyle: TextStyle = size === 'large' ? { fontSize: 36 } : {}
58
+
52
59
  return (
53
60
  <Input
54
61
  value={value}
@@ -60,6 +67,7 @@ export function CurrencyInput({
60
67
  placeholder={placeholder ?? `${prefix}0`}
61
68
  editable={editable}
62
69
  containerStyle={containerStyle}
70
+ style={[inputStyle, style]}
63
71
  />
64
72
  )
65
73
  }
@@ -39,11 +39,12 @@ export function EmptyState({ icon, title, description, action, size = 'default',
39
39
  <View style={styles.textWrapper}>
40
40
  <Text
41
41
  style={[styles.title, isCompact && styles.titleCompact, { color: colors.foreground }]}
42
+ allowFontScaling={true}
42
43
  >
43
44
  {title}
44
45
  </Text>
45
46
  {description && !isCompact ? (
46
- <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
47
+ <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>{description}</Text>
47
48
  ) : null}
48
49
  </View>
49
50
  {action && !isCompact ? <View style={styles.action}>{action}</View> : null}
@@ -1,46 +1,104 @@
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
3
  import { useTheme } from '../../theme'
4
4
 
5
+ const webInputResetStyle: any =
6
+ Platform.OS === 'web'
7
+ ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
8
+ : {}
9
+
5
10
  export interface InputProps extends TextInputProps {
6
11
  label?: string
7
12
  /** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
8
13
  error?: string
9
14
  /** Helper text shown below the input when there is no error. */
10
15
  hint?: string
11
- /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
16
+ /** Text or component rendered before the input text. */
17
+ prefix?: React.ReactNode
18
+ /** Text or component rendered after the input text. */
19
+ suffix?: React.ReactNode
20
+ /** Style applied to prefix text if prefix is a string. */
21
+ prefixStyle?: TextStyle
22
+ /** Style applied to suffix text if suffix is a string. */
23
+ suffixStyle?: TextStyle
24
+ /** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
25
+ type?: 'text' | 'password'
26
+ /** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
12
27
  containerStyle?: ViewStyle
13
28
  }
14
29
 
15
- export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
30
+ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
16
31
  const { colors } = useTheme()
17
32
  const [focused, setFocused] = useState(false)
33
+ const [showPassword, setShowPassword] = useState(false)
34
+
35
+ const isPassword = type === 'password'
36
+ const effectiveSecure = isPassword ? !showPassword : secureTextEntry
37
+
38
+ // If type is password and no suffix is provided, add the toggle button
39
+ const effectiveSuffix = isPassword && !suffix ? (
40
+ <TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
41
+ <Text style={[styles.suffixText, { color: colors.mutedForeground }]}>{showPassword ? '👁' : '👁‍🗨'}</Text>
42
+ </TouchableOpacity>
43
+ ) : suffix
18
44
 
19
45
  return (
20
46
  <View style={[styles.container, containerStyle]}>
21
47
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
22
- <TextInput
48
+ <View
23
49
  style={[
24
- styles.input,
50
+ styles.inputWrapper,
25
51
  {
26
- borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
27
- color: colors.foreground,
52
+ borderColor: error
53
+ ? colors.destructive
54
+ : focused
55
+ ? (colors.ring ?? colors.primary)
56
+ : colors.border,
28
57
  backgroundColor: colors.background,
29
58
  },
30
- style,
31
59
  ]}
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
- />
60
+ >
61
+ {prefix ? (
62
+ typeof prefix === 'string' ? (
63
+ <Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
64
+ {prefix}
65
+ </Text>
66
+ ) : (
67
+ <View style={styles.prefixContainer}>{prefix}</View>
68
+ )
69
+ ) : null}
70
+ <TextInput
71
+ style={[
72
+ styles.input,
73
+ {
74
+ color: colors.foreground,
75
+ },
76
+ webInputResetStyle,
77
+ style,
78
+ ]}
79
+ onFocus={(e) => {
80
+ setFocused(true)
81
+ onFocus?.(e)
82
+ }}
83
+ onBlur={(e) => {
84
+ setFocused(false)
85
+ onBlur?.(e)
86
+ }}
87
+ placeholderTextColor={colors.mutedForeground}
88
+ allowFontScaling={true}
89
+ secureTextEntry={effectiveSecure}
90
+ {...props}
91
+ />
92
+ {effectiveSuffix ? (
93
+ typeof effectiveSuffix === 'string' ? (
94
+ <Text style={[styles.suffixText, { color: colors.mutedForeground }, suffixStyle]} allowFontScaling={true}>
95
+ {effectiveSuffix}
96
+ </Text>
97
+ ) : (
98
+ <View style={styles.suffixContainer}>{effectiveSuffix}</View>
99
+ )
100
+ ) : null}
101
+ </View>
44
102
  {error ? (
45
103
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
46
104
  ) : null}
@@ -60,12 +118,35 @@ const styles = StyleSheet.create({
60
118
  fontWeight: '500',
61
119
  marginBottom: 6,
62
120
  },
63
- input: {
121
+ inputWrapper: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
64
124
  borderWidth: 1.5,
65
125
  borderRadius: 14,
66
126
  paddingHorizontal: 20,
67
127
  paddingVertical: 16,
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,
@@ -49,7 +49,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
49
49
  activeOpacity={0.6}
50
50
  touchSoundDisabled={true}
51
51
  >
52
- <Text style={[styles.arrowText, { color: colors.foreground }]}>‹</Text>
52
+ <Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>‹</Text>
53
53
  </TouchableOpacity>
54
54
  <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
55
55
  {MONTH_NAMES[value.month - 1]} {value.year}
@@ -60,7 +60,7 @@ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
60
60
  activeOpacity={0.6}
61
61
  touchSoundDisabled={true}
62
62
  >
63
- <Text style={[styles.arrowText, { color: colors.foreground }]}>›</Text>
63
+ <Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>›</Text>
64
64
  </TouchableOpacity>
65
65
  </View>
66
66
  )