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

This commit is contained in:
Matt Bruce 2025-12-16 17:39:30 -06:00
parent a898da7664
commit acd0064776
6 changed files with 1754 additions and 152 deletions

View File

@ -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.

View File

@ -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")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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(