swift-forms/Sources/LilithForms/TextInput.swift

238 lines
7 KiB
Swift
Executable file

// 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<String>,
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