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

This commit is contained in:
Matt Bruce 2025-12-22 23:50:09 -06:00
parent a2ecfcd580
commit 017000b197
16 changed files with 344 additions and 313 deletions

View File

@ -17,7 +17,7 @@ enum Design {
// MARK: - Debug // MARK: - Debug
/// Set to true to show layout debug borders on views /// Set to true to show layout debug borders on views
static let showDebugBorders = false static let showDebugBorders = true
// MARK: - Shared Constants (from CasinoKit) // MARK: - Shared Constants (from CasinoKit)
@ -32,73 +32,45 @@ enum Design {
typealias BaseFontSize = CasinoDesign.BaseFontSize typealias BaseFontSize = CasinoDesign.BaseFontSize
typealias IconSize = CasinoDesign.IconSize typealias IconSize = CasinoDesign.IconSize
// MARK: - Blackjack-Specific Component Sizes // MARK: - Blackjack-Specific Sizes (use CasinoDesign.Size for shared values)
enum Size { enum Size {
// Cards
static let cardWidth: CGFloat = 90
static let cardOverlap: CGFloat = -50 // Negative = more overlap
// Use shared scaling from CasinoKit // Player hands
static var handScale: CGFloat { CasinoDesign.Size.handScale } static let playerHandsHeight: CGFloat = 240
static var fontScale: CGFloat { CasinoDesign.Size.fontScale }
// Cards - scaled for better visibility // Hand labels
static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x static let handLabelFontSize: CGFloat = 14
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let handNumberFontSize: CGFloat = 12
static let handValueFontSize: CGFloat = 18
/// Card overlap (negative = cards stack left over right). // Hints
/// More negative = more overlap (less card visible). static let hintFontSize: CGFloat = 15
/// With 90pt cards: -40 = ~44% overlap, -50 = ~55% overlap static let hintIconSize: CGFloat = 24
static let cardOverlap: CGFloat = -50 static let hintPaddingH: CGFloat = 18
static let hintPaddingV: CGFloat = 12
// Player hands container height (accommodates larger cards + labels) // Hand icons
// Reduced from 180 to fit content more snugly static let handIconSize: CGFloat = 18
static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x
// Hand label font sizes (scaled) // Hi-Lo count badge
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale static let countBadgeFontSize: CGFloat = 10
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale // Same as label static let countBadgePaddingH: CGFloat = 6
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * fontScale static let countBadgePaddingV: CGFloat = 2
static let countBadgeOffset: CGFloat = 6
// Hint font size (scaled to match hands) // Betting
static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * fontScale static let bettingChipSize: CGFloat = 54
static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale
static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale
// Hand icons (scaled) // Card count display
static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale static let cardCountLabelSize: CGFloat = 11
static let cardCountValueSize: CGFloat = 18
// Hi-Lo count badge (scaled) // Table
static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * fontScale
static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale
static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale
static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale
// Betting zone (chip scales, but zone height stays reasonable)
static let bettingChipSize: CGFloat = 36 * handScale // 54pt at 1.5x
static let bettingZoneHeightScaled: CGFloat = CasinoDesign.Size.bettingZoneHeight // Keep original height to save space
// Card count display (scaled)
static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * fontScale
static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale
// Chips - use CasinoDesign values
static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge
// Buttons - use CasinoDesign values
static let actionButtonHeight: CGFloat = CasinoDesign.Size.actionButtonHeight
static let actionButtonMinWidth: CGFloat = CasinoDesign.Size.actionButtonMinWidth
static let bettingZoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
// Responsive - use CasinoDesign values
static let maxContentWidthPortrait: CGFloat = CasinoDesign.Size.maxContentWidthPortrait
static let maxContentWidthLandscape: CGFloat = CasinoDesign.Size.maxContentWidthLandscape
static let maxModalWidth: CGFloat = CasinoDesign.Size.maxModalWidth
// Blackjack-specific
static let tableHeight: CGFloat = 280 static let tableHeight: CGFloat = 280
// Settings - use CasinoDesign values
static let checkmark: CGFloat = CasinoDesign.Size.checkmark
} }
} }
@ -138,7 +110,7 @@ extension Color {
static let blackjack = Color.yellow static let blackjack = Color.yellow
} }
// MARK: - Action Button Colors // MARK: - Action Button Colors (Blackjack-specific)
enum Button { enum Button {
static let hit = Color(red: 0.2, green: 0.6, blue: 0.3) static let hit = Color(red: 0.2, green: 0.6, blue: 0.3)
@ -147,25 +119,6 @@ extension Color {
static let split = Color(red: 0.3, green: 0.5, blue: 0.7) static let split = Color(red: 0.3, green: 0.5, blue: 0.7)
static let surrender = Color(red: 0.6, green: 0.3, blue: 0.3) static let surrender = Color(red: 0.6, green: 0.3, blue: 0.3)
static let insurance = Color(red: 0.7, green: 0.6, blue: 0.2) static let insurance = Color(red: 0.7, green: 0.6, blue: 0.2)
static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
}
// MARK: - Settings Colors
enum Settings {
static let background = Color(red: 0.08, green: 0.12, blue: 0.18)
static let cardBackground = Color.white.opacity(CasinoDesign.Opacity.verySubtle)
static let accent = Color(red: 0.9, green: 0.75, blue: 0.3)
}
// MARK: - Modal Colors
enum Modal {
static let background = Color(red: 0.12, green: 0.18, blue: 0.25)
static let backgroundLight = Color(red: 0.15, green: 0.2, blue: 0.3)
static let backgroundDark = Color(red: 0.1, green: 0.15, blue: 0.25)
} }
// MARK: - TopBar Colors // MARK: - TopBar Colors

View File

@ -61,7 +61,7 @@ struct ActionButton: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark], colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )

View File

@ -13,16 +13,19 @@ struct CardCountView: View {
let runningCount: Int let runningCount: Int
let trueCount: Double let trueCount: Double
@ScaledMetric(relativeTo: .caption) private var labelSize: CGFloat = Design.Size.cardCountLabelSize
@ScaledMetric(relativeTo: .title) private var valueSize: CGFloat = Design.Size.cardCountValueSize
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.large) { HStack(spacing: Design.Spacing.large) {
// Running count // Running count
VStack(spacing: Design.Spacing.xxSmall) { VStack(spacing: Design.Spacing.xxSmall) {
Text("Running") Text("Running")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium)) .font(.system(size: labelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)") Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)")
.font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced)) .font(.system(size: valueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: runningCount)) .foregroundStyle(countColor(for: runningCount))
} }
@ -33,11 +36,11 @@ struct CardCountView: View {
// True count // True count
VStack(spacing: Design.Spacing.xxSmall) { VStack(spacing: Design.Spacing.xxSmall) {
Text("True") Text("True")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium)) .font(.system(size: labelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))") Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))")
.font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced)) .font(.system(size: valueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: Int(trueCount.rounded()))) .foregroundStyle(countColor(for: Int(trueCount.rounded())))
} }
} }

View File

@ -33,8 +33,8 @@ struct GameTableView: View {
private var maxContentWidth: CGFloat { private var maxContentWidth: CGFloat {
if isIPad { if isIPad {
return verticalSizeClass == .compact return verticalSizeClass == .compact
? Design.Size.maxContentWidthLandscape ? CasinoDesign.Size.maxContentWidthLandscape
: Design.Size.maxContentWidthPortrait : CasinoDesign.Size.maxContentWidthPortrait
} }
return .infinity return .infinity
} }
@ -72,18 +72,16 @@ struct GameTableView: View {
@ViewBuilder @ViewBuilder
private func mainGameView(state: GameState) -> some View { private func mainGameView(state: GameState) -> some View {
GeometryReader { geometry in
ZStack { ZStack {
// Background // Background
TableBackgroundView() TableBackgroundView()
mainContent(state: state, screenHeight: geometry.size.height) mainContent(state: state)
}
} }
} }
@ViewBuilder @ViewBuilder
private func mainContent(state: GameState, screenHeight: CGFloat) -> some View { private func mainContent(state: GameState) -> some View {
ZStack { ZStack {
VStack(spacing: 0) { VStack(spacing: 0) {
// Top bar // Top bar
@ -119,8 +117,7 @@ struct GameTableView: View {
// Table layout - fills available space // Table layout - fills available space
BlackjackTableView( BlackjackTableView(
state: state, state: state,
onPlaceBet: { placeBet(state: state) }, onPlaceBet: { placeBet(state: state) }
screenHeight: screenHeight
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)

View File

@ -121,7 +121,7 @@ struct ResultBannerView: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark], colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
@ -144,7 +144,7 @@ struct ResultBannerView: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark], colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
@ -158,7 +158,7 @@ struct ResultBannerView: View {
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge) RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark], colors: [Color.CasinoModal.backgroundLight, Color.CasinoModal.backgroundDark],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
@ -172,7 +172,7 @@ struct ResultBannerView: View {
) )
) )
.shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge) .shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge)
.frame(maxWidth: Design.Size.maxModalWidth) .frame(maxWidth: CasinoDesign.Size.maxModalWidth)
.padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides .padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides
.scaleEffect(showContent ? 1.0 : 0.8) .scaleEffect(showContent ? 1.0 : 0.8)
.opacity(showContent ? 1.0 : 0) .opacity(showContent ? 1.0 : 0)

View File

@ -191,7 +191,7 @@ struct RulesHelpView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ZStack { ZStack {
Color.Settings.background Color.Sheet.background
.ignoresSafeArea() .ignoresSafeArea()
VStack(spacing: 0) { VStack(spacing: 0) {
@ -208,7 +208,7 @@ struct RulesHelpView: View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
ForEach(pages.indices, id: \.self) { index in ForEach(pages.indices, id: \.self) { index in
Circle() Circle()
.fill(index == currentPage ? Color.Settings.accent : Color.white.opacity(Design.Opacity.light)) .fill(index == currentPage ? Color.Sheet.accent : Color.white.opacity(Design.Opacity.light))
.frame(width: 8, height: 8) .frame(width: 8, height: 8)
} }
} }
@ -222,10 +222,10 @@ struct RulesHelpView: View {
Button(String(localized: "Done")) { Button(String(localized: "Done")) {
dismiss() dismiss()
} }
.foregroundStyle(Color.Settings.accent) .foregroundStyle(Color.Sheet.accent)
} }
} }
.toolbarBackground(Color.Settings.background, for: .navigationBar) .toolbarBackground(Color.Sheet.background, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar)
} }
} }
@ -255,7 +255,7 @@ struct RulePageView: View {
// Icon // Icon
Image(systemName: page.icon) Image(systemName: page.icon)
.font(.system(size: iconSize)) .font(.system(size: iconSize))
.foregroundStyle(Color.Settings.accent) .foregroundStyle(Color.Sheet.accent)
.padding(.top, Design.Spacing.xxLarge) .padding(.top, Design.Spacing.xxLarge)
// Title // Title
@ -268,7 +268,7 @@ struct RulePageView: View {
ForEach(page.content.indices, id: \.self) { index in ForEach(page.content.indices, id: \.self) { index in
HStack(alignment: .top, spacing: Design.Spacing.medium) { HStack(alignment: .top, spacing: Design.Spacing.medium) {
Text("") Text("")
.foregroundStyle(Color.Settings.accent) .foregroundStyle(Color.Sheet.accent)
Text(page.content[index]) Text(page.content[index])
.font(.system(size: bodySize)) .font(.system(size: bodySize))

View File

@ -24,7 +24,7 @@ struct SettingsView: View {
} }
/// Accent color for settings components /// Accent color for settings components
private let accent = Color.Settings.accent private let accent = Color.Sheet.accent
var body: some View { var body: some View {
SheetContainerView( SheetContainerView(
@ -377,7 +377,7 @@ struct GameStylePicker: View {
title: style.displayName, title: style.displayName,
subtitle: style.description, subtitle: style.description,
isSelected: selection == style, isSelected: selection == style,
accentColor: Color.Settings.accent, accentColor: Color.Sheet.accent,
action: { selection = style } action: { selection = style }
) )
} }
@ -397,7 +397,7 @@ struct DeckCountPicker: View {
title: count.displayName, title: count.displayName,
subtitle: count.description, subtitle: count.description,
isSelected: selection == count, isSelected: selection == count,
accentColor: Color.Settings.accent, accentColor: Color.Sheet.accent,
action: { selection = count } action: { selection = count }
) )
} }
@ -417,8 +417,8 @@ struct TableLimitsPicker: View {
title: limit.displayName, title: limit.displayName,
subtitle: limit.detailedDescription, subtitle: limit.detailedDescription,
isSelected: selection == limit, isSelected: selection == limit,
accentColor: Color.Settings.accent, accentColor: Color.Sheet.accent,
badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Settings.accent) }, badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Sheet.accent) },
action: { selection = limit } action: { selection = limit }
) )
} }

View File

@ -72,7 +72,7 @@ struct StatisticsSheetView: View {
StatBox(title: String(localized: "Rounds"), value: "\(totalRounds)", color: .white) StatBox(title: String(localized: "Rounds"), value: "\(totalRounds)", color: .white)
StatBox(title: String(localized: "Win Rate"), value: formatPercent(winRate), color: winRate >= 50 ? .green : .orange) StatBox(title: String(localized: "Win Rate"), value: formatPercent(winRate), color: winRate >= 50 ? .green : .orange)
StatBox(title: String(localized: "Net"), value: formatMoney(totalWinnings), color: totalWinnings >= 0 ? .green : .red) StatBox(title: String(localized: "Net"), value: formatMoney(totalWinnings), color: totalWinnings >= 0 ? .green : .red)
StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Settings.accent) StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Sheet.accent)
} }
} }

View File

@ -15,6 +15,9 @@ struct BettingZoneView: View {
let onTap: () -> Void let onTap: () -> Void
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var detailFontSize: CGFloat = Design.Size.handNumberFontSize
@ScaledMetric(relativeTo: .body) private var chipSize: CGFloat = Design.Size.bettingChipSize
@ScaledMetric(relativeTo: .body) private var zoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
private var isAtMax: Bool { private var isAtMax: Bool {
betAmount >= maxBet betAmount >= maxBet
@ -34,7 +37,7 @@ struct BettingZoneView: View {
// Content // Content
if betAmount > 0 { if betAmount > 0 {
// Show chip with amount (scaled) // Show chip with amount (scaled)
ChipOnTableView(amount: betAmount, showMax: isAtMax, size: Design.Size.bettingChipSize) ChipOnTableView(amount: betAmount, showMax: isAtMax, size: chipSize)
} else { } else {
// Empty state // Empty state
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
@ -44,18 +47,18 @@ struct BettingZoneView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Min: $\(minBet)")) Text(String(localized: "Min: $\(minBet)"))
.font(.system(size: Design.Size.handNumberFontSize, weight: .medium)) .font(.system(size: detailFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light)) .foregroundStyle(.white.opacity(Design.Opacity.light))
Text(String(localized: "Max: $\(maxBet.formatted())")) Text(String(localized: "Max: $\(maxBet.formatted())"))
.font(.system(size: Design.Size.handNumberFontSize, weight: .medium)) .font(.system(size: detailFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light)) .foregroundStyle(.white.opacity(Design.Opacity.light))
} }
} }
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: Design.Size.bettingZoneHeightScaled) .frame(height: zoneHeight)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet") .accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet")

View File

@ -12,8 +12,15 @@ struct BlackjackTableView: View {
@Bindable var state: GameState @Bindable var state: GameState
let onPlaceBet: () -> Void let onPlaceBet: () -> Void
/// Screen height passed from parent for responsive sizing // MARK: - Environment
var screenHeight: CGFloat = 800
@Environment(\.verticalSizeClass) private var verticalSizeClass
// MARK: - State
/// Screen dimensions measured from container for responsive sizing
@State private var screenHeight: CGFloat = 800
@State private var screenWidth: CGFloat = 375
/// Whether to show Hi-Lo card count values on cards. /// Whether to show Hi-Lo card count values on cards.
var showCardCount: Bool { state.settings.showCardCount } var showCardCount: Bool { state.settings.showCardCount }
@ -24,10 +31,44 @@ struct BlackjackTableView: View {
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge @ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small @ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout // MARK: - Dynamic Card Sizing
private let cardWidth: CGFloat = Design.Size.cardWidth /// Whether we're in landscape mode
private let cardSpacing: CGFloat = Design.Size.cardOverlap /// - iPhones: use verticalSizeClass == .compact
/// - iPads: use screen dimensions (since iPads always report .regular)
private var isLandscape: Bool {
if DeviceInfo.isPad {
return screenWidth > screenHeight
}
return verticalSizeClass == .compact
}
/// Dynamic card width based on screen width
/// - iPhone SE (375pt): ~80pt
/// - iPhone 16 Pro (393pt): ~95pt
/// - iPhone 16 Pro Max (430pt): ~120pt
/// - iPad mini (landscape): ~90pt (reduced for smaller tablet screen)
/// - iPad: ~140pt (capped)
private var cardWidth: CGFloat {
// iPad mini in landscape gets smaller cards to fit better
if DeviceInfo.isPadMini && isLandscape {
return 90
}
let baseWidth: CGFloat = 80 // Minimum card width for smallest screens
let baseScreen: CGFloat = 375 // iPhone SE width
let scale: CGFloat = 0.7 // Scale factor (70% of width increase goes to cards)
let maxWidth: CGFloat = 140 // Cap for large screens
let calculated = baseWidth + (screenWidth - baseScreen) * scale
return min(maxWidth, max(baseWidth, calculated))
}
/// Card overlap scales proportionally with card width
private var cardSpacing: CGFloat {
// Overlap ratio: roughly -55% of card width
cardWidth * -0.55
}
/// Fixed height for the hint area to prevent layout shifts /// Fixed height for the hint area to prevent layout shifts
private let hintAreaHeight: CGFloat = 44 private let hintAreaHeight: CGFloat = 44
@ -116,6 +157,19 @@ struct BlackjackTableView: View {
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
screenHeight = geometry.size.height
screenWidth = geometry.size.width
}
.onChange(of: geometry.size) { _, newSize in
screenHeight = newSize.height
screenWidth = newSize.width
}
}
)
.debugBorder(showDebugBorders, color: .white, label: "TableView") .debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase) .animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
} }

View File

@ -12,17 +12,22 @@ import CasinoKit
struct HiLoCountBadge: View { struct HiLoCountBadge: View {
let card: Card let card: Card
@ScaledMetric(relativeTo: .caption2) private var fontSize: CGFloat = Design.Size.countBadgeFontSize
@ScaledMetric(relativeTo: .caption2) private var paddingH: CGFloat = Design.Size.countBadgePaddingH
@ScaledMetric(relativeTo: .caption2) private var paddingV: CGFloat = Design.Size.countBadgePaddingV
@ScaledMetric(relativeTo: .caption2) private var offsetAmount: CGFloat = Design.Size.countBadgeOffset
var body: some View { var body: some View {
Text(card.hiLoDisplayText) Text(card.hiLoDisplayText)
.font(.system(size: Design.Size.countBadgeFontSize, weight: .bold, design: .rounded)) .font(.system(size: fontSize, weight: .bold, design: .rounded))
.foregroundStyle(badgeTextColor) .foregroundStyle(badgeTextColor)
.padding(.horizontal, Design.Size.countBadgePaddingH) .padding(.horizontal, paddingH)
.padding(.vertical, Design.Size.countBadgePaddingV) .padding(.vertical, paddingV)
.background( .background(
Capsule() Capsule()
.fill(badgeBackgroundColor) .fill(badgeBackgroundColor)
) )
.offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset) .offset(x: -offsetAmount, y: offsetAmount)
} }
private var badgeBackgroundColor: Color { private var badgeBackgroundColor: Color {

View File

@ -14,17 +14,22 @@ import CasinoKit
struct HintView: View { struct HintView: View {
let hint: String let hint: String
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = Design.Size.hintPaddingV
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: "lightbulb.fill") Image(systemName: "lightbulb.fill")
.font(.system(size: Design.Size.hintIconSize)) .font(.system(size: iconSize))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text(String(localized: "Hint: \(hint)")) Text(String(localized: "Hint: \(hint)"))
.font(.system(size: Design.Size.hintFontSize, weight: .medium)) .font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
.padding(.horizontal, Design.Size.hintPaddingH) .padding(.horizontal, paddingH)
.padding(.vertical, Design.Size.hintPaddingV) .padding(.vertical, paddingV)
.background( .background(
Capsule() Capsule()
.fill(Color.black.opacity(Design.Opacity.light)) .fill(Color.black.opacity(Design.Opacity.light))
@ -42,6 +47,11 @@ struct BettingHintView: View {
let hint: String let hint: String
let trueCount: Double let trueCount: Double
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = Design.Size.hintPaddingV
private var hintColor: Color { private var hintColor: Color {
let tc = Int(trueCount.rounded()) let tc = Int(trueCount.rounded())
if tc >= 2 { if tc >= 2 {
@ -67,14 +77,14 @@ struct BettingHintView: View {
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: Design.Size.hintIconSize)) .font(.system(size: iconSize))
.foregroundStyle(hintColor) .foregroundStyle(hintColor)
Text(hint) Text(hint)
.font(.system(size: Design.Size.hintFontSize, weight: .medium)) .font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
.padding(.horizontal, Design.Size.hintPaddingH) .padding(.horizontal, paddingH)
.padding(.vertical, Design.Size.hintPaddingV) .padding(.vertical, paddingV)
.background( .background(
Capsule() Capsule()
.fill(Color.black.opacity(Design.Opacity.light)) .fill(Color.black.opacity(Design.Opacity.light))

View File

@ -71,7 +71,7 @@ struct InsurancePopupView: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark], colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
@ -84,7 +84,7 @@ struct InsurancePopupView: View {
.padding(Design.Spacing.xLarge) .padding(Design.Spacing.xLarge)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.Modal.background) .fill(Color.CasinoModal.backgroundDark)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusXLarge) .shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusXLarge)
) )
.overlay( .overlay(

View File

@ -25,7 +25,6 @@ struct PlayerHandsView: View {
} }
var body: some View { var body: some View {
GeometryReader { geometry in
ScrollViewReader { proxy in ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.large) { HStack(spacing: Design.Spacing.large) {
@ -46,7 +45,9 @@ struct PlayerHandsView: View {
} }
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.frame(minWidth: geometry.size.width) .containerRelativeFrame(.horizontal) { length, _ in
length // Ensures content fills container width for centering
}
} }
.scrollClipDisabled() .scrollClipDisabled()
.scrollBounceBehavior(.basedOnSize) .scrollBounceBehavior(.basedOnSize)
@ -66,8 +67,6 @@ struct PlayerHandsView: View {
} }
} }
} }
.frame(height: Design.Size.playerHandsHeight)
}
private func scrollToHand(proxy: ScrollViewProxy, index: Int) { private func scrollToHand(proxy: ScrollViewProxy, index: Int) {
withAnimation(.easeInOut(duration: Design.Animation.quick)) { withAnimation(.easeInOut(duration: Design.Animation.quick)) {
@ -89,6 +88,9 @@ struct PlayerHandView: View {
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.handIconSize
@ScaledMetric(relativeTo: .body) private var hintPaddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var hintPaddingV: CGFloat = Design.Size.hintPaddingV
var body: some View { var body: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
@ -126,8 +128,25 @@ struct PlayerHandView: View {
) )
) )
) )
.overlay {
// Result badge - centered on cards
if let result = hand.result {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(.white)
.padding(.horizontal, hintPaddingH)
.padding(.vertical, hintPaddingV)
.background(
Capsule()
.fill(result.color)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusMedium)
)
.transition(.scale.combined(with: .opacity))
}
}
.contentShape(Rectangle()) .contentShape(Rectangle())
.animation(.easeInOut(duration: Design.Animation.quick), value: isActive) .animation(.easeInOut(duration: Design.Animation.quick), value: isActive)
.animation(.spring(duration: Design.Animation.springDuration), value: hand.result != nil)
// Hand info // Hand info
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
@ -145,29 +164,16 @@ struct PlayerHandView: View {
if hand.isDoubledDown { if hand.isDoubledDown {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
.font(.system(size: Design.Size.handIconSize)) .font(.system(size: iconSize))
.foregroundStyle(.purple) .foregroundStyle(.purple)
} }
} }
// Result badge
if let result = hand.result {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(result.color)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(result.color.opacity(Design.Opacity.hint))
)
}
// Bet amount // Bet amount
if hand.bet > 0 { if hand.bet > 0 {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "dollarsign.circle.fill") Image(systemName: "dollarsign.circle.fill")
.font(.system(size: Design.Size.handIconSize)) .font(.system(size: iconSize))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))") Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
.font(.system(size: handNumberSize, weight: .bold, design: .rounded)) .font(.system(size: handNumberSize, weight: .bold, design: .rounded))