diff --git a/Baccarat.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Baccarat.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..02d6613 --- /dev/null +++ b/Baccarat.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Resources/Localizable.xcstrings index 4bd767d..2a00a22 100644 --- a/Baccarat/Resources/Localizable.xcstrings +++ b/Baccarat/Resources/Localizable.xcstrings @@ -143,6 +143,7 @@ }, "BALANCE" : { "comment" : "The label for the user's balance in the top bar.", + "extractionState" : "stale", "localizations" : { "es" : { "stringUnit" : { diff --git a/Baccarat/Views/GameTableView.swift b/Baccarat/Views/GameTableView.swift index 59e0a59..a56e882 100644 --- a/Baccarat/Views/GameTableView.swift +++ b/Baccarat/Views/GameTableView.swift @@ -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,36 +483,42 @@ 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)) + .foregroundStyle(.yellow.opacity(0.8)) - HStack(spacing: Design.Spacing.xSmall) { - Text("$") - .font(.system(size: currencyFontSize, weight: .bold)) - .foregroundStyle(.yellow.opacity(0.8)) - - Text(balance, format: .number) - .font(.system(size: balanceFontSize, weight: .black, design: .rounded)) - .foregroundStyle(.white) - .contentTransition(.numericText()) - .animation(.spring(duration: Design.Animation.quick), value: balance) - } + Text(balance, format: .number) + .font(.system(size: balanceFontSize, weight: .black, design: .rounded)) + .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,65 +574,33 @@ 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 - Button("Clear", systemImage: "xmark.circle", action: onClear) - .font(.system(size: clearButtonFontSize, weight: .semibold)) - .foregroundStyle(.white) - .padding(.horizontal, Design.Spacing.xLarge) - .padding(.vertical, Design.Spacing.medium) - .background( - Capsule() - .fill(Color.Button.destructive) - ) - .opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0) - .disabled(gameState.currentBets.isEmpty) + // Clear bets button - icon only at accessibility sizes + clearButton - // Deal button - Button("Deal", systemImage: "play.fill", action: onDeal) - .font(.system(size: primaryButtonFontSize, weight: .bold)) - .foregroundStyle(.black) - .padding(.horizontal, Design.Spacing.xxxLarge) - .padding(.vertical, Design.Spacing.medium) - .background( - Capsule() - .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) + // Deal button - icon only at accessibility sizes + dealButton } else if gameState.currentPhase == .roundComplete { - // New round button - Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) - .font(.system(size: primaryButtonFontSize, weight: .bold)) - .foregroundStyle(.black) - .padding(.horizontal, Design.Spacing.xxxLarge) - .padding(.vertical, Design.Spacing.medium) - .background( - Capsule() - .fill( - LinearGradient( - colors: [Color.Button.goldLight, Color.Button.goldDark], - startPoint: .top, - endPoint: .bottom - ) - ) - ) - .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) + // New round button - icon only at accessibility sizes + newRoundButton } else { // Playing indicator HStack(spacing: Design.Spacing.xSmall) { @@ -635,12 +610,128 @@ struct ActionButtonsView: View { 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) + .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) + .background( + Capsule() + .fill(Color.Button.destructive) + ) + .opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0) + .disabled(gameState.currentBets.isEmpty) + } + } + + @ViewBuilder + private var dealButton: some View { + if isAccessibilitySize { + Button("Deal", systemImage: "play.fill", action: onDeal) + .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) + .background( + Capsule() + .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) + } + } + + @ViewBuilder + private var newRoundButton: some View { + if isAccessibilitySize { + Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) + .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) + .background( + Capsule() + .fill( + LinearGradient( + colors: [Color.Button.goldLight, Color.Button.goldDark], + startPoint: .top, + endPoint: .bottom + ) + ) + ) + .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) + } + } } #Preview { diff --git a/Baccarat/Views/MiniBaccaratTableView.swift b/Baccarat/Views/MiniBaccaratTableView.swift index b478ad0..5a3b415 100644 --- a/Baccarat/Views/MiniBaccaratTableView.swift +++ b/Baccarat/Views/MiniBaccaratTableView.swift @@ -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) }