Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a2ecfcd580
commit
017000b197
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user