Files
native/src/components/Input.tsx
T

110 lines
3.0 KiB
TypeScript

import React from 'react';
import { TextInput as RNTextInput, View, Text, StyleSheet, ViewStyle, TextInputProps } from 'react-native';
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens';
import { useColors } from '../theme/colors';
interface InputProps extends TextInputProps {
label?: string;
error?: string;
containerStyle?: ViewStyle;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
/** React 19 passes `ref` as a prop; it reaches the TextInput (focus chaining). */
ref?: React.Ref<RNTextInput>;
}
/**
* Matches webmail input.tsx:
* - h-10 w-full rounded-md border border-input bg-background
* - px-3 py-2 text-sm text-foreground
* - placeholder:text-muted-foreground
* - hover:border-muted-foreground
* - focus:ring-2 ring-ring border-ring
* - disabled:opacity-50
*/
export default function Input({
label,
error,
containerStyle,
leftIcon,
rightIcon,
style,
ref,
...props
}: InputProps) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const [focused, setFocused] = React.useState(false);
return (
<View style={containerStyle}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={[
styles.inputWrapper,
focused && styles.inputFocused,
error && styles.inputError,
props.editable === false && styles.inputDisabled,
]}>
{leftIcon && <View style={styles.iconLeft}>{leftIcon}</View>}
<RNTextInput
ref={ref}
{...props}
style={[styles.input, style]}
placeholderTextColor={c.textMuted}
onFocus={(e) => { setFocused(true); props.onFocus?.(e); }}
onBlur={(e) => { setFocused(false); props.onBlur?.(e); }}
/>
{rightIcon && <View style={styles.iconRight}>{rightIcon}</View>}
</View>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
}
function makeStyles(c: ThemePalette) {
return StyleSheet.create({
label: {
...typography.bodyMedium,
color: c.textSecondary,
marginBottom: spacing.sm,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
minHeight: componentSizes.inputHeight, // h-10 = 40px; grows with a larger system font
borderWidth: 1,
borderColor: c.border, // border-input
borderRadius: radius.sm, // rounded-md = 6px
backgroundColor: c.background, // bg-background
paddingHorizontal: spacing.md, // px-3 = 12px
},
inputFocused: {
borderColor: c.borderFocus, // border-ring (blue-500)
borderWidth: 2,
},
inputError: {
borderColor: c.error,
},
inputDisabled: {
opacity: 0.5,
},
input: {
flex: 1,
...typography.body, // text-sm = 14px
color: c.text, // text-foreground
paddingVertical: 0,
},
iconLeft: {
marginRight: spacing.sm,
},
iconRight: {
marginLeft: spacing.sm,
},
errorText: {
...typography.caption,
color: c.error,
marginTop: spacing.xs,
},
});
}