From acd0064776f50db2e2f9f6f15c8aa7cb6546314c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Dec 2025 17:39:30 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Baccarat/Agents.md | 64 + Baccarat/Models/GameResult.swift | 8 +- Baccarat/Resources/Localizable.xcstrings | 1287 ++++++++++++++++++++ Baccarat/Theme/DesignConstants.swift | 203 +++ Baccarat/Views/GameTableView.swift | 78 +- Baccarat/Views/MiniBaccaratTableView.swift | 266 ++-- 6 files changed, 1754 insertions(+), 152 deletions(-) create mode 100644 Baccarat/Resources/Localizable.xcstrings create mode 100644 Baccarat/Theme/DesignConstants.swift diff --git a/Baccarat/Agents.md b/Baccarat/Agents.md index 193adc8..599ec98 100644 --- a/Baccarat/Agents.md +++ b/Baccarat/Agents.md @@ -64,6 +64,70 @@ If SwiftData is configured to use CloudKit: - All relationships must be marked optional. +## Localization instructions + +- Use **String Catalogs** (`.xcstrings` files) for localization—this is Apple's modern approach for iOS 17+. +- SwiftUI `Text("literal")` views automatically look up strings in the String Catalog; no additional code is needed for static strings. +- For strings outside of `Text` views or with dynamic content, use `String(localized:)` or create a helper extension: + ```swift + extension String { + static func localized(_ key: String) -> String { + String(localized: String.LocalizationValue(key)) + } + static func localized(_ key: String, _ arguments: CVarArg...) -> String { + let format = String(localized: String.LocalizationValue(key)) + return String(format: format, arguments: arguments) + } + } + ``` +- For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use `String.localized("key", value)`. +- Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views. +- Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA). +- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API. + + +## Design constants instructions + +- Avoid magic numbers for layout values (padding, spacing, corner radii, font sizes, etc.). +- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing: + ```swift + enum Design { + enum Spacing { + static let small: CGFloat = 8 + static let medium: CGFloat = 12 + static let large: CGFloat = 16 + } + enum CornerRadius { + static let small: CGFloat = 8 + static let medium: CGFloat = 12 + } + enum FontSize { + static let body: CGFloat = 14 + static let title: CGFloat = 24 + } + } + ``` +- For colors used across the app, extend `Color` with semantic color definitions: + ```swift + extension Color { + enum Primary { + static let background = Color(red: 0.1, green: 0.2, blue: 0.3) + static let accent = Color(red: 0.8, green: 0.6, blue: 0.2) + } + } + ``` +- Within each view, extract view-specific magic numbers to private constants at the top of the struct: + ```swift + struct MyView: View { + private let cardWidth: CGFloat = 45 + private let headerFontSize: CGFloat = 18 + // ... + } + ``` +- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`. +- Keep design constants organized by category: Spacing, CornerRadius, FontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow. + + ## Project structure - Use a consistent project structure, with folder layout determined by app features. diff --git a/Baccarat/Models/GameResult.swift b/Baccarat/Models/GameResult.swift index 687ed88..9cb34d1 100644 --- a/Baccarat/Models/GameResult.swift +++ b/Baccarat/Models/GameResult.swift @@ -14,12 +14,12 @@ enum GameResult: Equatable { case bankerWins case tie - /// Display text for the result. + /// Display text for the result (localized). var displayText: String { switch self { - case .playerWins: return "Player Wins!" - case .bankerWins: return "Banker Wins!" - case .tie: return "Tie!" + case .playerWins: return String.localized("PLAYER WINS") + case .bankerWins: return String.localized("BANKER WINS") + case .tie: return String.localized("TIE GAME") } } diff --git a/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Resources/Localizable.xcstrings new file mode 100644 index 0000000..8cc60ad --- /dev/null +++ b/Baccarat/Resources/Localizable.xcstrings @@ -0,0 +1,1287 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "%lld" : { + "comment" : "The number of rounds a player has played in the game.", + "isCommentAutoGenerated" : true + }, + "$" : { + "comment" : "The currency symbol \"$\".", + "isCommentAutoGenerated" : true + }, + "$%lldK" : { + "comment" : "A button that allows the user to select a starting balance for the game. The text inside the button changes based on whether the button is currently selected or not.", + "isCommentAutoGenerated" : true + }, + "1 Deck" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Deck" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Baraja" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Jeu" + } + } + } + }, + "6 Decks" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "6 Decks" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "6 Barajas" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "6 Jeux" + } + } + } + }, + "8 Decks" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "8 Decks" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "8 Barajas" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "8 Jeux" + } + } + } + }, + "Animate dealing and flipping" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animate dealing and flipping" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animar reparto y volteo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animer la distribution et le retournement" + } + } + } + }, + "ANIMATIONS" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANIMATIONS" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANIMACIONES" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANIMATIONS" + } + } + } + }, + "B" : { + "comment" : "The letter \"B\" displayed in the center of the playing card's back.", + "isCommentAutoGenerated" : true + }, + "BALANCE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BALANCE" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "SALDO" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "SOLDE" + } + } + } + }, + "Balanced gameplay" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Balanced gameplay" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Juego equilibrado" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jeu équilibré" + } + } + } + }, + "BANKER" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BANKER" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "BANCA" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "BANQUE" + } + } + } + }, + "BANKER WINS" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "BANKER WINS" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "GANA BANCA" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "BANQUE GAGNE" + } + } + } + }, + "Cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + } + } + }, + "Card Animations" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Card Animations" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animaciones de cartas" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animations des cartes" + } + } + } + }, + "Clear" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clear" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer" + } + } + } + }, + "Common in casinos" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Common in casinos" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Común en casinos" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Courant dans les casinos" + } + } + } + }, + "Deal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deal" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repartir" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distribuer" + } + } + } + }, + "Dealing Speed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dealing Speed" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad de reparto" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vitesse de distribution" + } + } + } + }, + "Dealing..." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dealing..." + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repartiendo..." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distribution..." + } + } + } + }, + "DECK SETTINGS" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DECK SETTINGS" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "CONFIGURACIÓN DE BARAJAS" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PARAMÈTRES DU JEU" + } + } + } + }, + "DISPLAY" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DISPLAY" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "PANTALLA" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "AFFICHAGE" + } + } + } + }, + "Display deck counter at top" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display deck counter at top" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar contador de cartas arriba" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le compteur en haut" + } + } + } + }, + "Display result road map" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display result road map" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar historial de resultados" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher l'historique des résultats" + } + } + } + }, + "Done" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminé" + } + } + } + }, + "Fast" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fast" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rápido" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rapide" + } + } + } + }, + "For experienced players" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "For experienced players" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para jugadores experimentados" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pour joueurs expérimentés" + } + } + } + }, + "GAME OVER" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "GAME OVER" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "FIN DEL JUEGO" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "FIN DE PARTIE" + } + } + } + }, + "High Roller" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "High Roller" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apostador alto" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gros joueur" + } + } + } + }, + "HISTORY" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HISTORY" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "HISTORIAL" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "HISTORIQUE" + } + } + } + }, + "Low Stakes" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Low Stakes" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apuestas bajas" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mises basses" + } + } + } + }, + "MAX" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAX" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "MÁX" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAX" + } + } + } + }, + "Maximum excitement" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximum excitement" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máxima emoción" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Excitation maximale" + } + } + } + }, + "New Round" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New Round" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nueva ronda" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouvelle manche" + } + } + } + }, + "Normal" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normal" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normal" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normal" + } + } + } + }, + "PAYS 0.95 TO 1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAYS 0.95 TO 1" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAGA 0.95 A 1" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAIE 0,95 POUR 1" + } + } + } + }, + "PAYS 1 TO 1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAYS 1 TO 1" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAGA 1 A 1" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAIE 1 POUR 1" + } + } + } + }, + "PAYS 8 TO 1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAYS 8 TO 1" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAGA 8 A 1" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAIE 8 POUR 1" + } + } + } + }, + "Perfect for beginners" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfect for beginners" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfecto para principiantes" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parfait pour les débutants" + } + } + } + }, + "Play Again" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play Again" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jugar de nuevo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rejouer" + } + } + } + }, + "PLAYER" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PLAYER" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "JUGADOR" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "JOUEUR" + } + } + } + }, + "PLAYER WINS" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PLAYER WINS" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "GANA JUGADOR" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "JOUEUR GAGNE" + } + } + } + }, + "Practice mode" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Practice mode" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo práctica" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mode pratique" + } + } + } + }, + "Reset" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réinitialiser" + } + } + } + }, + "Reset to Defaults" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset to Defaults" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restaurar valores predeterminados" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réinitialiser par défaut" + } + } + } + }, + "Rounds Played" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rounds Played" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rondas jugadas" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manches jouées" + } + } + } + }, + "Settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres" + } + } + } + }, + "Show Cards Remaining" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show Cards Remaining" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar cartas restantes" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les cartes restantes" + } + } + } + }, + "Show History" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show History" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar historial" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher l'historique" + } + } + } + }, + "Slow" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slow" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lento" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lent" + } + } + } + }, + "Standard" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + } + } + }, + "Standard (Recommended)" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard (Recommended)" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar (Recomendado)" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard (Recommandé)" + } + } + } + }, + "STARTING BALANCE" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "STARTING BALANCE" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "SALDO INICIAL" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "SOLDE DE DÉPART" + } + } + } + }, + "TABLE LIMITS" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TABLE LIMITS" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "LÍMITES DE MESA" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "LIMITES DE TABLE" + } + } + } + }, + "tableLimitsFormat" : { + "comment" : "Format for table limits display", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TABLE LIMITS: $%@ - $%@" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "LÍMITES: $%@ - $%@" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "LIMITES: %@ $ - %@ $" + } + } + } + }, + "TIE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TIE" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "EMPATE" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "ÉGALITÉ" + } + } + } + }, + "TIE GAME" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TIE GAME" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "EMPATE" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "ÉGALITÉ" + } + } + } + }, + "VIP" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "VIP" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "VIP" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "VIP" + } + } + } + }, + "WIN" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "WIN" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "GANA" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "GAGNÉ" + } + } + } + }, + "You've run out of chips!" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You've run out of chips!" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Te quedaste sin fichas!" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous n'avez plus de jetons!" + } + } + } + } + }, + "version" : "1.1" +} \ No newline at end of file diff --git a/Baccarat/Theme/DesignConstants.swift b/Baccarat/Theme/DesignConstants.swift new file mode 100644 index 0000000..83ce7ab --- /dev/null +++ b/Baccarat/Theme/DesignConstants.swift @@ -0,0 +1,203 @@ +// +// DesignConstants.swift +// Baccarat +// +// Design system constants for consistent styling across the app. +// + +import SwiftUI + +/// Design constants for the Baccarat app. +enum Design { + + // MARK: - Spacing + + enum Spacing { + static let xxSmall: CGFloat = 2 + static let xSmall: CGFloat = 4 + static let small: CGFloat = 8 + static let medium: CGFloat = 12 + static let large: CGFloat = 16 + static let xLarge: CGFloat = 20 + static let xxLarge: CGFloat = 24 + static let xxxLarge: CGFloat = 32 + } + + // MARK: - Corner Radii + + enum CornerRadius { + static let small: CGFloat = 8 + static let medium: CGFloat = 10 + static let large: CGFloat = 12 + static let xLarge: CGFloat = 14 + static let xxLarge: CGFloat = 20 + static let xxxLarge: CGFloat = 28 + } + + // MARK: - Font Sizes + + enum FontSize { + static let xxSmall: CGFloat = 7 + static let xSmall: CGFloat = 9 + static let small: CGFloat = 10 + static let body: CGFloat = 12 + static let medium: CGFloat = 14 + static let large: CGFloat = 16 + static let xLarge: CGFloat = 18 + static let xxLarge: CGFloat = 20 + static let title: CGFloat = 32 + static let largeTitle: CGFloat = 36 + static let display: CGFloat = 70 + } + + // MARK: - Icon Sizes + + enum IconSize { + static let small: CGFloat = 12 + static let medium: CGFloat = 16 + static let large: CGFloat = 22 + static let xLarge: CGFloat = 60 + static let xxLarge: CGFloat = 70 + } + + // MARK: - Component Sizes + + enum Size { + static let chipSmall: CGFloat = 36 + static let chipMedium: CGFloat = 50 + static let cardWidthSmall: CGFloat = 45 + static let cardWidthMedium: CGFloat = 55 + static let cardWidthLarge: CGFloat = 65 + static let valueBadge: CGFloat = 26 + static let checkmark: CGFloat = 22 + static let tableAspectRatio: CGFloat = 1.6 + } + + // MARK: - Animation + + enum Animation { + static let springDuration: Double = 0.4 + static let springBounce: Double = 0.3 + static let fadeInDuration: Double = 0.3 + static let cardFlipDuration: Double = 0.5 + } + + // MARK: - Opacity + + enum Opacity { + static let disabled: Double = 0.5 + static let subtle: Double = 0.1 + static let light: Double = 0.3 + static let medium: Double = 0.5 + static let strong: Double = 0.7 + static let heavy: Double = 0.8 + static let nearOpaque: Double = 0.85 + } + + // MARK: - Line Widths + + enum LineWidth { + static let thin: CGFloat = 1 + static let medium: CGFloat = 2 + static let thick: CGFloat = 3 + static let heavy: CGFloat = 4 + } + + // MARK: - Shadow + + enum Shadow { + static let radiusSmall: CGFloat = 3 + static let radiusMedium: CGFloat = 6 + static let radiusLarge: CGFloat = 10 + static let radiusXLarge: CGFloat = 12 + static let radiusXXLarge: CGFloat = 30 + } +} + +// MARK: - App Colors + +extension Color { + + // MARK: - Table Colors + + enum Table { + static let feltDark = Color(red: 0.0, green: 0.28, blue: 0.12) + static let feltLight = Color(red: 0.0, green: 0.35, blue: 0.18) + static let backgroundDark = Color(red: 0.01, green: 0.12, blue: 0.06) + static let backgroundLight = Color(red: 0.03, green: 0.25, blue: 0.12) + static let baseDark = Color(red: 0.02, green: 0.15, blue: 0.08) + } + + // MARK: - Border Colors + + enum Border { + static let goldLight = Color(red: 0.85, green: 0.7, blue: 0.35) + static let goldDark = Color(red: 0.65, green: 0.5, blue: 0.2) + static let gold = Color(red: 0.7, green: 0.55, blue: 0.25) + static let silver = Color(red: 0.6, green: 0.6, blue: 0.65) + } + + // MARK: - Betting Zone Colors + + enum BettingZone { + // Player (Blue) + static let playerLight = Color(red: 0.1, green: 0.25, blue: 0.55) + static let playerDark = Color(red: 0.05, green: 0.15, blue: 0.4) + static let playerMaxLight = Color(red: 0.08, green: 0.18, blue: 0.4) + static let playerMaxDark = Color(red: 0.04, green: 0.1, blue: 0.28) + + // Banker (Red) + static let bankerLight = Color(red: 0.55, green: 0.12, blue: 0.12) + static let bankerDark = Color(red: 0.4, green: 0.08, blue: 0.08) + static let bankerMaxLight = Color(red: 0.4, green: 0.1, blue: 0.1) + static let bankerMaxDark = Color(red: 0.28, green: 0.06, blue: 0.06) + + // Tie (Green) + static let tie = Color(red: 0.1, green: 0.45, blue: 0.25) + static let tieMax = Color(red: 0.08, green: 0.32, blue: 0.18) + } + + // MARK: - Button Colors + + enum Button { + 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) + static let destructive = Color(red: 0.6, green: 0.2, blue: 0.2) + } + + // MARK: - Chip Colors + + enum Chip { + static let gold = Color(red: 0.8, green: 0.65, blue: 0.2) + } + + // MARK: - Modal Colors + + enum Modal { + static let backgroundLight = Color(red: 0.12, green: 0.12, blue: 0.14) + static let backgroundDark = Color(red: 0.08, green: 0.08, blue: 0.1) + } + + // MARK: - Settings Colors + + enum Settings { + static let background = Color(red: 0.08, green: 0.12, blue: 0.08) + } +} + +// MARK: - Localized Strings Helper + +extension String { + + /// Returns a localized string for the given key. + static func localized(_ key: String) -> String { + String(localized: String.LocalizationValue(key)) + } + + /// Returns a localized string with format arguments. + static func localized(_ key: String, _ arguments: CVarArg...) -> String { + let format = String(localized: String.LocalizationValue(key)) + return String(format: format, arguments: arguments) + } +} + diff --git a/Baccarat/Views/GameTableView.swift b/Baccarat/Views/GameTableView.swift index 488b107..c632cac 100644 --- a/Baccarat/Views/GameTableView.swift +++ b/Baccarat/Views/GameTableView.swift @@ -148,6 +148,22 @@ struct GameOverView: View { @State private var showContent = false + // MARK: - Layout Constants + + private let iconSize = Design.FontSize.display + private let titleFontSize = Design.FontSize.largeTitle + private let messageFontSize = Design.FontSize.xLarge + private let statsFontSize: CGFloat = 17 + private let buttonFontSize = Design.FontSize.xLarge + 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 @@ -155,110 +171,104 @@ struct GameOverView: View { .ignoresSafeArea() // Modal card - VStack(spacing: 28) { + VStack(spacing: contentSpacing) { // Broke icon Image(systemName: "creditcard.trianglebadge.exclamationmark") - .font(.system(size: 70)) + .font(.system(size: iconSize)) .foregroundStyle(.red) .symbolEffect(.pulse, options: .repeating) // Title Text("GAME OVER") - .font(.system(size: 36, weight: .black, design: .rounded)) + .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) // Message Text("You've run out of chips!") - .font(.system(size: 18, weight: .medium)) - .foregroundStyle(.white.opacity(0.7)) + .font(.system(size: messageFontSize, weight: .medium)) + .foregroundStyle(.white.opacity(Design.Opacity.strong)) // Stats card - VStack(spacing: 12) { + VStack(spacing: Design.Spacing.medium) { HStack { Text("Rounds Played") - .foregroundStyle(.white.opacity(0.6)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) Spacer() Text("\(roundsPlayed)") .bold() .foregroundStyle(.white) } } - .font(.system(size: 17)) + .font(.system(size: statsFontSize)) .padding() .background( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: statsCornerRadius) .fill(Color.white.opacity(0.08)) .overlay( - RoundedRectangle(cornerRadius: 12) - .strokeBorder(Color.white.opacity(0.1), lineWidth: 1) + RoundedRectangle(cornerRadius: statsCornerRadius) + .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) ) ) - .padding(.horizontal, 20) + .padding(.horizontal, Design.Spacing.xLarge) // Play Again button Button { onPlayAgain() } label: { - HStack(spacing: 8) { + HStack(spacing: Design.Spacing.small) { Image(systemName: "arrow.counterclockwise") Text("Play Again") } - .font(.system(size: 18, weight: .bold)) + .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) - .padding(.horizontal, 48) - .padding(.vertical, 18) + .padding(.horizontal, buttonHorizontalPadding) + .padding(.vertical, buttonVerticalPadding) .background( Capsule() .fill( LinearGradient( - colors: [ - Color(red: 1.0, green: 0.85, blue: 0.3), - Color(red: 0.9, green: 0.7, blue: 0.2) - ], + colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) - .shadow(color: .yellow.opacity(0.4), radius: 12) + .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXLarge) } - .padding(.top, 12) + .padding(.top, Design.Spacing.medium) } - .padding(32) + .padding(cardPadding) .background( - RoundedRectangle(cornerRadius: 28) + RoundedRectangle(cornerRadius: modalCornerRadius) .fill( LinearGradient( - colors: [ - Color(red: 0.12, green: 0.12, blue: 0.14), - Color(red: 0.08, green: 0.08, blue: 0.1) - ], + colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark], startPoint: .top, endPoint: .bottom ) ) .overlay( - RoundedRectangle(cornerRadius: 28) + RoundedRectangle(cornerRadius: modalCornerRadius) .strokeBorder( LinearGradient( colors: [ - Color.red.opacity(0.5), + Color.red.opacity(Design.Opacity.medium), Color.red.opacity(0.2) ], startPoint: .topLeading, endPoint: .bottomTrailing ), - lineWidth: 2 + lineWidth: Design.LineWidth.medium ) ) ) - .shadow(color: .red.opacity(0.2), radius: 30) - .padding(.horizontal, 24) + .shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge) + .padding(.horizontal, Design.Spacing.xxLarge) .scaleEffect(showContent ? 1.0 : 0.8) .opacity(showContent ? 1.0 : 0) } .onAppear { - withAnimation(.spring(duration: 0.4, bounce: 0.3)) { + withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) { showContent = true } } diff --git a/Baccarat/Views/MiniBaccaratTableView.swift b/Baccarat/Views/MiniBaccaratTableView.swift index 7301b18..56c0c6e 100644 --- a/Baccarat/Views/MiniBaccaratTableView.swift +++ b/Baccarat/Views/MiniBaccaratTableView.swift @@ -12,6 +12,20 @@ struct MiniBaccaratTableView: View { @Bindable var gameState: GameState let selectedChip: ChipDenomination + // MARK: - Layout Constants + + private let tableLimitsFontSize = Design.FontSize.small + private let tieZoneHeight: CGFloat = 55 + private let mainZoneHeight: CGFloat = 60 + private let tieHorizontalPadding: CGFloat = 50 + private let bankerHorizontalPadding: CGFloat = 30 + private let playerHorizontalPadding: CGFloat = 20 + private let zoneTopPadding = Design.Spacing.medium + private let zoneBottomPadding = Design.Spacing.medium + private let minSpacerLength = Design.Spacing.small + + // MARK: - Computed Properties + private func betAmount(for type: BetType) -> Int { gameState.betAmount(for: type) } @@ -34,12 +48,22 @@ struct MiniBaccaratTableView: View { gameState.mainBet?.type == .banker } + private var tableLimitsText: String { + String.localized( + "tableLimitsFormat", + gameState.minBet.formatted(), + gameState.maxBet.formatted() + ) + } + + // MARK: - Body + var body: some View { - VStack(spacing: 4) { + VStack(spacing: Design.Spacing.xSmall) { // Table limits label - Text("TABLE LIMITS: $\(gameState.minBet) - $\(gameState.maxBet.formatted())") - .font(.system(size: 10, weight: .bold, design: .rounded)) - .foregroundStyle(.white.opacity(0.5)) + Text(tableLimitsText) + .font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) .tracking(1) ZStack { @@ -47,10 +71,7 @@ struct MiniBaccaratTableView: View { TableFeltShape() .fill( LinearGradient( - colors: [ - Color(red: 0.0, green: 0.35, blue: 0.18), - Color(red: 0.0, green: 0.28, blue: 0.12) - ], + colors: [Color.Table.feltLight, Color.Table.feltDark], startPoint: .top, endPoint: .bottom ) @@ -60,14 +81,11 @@ struct MiniBaccaratTableView: View { TableFeltShape() .strokeBorder( LinearGradient( - colors: [ - Color(red: 0.85, green: 0.7, blue: 0.35), - Color(red: 0.65, green: 0.5, blue: 0.2) - ], + colors: [Color.Border.goldLight, Color.Border.goldDark], startPoint: .top, endPoint: .bottom ), - lineWidth: 4 + lineWidth: Design.LineWidth.heavy ) // Betting zones layout @@ -80,11 +98,11 @@ struct MiniBaccaratTableView: View { ) { gameState.placeBet(type: .tie, amount: selectedChip.rawValue) } - .frame(height: 55) - .padding(.horizontal, 50) - .padding(.top, 12) + .frame(height: tieZoneHeight) + .padding(.horizontal, tieHorizontalPadding) + .padding(.top, zoneTopPadding) - Spacer(minLength: 8) + Spacer(minLength: minSpacerLength) // BANKER zone in middle BankerBettingZone( @@ -95,10 +113,10 @@ struct MiniBaccaratTableView: View { ) { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) } - .frame(height: 60) - .padding(.horizontal, 30) + .frame(height: mainZoneHeight) + .padding(.horizontal, bankerHorizontalPadding) - Spacer(minLength: 8) + Spacer(minLength: minSpacerLength) // PLAYER zone at bottom PlayerBettingZone( @@ -109,12 +127,12 @@ struct MiniBaccaratTableView: View { ) { gameState.placeBet(type: .player, amount: selectedChip.rawValue) } - .frame(height: 60) - .padding(.horizontal, 20) - .padding(.bottom, 12) + .frame(height: mainZoneHeight) + .padding(.horizontal, playerHorizontalPadding) + .padding(.bottom, zoneBottomPadding) } } - .aspectRatio(1.6, contentMode: .fit) + .aspectRatio(Design.Size.tableAspectRatio, contentMode: .fit) } } } @@ -123,12 +141,14 @@ struct MiniBaccaratTableView: View { struct TableFeltShape: InsettableShape { var insetAmount: CGFloat = 0 + private let shapeCornerRadius = Design.CornerRadius.xxLarge + func path(in rect: CGRect) -> Path { var path = Path() let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) let height = insetRect.height - let cornerRadius: CGFloat = 20 + let cornerRadius = shapeCornerRadius // Start from bottom left path.move(to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY)) @@ -178,22 +198,25 @@ struct TieBettingZone: View { var isAtMax: Bool = false let action: () -> Void + // MARK: - Layout Constants + + private let cornerRadius = Design.CornerRadius.small + private let titleFontSize = Design.FontSize.medium + private let subtitleFontSize = Design.FontSize.xSmall + private let chipTrailingPadding = Design.Spacing.small + + // MARK: - Computed Properties + private var backgroundColor: Color { - if isAtMax { - // Darker/muted green when at max - return Color(red: 0.08, green: 0.32, blue: 0.18) - } - return Color(red: 0.1, green: 0.45, blue: 0.25) + isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie } private var borderColor: Color { - if isAtMax { - // Silver border when at max - return Color(red: 0.6, green: 0.6, blue: 0.65) - } - return Color(red: 0.7, green: 0.55, blue: 0.25) + isAtMax ? Color.Border.silver : Color.Border.gold } + // MARK: - Body + var body: some View { Button { if isEnabled { @@ -202,22 +225,22 @@ struct TieBettingZone: View { } label: { ZStack { // Background - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: cornerRadius) .fill(backgroundColor) // Border - RoundedRectangle(cornerRadius: 8) - .strokeBorder(borderColor, lineWidth: 2) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(borderColor, lineWidth: Design.LineWidth.medium) // Centered text content - VStack(spacing: 2) { + VStack(spacing: Design.Spacing.xxSmall) { Text("TIE") - .font(.system(size: 14, weight: .black, design: .rounded)) + .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(2) Text("PAYS 8 TO 1") - .font(.system(size: 9, weight: .medium)) - .opacity(0.8) + .font(.system(size: subtitleFontSize, weight: .medium)) + .opacity(Design.Opacity.heavy) } .foregroundStyle(.white) } @@ -225,7 +248,7 @@ struct TieBettingZone: View { .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) - .padding(.trailing, 8) + .padding(.trailing, chipTrailingPadding) } } } @@ -241,27 +264,28 @@ struct BankerBettingZone: View { var isAtMax: Bool = false let action: () -> Void + // MARK: - Layout Constants + + private let cornerRadius = Design.CornerRadius.medium + private let titleFontSize = Design.FontSize.large + private let subtitleFontSize = Design.FontSize.xSmall + private let chipTrailingPadding = Design.Spacing.medium + private let selectionShadowRadius = Design.Shadow.radiusSmall + + // MARK: - Computed Properties + private var backgroundColors: [Color] { - if isAtMax { - // Darker/muted red when at max - return [ - Color(red: 0.4, green: 0.1, blue: 0.1), - Color(red: 0.28, green: 0.06, blue: 0.06) - ] - } - return [ - Color(red: 0.55, green: 0.12, blue: 0.12), - Color(red: 0.4, green: 0.08, blue: 0.08) - ] + isAtMax + ? [Color.BettingZone.bankerMaxLight, Color.BettingZone.bankerMaxDark] + : [Color.BettingZone.bankerLight, Color.BettingZone.bankerDark] } private var borderColor: Color { - if isAtMax { - return Color(red: 0.6, green: 0.6, blue: 0.65) - } - return Color(red: 0.7, green: 0.55, blue: 0.25) + isAtMax ? Color.Border.silver : Color.Border.gold } + // MARK: - Body + var body: some View { Button { if isEnabled { @@ -270,7 +294,7 @@ struct BankerBettingZone: View { } label: { ZStack { // Background - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: cornerRadius) .fill( LinearGradient( colors: backgroundColors, @@ -281,24 +305,24 @@ struct BankerBettingZone: View { // Selection glow if isSelected { - RoundedRectangle(cornerRadius: 10) - .strokeBorder(Color.yellow, lineWidth: 3) - .shadow(color: .yellow.opacity(0.5), radius: 8) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick) + .shadow(color: .yellow.opacity(Design.Opacity.medium), radius: selectionShadowRadius) } // Border - RoundedRectangle(cornerRadius: 10) - .strokeBorder(borderColor, lineWidth: 2) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(borderColor, lineWidth: Design.LineWidth.medium) // Centered text content - VStack(spacing: 2) { + VStack(spacing: Design.Spacing.xxSmall) { Text("BANKER") - .font(.system(size: 16, weight: .black, design: .rounded)) + .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(3) Text("PAYS 0.95 TO 1") - .font(.system(size: 9, weight: .medium)) - .opacity(0.8) + .font(.system(size: subtitleFontSize, weight: .medium)) + .opacity(Design.Opacity.heavy) } .foregroundStyle(.white) } @@ -306,7 +330,7 @@ struct BankerBettingZone: View { .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) - .padding(.trailing, 12) + .padding(.trailing, chipTrailingPadding) } } } @@ -322,27 +346,28 @@ struct PlayerBettingZone: View { var isAtMax: Bool = false let action: () -> Void + // MARK: - Layout Constants + + private let cornerRadius = Design.CornerRadius.medium + private let titleFontSize = Design.FontSize.large + private let subtitleFontSize = Design.FontSize.xSmall + private let chipTrailingPadding = Design.Spacing.medium + private let selectionShadowRadius = Design.Shadow.radiusSmall + + // MARK: - Computed Properties + private var backgroundColors: [Color] { - if isAtMax { - // Darker/muted blue when at max - return [ - Color(red: 0.08, green: 0.18, blue: 0.4), - Color(red: 0.04, green: 0.1, blue: 0.28) - ] - } - return [ - Color(red: 0.1, green: 0.25, blue: 0.55), - Color(red: 0.05, green: 0.15, blue: 0.4) - ] + isAtMax + ? [Color.BettingZone.playerMaxLight, Color.BettingZone.playerMaxDark] + : [Color.BettingZone.playerLight, Color.BettingZone.playerDark] } private var borderColor: Color { - if isAtMax { - return Color(red: 0.6, green: 0.6, blue: 0.65) - } - return Color(red: 0.7, green: 0.55, blue: 0.25) + isAtMax ? Color.Border.silver : Color.Border.gold } + // MARK: - Body + var body: some View { Button { if isEnabled { @@ -351,7 +376,7 @@ struct PlayerBettingZone: View { } label: { ZStack { // Background - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: cornerRadius) .fill( LinearGradient( colors: backgroundColors, @@ -362,24 +387,24 @@ struct PlayerBettingZone: View { // Selection glow if isSelected { - RoundedRectangle(cornerRadius: 10) - .strokeBorder(Color.yellow, lineWidth: 3) - .shadow(color: .yellow.opacity(0.5), radius: 8) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick) + .shadow(color: .yellow.opacity(Design.Opacity.medium), radius: selectionShadowRadius) } // Border - RoundedRectangle(cornerRadius: 10) - .strokeBorder(borderColor, lineWidth: 2) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(borderColor, lineWidth: Design.LineWidth.medium) // Centered text content - VStack(spacing: 2) { + VStack(spacing: Design.Spacing.xxSmall) { Text("PLAYER") - .font(.system(size: 16, weight: .black, design: .rounded)) + .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(3) Text("PAYS 1 TO 1") - .font(.system(size: 9, weight: .medium)) - .opacity(0.8) + .font(.system(size: subtitleFontSize, weight: .medium)) + .opacity(Design.Opacity.heavy) } .foregroundStyle(.white) } @@ -387,7 +412,7 @@ struct PlayerBettingZone: View { .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) - .padding(.trailing, 12) + .padding(.trailing, chipTrailingPadding) } } } @@ -400,24 +425,37 @@ struct ChipOnTable: View { let amount: Int var showMax: Bool = false + // MARK: - Layout Constants + + private let chipSize = Design.Size.chipSmall + private let innerRingSize: CGFloat = 26 + private let gradientEndRadius: CGFloat = 20 + private let maxBadgeFontSize = Design.FontSize.xxSmall + private let maxBadgeOffsetX: CGFloat = 6 + private let maxBadgeOffsetY: CGFloat = -4 + + // MARK: - Computed Properties + private var chipColor: Color { switch amount { case 0..<50: return .blue case 50..<100: return .orange case 100..<500: return .black case 500..<1000: return .purple - default: return Color(red: 0.8, green: 0.65, blue: 0.2) + default: return Color.Chip.gold } } private var displayText: String { - if amount >= 1000 { - return "\(amount / 1000)K" - } else { - return "\(amount)" - } + amount >= 1000 ? "\(amount / 1000)K" : "\(amount)" } + private var textFontSize: CGFloat { + amount >= 1000 ? Design.FontSize.small : 11 + } + + // MARK: - Body + var body: some View { ZStack { Circle() @@ -426,36 +464,36 @@ struct ChipOnTable: View { colors: [chipColor.opacity(0.9), chipColor], center: .topLeading, startRadius: 0, - endRadius: 20 + endRadius: gradientEndRadius ) ) - .frame(width: 36, height: 36) + .frame(width: chipSize, height: chipSize) Circle() - .strokeBorder(Color.white.opacity(0.8), lineWidth: 2) - .frame(width: 36, height: 36) + .strokeBorder(Color.white.opacity(Design.Opacity.heavy), lineWidth: Design.LineWidth.medium) + .frame(width: chipSize, height: chipSize) Circle() - .strokeBorder(Color.white.opacity(0.4), lineWidth: 1) - .frame(width: 26, height: 26) + .strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin) + .frame(width: innerRingSize, height: innerRingSize) Text(displayText) - .font(.system(size: amount >= 1000 ? 10 : 11, weight: .bold)) + .font(.system(size: textFontSize, weight: .bold)) .foregroundStyle(.white) } - .shadow(color: .black.opacity(0.4), radius: 3, x: 1, y: 2) + .shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, x: 1, y: 2) .overlay(alignment: .topTrailing) { if showMax { Text("MAX") - .font(.system(size: 7, weight: .black)) + .font(.system(size: maxBadgeFontSize, weight: .black)) .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 2) + .padding(.horizontal, Design.Spacing.xSmall) + .padding(.vertical, Design.Spacing.xxSmall) .background( Capsule() .fill(Color.red) ) - .offset(x: 6, y: -4) + .offset(x: maxBadgeOffsetX, y: maxBadgeOffsetY) } } } @@ -463,7 +501,7 @@ struct ChipOnTable: View { #Preview { ZStack { - Color(red: 0.05, green: 0.2, blue: 0.1) + Color.Table.baseDark .ignoresSafeArea() MiniBaccaratTableView(