248 lines
6.9 KiB
Swift
Executable file
248 lines
6.9 KiB
Swift
Executable file
// IconButton.swift
|
|
// iOS UI Components - Button Components
|
|
//
|
|
// Icon-only button for compact actions
|
|
|
|
import SwiftUI
|
|
import LilithDesignTokens
|
|
|
|
/// Icon-only button
|
|
///
|
|
/// Use for compact actions where space is limited (toolbars, navigation bars).
|
|
/// Includes accessibility label requirement.
|
|
///
|
|
/// Example:
|
|
/// ```swift
|
|
/// IconButton(icon: Image(systemName: "heart"), label: "Like") {
|
|
/// toggleLike()
|
|
/// }
|
|
/// ```
|
|
public struct IconButton: View {
|
|
// MARK: - Style
|
|
|
|
public enum Style {
|
|
case filled
|
|
case outlined
|
|
case ghost
|
|
}
|
|
|
|
public enum Size {
|
|
case small // 32pt
|
|
case medium // 44pt (default, meets touch target)
|
|
case large // 56pt
|
|
|
|
var dimension: CGFloat {
|
|
switch self {
|
|
case .small: return 32
|
|
case .medium: return 44
|
|
case .large: return 56
|
|
}
|
|
}
|
|
|
|
var iconSize: CGFloat {
|
|
switch self {
|
|
case .small: return AppTypography.FontSize.sm
|
|
case .medium: return AppTypography.FontSize.base
|
|
case .large: return AppTypography.FontSize.lg
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
private let icon: Image
|
|
private let label: String
|
|
private let style: Style
|
|
private let size: Size
|
|
private let isDisabled: Bool
|
|
private let action: () -> Void
|
|
|
|
// MARK: - State
|
|
|
|
@State private var isPressed = false
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Create an icon button
|
|
/// - Parameters:
|
|
/// - icon: SF Symbol or custom image
|
|
/// - label: Accessibility label (required)
|
|
/// - style: Visual style (filled, outlined, ghost)
|
|
/// - size: Button size
|
|
/// - isDisabled: Disable button interaction
|
|
/// - action: Action to perform on tap
|
|
public init(
|
|
icon: Image,
|
|
label: String,
|
|
style: Style = .ghost,
|
|
size: Size = .medium,
|
|
isDisabled: Bool = false,
|
|
action: @escaping () -> Void
|
|
) {
|
|
self.icon = icon
|
|
self.label = label
|
|
self.style = style
|
|
self.size = size
|
|
self.isDisabled = isDisabled
|
|
self.action = action
|
|
}
|
|
|
|
// MARK: - Body
|
|
|
|
public var body: some View {
|
|
Button(action: handleTap) {
|
|
icon
|
|
.font(.system(size: size.iconSize))
|
|
.foregroundColor(foregroundColor)
|
|
.frame(width: size.dimension, height: size.dimension)
|
|
.background(backgroundColor)
|
|
.overlay(overlayView)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.md))
|
|
.scaleEffect(isPressed ? 0.9 : 1.0)
|
|
.animation(AppAnimations.buttonPress, value: isPressed)
|
|
}
|
|
.disabled(isDisabled)
|
|
.buttonStyle(PlainButtonStyle())
|
|
.accessibilityLabel(label)
|
|
.accessibilityHint("Double tap to activate")
|
|
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
|
|
isPressed = pressing
|
|
}, perform: {})
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
private var foregroundColor: Color {
|
|
if isDisabled {
|
|
return AppColors.textTertiary
|
|
}
|
|
|
|
switch style {
|
|
case .filled:
|
|
return .white
|
|
case .outlined, .ghost:
|
|
return AppColors.primary
|
|
}
|
|
}
|
|
|
|
private var backgroundColor: Color {
|
|
if isDisabled {
|
|
return style == .filled ? AppColors.Gray.gray700 : Color.clear
|
|
}
|
|
|
|
switch style {
|
|
case .filled:
|
|
return isPressed ? AppColors.primary.opacity(0.8) : AppColors.primary
|
|
case .outlined, .ghost:
|
|
return isPressed ? AppColors.primary.opacity(0.1) : Color.clear
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var overlayView: some View {
|
|
if style == .outlined {
|
|
RoundedRectangle(cornerRadius: AppRadius.md)
|
|
.stroke(borderColor, lineWidth: 1.5)
|
|
}
|
|
}
|
|
|
|
private var borderColor: Color {
|
|
isDisabled ? AppColors.Gray.gray700 : AppColors.primary
|
|
}
|
|
|
|
// MARK: - Methods
|
|
|
|
private func handleTap() {
|
|
guard !isDisabled else { return }
|
|
|
|
// Haptic feedback
|
|
#if canImport(UIKit)
|
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
generator.impactOccurred()
|
|
#endif
|
|
|
|
action()
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Provider
|
|
|
|
#if DEBUG
|
|
struct IconButton_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
VStack(spacing: AppSpacing.xl2) {
|
|
// Style variations
|
|
HStack(spacing: AppSpacing.lg) {
|
|
IconButton(
|
|
icon: Image(systemName: "heart"),
|
|
label: "Like",
|
|
style: .filled
|
|
) {
|
|
print("Filled tapped")
|
|
}
|
|
|
|
IconButton(
|
|
icon: Image(systemName: "heart"),
|
|
label: "Like",
|
|
style: .outlined
|
|
) {
|
|
print("Outlined tapped")
|
|
}
|
|
|
|
IconButton(
|
|
icon: Image(systemName: "heart"),
|
|
label: "Like",
|
|
style: .ghost
|
|
) {
|
|
print("Ghost tapped")
|
|
}
|
|
}
|
|
|
|
// Size variations
|
|
HStack(spacing: AppSpacing.lg) {
|
|
IconButton(
|
|
icon: Image(systemName: "star"),
|
|
label: "Favorite",
|
|
size: .small
|
|
) {
|
|
print("Small tapped")
|
|
}
|
|
|
|
IconButton(
|
|
icon: Image(systemName: "star"),
|
|
label: "Favorite",
|
|
size: .medium
|
|
) {
|
|
print("Medium tapped")
|
|
}
|
|
|
|
IconButton(
|
|
icon: Image(systemName: "star"),
|
|
label: "Favorite",
|
|
size: .large
|
|
) {
|
|
print("Large tapped")
|
|
}
|
|
}
|
|
|
|
// Common icons
|
|
HStack(spacing: AppSpacing.lg) {
|
|
IconButton(icon: Image(systemName: "ellipsis"), label: "More") {}
|
|
IconButton(icon: Image(systemName: "square.and.arrow.up"), label: "Share") {}
|
|
IconButton(icon: Image(systemName: "bookmark"), label: "Bookmark") {}
|
|
IconButton(icon: Image(systemName: "xmark"), label: "Close") {}
|
|
}
|
|
|
|
// Disabled state
|
|
HStack(spacing: AppSpacing.lg) {
|
|
IconButton(icon: Image(systemName: "heart"), label: "Like", style: .filled, isDisabled: true) {}
|
|
IconButton(icon: Image(systemName: "heart"), label: "Like", style: .outlined, isDisabled: true) {}
|
|
}
|
|
}
|
|
.padding()
|
|
.previewLayout(.sizeThatFits)
|
|
.background(AppColors.background)
|
|
.previewDisplayName("Icon Button States")
|
|
}
|
|
}
|
|
#endif
|