mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
110 lines
3.0 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
}
|