CasinoGames/CasinoKit/Sources/CasinoKit/Views/Buttons/ActionButton.swift

158 lines
4.7 KiB
Swift

//
// ActionButton.swift
// CasinoKit
//
// Reusable action buttons for casino games (Deal, Hit, Stand, etc.).
//
import SwiftUI
/// A styled action button for casino games.
public struct ActionButton: View {
/// The button title.
public let title: String
/// Optional SF Symbol icon.
public let icon: String?
/// Button style variant.
public let style: ActionButtonStyle
/// Whether the button is enabled.
public let isEnabled: Bool
/// The action to perform.
public let action: () -> Void
// Font sizes
private let fontSize: CGFloat = 18
private let iconSize: CGFloat = 20
/// Minimum button width to prevent tiny buttons for short words like "Hit"
private let minButtonWidth: CGFloat = CasinoDesign.Size.actionButtonMinWidth
/// Creates an action button.
/// - Parameters:
/// - title: The button title.
/// - icon: Optional SF Symbol name.
/// - style: The button style.
/// - isEnabled: Whether enabled.
/// - action: The action to perform.
public init(
_ title: String,
icon: String? = nil,
style: ActionButtonStyle = .primary,
isEnabled: Bool = true,
action: @escaping () -> Void
) {
self.title = title
self.icon = icon
self.style = style
self.isEnabled = isEnabled
self.action = action
}
public var body: some View {
Button(action: action) {
HStack(spacing: CasinoDesign.Spacing.small) {
if let icon = icon {
Image(systemName: icon)
.font(.system(size: iconSize, weight: .semibold))
}
Text(title)
.font(.system(size: fontSize, weight: .bold))
.lineLimit(1)
.minimumScaleFactor(CasinoDesign.MinScaleFactor.relaxed)
}
.foregroundStyle(style.foregroundColor)
.frame(minWidth: minButtonWidth)
.padding(.horizontal, CasinoDesign.Spacing.xLarge)
.padding(.vertical, CasinoDesign.Spacing.medium)
.background(style.background)
.shadow(color: style.shadowColor, radius: CasinoDesign.Shadow.radiusMedium)
}
.disabled(!isEnabled)
.opacity(isEnabled ? 1.0 : CasinoDesign.Opacity.medium)
.accessibilityLabel(title)
}
}
/// Style variants for action buttons.
public enum ActionButtonStyle {
/// Primary gold button (Deal, Hit, etc.)
case primary
/// Destructive red button (Clear, Fold, etc.)
case destructive
/// Secondary subtle button
case secondary
/// Custom color button for game-specific actions
case custom(Color)
var foregroundColor: Color {
switch self {
case .primary: return .black
case .destructive, .secondary, .custom: return .white
}
}
@ViewBuilder
var background: some View {
switch self {
case .primary:
Capsule()
.fill(
LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
case .destructive:
Capsule()
.fill(Color.CasinoButton.destructive)
case .secondary:
Capsule()
.fill(Color.white.opacity(CasinoDesign.Opacity.hint))
case .custom(let color):
Capsule()
.fill(color)
}
}
var shadowColor: Color {
switch self {
case .primary: return .yellow.opacity(CasinoDesign.Opacity.light)
case .destructive: return .red.opacity(CasinoDesign.Opacity.light)
case .secondary, .custom: return .clear
}
}
}
#Preview {
ZStack {
Color.CasinoTable.felt.ignoresSafeArea()
VStack(spacing: 20) {
ActionButton("Deal", icon: "play.fill", style: .primary) { }
ActionButton("Clear", icon: "xmark.circle", style: .destructive) { }
ActionButton("Stand", style: .secondary) { }
ActionButton("Hit", style: .custom(.green)) { }
ActionButton("Disabled", style: .primary, isEnabled: false) { }
}
}
}
#Preview("Casino Action Buttons") {
ZStack {
Color.CasinoTable.felt.ignoresSafeArea()
HStack(spacing: CasinoDesign.Spacing.medium) {
ActionButton("Hit", style: .custom(.green)) { }
ActionButton("Stand", style: .secondary) { }
ActionButton("Double", style: .custom(Color.purple)) {}
ActionButton("Split", style: .custom(Color.yellow)) {}
}
}
}