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" : {
"comment" : "The label for the user's balance in the top bar.",
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {

View File

@ -290,9 +290,10 @@ struct CardsDisplayArea: View {
let bankerIsWinner: 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 {
HStack(spacing: Design.Spacing.xxxLarge) {
@ -482,23 +483,28 @@ struct TopBarView: View {
let onReset: () -> Void
let onSettings: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
// MARK: - Environment
@ScaledMetric(relativeTo: .caption2) private var labelFontSize: CGFloat = 9
@ScaledMetric(relativeTo: .body) private var currencyFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .title3) private var balanceFontSize: CGFloat = 20
@ScaledMetric(relativeTo: .caption) private var smallFontSize: CGFloat = 12
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = 16
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
/// Whether the current text size is an accessibility size (very large)
private var isAccessibilitySize: Bool {
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 {
HStack {
// Balance display
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("BALANCE")
.font(.system(size: labelFontSize, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.tracking(1)
// Balance display - simplified at accessibility sizes
HStack(spacing: Design.Spacing.xSmall) {
Text("$")
.font(.system(size: currencyFontSize, weight: .bold))
@ -509,9 +515,10 @@ struct TopBarView: View {
.foregroundStyle(.white)
.contentTransition(.numericText())
.animation(.spring(duration: Design.Animation.quick), value: balance)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
}
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
@ -520,8 +527,8 @@ struct TopBarView: View {
Spacer()
// Cards remaining indicator (if enabled)
if showCardsRemaining {
// Cards remaining indicator - hidden at accessibility sizes to save space
if showCardsRemaining && !isAccessibilitySize {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill")
.font(.system(size: smallFontSize))
@ -533,7 +540,7 @@ struct TopBarView: View {
Spacer()
}
// Settings button
// Settings button (icon only)
Button("Settings", systemImage: "gearshape.fill", action: onSettings)
.labelStyle(.iconOnly)
.font(.system(size: buttonFontSize))
@ -544,14 +551,14 @@ struct TopBarView: View {
.fill(Color.black.opacity(Design.Opacity.overlay))
)
// Reset button
// Reset button (icon only to save space)
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
.font(.system(size: smallFontSize, weight: .medium))
.labelStyle(.iconOnly)
.font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6))
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.padding(Design.Spacing.small)
.background(
Capsule()
Circle()
.fill(Color.black.opacity(Design.Opacity.overlay))
)
}
@ -567,18 +574,69 @@ struct ActionButtonsView: View {
let onClear: () -> Void
let onNewRound: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
// MARK: - Environment
@ScaledMetric(relativeTo: .body) private var clearButtonFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .headline) private var primaryButtonFontSize: CGFloat = 16
@ScaledMetric(relativeTo: .body) private var statusFontSize: CGFloat = 14
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
/// 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 {
HStack(spacing: Design.Spacing.medium) {
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)
.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)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
@ -588,10 +646,34 @@ struct ActionButtonsView: View {
)
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty)
}
}
// Deal button
@ViewBuilder
private var dealButton: some View {
if isAccessibilitySize {
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)
.padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium)
@ -608,10 +690,32 @@ struct ActionButtonsView: View {
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.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)
.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)
.padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium)
@ -626,19 +730,6 @@ struct ActionButtonsView: View {
)
)
.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
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
@ -68,6 +69,8 @@ struct MiniBaccaratTableView: View {
.font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.tracking(1)
.lineLimit(1)
.minimumScaleFactor(0.6)
ZStack {
// Table felt background with arc shape
@ -201,10 +204,11 @@ struct TieBettingZone: View {
var isAtMax: Bool = false
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
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
private let titleFontSize: CGFloat = Design.BaseFontSize.medium
private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants
@ -243,10 +247,14 @@ struct TieBettingZone: View {
Text("TIE")
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(2)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 8 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.foregroundStyle(.white)
}
@ -270,10 +278,11 @@ struct BankerBettingZone: View {
var isAtMax: Bool = false
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
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
private let titleFontSize: CGFloat = Design.BaseFontSize.large
private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants
@ -328,10 +337,14 @@ struct BankerBettingZone: View {
Text("BANKER")
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 0.95 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.foregroundStyle(.white)
}
@ -355,10 +368,11 @@ struct PlayerBettingZone: View {
var isAtMax: Bool = false
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
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
private let titleFontSize: CGFloat = Design.BaseFontSize.large
private let subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants
@ -413,10 +427,14 @@ struct PlayerBettingZone: View {
Text("PLAYER")
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.tracking(3)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("PAYS 1 TO 1")
.font(.system(size: subtitleFontSize, weight: .medium))
.opacity(Design.Opacity.heavy)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.foregroundStyle(.white)
}