frontend/input: fix password mode toggle & add text area
Parents:
8b59f812 file(s) changed
- frontend/components/ui/Input.tsx +98 -19
- frontend/constants/theme.ts +3 -0
frontend/components/ui/Input.tsx
@@ -6,14 +6,51 @@ StyleSheet,
6 6 View,
7 7 Text,
8 8 TouchableOpacity,
9 - Platform
9 + Platform,
10 10 } from 'react-native';
11 11 import { useThemeColor } from '@/hooks/use-theme-color';
12 12 import { Ionicons } from '@expo/vector-icons';
13 13
14 - interface InputProps extends TextInputProps {
14 + type InputVariant = 'default' | 'ghost' | 'darkGhost';
15 +
16 + interface BaseInputProps {
15 17 label?: string;
16 18 error?: string;
19 + variant?: InputVariant;
20 + }
21 +
22 + function useInputStyles(variant: InputVariant, error?: string) {
23 + let backgroundColor = null;
24 +
25 + const textColor = useThemeColor({}, 'text');
26 + const defaultBackgroundColor = useThemeColor({}, 'inputBackgroundColor');
27 + const backgroundDarkGhostColor = useThemeColor({}, 'inputBackgroundDarkGhostColor');
28 + const borderColor = useThemeColor({}, 'inputBorderColor');
29 + const placeholderColor = useThemeColor({}, 'inputPlaceholderColor');
30 + const errorColor = '#ef4444';
31 +
32 + const isGhost = variant === 'ghost';
33 + const isDarkGhost = variant === 'darkGhost';
34 +
35 + if (isDarkGhost) {
36 + backgroundColor = backgroundDarkGhostColor;
37 + } else if (isGhost) {
38 + backgroundColor = 'transparent';
39 + } else {
40 + backgroundColor = defaultBackgroundColor;
41 + }
42 +
43 + const inputStyle = {
44 + color: textColor,
45 + backgroundColor: backgroundColor,
46 + borderColor: error ? errorColor : borderColor,
47 + borderWidth: isGhost || isDarkGhost ? 0 : 1,
48 + };
49 +
50 + return { textColor, placeholderColor, errorColor, inputStyle };
51 + }
52 +
53 + interface InputProps extends TextInputProps, BaseInputProps {
17 54 type?: 'text' | 'email' | 'password';
18 55 showPasswordToggle?: boolean;
19 56 }
@@ -21,17 +58,14 @@
21 58 export const Input: React.FC<InputProps> = ({
22 59 label,
23 60 error,
61 + variant = 'default',
24 62 type = 'text',
25 63 showPasswordToggle = false,
26 64 style,
27 65 ...props
28 66 }) => {
29 67 const [showPassword, setShowPassword] = useState(false);
30 - const textColor = useThemeColor({}, 'text');
31 - const backgroundColor = useThemeColor({}, 'inputBackgroundColor');
32 - const borderColor = useThemeColor({}, 'inputBorderColor');
33 - const errorColor = '#ef4444';
34 - const placeholderColor = useThemeColor({}, 'inputPlaceholderColor');
68 + const { textColor, placeholderColor, errorColor, inputStyle } = useInputStyles(variant, error);
35 69
36 70 const keyboardType = type === 'email' ? 'email-address' : 'default';
37 71 const secureTextEntry = type === 'password' && !showPassword;
@@ -43,15 +77,8 @@ <Text style={[styles.label, { color: textColor }]}>{label}</Text>
43 77 )}
44 78 <View style={styles.inputContainer}>
45 79 <TextInput
46 - style={[
47 - styles.input,
48 - {
49 - color: textColor,
50 - backgroundColor,
51 - borderColor: error ? errorColor : borderColor,
52 - },
53 - style,
54 - ]}
80 + key={`${type}-${showPassword}`}
81 + style={[styles.input, inputStyle, style]}
55 82 placeholderTextColor={placeholderColor}
56 83 keyboardType={keyboardType}
57 84 secureTextEntry={secureTextEntry}
@@ -62,7 +89,8 @@ />
62 89 {type === 'password' && showPasswordToggle && (
63 90 <TouchableOpacity
64 91 style={styles.passwordToggle}
65 - onPress={() => setShowPassword(!showPassword)}
92 + onPress={() => setShowPassword((prev) => !prev)}
93 + hitSlop={8}
66 94 >
67 95 <Ionicons
68 96 name={showPassword ? 'eye-off' : 'eye'}
@@ -73,7 +101,51 @@ </TouchableOpacity>
73 101 )}
74 102 </View>
75 103 {error && (
76 - <Text style={[styles.error, { color: errorColor }]}>{error}</Text>
104 + <Text style={[styles.errorText, { color: errorColor }]}>{error}</Text>
105 + )}
106 + </View>
107 + );
108 + };
109 +
110 + interface TextAreaProps extends TextInputProps, BaseInputProps {
111 + minRows?: number;
112 + maxRows?: number;
113 + }
114 +
115 + export const TextArea: React.FC<TextAreaProps> = ({
116 + label,
117 + error,
118 + variant = 'default',
119 + minRows = 3,
120 + maxRows,
121 + style,
122 + ...props
123 + }) => {
124 + const { textColor, placeholderColor, errorColor, inputStyle } = useInputStyles(variant, error);
125 +
126 + const minHeight = minRows * 24;
127 + const maxHeight = maxRows ? maxRows * 24 : undefined;
128 +
129 + return (
130 + <View style={styles.container}>
131 + {label && (
132 + <Text style={[styles.label, { color: textColor }]}>{label}</Text>
133 + )}
134 + <TextInput
135 + style={[
136 + styles.textArea,
137 + inputStyle,
138 + { minHeight, maxHeight },
139 + style,
140 + ]}
141 + placeholderTextColor={placeholderColor}
142 + multiline
143 + textAlignVertical="top"
144 + scrollEnabled={!!maxRows}
145 + {...props}
146 + />
147 + {error && (
148 + <Text style={[styles.errorText, { color: errorColor }]}>{error}</Text>
77 149 )}
78 150 </View>
79 151 );
@@ -99,12 +171,19 @@ borderWidth: 1,
99 171 borderRadius: 8,
100 172 fontSize: 16,
101 173 },
174 + textArea: {
175 + paddingHorizontal: 12,
176 + paddingVertical: 12,
177 + borderWidth: 1,
178 + borderRadius: 8,
179 + fontSize: 16,
180 + },
102 181 passwordToggle: {
103 182 position: 'absolute',
104 183 right: 12,
105 184 top: 13,
106 185 },
107 - error: {
186 + errorText: {
108 187 fontSize: 12,
109 188 marginTop: 4,
110 189 },
frontend/constants/theme.ts
@@ -12,6 +12,7 @@
12 12 // Surface & Background
13 13 const BACKGROUND_LIGHT = '#f6f8f6';
14 14 const SURFACE_LIGHT = '#ffffff';
15 + const BACKGROUND_DARK_GHOST = '#F8FAFC';
15 16
16 17 // Text Colors
17 18 const TEXT_PRIMARY = '#0f172a';
@@ -42,6 +43,7 @@ textSecondary: TEXT_SECONDARY,
42 43 background: BACKGROUND_LIGHT,
43 44 surface: SURFACE_LIGHT,
44 45 tint: NEON_GREEN,
46 + backgroundDarkGhost: BACKGROUND_DARK_GHOST,
45 47
46 48 // Interactive States
47 49 icon: TEXT_SECONDARY,
@@ -50,6 +52,7 @@ tabIconSelected: NEON_GREEN,
50 52
51 53 // Form Inputs
52 54 inputBackgroundColor: SURFACE_LIGHT,
55 + inputBackgroundDarkGhostColor: BACKGROUND_DARK_GHOST,
53 56 inputBorderColor: BORDER_SUBTLE_GRAY,
54 57 inputFocusBorderColor: NEON_GREEN,
55 58 inputPlaceholderColor: TEXT_PLACEHOLDER,