swift-buttons/Sources/LilithButtons/SecondaryButton.swift
2026-02-16 09:29:42 -08:00

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