172 lines
4.9 KiB
Swift
Executable file
172 lines
4.9 KiB
Swift
Executable file
// SecondaryButton.swift
|
|
// iOS UI Components - Button Components
|
|
//
|
|
// Secondary action button with outlined style
|
|
|
|
import SwiftUI
|
|
import LilithDesignTokens
|
|
|
|
/// Secondary action button
|
|
///
|
|
/// Use for less prominent actions or alternate choices.
|
|
/// Features outlined style with transparent background.
|
|
///
|
|
/// Example:
|
|
/// ```swift
|
|
/// SecondaryButton("Cancel") {
|
|
/// dismiss()
|
|
/// }
|
|
/// ```
|
|
public struct SecondaryButton: View {
|
|
// MARK: - Properties
|
|
|
|
private let title: String
|
|
private let icon: Image?
|
|
private let isLoading: Bool
|
|
private let isDisabled: Bool
|
|
private let fullWidth: Bool
|
|
private let action: () -> Void
|
|
|
|
// MARK: - State
|
|
|
|
@State private var isPressed = false
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Create a secondary button
|
|
/// - Parameters:
|
|
/// - title: Button label text
|
|
/// - icon: Optional leading icon
|
|
/// - isLoading: Show loading indicator
|
|
/// - isDisabled: Disable button interaction
|
|
/// - fullWidth: Expand to fill available width
|
|
/// - action: Action to perform on tap
|
|
public init(
|
|
_ title: String,
|
|
icon: Image? = nil,
|
|
isLoading: Bool = false,
|
|
isDisabled: Bool = false,
|
|
fullWidth: Bool = false,
|
|
action: @escaping () -> Void
|
|
) {
|
|
self.title = title
|
|
self.icon = icon
|
|
self.isLoading = isLoading
|
|
self.isDisabled = isDisabled
|
|
self.fullWidth = fullWidth
|
|
self.action = action
|
|
}
|
|
|
|
// MARK: - Body
|
|
|
|
public var body: some View {
|
|
Button(action: handleTap) {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
if isLoading {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
|
|
.scaleEffect(0.8)
|
|
} else if let icon = icon {
|
|
icon
|
|
.font(.system(size: AppTypography.FontSize.base))
|
|
}
|
|
|
|
Text(title)
|
|
.font(AppTypography.body(weight: .semibold))
|
|
}
|
|
.foregroundColor(foregroundColor)
|
|
.frame(maxWidth: fullWidth ? .infinity : nil)
|
|
.frame(height: AppSpacing.touchTarget)
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
.background(backgroundColor)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: AppRadius.button)
|
|
.stroke(borderColor, lineWidth: 2)
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.button))
|
|
.scaleEffect(isPressed ? 0.95 : 1.0)
|
|
.animation(AppAnimations.buttonPress, value: isPressed)
|
|
}
|
|
.disabled(isDisabled || isLoading)
|
|
.buttonStyle(PlainButtonStyle())
|
|
.accessibilityLabel(title)
|
|
.accessibilityHint(isLoading ? "Loading" : "Double tap to activate")
|
|
.accessibilityAddTraits(isLoading ? [.updatesFrequently] : [])
|
|
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
|
|
isPressed = pressing
|
|
}, perform: {})
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
private var foregroundColor: Color {
|
|
if isDisabled {
|
|
return AppColors.textTertiary
|
|
}
|
|
return AppColors.primary
|
|
}
|
|
|
|
private var borderColor: Color {
|
|
if isDisabled {
|
|
return AppColors.Gray.gray700
|
|
}
|
|
return AppColors.primary
|
|
}
|
|
|
|
private var backgroundColor: Color {
|
|
isPressed ? AppColors.primary.opacity(0.1) : Color.clear
|
|
}
|
|
|
|
// MARK: - Methods
|
|
|
|
private func handleTap() {
|
|
guard !isLoading && !isDisabled else { return }
|
|
|
|
// Haptic feedback
|
|
#if canImport(UIKit)
|
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
generator.impactOccurred()
|
|
#endif
|
|
|
|
action()
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Provider
|
|
|
|
#if DEBUG
|
|
struct SecondaryButton_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
VStack(spacing: AppSpacing.xl) {
|
|
// Default state
|
|
SecondaryButton("Cancel") {
|
|
print("Secondary button tapped")
|
|
}
|
|
|
|
// With icon
|
|
SecondaryButton("Share", icon: Image(systemName: "square.and.arrow.up")) {
|
|
print("Share tapped")
|
|
}
|
|
|
|
// Loading state
|
|
SecondaryButton("Loading", isLoading: true) {
|
|
print("Should not fire")
|
|
}
|
|
|
|
// Disabled state
|
|
SecondaryButton("Unavailable", isDisabled: true) {
|
|
print("Should not fire")
|
|
}
|
|
|
|
// Full width
|
|
SecondaryButton("Secondary Action", fullWidth: true) {
|
|
print("Full width button tapped")
|
|
}
|
|
}
|
|
.padding()
|
|
.previewLayout(.sizeThatFits)
|
|
.background(AppColors.background)
|
|
.previewDisplayName("Secondary Button States")
|
|
}
|
|
}
|
|
#endif
|