swift-ui-forms/Sources/LilithForms/SecureInput.swift
2026-02-16 04:57:38 -08:00

197 lines
5.6 KiB
Swift
Executable file

// SecureInput.swift
// iOS UI Components - Form Components
//
// Secure password input with reveal toggle
import SwiftUI
import LilithDesignTokens
/// Secure text input for passwords
///
/// Password field with show/hide toggle.
///
/// Example:
/// ```swift
/// @State private var password = ""
///
/// SecureInput("Password", text: $password)
/// ```
public struct SecureInput: 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 isDisabled: Bool
private let onCommit: (() -> Void)?
// MARK: - State
@State private var isSecure = true
@FocusState private var isFocused: Bool
// MARK: - Initialization
public init(
_ label: String,
text: Binding<String>,
placeholder: String = "",
errorMessage: String? = nil,
helperText: String? = nil,
isDisabled: Bool = false,
onCommit: (() -> Void)? = nil
) {
self.label = label
self._text = text
self.placeholder = placeholder
self.errorMessage = errorMessage
self.helperText = helperText
self.isDisabled = isDisabled
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) {
Image(systemName: "lock")
.font(.system(size: AppTypography.FontSize.base))
.foregroundColor(iconColor)
if isSecure {
SecureField(placeholder, text: $text)
.font(AppTypography.body())
.foregroundColor(AppColors.textPrimary)
.disabled(isDisabled)
.focused($isFocused)
.onSubmit {
onCommit?()
}
} else {
TextField(placeholder, text: $text)
.font(AppTypography.body())
.foregroundColor(AppColors.textPrimary)
.disabled(isDisabled)
.focused($isFocused)
.onSubmit {
onCommit?()
}
}
// Show/hide toggle
Button(action: { isSecure.toggle() }) {
Image(systemName: isSecure ? "eye" : "eye.slash")
.font(.system(size: AppTypography.FontSize.base))
.foregroundColor(AppColors.textSecondary)
}
}
.padding(.horizontal, AppSpacing.md)
.padding(.vertical, AppSpacing.sm)
.background(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.input)
.stroke(borderColor, lineWidth: isFocused ? 2 : 1)
)
.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 SecureInput_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: AppSpacing.xl) {
SecureInput("Password", text: .constant(""))
SecureInput("Password", text: .constant("secret123"))
SecureInput(
"Password",
text: .constant(""),
helperText: "Minimum 8 characters"
)
SecureInput(
"Password",
text: .constant("weak"),
errorMessage: "Password is too weak"
)
SecureInput(
"Password",
text: .constant("locked"),
isDisabled: true
)
}
.padding()
.background(AppColors.background)
.previewDisplayName("Secure Input States")
}
}
#endif