import React, { forwardRef, useState } from "react"; import { type NativeSyntheticEvent, Pressable, type TargetedEvent, TextInput, type TextInputProps, View, type ViewStyle, } from "react-native"; import { cn } from "../utils/cn"; import { ErrorText, Label } from "./text"; import { Ionicons } from "@expo/vector-icons"; import { useColorScheme } from "nativewind"; export interface InputProps extends TextInputProps { className?: string; containerClassName?: string; label?: string; labelClassName?: string; error?: string; errorClassName?: string; /** * Render the input in the invalid (red border) state without showing * inline error text. Pair with a consolidated form-level error slot. */ invalid?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; isPassword?: boolean; showPasswordToggle?: boolean; containerStyle?: ViewStyle; } export const Input = forwardRef( ( { className, containerClassName, label, labelClassName, error, errorClassName, invalid, leftIcon, rightIcon, isPassword = false, showPasswordToggle = true, containerStyle, onFocus, onBlur, secureTextEntry, ...props }, ref ) => { const showInvalid = invalid || !!error; const [showPassword, setShowPassword] = useState(false); const [isFocused, setIsFocused] = useState(false); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const handleFocus = (e: NativeSyntheticEvent) => { setIsFocused(true); onFocus?.(e); }; const handleBlur = (e: NativeSyntheticEvent) => { setIsFocused(false); onBlur?.(e); }; const shouldShowPassword = isPassword && !showPassword; const showToggle = isPassword && showPasswordToggle; // Each border color carries an explicit `dark:` variant matching that CSS // variable's `.dark` value one-for-one (see button.tsx for why: a bare // CSS-var utility with no `dark:` variant has been observed stuck on its // light value on native regardless of actual system theme). // // Destructive is the one exception: `--destructive`'s dark value // (rgb(127 29 29), a fill-appropriate dark red) has too little contrast // as a 2px outline against a black surface. red-400 matches this // package's own convention for destructive text on dark (see // ErrorText in text.tsx) — legible, not just "technically correct." let borderClass: string; if (showInvalid) { borderClass = "border-2 border-destructive dark:border-red-400"; } else if (isFocused) { borderClass = "border-2 border-primary dark:border-white"; } else { borderClass = "border border-border dark:border-neutral-800"; } return ( {label ? : null} {leftIcon ? {leftIcon} : null} {showToggle ? ( setShowPassword(!showPassword)} className="px-2 py-2" hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > ) : null} {rightIcon && !showToggle ? {rightIcon} : null} {error ? {error} : null} ); } ); Input.displayName = "Input"; export const SearchInput = forwardRef(({ className, ...props }, ref) => { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; return ( } placeholder="Search..." returnKeyType="search" {...props} /> ); }); SearchInput.displayName = "SearchInput"; export const EmailInput = forwardRef(({ className, ...props }, ref) => { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; return ( } placeholder="you@example.com" keyboardType="email-address" autoCapitalize="none" autoCorrect={false} textContentType="emailAddress" autoComplete="email" {...props} /> ); }); EmailInput.displayName = "EmailInput"; export const PasswordInput = forwardRef(({ className, ...props }, ref) => { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; return ( } placeholder="••••••••" isPassword autoCapitalize="none" autoCorrect={false} textContentType="password" autoComplete="password" {...props} /> ); }); PasswordInput.displayName = "PasswordInput"; export const TextArea = forwardRef( ({ className, containerClassName, ...props }, ref) => { const [isFocused, setIsFocused] = useState(false); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const handleFocus = (e: NativeSyntheticEvent) => { setIsFocused(true); props.onFocus?.(e); }; const handleBlur = (e: NativeSyntheticEvent) => { setIsFocused(false); props.onBlur?.(e); }; return ( {props.label ? : null} {props.error ? {props.error} : null} ); } ); TextArea.displayName = "TextArea";