// // GameTableView.swift // Baccarat // // The main baccarat table view with all game elements. // import SwiftUI import CasinoKit /// The main game table view containing all game elements. struct GameTableView: View { @State private var settings = GameSettings() @State private var gameState: GameState? @State private var selectedChip: ChipDenomination = .hundred @State private var showSettings = false @State private var showRules = false @State private var showStats = false @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass /// Whether we're on iPad or large screen private var isLargeScreen: Bool { horizontalSizeClass == .regular } /// Whether we're in landscape mode (compact vertical on iPad) private var isLandscape: Bool { verticalSizeClass == .compact } /// Maximum width for game content on large screens private var maxContentWidth: CGFloat { isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait } private var state: GameState { gameState ?? GameState(settings: settings) } private var playerIsWinner: Bool { state.lastResult == .playerWins } private var bankerIsWinner: Bool { state.lastResult == .bankerWins } private var isTie: Bool { state.lastResult == .tie } var body: some View { ZStack { // Table background TableBackgroundView() // Main content VStack(spacing: 0) { // Top bar with balance and info TopBarView( balance: state.balance, cardsRemaining: state.engine.shoe.cardsRemaining, showCardsRemaining: settings.showCardsRemaining, onReset: { state.resetGame() }, onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } ) Spacer(minLength: Design.Spacing.xSmall) // Cards display area - constrained width on iPad CardsDisplayArea( playerCards: state.visiblePlayerCards, bankerCards: state.visibleBankerCards, playerCardsFaceUp: state.playerCardsFaceUp, bankerCardsFaceUp: state.bankerCardsFaceUp, playerValue: state.playerHandValue, bankerValue: state.bankerHandValue, playerIsWinner: playerIsWinner, bankerIsWinner: bankerIsWinner, isTie: isTie ) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) Spacer(minLength: Design.Spacing.xSmall) // Road map history - constrained width on iPad if settings.showHistory && !state.roundHistory.isEmpty { RoadMapView(results: state.recentResults) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) .padding(.horizontal) } Spacer(minLength: Design.Spacing.small) // Mini Baccarat betting table - constrained width on iPad MiniBaccaratTableView( gameState: state, selectedChip: selectedChip ) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) .padding(.horizontal, Design.Spacing.medium) Spacer(minLength: Design.Spacing.medium) // Chip selector - constrained width on iPad ChipSelectorView( selectedChip: $selectedChip, balance: state.balance, currentBet: state.totalBetAmount, maxBet: state.maxBet ) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) Spacer(minLength: Design.Spacing.small) // Action buttons - constrained width on iPad ActionButtonsView( gameState: state, onDeal: { Task { await state.deal() } }, onClear: { state.clearBets() }, onNewRound: { state.newRound() } ) .frame(maxWidth: isLargeScreen ? maxContentWidth * 0.8 : .infinity) .padding(.horizontal) .padding(.bottom, Design.Spacing.xSmall) } .safeAreaPadding(.bottom) // Result banner overlay (handles its own iPad sizing) if state.showResultBanner, let result = state.lastResult { ResultBannerView( result: result, totalWinnings: state.lastWinnings, betResults: state.betResults, playerHadPair: state.playerHadPair, bankerHadPair: state.bankerHadPair, currentBalance: state.balance, minBet: state.minBet, onNewRound: { state.newRound() }, onGameOver: { // Reset game (sound already played when banner appeared) state.resetGame() } ) .transition(.opacity) // Confetti for wins if state.lastWinnings > 0 { ConfettiView() } } // Game Over overlay (handles its own iPad sizing) if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating { GameOverView( roundsPlayed: state.roundHistory.count, onPlayAgain: { state.resetGame() } ) .transition(.opacity) } } .onAppear { if gameState == nil { gameState = GameState(settings: settings) } } .sheet(isPresented: $showSettings) { if let state = gameState { SettingsView(settings: settings, gameState: state) { // Apply settings when changed gameState?.applySettings() } } } .fullScreenCover(isPresented: $showRules) { RulesHelpView() } .sheet(isPresented: $showStats) { StatisticsSheetView(results: state.roundHistory) } } } /// Game over screen shown when player runs out of money. struct GameOverView: View { let roundsPlayed: Int let onPlayAgain: () -> Void @State private var showContent = false @Environment(\.horizontalSizeClass) private var horizontalSizeClass /// Maximum width for the modal card on iPad private var maxModalWidth: CGFloat { horizontalSizeClass == .regular ? Design.Size.maxModalWidth : .infinity } // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .largeTitle) private var iconSize: CGFloat = Design.BaseFontSize.display @ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle @ScaledMetric(relativeTo: .body) private var messageFontSize: CGFloat = Design.BaseFontSize.xLarge @ScaledMetric(relativeTo: .body) private var statsFontSize: CGFloat = 17 @ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.xLarge // MARK: - Layout Constants private let modalCornerRadius = Design.CornerRadius.xxxLarge private let statsCornerRadius = Design.CornerRadius.large private let cardPadding = Design.Spacing.xxxLarge private let contentSpacing: CGFloat = 28 private let buttonHorizontalPadding: CGFloat = 48 private let buttonVerticalPadding: CGFloat = 18 // MARK: - Body var body: some View { ZStack { // Solid dark backdrop - fully opaque Color.black .ignoresSafeArea() // Modal card VStack(spacing: contentSpacing) { // Broke icon Image(systemName: "creditcard.trianglebadge.exclamationmark") .font(.system(size: iconSize)) .foregroundStyle(.red) .symbolEffect(.pulse, options: .repeating) // Title Text("GAME OVER") .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) // Message Text("You've run out of chips!") .font(.system(size: messageFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) // Stats card VStack(spacing: Design.Spacing.medium) { HStack { Text("Rounds Played") .foregroundStyle(.white.opacity(Design.Opacity.medium)) Spacer() Text("\(roundsPlayed)") .bold() .foregroundStyle(.white) } } .font(.system(size: statsFontSize)) .padding() .background( RoundedRectangle(cornerRadius: statsCornerRadius) .fill(Color.white.opacity(Design.Opacity.subtle)) .overlay( RoundedRectangle(cornerRadius: statsCornerRadius) .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) ) ) .padding(.horizontal, Design.Spacing.xLarge) // Play Again button Button { onPlayAgain() } label: { HStack(spacing: Design.Spacing.small) { Image(systemName: "arrow.counterclockwise") Text("Play Again") } .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, buttonHorizontalPadding) .padding(.vertical, buttonVerticalPadding) .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.radiusXLarge) } .padding(.top, Design.Spacing.medium) } .padding(cardPadding) .background( RoundedRectangle(cornerRadius: modalCornerRadius) .fill( LinearGradient( colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark], startPoint: .top, endPoint: .bottom ) ) .overlay( RoundedRectangle(cornerRadius: modalCornerRadius) .strokeBorder( LinearGradient( colors: [ Color.red.opacity(Design.Opacity.medium), Color.red.opacity(Design.Opacity.hint) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: Design.LineWidth.medium ) ) ) .shadow(color: .red.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXXLarge) .frame(maxWidth: maxModalWidth) .padding(.horizontal, Design.Spacing.xxLarge) .scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink) .opacity(showContent ? 1.0 : 0) } .onAppear { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) { showContent = true } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Game Over")) .accessibilityAddTraits(.isModal) } } /// The cards display area showing both hands. struct CardsDisplayArea: View { let playerCards: [Card] let bankerCards: [Card] let playerCardsFaceUp: [Bool] let bankerCardsFaceUp: [Bool] let playerValue: Int let bankerValue: Int let playerIsWinner: Bool let bankerIsWinner: Bool let isTie: Bool // MARK: - Fixed font sizes for card area // Fixed because the card display has strict layout constraints private let labelFontSize: CGFloat = 14 // MARK: - Accessibility private var playerHandDescription: String { if playerCards.isEmpty { return String(localized: "No cards") } let visibleCards = zip(playerCards, playerCardsFaceUp) .filter { $1 } .map { "\($0.0.rank.accessibilityName) of \($0.0.suit.accessibilityName)" } if visibleCards.isEmpty { return String(localized: "Cards face down") } let format = String(localized: "handValueFormat") return visibleCards.joined(separator: ", ") + ". " + String(format: format, playerValue) } private var bankerHandDescription: String { if bankerCards.isEmpty { return String(localized: "No cards") } let visibleCards = zip(bankerCards, bankerCardsFaceUp) .filter { $1 } .map { "\($0.0.rank.accessibilityName) of \($0.0.suit.accessibilityName)" } if visibleCards.isEmpty { return String(localized: "Cards face down") } let format = String(localized: "handValueFormat") return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue) } var body: some View { HStack(spacing: Design.Spacing.xxxLarge) { // Player side VStack(spacing: Design.Spacing.small) { // Label with value HStack(spacing: Design.Spacing.small) { Text("PLAYER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white) if !playerCards.isEmpty && playerCardsFaceUp.contains(true) { ValueBadge(value: playerValue, color: .blue) } } .frame(minHeight: 30) // Cards CompactHandView( cards: playerCards, cardsFaceUp: playerCardsFaceUp, isWinner: playerIsWinner ) } .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Player hand")) .accessibilityValue(playerHandDescription + (playerIsWinner ? ", " + String(localized: "Winner") : "")) // Banker side VStack(spacing: Design.Spacing.small) { // Label with value HStack(spacing: Design.Spacing.small) { Text("BANKER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white) if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) { ValueBadge(value: bankerValue, color: .red) } } .frame(minHeight: 30) // Cards CompactHandView( cards: bankerCards, cardsFaceUp: bankerCardsFaceUp, isWinner: bankerIsWinner ) } .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Banker hand")) .accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : "")) } .padding(.top, Design.Spacing.large) .padding(.bottom, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) .fill(Color.black.opacity(Design.Opacity.quarter)) .accessibilityHidden(true) ) .padding(.horizontal) } } /// A compact hand view showing cards in a row. struct CompactHandView: View { let cards: [Card] let cardsFaceUp: [Bool] let isWinner: Bool // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10 // MARK: - Layout Constants // Fixed size: cards have strict visual constraints private let cardWidth: CGFloat = Design.Size.cardWidthTable private let cardHeight: CGFloat = Design.Size.cardWidthTable * Design.Size.cardAspectRatio private let cardOverlap: CGFloat = Design.Size.cardOverlap private let placeholderSpacing: CGFloat = Design.Spacing.small /// Fixed container width to prevent resizing during deal /// Calculated as: 3 cards with overlap + padding /// = cardWidth + (cardWidth + overlap) * 2 + padding * 2 private var fixedContainerWidth: CGFloat { // Max 3 cards: first card full width + 2 more with overlap let cardsWidth = cardWidth + (cardWidth + cardOverlap) * 2 return cardsWidth + Design.Spacing.xSmall * 2 } /// Fixed container height private var fixedContainerHeight: CGFloat { cardHeight + Design.Spacing.xSmall * 2 } var body: some View { ZStack { // Fixed-size container Color.clear .frame(width: fixedContainerWidth, height: fixedContainerHeight) // Cards content centered in fixed container HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) { if cards.isEmpty { // Placeholders - no overlap, just side by side ForEach(0..<2, id: \.self) { _ in CardPlaceholderView(width: cardWidth) } } else { ForEach(cards.indices, id: \.self) { index in let isFaceUp = index < cardsFaceUp.count ? cardsFaceUp[index] : false CardView( card: cards[index], isFaceUp: isFaceUp, cardWidth: cardWidth ) .zIndex(Double(index)) } } } } .background( RoundedRectangle(cornerRadius: Design.CornerRadius.small) .strokeBorder( isWinner ? Color.yellow : Color.clear, lineWidth: Design.LineWidth.standard ) ) .overlay(alignment: .bottom) { if isWinner { Text("WIN") .font(.system(size: winBadgeFontSize, weight: .black)) .foregroundStyle(.black) .padding(.horizontal, Design.Spacing.small) .padding(.vertical, Design.Spacing.xxSmall) .background( Capsule() .fill(Color.yellow) ) .offset(y: Design.Spacing.small) } } } } /// A small badge showing the hand value. struct ValueBadge: View { let value: Int let color: Color // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15 @ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = 26 var body: some View { Text("\(value)") .font(.system(size: valueFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) .frame(width: badgeSize, height: badgeSize) .background( Circle() .fill(color) ) } } /// The casino table background. struct TableBackgroundView: View { var body: some View { ZStack { // Base dark green Color.Table.baseDark // Radial gradient for depth RadialGradient( colors: [ Color.Table.backgroundLight, Color.Table.backgroundDark ], center: .center, startRadius: 50, endRadius: 500 ) // Subtle felt texture FeltPatternView() .opacity(Design.Opacity.subtle / 3) } .ignoresSafeArea() .accessibilityHidden(true) // Decorative element } } /// Subtle felt texture pattern. struct FeltPatternView: View { var body: some View { Canvas { context, size in for _ in 0..<2000 { let x = Double.random(in: 0...size.width) let y = Double.random(in: 0...size.height) let dotSize = Double.random(in: 1...2) let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize) context.fill(Path(ellipseIn: rect), with: .color(.white)) } } } } /// Top bar showing balance and game info. struct TopBarView: View { let balance: Int let cardsRemaining: Int let showCardsRemaining: Bool let onReset: () -> Void let onSettings: () -> Void let onHelp: () -> Void let onStats: () -> Void // MARK: - Environment @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 - simplified at accessibility sizes HStack(spacing: Design.Spacing.xSmall) { Text("$") .font(.system(size: currencyFontSize, weight: .bold)) .foregroundStyle(.yellow.opacity(Design.Opacity.heavy)) 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(Design.MinScaleFactor.tight) } .padding(.horizontal, Design.Spacing.medium) .padding(.vertical, Design.Spacing.xSmall) .background( Capsule() .fill(Color.black.opacity(Design.Opacity.overlay)) ) .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Balance")) .accessibilityValue("$\(balance.formatted())") Spacer() // 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)) Text("\(cardsRemaining)") .font(.system(size: smallFontSize, weight: .medium)) } .foregroundStyle(.white.opacity(Design.Opacity.secondary)) .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Cards remaining in shoe")) .accessibilityValue("\(cardsRemaining)") Spacer() } // Statistics button Button("Statistics", systemImage: "chart.bar.fill", action: onStats) .labelStyle(.iconOnly) .font(.system(size: buttonFontSize)) .foregroundStyle(.white.opacity(Design.Opacity.accent)) .padding(Design.Spacing.small) .background( Circle() .fill(Color.black.opacity(Design.Opacity.overlay)) ) .accessibilityHint(String(localized: "View detailed game statistics")) // Help/Rules button Button("Help", systemImage: "info.circle.fill", action: onHelp) .labelStyle(.iconOnly) .font(.system(size: buttonFontSize)) .foregroundStyle(.white.opacity(Design.Opacity.accent)) .padding(Design.Spacing.small) .background( Circle() .fill(Color.black.opacity(Design.Opacity.overlay)) ) // Settings button (icon only) Button("Settings", systemImage: "gearshape.fill", action: onSettings) .labelStyle(.iconOnly) .font(.system(size: buttonFontSize)) .foregroundStyle(.white.opacity(Design.Opacity.accent)) .padding(Design.Spacing.small) .background( Circle() .fill(Color.black.opacity(Design.Opacity.overlay)) ) // Reset button (icon only to save space) Button("Reset", systemImage: "arrow.counterclockwise", action: onReset) .labelStyle(.iconOnly) .font(.system(size: buttonFontSize)) .foregroundStyle(.white.opacity(Design.Opacity.accent)) .padding(Design.Spacing.small) .background( Circle() .fill(Color.black.opacity(Design.Opacity.overlay)) ) } .padding(.horizontal) .padding(.top, Design.Spacing.xSmall) } } /// Action buttons for deal, clear, and new round. struct ActionButtonsView: View { @Bindable var gameState: GameState let onDeal: () -> Void let onClear: () -> Void let onNewRound: () -> Void // MARK: - Environment @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 = Design.BaseFontSize.xLarge private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall private let statusFontSize: CGFloat = Design.BaseFontSize.medium /// Fixed height to prevent layout shifts when buttons change private let containerHeight: CGFloat = 50 var body: some View { ZStack { // Fixed height container to prevent layout shifts Color.clear .frame(height: containerHeight) // Content changes with animation Group { if gameState.currentPhase == .betting { // Clear bets button - icon only at accessibility sizes HStack(spacing: Design.Spacing.medium) { clearButton dealButton } } else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner { // New round button - only shown after banner is dismissed // (The banner itself has a New Round button) newRoundButton } else if !gameState.showResultBanner { // 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(Design.MinScaleFactor.relaxed) } .padding(.horizontal, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.medium) } } .animation(.easeInOut(duration: Design.Animation.quick), value: gameState.currentPhase) .animation(.easeInOut(duration: Design.Animation.quick), value: gameState.showResultBanner) } } @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 + Design.Spacing.xxSmall) .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.xxLarge) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .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 + Design.Spacing.xxSmall) .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 + Design.Spacing.small) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .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 + Design.Spacing.xxSmall) .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 + Design.Spacing.small) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .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 { GameTableView() }