Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-23 13:54:19 -06:00
parent 28eb33439e
commit 80df405a69
7 changed files with 39 additions and 314 deletions

View File

@ -50,8 +50,8 @@ enum Design {
// Hints
static let hintFontSize: CGFloat = 15
static let hintIconSize: CGFloat = 24
static let hintPaddingH: CGFloat = 18
static let hintPaddingV: CGFloat = 12
static let hintPaddingH: CGFloat = 10
static let hintPaddingV: CGFloat = 10
// Hand icons
static let handIconSize: CGFloat = 18

View File

@ -1,108 +0,0 @@
//
// ActionButton.swift
// Blackjack
//
// Reusable styled button for game actions.
//
import SwiftUI
import CasinoKit
struct ActionButton: View {
let title: String
let icon: String?
let style: ButtonStyle
let action: () -> Void
enum ButtonStyle {
case primary // Gold gradient (Deal, New Round)
case destructive // Red (Clear)
case secondary // Subtle white
case custom(Color) // Game-specific colors (Hit, Stand, etc.)
var foregroundColor: Color {
switch self {
case .primary: return .black
case .destructive, .secondary, .custom: return .white
}
}
}
init(_ title: String, icon: String? = nil, style: ButtonStyle = .primary, action: @escaping () -> Void) {
self.title = title
self.icon = icon
self.style = style
self.action = action
}
var body: some View {
Button(action: action) {
HStack(spacing: Design.Spacing.small) {
if let icon = icon {
Image(systemName: icon)
}
Text(title)
.lineLimit(1)
.minimumScaleFactor(CasinoDesign.MinScaleFactor.relaxed)
}
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(style.foregroundColor)
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(backgroundView)
}
.accessibilityLabel(title)
}
@ViewBuilder
private var backgroundView: some View {
switch style {
case .primary:
Capsule()
.fill(
LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
case .destructive:
Capsule()
.fill(Color.red.opacity(Design.Opacity.heavy))
case .secondary:
Capsule()
.fill(Color.white.opacity(Design.Opacity.hint))
case .custom(let color):
Capsule()
.fill(color)
}
}
}
// MARK: - Previews
#Preview("Primary") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButton("Deal", icon: "play.fill", style: .primary) {}
}
}
#Preview("Destructive") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButton("Clear", icon: "xmark.circle", style: .destructive) {}
}
}
#Preview("Custom Colors") {
ZStack {
Color.Table.felt.ignoresSafeArea()
HStack(spacing: Design.Spacing.medium) {
ActionButton("Hit", style: .custom(Color.Button.hit)) {}
ActionButton("Stand", style: .custom(Color.Button.stand)) {}
ActionButton("Double", style: .custom(Color.Button.doubleDown)) {}
}
}
}

View File

@ -73,8 +73,8 @@ struct BlackjackTableView: View {
/// - iPad Pro 12.9" (~1366pt): ~150pt (capped)
private var dealerPlayerSpacing: CGFloat {
let baseline: CGFloat = 550 // Below this, use minimum
let scale: CGFloat = 0.2 // 20% of height above baseline
let minSpacing: CGFloat = 20 // Floor for smallest screens
let scale: CGFloat = 0.18 // 20% of height above baseline
let minSpacing: CGFloat = 10 // Floor for smallest screens
let maxSpacing: CGFloat = 150 // Ceiling for largest screens
let calculated = (screenHeight - baseline) * scale
@ -107,6 +107,7 @@ struct BlackjackTableView: View {
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.padding(.bottom, 5)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .green, label: "Player")
}

View File

@ -30,17 +30,17 @@ struct InsurancePopupView: View {
// Title
Text(String(localized: "INSURANCE?"))
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.font(.system(size: Design.BaseFontSize.largeTitle, weight: .black, design: .rounded))
.foregroundStyle(.white)
// Subtitle
Text(String(localized: "Dealer showing Ace"))
.font(.system(size: Design.BaseFontSize.medium))
.font(.system(size: Design.BaseFontSize.xxLarge))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
// Cost info
Text(String(localized: "Cost: $\(betAmount) (half your bet)"))
.font(.system(size: Design.BaseFontSize.small))
.font(.system(size: Design.BaseFontSize.xxLarge))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.padding(.bottom, Design.Spacing.small)

View File

@ -2066,6 +2066,7 @@
}
},
"WIN" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {

View File

@ -28,6 +28,9 @@ public struct ActionButton: View {
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 = 35
/// Creates an action button.
/// - Parameters:
/// - title: The button title.
@ -58,15 +61,19 @@ public struct ActionButton: View {
}
Text(title)
.font(.system(size: fontSize, weight: .bold))
.lineLimit(1)
.minimumScaleFactor(CasinoDesign.MinScaleFactor.relaxed)
}
.foregroundStyle(style.foregroundColor)
.padding(.horizontal, CasinoDesign.Spacing.xxLarge)
.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)
}
}
@ -78,12 +85,13 @@ public enum ActionButtonStyle {
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: return .white
case .secondary: return .white
case .destructive, .secondary, .custom: return .white
}
}
@ -105,6 +113,9 @@ public enum ActionButtonStyle {
case .secondary:
Capsule()
.fill(Color.white.opacity(CasinoDesign.Opacity.hint))
case .custom(let color):
Capsule()
.fill(color)
}
}
@ -112,7 +123,7 @@ public enum ActionButtonStyle {
switch self {
case .primary: return .yellow.opacity(CasinoDesign.Opacity.light)
case .destructive: return .red.opacity(CasinoDesign.Opacity.light)
case .secondary: return .clear
case .secondary, .custom: return .clear
}
}
}
@ -125,7 +136,21 @@ public enum ActionButtonStyle {
ActionButton("Deal", icon: "play.fill", style: .primary) { }
ActionButton("Clear", icon: "xmark.circle", style: .destructive) { }
ActionButton("Stand", style: .secondary) { }
ActionButton("Hit", style: .primary, isEnabled: false) { }
ActionButton("Hit", style: .custom(.green)) { }
ActionButton("Disabled", style: .primary, isEnabled: false) { }
}
}
}
#Preview("BlackJack 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)) {}
}
}
}

View File

@ -1,194 +0,0 @@
//
// HandDisplayView.swift
// CasinoKit
//
// A generic view for displaying a hand of cards with optional overlap.
//
import SwiftUI
/// A view displaying a hand of cards with configurable layout.
public struct HandDisplayView: View {
/// The cards in the hand.
public let cards: [Card]
/// Which cards are face up (by index).
public let cardsFaceUp: [Bool]
/// The width of each card.
public let cardWidth: CGFloat
/// The overlap between cards (negative = overlap, positive = gap).
public let cardSpacing: CGFloat
/// Whether this hand is the winner.
public let isWinner: Bool
/// Optional label to show (e.g., "PLAYER", "DEALER").
public let label: String?
/// Optional value badge to show.
public let value: Int?
/// Badge color for the value.
public let valueColor: Color
/// Maximum number of card slots to reserve space for.
public let maxCards: Int
// Layout
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10
/// Creates a hand display view.
/// - Parameters:
/// - cards: The cards to display.
/// - cardsFaceUp: Which cards are face up.
/// - cardWidth: Width of each card.
/// - cardSpacing: Spacing between cards (negative for overlap).
/// - isWinner: Whether to show winner styling.
/// - label: Optional label above cards.
/// - value: Optional value badge to show.
/// - valueColor: Color for value badge.
/// - maxCards: Max cards to reserve space for (default: 3).
public init(
cards: [Card],
cardsFaceUp: [Bool] = [],
cardWidth: CGFloat = 45,
cardSpacing: CGFloat = -12,
isWinner: Bool = false,
label: String? = nil,
value: Int? = nil,
valueColor: Color = .blue,
maxCards: Int = 3
) {
self.cards = cards
self.cardsFaceUp = cardsFaceUp
self.cardWidth = cardWidth
self.cardSpacing = cardSpacing
self.isWinner = isWinner
self.label = label
self.value = value
self.valueColor = valueColor
self.maxCards = maxCards
}
/// Card height based on aspect ratio.
private var cardHeight: CGFloat {
cardWidth * CasinoDesign.Size.cardAspectRatio
}
/// Fixed container width based on max cards.
private var containerWidth: CGFloat {
if maxCards <= 1 {
return cardWidth + CasinoDesign.Spacing.xSmall * 2
}
let cardsWidth = cardWidth + (cardWidth + cardSpacing) * CGFloat(maxCards - 1)
return cardsWidth + CasinoDesign.Spacing.xSmall * 2
}
/// Fixed container height.
private var containerHeight: CGFloat {
cardHeight + CasinoDesign.Spacing.xSmall * 2
}
public var body: some View {
VStack(spacing: CasinoDesign.Spacing.small) {
// Label with optional value badge
if label != nil || value != nil {
HStack(spacing: CasinoDesign.Spacing.small) {
if let label = label {
Text(label)
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
if let value = value, !cards.isEmpty {
ValueBadge(value: value, color: valueColor)
}
}
.frame(minHeight: CasinoDesign.Spacing.xxxLarge)
}
// Cards container
ZStack {
// Fixed-size container
Color.clear
.frame(width: containerWidth, height: containerHeight)
// Cards
HStack(spacing: cards.isEmpty ? CasinoDesign.Spacing.small : cardSpacing) {
if cards.isEmpty {
// Placeholders
ForEach(0..<min(2, maxCards), id: \.self) { _ in
CardPlaceholderView(width: cardWidth)
}
} else {
ForEach(cards.indices, id: \.self) { index in
let isFaceUp = index < cardsFaceUp.count ? cardsFaceUp[index] : true
CardView(
card: cards[index],
isFaceUp: isFaceUp,
cardWidth: cardWidth
)
.zIndex(Double(index))
}
}
}
}
.background(
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
.strokeBorder(
isWinner ? Color.yellow : Color.clear,
lineWidth: CasinoDesign.LineWidth.medium
)
)
.overlay(alignment: .bottom) {
if isWinner {
Text(String(localized: "WIN", bundle: .module))
.font(.system(size: winBadgeFontSize, weight: .black))
.foregroundStyle(.black)
.padding(.horizontal, CasinoDesign.Spacing.small)
.padding(.vertical, CasinoDesign.Spacing.xxSmall)
.background(
Capsule()
.fill(Color.yellow)
)
.offset(y: CasinoDesign.Spacing.small)
}
}
}
}
}
#Preview {
ZStack {
Color.CasinoTable.felt.ignoresSafeArea()
HStack(spacing: 40) {
HandDisplayView(
cards: [
Card(suit: .hearts, rank: .ace),
Card(suit: .spades, rank: .king)
],
cardsFaceUp: [true, true],
isWinner: true,
label: "PLAYER",
value: 21,
valueColor: .blue
)
HandDisplayView(
cards: [
Card(suit: .diamonds, rank: .seven),
Card(suit: .clubs, rank: .ten)
],
cardsFaceUp: [true, false],
label: "DEALER",
value: 17,
valueColor: .red
)
}
}
}