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

This commit is contained in:
Matt Bruce 2025-12-16 20:10:58 -06:00
parent 7913cb2d45
commit 2b7b770d3a
4 changed files with 231 additions and 97 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "88E73C19-3668-4217-9B1F-56B52ACBA7E0"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "B46496BF-3442-4357-9248-38B07C7AB65D"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Baccarat/Views/GameTableView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "68"
endingLineNumber = "68"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -143,6 +143,7 @@
}, },
"BALANCE" : { "BALANCE" : {
"comment" : "The label for the user's balance in the top bar.", "comment" : "The label for the user's balance in the top bar.",
"extractionState" : "stale",
"localizations" : { "localizations" : {
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {

View File

@ -290,9 +290,10 @@ struct CardsDisplayArea: View {
let bankerIsWinner: Bool let bankerIsWinner: Bool
let isTie: Bool let isTie: Bool
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Fixed font sizes for card area
// Fixed because the card display has strict layout constraints
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14 private let labelFontSize: CGFloat = 14
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.xxxLarge) { HStack(spacing: Design.Spacing.xxxLarge) {
@ -482,23 +483,28 @@ struct TopBarView: View {
let onReset: () -> Void let onReset: () -> Void
let onSettings: () -> Void let onSettings: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Environment
@ScaledMetric(relativeTo: .caption2) private var labelFontSize: CGFloat = 9 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ScaledMetric(relativeTo: .body) private var currencyFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .title3) private var balanceFontSize: CGFloat = 20 /// Whether the current text size is an accessibility size (very large)
@ScaledMetric(relativeTo: .caption) private var smallFontSize: CGFloat = 12 private var isAccessibilitySize: Bool {
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = 16 dynamicTypeSize.isAccessibilitySize
}
// MARK: - Fixed font sizes for constrained top bar
// These use fixed sizes because the top bar has strict space constraints
// and must remain readable at all accessibility settings
private let labelFontSize: CGFloat = 9
private let currencyFontSize: CGFloat = 14
private let balanceFontSize: CGFloat = 20
private let smallFontSize: CGFloat = 12
private let buttonFontSize: CGFloat = 16
var body: some View { var body: some View {
HStack { HStack {
// Balance display // Balance display - simplified at accessibility sizes
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("BALANCE")
.font(.system(size: labelFontSize, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.tracking(1)
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Text("$") Text("$")
.font(.system(size: currencyFontSize, weight: .bold)) .font(.system(size: currencyFontSize, weight: .bold))
@ -509,9 +515,10 @@ struct TopBarView: View {
.foregroundStyle(.white) .foregroundStyle(.white)
.contentTransition(.numericText()) .contentTransition(.numericText())
.animation(.spring(duration: Design.Animation.quick), value: balance) .animation(.spring(duration: Design.Animation.quick), value: balance)
.lineLimit(1)
.minimumScaleFactor(0.5)
} }
} .padding(.horizontal, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background( .background(
Capsule() Capsule()
@ -520,8 +527,8 @@ struct TopBarView: View {
Spacer() Spacer()
// Cards remaining indicator (if enabled) // Cards remaining indicator - hidden at accessibility sizes to save space
if showCardsRemaining { if showCardsRemaining && !isAccessibilitySize {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill") Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill")
.font(.system(size: smallFontSize)) .font(.system(size: smallFontSize))
@ -533,7 +540,7 @@ struct TopBarView: View {
Spacer() Spacer()
} }
// Settings button // Settings button (icon only)
Button("Settings", systemImage: "gearshape.fill", action: onSettings) Button("Settings", systemImage: "gearshape.fill", action: onSettings)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: buttonFontSize)) .font(.system(size: buttonFontSize))
@ -544,14 +551,14 @@ struct TopBarView: View {
.fill(Color.black.opacity(Design.Opacity.overlay)) .fill(Color.black.opacity(Design.Opacity.overlay))
) )
// Reset button // Reset button (icon only to save space)
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset) Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
.font(.system(size: smallFontSize, weight: .medium)) .labelStyle(.iconOnly)
.font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
.padding(.horizontal, Design.Spacing.small) .padding(Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background( .background(
Capsule() Circle()
.fill(Color.black.opacity(Design.Opacity.overlay)) .fill(Color.black.opacity(Design.Opacity.overlay))
) )
} }
@ -567,18 +574,69 @@ struct ActionButtonsView: View {
let onClear: () -> Void let onClear: () -> Void
let onNewRound: () -> Void let onNewRound: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Environment
@ScaledMetric(relativeTo: .body) private var clearButtonFontSize: CGFloat = 14 @Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ScaledMetric(relativeTo: .headline) private var primaryButtonFontSize: CGFloat = 16
@ScaledMetric(relativeTo: .body) private var statusFontSize: CGFloat = 14 /// Whether the current text size is an accessibility size (very large)
private var isAccessibilitySize: Bool {
dynamicTypeSize.isAccessibilitySize
}
// MARK: - Fixed font sizes for action buttons
// Fixed because buttons have constrained space and must remain usable
private let buttonFontSize: CGFloat = 16
private let iconSize: CGFloat = 24
private let statusFontSize: CGFloat = 14
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
if gameState.currentPhase == .betting { if gameState.currentPhase == .betting {
// Clear bets button // Clear bets button - icon only at accessibility sizes
clearButton
// Deal button - icon only at accessibility sizes
dealButton
} else if gameState.currentPhase == .roundComplete {
// New round button - icon only at accessibility sizes
newRoundButton
} else {
// Playing indicator
HStack(spacing: Design.Spacing.xSmall) {
ProgressView()
.tint(.white)
.scaleEffect(0.8)
Text("Dealing...")
.font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
.lineLimit(1)
.minimumScaleFactor(0.7)
}
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
}
}
}
@ViewBuilder
private var clearButton: some View {
if isAccessibilitySize {
Button("Clear", systemImage: "xmark.circle", action: onClear) Button("Clear", systemImage: "xmark.circle", action: onClear)
.font(.system(size: clearButtonFontSize, weight: .semibold)) .labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .semibold))
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(
Circle()
.fill(Color.Button.destructive)
)
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty)
} else {
Button("Clear", systemImage: "xmark.circle", action: onClear)
.labelStyle(.titleOnly)
.font(.system(size: buttonFontSize, weight: .semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
@ -588,10 +646,34 @@ struct ActionButtonsView: View {
) )
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0) .opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty) .disabled(gameState.currentBets.isEmpty)
}
}
// Deal button @ViewBuilder
private var dealButton: some View {
if isAccessibilitySize {
Button("Deal", systemImage: "play.fill", action: onDeal) Button("Deal", systemImage: "play.fill", action: onDeal)
.font(.system(size: primaryButtonFontSize, weight: .bold)) .labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .bold))
.foregroundStyle(.black)
.padding(Design.Spacing.medium)
.background(
Circle()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.disabled(!gameState.canDeal)
} else {
Button("Deal", systemImage: "play.fill", action: onDeal)
.labelStyle(.titleOnly)
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxxLarge) .padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
@ -608,10 +690,32 @@ struct ActionButtonsView: View {
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled) .opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.disabled(!gameState.canDeal) .disabled(!gameState.canDeal)
} else if gameState.currentPhase == .roundComplete { }
// New round button }
@ViewBuilder
private var newRoundButton: some View {
if isAccessibilitySize {
Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) Button("New Round", systemImage: "arrow.right.circle", action: onNewRound)
.font(.system(size: primaryButtonFontSize, weight: .bold)) .labelStyle(.iconOnly)
.font(.system(size: iconSize, weight: .bold))
.foregroundStyle(.black)
.padding(Design.Spacing.medium)
.background(
Circle()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
} else {
Button("New Round", systemImage: "arrow.right.circle", action: onNewRound)
.labelStyle(.titleOnly)
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxxLarge) .padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
@ -626,19 +730,6 @@ struct ActionButtonsView: View {
) )
) )
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
} else {
// Playing indicator
HStack(spacing: Design.Spacing.xSmall) {
ProgressView()
.tint(.white)
.scaleEffect(0.8)
Text("Dealing...")
.font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
}
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
}
} }
} }
} }

View File

@ -12,9 +12,10 @@ struct MiniBaccaratTableView: View {
@Bindable var gameState: GameState @Bindable var gameState: GameState
let selectedChip: ChipDenomination let selectedChip: ChipDenomination
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Fixed Font Sizes
// Fixed because the table area has strict layout constraints
@ScaledMetric(relativeTo: .caption) private var tableLimitsFontSize: CGFloat = Design.BaseFontSize.small private let tableLimitsFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout Constants // MARK: - Layout Constants
@ -68,6 +69,8 @@ struct MiniBaccaratTableView: View {
.font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded)) .font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
.tracking(1) .tracking(1)
.lineLimit(1)
.minimumScaleFactor(0.6)
ZStack { ZStack {
// Table felt background with arc shape // Table felt background with arc shape
@ -201,10 +204,11 @@ struct TieBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Fixed Font Sizes
// Fixed because betting zones have strict space constraints
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.medium private let titleFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
@ -243,10 +247,14 @@ struct TieBettingZone: View {
Text("TIE") Text("TIE")
.font(.system(size: titleFontSize, weight: .black, design: .rounded)) .font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(2) .tracking(2)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 8 TO 1") Text("PAYS 8 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium)) .font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy) .opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
} }
.foregroundStyle(.white) .foregroundStyle(.white)
} }
@ -270,10 +278,11 @@ struct BankerBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Fixed Font Sizes
// Fixed because betting zones have strict space constraints
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.large private let titleFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
@ -328,10 +337,14 @@ struct BankerBettingZone: View {
Text("BANKER") Text("BANKER")
.font(.system(size: titleFontSize, weight: .black, design: .rounded)) .font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3) .tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 0.95 TO 1") Text("PAYS 0.95 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium)) .font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy) .opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
} }
.foregroundStyle(.white) .foregroundStyle(.white)
} }
@ -355,10 +368,11 @@ struct PlayerBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Fixed Font Sizes
// Fixed because betting zones have strict space constraints
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.large private let titleFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
@ -413,10 +427,14 @@ struct PlayerBettingZone: View {
Text("PLAYER") Text("PLAYER")
.font(.system(size: titleFontSize, weight: .black, design: .rounded)) .font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3) .tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 1 TO 1") Text("PAYS 1 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium)) .font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy) .opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
} }
.foregroundStyle(.white) .foregroundStyle(.white)
} }