Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
28eb33439e
commit
80df405a69
@ -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
|
||||
|
||||
@ -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)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -2066,6 +2066,7 @@
|
||||
}
|
||||
},
|
||||
"WIN" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
|
||||
@ -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)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user