241 lines
7.2 KiB
Swift
Executable file
241 lines
7.2 KiB
Swift
Executable file
// Checkbox.swift
|
|
// iOS UI Components - Form Components
|
|
//
|
|
// Checkbox with label and indeterminate state
|
|
|
|
import SwiftUI
|
|
import LilithDesignTokens
|
|
|
|
/// Checkbox control
|
|
///
|
|
/// Checkbox with optional label and indeterminate state support.
|
|
///
|
|
/// Example:
|
|
/// ```swift
|
|
/// @State private var isAgreed = false
|
|
///
|
|
/// Checkbox(
|
|
/// "I agree to the terms",
|
|
/// isChecked: $isAgreed
|
|
/// )
|
|
/// ```
|
|
public struct Checkbox: View {
|
|
// MARK: - CheckboxState
|
|
|
|
public enum CheckboxState {
|
|
case unchecked
|
|
case checked
|
|
case indeterminate
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
private let label: String?
|
|
@Binding private var isChecked: Bool
|
|
private let isIndeterminate: Bool
|
|
private let isDisabled: Bool
|
|
private let onChange: ((Bool) -> Void)?
|
|
|
|
// MARK: - State
|
|
|
|
@State private var isPressed = false
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Create a checkbox
|
|
/// - Parameters:
|
|
/// - label: Optional label text
|
|
/// - isChecked: Binding to checked state
|
|
/// - isIndeterminate: Show indeterminate state
|
|
/// - isDisabled: Disable interaction
|
|
/// - onChange: Callback when checked state changes
|
|
public init(
|
|
_ label: String? = nil,
|
|
isChecked: Binding<Bool>,
|
|
isIndeterminate: Bool = false,
|
|
isDisabled: Bool = false,
|
|
onChange: ((Bool) -> Void)? = nil
|
|
) {
|
|
self.label = label
|
|
self._isChecked = isChecked
|
|
self.isIndeterminate = isIndeterminate
|
|
self.isDisabled = isDisabled
|
|
self.onChange = onChange
|
|
}
|
|
|
|
// MARK: - Body
|
|
|
|
public var body: some View {
|
|
Button(action: handleTap) {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
// Checkbox box
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: AppRadius.xs)
|
|
.fill(boxBackgroundColor)
|
|
.frame(width: 24, height: 24)
|
|
|
|
RoundedRectangle(cornerRadius: AppRadius.xs)
|
|
.stroke(borderColor, lineWidth: 2)
|
|
.frame(width: 24, height: 24)
|
|
|
|
// Checkmark or dash
|
|
if isIndeterminate {
|
|
Rectangle()
|
|
.fill(checkmarkColor)
|
|
.frame(width: 12, height: 2)
|
|
} else if isChecked {
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 14, weight: .bold))
|
|
.foregroundColor(checkmarkColor)
|
|
}
|
|
}
|
|
.scaleEffect(isPressed ? 0.9 : 1.0)
|
|
.animation(AppAnimations.buttonPress, value: isPressed)
|
|
|
|
// Label
|
|
if let label = label {
|
|
Text(label)
|
|
.font(AppTypography.body())
|
|
.foregroundColor(labelColor)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
}
|
|
}
|
|
.disabled(isDisabled)
|
|
.buttonStyle(PlainButtonStyle())
|
|
.accessibilityElement(children: .combine)
|
|
.accessibilityLabel(label ?? "Checkbox")
|
|
.accessibilityAddTraits(isChecked ? [.isSelected] : [])
|
|
.accessibilityHint("Double tap to \(isChecked ? "uncheck" : "check")")
|
|
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
|
|
if !isDisabled {
|
|
isPressed = pressing
|
|
}
|
|
}, perform: {})
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
private var state: CheckboxState {
|
|
if isIndeterminate {
|
|
return .indeterminate
|
|
}
|
|
return isChecked ? .checked : .unchecked
|
|
}
|
|
|
|
private var boxBackgroundColor: Color {
|
|
if isDisabled {
|
|
return AppColors.Gray.gray800
|
|
}
|
|
if isChecked || isIndeterminate {
|
|
return AppColors.primary
|
|
}
|
|
return AppColors.surface
|
|
}
|
|
|
|
private var borderColor: Color {
|
|
if isDisabled {
|
|
return AppColors.Gray.gray600
|
|
}
|
|
if isChecked || isIndeterminate {
|
|
return AppColors.primary
|
|
}
|
|
return AppColors.border
|
|
}
|
|
|
|
private var checkmarkColor: Color {
|
|
.white
|
|
}
|
|
|
|
private var labelColor: Color {
|
|
isDisabled ? AppColors.textTertiary : AppColors.textPrimary
|
|
}
|
|
|
|
// MARK: - Methods
|
|
|
|
private func handleTap() {
|
|
guard !isDisabled else { return }
|
|
|
|
isChecked.toggle()
|
|
|
|
// Light haptic feedback
|
|
#if canImport(UIKit)
|
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
generator.impactOccurred()
|
|
#endif
|
|
|
|
onChange?(isChecked)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Provider
|
|
|
|
#if DEBUG
|
|
struct Checkbox_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
VStack(alignment: .leading, spacing: AppSpacing.xl) {
|
|
// Unchecked
|
|
Checkbox("Unchecked", isChecked: .constant(false))
|
|
|
|
// Checked
|
|
Checkbox("Checked", isChecked: .constant(true))
|
|
|
|
// Indeterminate
|
|
Checkbox("Indeterminate", isChecked: .constant(false), isIndeterminate: true)
|
|
|
|
// Disabled unchecked
|
|
Checkbox("Disabled Unchecked", isChecked: .constant(false), isDisabled: true)
|
|
|
|
// Disabled checked
|
|
Checkbox("Disabled Checked", isChecked: .constant(true), isDisabled: true)
|
|
|
|
// Without label
|
|
HStack {
|
|
Checkbox(isChecked: .constant(false))
|
|
Checkbox(isChecked: .constant(true))
|
|
Checkbox(isChecked: .constant(false), isIndeterminate: true)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// In context - Settings form
|
|
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
|
Text("Notification Settings")
|
|
.font(AppTypography.h3())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Checkbox("Email notifications", isChecked: .constant(true))
|
|
Checkbox("Push notifications", isChecked: .constant(true))
|
|
Checkbox("SMS notifications", isChecked: .constant(false))
|
|
Checkbox("Marketing emails", isChecked: .constant(false), isDisabled: true)
|
|
}
|
|
.padding()
|
|
.background(AppColors.surface)
|
|
.cornerRadius(AppRadius.card)
|
|
|
|
Divider()
|
|
|
|
// In context - Terms agreement
|
|
HStack(alignment: .top, spacing: AppSpacing.sm) {
|
|
Checkbox(isChecked: .constant(false))
|
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
|
Text("I agree to the Terms of Service and Privacy Policy")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Required to create an account")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(AppColors.surface)
|
|
.cornerRadius(AppRadius.card)
|
|
}
|
|
.padding()
|
|
.background(AppColors.background)
|
|
.previewDisplayName("Checkbox States")
|
|
}
|
|
}
|
|
#endif
|