// TextInput.swift // iOS UI Components - Form Components // // Text input field with validation and states (iOS only) #if canImport(UIKit) import SwiftUI import UIKit import LilithDesignTokens /// Text input field /// /// Styled text field with label, placeholder, validation, and error states. /// /// Example: /// ```swift /// @State private var email = "" /// @State private var emailError: String? /// /// TextInput( /// "Email", /// text: $email, /// placeholder: "you@example.com", /// errorMessage: emailError, /// keyboardType: .emailAddress /// ) /// ``` @available(iOS 13.0, *) public struct TextInput: View { // MARK: - Properties private let label: String @Binding private var text: String private let placeholder: String private let errorMessage: String? private let helperText: String? private let keyboardType: UIKeyboardType private let autocapitalization: TextInputAutocapitalization private let isDisabled: Bool private let icon: Image? private let onCommit: (() -> Void)? // MARK: - Focus State @FocusState private var isFocused: Bool // MARK: - Initialization /// Create a text input /// - Parameters: /// - label: Input label /// - text: Binding to text value /// - placeholder: Placeholder text /// - errorMessage: Error message to display /// - helperText: Helper text below input /// - keyboardType: Keyboard type /// - autocapitalization: Autocapitalization behavior /// - isDisabled: Disable input /// - icon: Optional leading icon /// - onCommit: Action when return is pressed public init( _ label: String, text: Binding, placeholder: String = "", errorMessage: String? = nil, helperText: String? = nil, keyboardType: UIKeyboardType = .default, autocapitalization: TextInputAutocapitalization = .sentences, isDisabled: Bool = false, icon: Image? = nil, onCommit: (() -> Void)? = nil ) { self.label = label self._text = text self.placeholder = placeholder self.errorMessage = errorMessage self.helperText = helperText self.keyboardType = keyboardType self.autocapitalization = autocapitalization self.isDisabled = isDisabled self.icon = icon self.onCommit = onCommit } // MARK: - Body public var body: some View { VStack(alignment: .leading, spacing: AppSpacing.xs) { // Label Text(label) .font(AppTypography.label(weight: .medium)) .foregroundColor(labelColor) // Input container HStack(spacing: AppSpacing.sm) { if let icon = icon { icon .font(.system(size: AppTypography.FontSize.base)) .foregroundColor(iconColor) } TextField(placeholder, text: $text) .font(AppTypography.body()) .foregroundColor(AppColors.textPrimary) .keyboardType(keyboardType) .textInputAutocapitalization(autocapitalization) .disabled(isDisabled) .focused($isFocused) .onSubmit { onCommit?() } // Clear button if !text.isEmpty && isFocused { Button(action: { text = "" }) { Image(systemName: "xmark.circle.fill") .font(.system(size: AppTypography.FontSize.base)) .foregroundColor(AppColors.textTertiary) } } } .padding(.horizontal, AppSpacing.md) .padding(.vertical, AppSpacing.sm) .background(backgroundColor) .overlay( RoundedRectangle(cornerRadius: AppRadius.input) .stroke(borderColor, lineWidth: isFocused ? 2 : 1) ) .clipShape(RoundedRectangle(cornerRadius: AppRadius.input)) // Helper or error text if let errorMessage = errorMessage { Text(errorMessage) .font(AppTypography.caption()) .foregroundColor(AppColors.Semantic.error) } else if let helperText = helperText { Text(helperText) .font(AppTypography.caption()) .foregroundColor(AppColors.textSecondary) } } .animation(AppAnimations.fast, value: isFocused) .animation(AppAnimations.fast, value: errorMessage) } // MARK: - Computed Properties private var labelColor: Color { if isDisabled { return AppColors.textTertiary } if errorMessage != nil { return AppColors.Semantic.error } if isFocused { return AppColors.primary } return AppColors.textSecondary } private var borderColor: Color { if isDisabled { return AppColors.Gray.gray700 } if errorMessage != nil { return AppColors.Semantic.error } if isFocused { return AppColors.primary } return AppColors.border } private var backgroundColor: Color { isDisabled ? AppColors.Gray.gray800 : AppColors.surface } private var iconColor: Color { if isDisabled { return AppColors.textTertiary } if isFocused { return AppColors.primary } return AppColors.textSecondary } } // MARK: - Preview Provider #if DEBUG struct TextInput_Previews: PreviewProvider { static var previews: some View { VStack(spacing: AppSpacing.xl) { // Default state TextInput("Email", text: .constant(""), placeholder: "you@example.com") // With value TextInput("Name", text: .constant("John Doe")) // With icon TextInput( "Email", text: .constant(""), placeholder: "you@example.com", icon: Image(systemName: "envelope") ) // With helper text TextInput( "Username", text: .constant(""), placeholder: "Choose a username", helperText: "Must be unique and 3-20 characters" ) // With error TextInput( "Email", text: .constant("invalid"), placeholder: "you@example.com", errorMessage: "Please enter a valid email address" ) // Disabled TextInput( "Locked Field", text: .constant("Cannot edit"), isDisabled: true ) } .padding() .background(AppColors.background) .previewDisplayName("Text Input States") } } #endif #endif