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

This commit is contained in:
Matt Bruce 2025-12-16 18:13:45 -06:00
parent da7dcc1633
commit 884bc988f6
8 changed files with 268 additions and 200 deletions

View File

@ -67,17 +67,20 @@ enum Design {
enum Size {
static let chipSmall: CGFloat = 36
static let chipMedium: CGFloat = 50
static let chipSelector: 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
static let roadMapCell: CGFloat = 16
}
// MARK: - Animation
enum Animation {
static let quick: Double = 0.3
static let springDuration: Double = 0.4
static let springBounce: Double = 0.3
static let fadeInDuration: Double = 0.3
@ -90,7 +93,9 @@ enum Design {
static let disabled: Double = 0.5
static let subtle: Double = 0.1
static let light: Double = 0.3
static let overlay: Double = 0.4
static let medium: Double = 0.5
static let secondary: Double = 0.5
static let strong: Double = 0.7
static let heavy: Double = 0.8
static let nearOpaque: Double = 0.85
@ -100,6 +105,7 @@ enum Design {
enum LineWidth {
static let thin: CGFloat = 1
static let standard: CGFloat = 2
static let medium: CGFloat = 2
static let thick: CGFloat = 3
static let heavy: CGFloat = 4
@ -128,6 +134,7 @@ extension Color {
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)
static let preview = Color(red: 0.0, green: 0.3, blue: 0.15)
}
// MARK: - Border Colors
@ -171,6 +178,49 @@ extension Color {
enum Chip {
static let gold = Color(red: 0.8, green: 0.65, blue: 0.2)
// Chip base colors
static let tenBase = Color(red: 0.2, green: 0.4, blue: 0.8)
static let tenHighlight = Color(red: 0.3, green: 0.5, blue: 0.9)
static let twentyFiveBase = Color(red: 0.1, green: 0.6, blue: 0.3)
static let twentyFiveHighlight = Color(red: 0.2, green: 0.7, blue: 0.4)
static let fiftyBase = Color(red: 0.8, green: 0.5, blue: 0.1)
static let fiftyHighlight = Color(red: 0.9, green: 0.6, blue: 0.2)
static let hundredBase = Color(red: 0.1, green: 0.1, blue: 0.1)
static let hundredHighlight = Color(red: 0.3, green: 0.3, blue: 0.3)
static let fiveHundredBase = Color(red: 0.6, green: 0.2, blue: 0.6)
static let fiveHundredHighlight = Color(red: 0.7, green: 0.3, blue: 0.7)
static let thousandBase = Color(red: 0.8, green: 0.65, blue: 0.2)
static let thousandHighlight = Color(red: 0.9, green: 0.75, blue: 0.3)
static let fiveThousandBase = Color(red: 0.7, green: 0.1, blue: 0.2)
static let fiveThousandHighlight = Color(red: 0.85, green: 0.2, blue: 0.3)
static let tenThousandBase = Color(red: 0.2, green: 0.5, blue: 0.5)
static let tenThousandHighlight = Color(red: 0.3, green: 0.6, blue: 0.6)
static let twentyFiveThousandBase = Color(red: 0.5, green: 0.3, blue: 0.1)
static let twentyFiveThousandHighlight = Color(red: 0.65, green: 0.45, blue: 0.2)
static let fiftyThousandBase = Color(red: 0.75, green: 0.75, blue: 0.8)
static let fiftyThousandHighlight = Color(red: 0.85, green: 0.85, blue: 0.9)
static let hundredThousandBase = Color(red: 0.9, green: 0.1, blue: 0.3)
static let hundredThousandHighlight = Color(red: 1.0, green: 0.2, blue: 0.4)
// Accent stripe colors
static let goldStripe = Color(red: 0.9, green: 0.75, blue: 0.3)
static let darkStripe = Color(red: 0.2, green: 0.2, blue: 0.3)
static let goldRubyStripe = Color(red: 0.9, green: 0.85, blue: 0.3)
}
// MARK: - Card Colors
enum Card {
// Card back
static let backDark = Color(red: 0.6, green: 0.1, blue: 0.15)
static let backLight = Color(red: 0.4, green: 0.05, blue: 0.1)
static let patternLight = Color(red: 0.9, green: 0.7, blue: 0.4)
static let patternDark = Color(red: 0.7, green: 0.5, blue: 0.2)
static let innerDark = Color(red: 0.5, green: 0.08, blue: 0.12)
static let innerLight = Color(red: 0.35, green: 0.04, blue: 0.08)
static let diamondPattern = Color(red: 0.9, green: 0.7, blue: 0.4)
static let logoText = Color(red: 0.4, green: 0.05, blue: 0.1)
}
// MARK: - Modal Colors

View File

@ -123,12 +123,12 @@ struct CardBackView: View {
var body: some View {
ZStack {
// Base
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(
LinearGradient(
colors: [
Color(red: 0.6, green: 0.1, blue: 0.15),
Color(red: 0.4, green: 0.05, blue: 0.1)
Color.Card.backDark,
Color.Card.backLight
],
startPoint: .topLeading,
endPoint: .bottomTrailing
@ -136,26 +136,26 @@ struct CardBackView: View {
)
// Border
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.strokeBorder(
LinearGradient(
colors: [
Color(red: 0.9, green: 0.7, blue: 0.4),
Color(red: 0.7, green: 0.5, blue: 0.2)
Color.Card.patternLight,
Color.Card.patternDark
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 2
lineWidth: Design.LineWidth.medium
)
// Inner pattern area
RoundedRectangle(cornerRadius: 4)
RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2)
.fill(
LinearGradient(
colors: [
Color(red: 0.5, green: 0.08, blue: 0.12),
Color(red: 0.35, green: 0.04, blue: 0.08)
Color.Card.innerDark,
Color.Card.innerLight
],
startPoint: .top,
endPoint: .bottom
@ -166,18 +166,18 @@ struct CardBackView: View {
// Diamond pattern overlay
DiamondPatternView()
.foregroundStyle(
Color(red: 0.9, green: 0.7, blue: 0.4).opacity(0.3)
Color.Card.diamondPattern.opacity(Design.Opacity.light)
)
.padding(width * 0.12)
.clipShape(RoundedRectangle(cornerRadius: 4))
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.small / 2))
// Center emblem
Circle()
.fill(
RadialGradient(
colors: [
Color(red: 0.9, green: 0.7, blue: 0.4),
Color(red: 0.7, green: 0.5, blue: 0.2)
Color.Card.patternLight,
Color.Card.patternDark
],
center: .center,
startRadius: 0,
@ -189,10 +189,10 @@ struct CardBackView: View {
// B for Baccarat
Text("B")
.font(.system(size: width * 0.18, weight: .bold, design: .serif))
.foregroundStyle(Color(red: 0.4, green: 0.05, blue: 0.1))
.foregroundStyle(Color.Card.logoText)
}
.frame(width: width, height: height)
.shadow(color: .black.opacity(0.3), radius: 4, x: 2, y: 2)
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, x: 2, y: 2)
}
}
@ -230,10 +230,10 @@ struct CardPlaceholderView: View {
}
var body: some View {
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.strokeBorder(
Color.white.opacity(0.3),
style: StrokeStyle(lineWidth: 2, dash: [8, 4])
Color.white.opacity(Design.Opacity.light),
style: StrokeStyle(lineWidth: Design.LineWidth.medium, dash: [8, 4])
)
.frame(width: width, height: height)
}
@ -241,10 +241,10 @@ struct CardPlaceholderView: View {
#Preview {
ZStack {
Color(red: 0.0, green: 0.3, blue: 0.15)
Color.Table.preview
.ignoresSafeArea()
HStack(spacing: 20) {
HStack(spacing: Design.Spacing.xLarge) {
CardView(card: Card(suit: .hearts, rank: .ace), isFaceUp: true)
CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true)
CardView(card: Card(suit: .diamonds, rank: .seven), isFaceUp: false)

View File

@ -28,24 +28,24 @@ struct ChipSelectorView: View {
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 10) {
HStack(spacing: Design.Spacing.small) {
ForEach(availableChips) { denomination in
Button {
selectedChip = denomination
} label: {
ChipView(
denomination: denomination,
size: 50,
size: Design.Size.chipSelector,
isSelected: selectedChip == denomination
)
}
.buttonStyle(.plain)
.opacity(balance >= denomination.rawValue ? 1.0 : 0.4)
.opacity(balance >= denomination.rawValue ? 1.0 : Design.Opacity.overlay)
.disabled(balance < denomination.rawValue)
}
}
.padding(.horizontal)
.padding(.vertical, 6) // Extra padding for selection scale effect
.padding(.vertical, Design.Spacing.xSmall) // Extra padding for selection scale effect
}
.scrollIndicators(.hidden)
.onChange(of: balance) { _, newBalance in
@ -61,10 +61,10 @@ struct ChipSelectorView: View {
#Preview {
ZStack {
Color(red: 0.0, green: 0.3, blue: 0.15)
Color.Table.preview
.ignoresSafeArea()
VStack(spacing: 20) {
VStack(spacing: Design.Spacing.xLarge) {
Text("Balance: $50,000")
.foregroundStyle(.white)

View File

@ -68,34 +68,34 @@ enum ChipDenomination: Int, CaseIterable, Identifiable {
/// The primary color for this chip.
var primaryColor: Color {
switch self {
case .ten: return Color(red: 0.2, green: 0.4, blue: 0.8) // Blue
case .twentyFive: return Color(red: 0.1, green: 0.6, blue: 0.3) // Green
case .fifty: return Color(red: 0.8, green: 0.5, blue: 0.1) // Orange
case .hundred: return Color(red: 0.1, green: 0.1, blue: 0.1) // Black
case .fiveHundred: return Color(red: 0.6, green: 0.2, blue: 0.6) // Purple
case .thousand: return Color(red: 0.8, green: 0.65, blue: 0.2) // Gold
case .fiveThousand: return Color(red: 0.7, green: 0.1, blue: 0.2) // Crimson
case .tenThousand: return Color(red: 0.2, green: 0.5, blue: 0.5) // Teal
case .twentyFiveThousand: return Color(red: 0.5, green: 0.3, blue: 0.1) // Bronze
case .fiftyThousand: return Color(red: 0.75, green: 0.75, blue: 0.8) // Platinum
case .hundredThousand: return Color(red: 0.9, green: 0.1, blue: 0.3) // Ruby
case .ten: return Color.Chip.tenBase
case .twentyFive: return Color.Chip.twentyFiveBase
case .fifty: return Color.Chip.fiftyBase
case .hundred: return Color.Chip.hundredBase
case .fiveHundred: return Color.Chip.fiveHundredBase
case .thousand: return Color.Chip.thousandBase
case .fiveThousand: return Color.Chip.fiveThousandBase
case .tenThousand: return Color.Chip.tenThousandBase
case .twentyFiveThousand: return Color.Chip.twentyFiveThousandBase
case .fiftyThousand: return Color.Chip.fiftyThousandBase
case .hundredThousand: return Color.Chip.hundredThousandBase
}
}
/// The secondary/accent color for this chip.
var secondaryColor: Color {
switch self {
case .ten: return Color(red: 0.3, green: 0.5, blue: 0.9)
case .twentyFive: return Color(red: 0.2, green: 0.7, blue: 0.4)
case .fifty: return Color(red: 0.9, green: 0.6, blue: 0.2)
case .hundred: return Color(red: 0.3, green: 0.3, blue: 0.3)
case .fiveHundred: return Color(red: 0.7, green: 0.3, blue: 0.7)
case .thousand: return Color(red: 0.9, green: 0.75, blue: 0.3)
case .fiveThousand: return Color(red: 0.85, green: 0.2, blue: 0.3)
case .tenThousand: return Color(red: 0.3, green: 0.6, blue: 0.6)
case .twentyFiveThousand: return Color(red: 0.65, green: 0.45, blue: 0.2)
case .fiftyThousand: return Color(red: 0.85, green: 0.85, blue: 0.9)
case .hundredThousand: return Color(red: 1.0, green: 0.2, blue: 0.4)
case .ten: return Color.Chip.tenHighlight
case .twentyFive: return Color.Chip.twentyFiveHighlight
case .fifty: return Color.Chip.fiftyHighlight
case .hundred: return Color.Chip.hundredHighlight
case .fiveHundred: return Color.Chip.fiveHundredHighlight
case .thousand: return Color.Chip.thousandHighlight
case .fiveThousand: return Color.Chip.fiveThousandHighlight
case .tenThousand: return Color.Chip.tenThousandHighlight
case .twentyFiveThousand: return Color.Chip.twentyFiveThousandHighlight
case .fiftyThousand: return Color.Chip.fiftyThousandHighlight
case .hundredThousand: return Color.Chip.hundredThousandHighlight
}
}
@ -103,14 +103,14 @@ enum ChipDenomination: Int, CaseIterable, Identifiable {
var stripeColor: Color {
switch self {
case .ten, .twentyFive, .fifty: return .white
case .hundred: return Color(red: 0.9, green: 0.75, blue: 0.3)
case .hundred: return Color.Chip.goldStripe
case .fiveHundred: return .white
case .thousand: return .black
case .fiveThousand: return Color(red: 0.9, green: 0.75, blue: 0.3) // Gold stripes
case .fiveThousand: return Color.Chip.goldStripe
case .tenThousand: return .white
case .twentyFiveThousand: return Color(red: 0.9, green: 0.75, blue: 0.3)
case .fiftyThousand: return Color(red: 0.2, green: 0.2, blue: 0.3) // Dark stripes
case .hundredThousand: return Color(red: 0.9, green: 0.85, blue: 0.3) // Gold stripes
case .twentyFiveThousand: return Color.Chip.goldStripe
case .fiftyThousand: return Color.Chip.darkStripe
case .hundredThousand: return Color.Chip.goldRubyStripe
}
}
}
@ -274,13 +274,13 @@ struct ChipStackView: View {
#Preview {
ZStack {
Color(red: 0.0, green: 0.3, blue: 0.15)
Color.Table.preview
.ignoresSafeArea()
VStack(spacing: 30) {
HStack(spacing: 15) {
VStack(spacing: Design.Spacing.xxxLarge) {
HStack(spacing: Design.Spacing.xLarge) {
ForEach(ChipDenomination.allCases) { denom in
ChipView(denomination: denom, size: 50)
ChipView(denomination: denom, size: Design.Size.chipSelector)
}
}

View File

@ -46,7 +46,7 @@ struct GameTableView: View {
onSettings: { showSettings = true }
)
Spacer(minLength: 4)
Spacer(minLength: Design.Spacing.xSmall)
// Cards display area
CardsDisplayArea(
@ -61,7 +61,7 @@ struct GameTableView: View {
isTie: isTie
)
Spacer(minLength: 4)
Spacer(minLength: Design.Spacing.xSmall)
// Road map history
if settings.showHistory && !state.roundHistory.isEmpty {
@ -69,16 +69,16 @@ struct GameTableView: View {
.padding(.horizontal)
}
Spacer(minLength: 8)
Spacer(minLength: Design.Spacing.small)
// Mini Baccarat betting table
MiniBaccaratTableView(
gameState: state,
selectedChip: selectedChip
)
.padding(.horizontal, 12)
.padding(.horizontal, Design.Spacing.medium)
Spacer(minLength: 8)
Spacer(minLength: Design.Spacing.small)
// Chip selector - shows higher chips as you win more!
ChipSelectorView(
@ -86,7 +86,7 @@ struct GameTableView: View {
balance: state.balance,
maxBet: state.maxBet
)
.padding(.bottom, 12)
.padding(.bottom, Design.Spacing.medium)
// Action buttons
ActionButtonsView(
@ -100,7 +100,7 @@ struct GameTableView: View {
onNewRound: { state.newRound() }
)
.padding(.horizontal)
.padding(.bottom, 4)
.padding(.bottom, Design.Spacing.xSmall)
}
.safeAreaPadding(.bottom)
@ -295,11 +295,11 @@ struct CardsDisplayArea: View {
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14
var body: some View {
HStack(spacing: 32) {
HStack(spacing: Design.Spacing.xxxLarge) {
// Player side
VStack(spacing: 10) {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: 8) {
HStack(spacing: Design.Spacing.small) {
Text("PLAYER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
@ -319,9 +319,9 @@ struct CardsDisplayArea: View {
}
// Banker side
VStack(spacing: 10) {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: 8) {
HStack(spacing: Design.Spacing.small) {
Text("BANKER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
@ -340,11 +340,11 @@ struct CardsDisplayArea: View {
)
}
}
.padding(.top, 16)
.padding(.bottom, 14)
.padding(.horizontal, 20)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.xLarge)
.padding(.horizontal, Design.Spacing.xLarge)
.background(
RoundedRectangle(cornerRadius: 14)
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.black.opacity(0.25))
)
.padding(.horizontal)
@ -385,12 +385,12 @@ struct CompactHandView: View {
}
}
}
.padding(6)
.padding(Design.Spacing.xSmall)
.background(
RoundedRectangle(cornerRadius: 8)
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.strokeBorder(
isWinner ? Color.yellow : Color.clear,
lineWidth: 2
lineWidth: Design.LineWidth.standard
)
)
.overlay(alignment: .bottom) {
@ -398,13 +398,13 @@ struct CompactHandView: View {
Text("WIN")
.font(.system(size: winBadgeFontSize, weight: .black))
.foregroundStyle(.black)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall)
.background(
Capsule()
.fill(Color.yellow)
)
.offset(y: 10)
.offset(y: Design.Spacing.small)
}
}
}
@ -437,13 +437,13 @@ struct TableBackgroundView: View {
var body: some View {
ZStack {
// Base dark green
Color(red: 0.02, green: 0.15, blue: 0.08)
Color.Table.baseDark
// Radial gradient for depth
RadialGradient(
colors: [
Color(red: 0.03, green: 0.25, blue: 0.12),
Color(red: 0.01, green: 0.12, blue: 0.06)
Color.Table.backgroundLight,
Color.Table.backgroundDark
],
center: .center,
startRadius: 50,
@ -452,7 +452,7 @@ struct TableBackgroundView: View {
// Subtle felt texture
FeltPatternView()
.opacity(0.03)
.opacity(Design.Opacity.subtle / 3)
}
.ignoresSafeArea()
}
@ -493,13 +493,13 @@ struct TopBarView: View {
var body: some View {
HStack {
// Balance display
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("BALANCE")
.font(.system(size: labelFontSize, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.tracking(1)
HStack(spacing: 4) {
HStack(spacing: Design.Spacing.xSmall) {
Text("$")
.font(.system(size: currencyFontSize, weight: .bold))
.foregroundStyle(.yellow.opacity(0.8))
@ -508,27 +508,27 @@ struct TopBarView: View {
.font(.system(size: balanceFontSize, weight: .black, design: .rounded))
.foregroundStyle(.white)
.contentTransition(.numericText())
.animation(.spring(duration: 0.3), value: balance)
.animation(.spring(duration: Design.Animation.quick), value: balance)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 6)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
.fill(Color.black.opacity(0.4))
.fill(Color.black.opacity(Design.Opacity.overlay))
)
Spacer()
// Cards remaining indicator (if enabled)
if showCardsRemaining {
HStack(spacing: 4) {
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(0.5))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
Spacer()
}
@ -538,25 +538,25 @@ struct TopBarView: View {
.labelStyle(.iconOnly)
.font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6))
.padding(8)
.padding(Design.Spacing.small)
.background(
Circle()
.fill(Color.black.opacity(0.4))
.fill(Color.black.opacity(Design.Opacity.overlay))
)
// Reset button
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
.font(.system(size: smallFontSize, weight: .medium))
.foregroundStyle(.white.opacity(0.6))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
.fill(Color.black.opacity(0.4))
.fill(Color.black.opacity(Design.Opacity.overlay))
)
}
.padding(.horizontal)
.padding(.top, 4)
.padding(.top, Design.Spacing.xSmall)
}
}
@ -574,14 +574,14 @@ struct ActionButtonsView: View {
@ScaledMetric(relativeTo: .body) private var statusFontSize: CGFloat = 14
var body: some View {
HStack(spacing: 12) {
HStack(spacing: Design.Spacing.medium) {
if gameState.currentPhase == .betting {
// Clear bets button
Button("Clear", systemImage: "xmark.circle", action: onClear)
.font(.system(size: clearButtonFontSize, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(Color.Button.destructive)
@ -593,8 +593,8 @@ struct ActionButtonsView: View {
Button("Deal", systemImage: "play.fill", action: onDeal)
.font(.system(size: primaryButtonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, 32)
.padding(.vertical, 12)
.padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
@ -613,8 +613,8 @@ struct ActionButtonsView: View {
Button("New Round", systemImage: "arrow.right.circle", action: onNewRound)
.font(.system(size: primaryButtonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, 32)
.padding(.vertical, 12)
.padding(.horizontal, Design.Spacing.xxxLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
@ -628,7 +628,7 @@ struct ActionButtonsView: View {
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
} else {
// Playing indicator
HStack(spacing: 6) {
HStack(spacing: Design.Spacing.xSmall) {
ProgressView()
.tint(.white)
.scaleEffect(0.8)
@ -636,8 +636,8 @@ struct ActionButtonsView: View {
.font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
}
}
}

View File

@ -16,18 +16,23 @@ struct ResultBannerView: View {
@State private var showText = false
@State private var showWinnings = false
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
@ScaledMetric(relativeTo: .title2) private var winningsFontSize: CGFloat = 28
var body: some View {
ZStack {
// Background overlay
Color.black.opacity(showBanner ? 0.5 : 0)
Color.black.opacity(showBanner ? Design.Opacity.medium : 0)
.ignoresSafeArea()
.animation(.easeIn(duration: 0.3), value: showBanner)
.animation(.easeIn(duration: Design.Animation.fadeInDuration), value: showBanner)
// Banner
VStack(spacing: 20) {
VStack(spacing: Design.Spacing.xLarge) {
// Result text
Text(result.displayText)
.font(.system(size: 36, weight: .black, design: .rounded))
.font(.system(size: resultFontSize, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [.white, result.color],
@ -35,13 +40,13 @@ struct ResultBannerView: View {
endPoint: .bottom
)
)
.shadow(color: result.color.opacity(0.8), radius: 10)
.shadow(color: result.color.opacity(Design.Opacity.heavy), radius: Design.Shadow.radiusLarge)
.scaleEffect(showText ? 1.0 : 0.5)
.opacity(showText ? 1.0 : 0)
// Winnings display
if winnings != 0 {
HStack(spacing: 8) {
HStack(spacing: Design.Spacing.small) {
if winnings > 0 {
Image(systemName: "plus.circle.fill")
.foregroundStyle(.green)
@ -54,14 +59,14 @@ struct ResultBannerView: View {
.foregroundStyle(.red)
}
}
.font(.system(size: 28, weight: .bold, design: .rounded))
.font(.system(size: winningsFontSize, weight: .bold, design: .rounded))
.scaleEffect(showWinnings ? 1.0 : 0.5)
.opacity(showWinnings ? 1.0 : 0)
}
}
.padding(40)
.padding(Design.Spacing.xxxLarge + Design.Spacing.small)
.background(
RoundedRectangle(cornerRadius: 24)
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall)
.fill(
LinearGradient(
colors: [
@ -73,34 +78,34 @@ struct ResultBannerView: View {
)
)
.overlay(
RoundedRectangle(cornerRadius: 24)
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall)
.strokeBorder(
LinearGradient(
colors: [
result.color.opacity(0.8),
result.color.opacity(0.3)
result.color.opacity(Design.Opacity.heavy),
result.color.opacity(Design.Opacity.light)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 3
lineWidth: Design.LineWidth.thick
)
)
)
.shadow(color: result.color.opacity(0.3), radius: 30)
.shadow(color: result.color.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXXLarge)
.scaleEffect(showBanner ? 1.0 : 0.8)
.opacity(showBanner ? 1.0 : 0)
}
.onAppear {
withAnimation(.spring(duration: 0.4, bounce: 0.3)) {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
showBanner = true
}
withAnimation(.spring(duration: 0.4, bounce: 0.3).delay(0.2)) {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(0.2)) {
showText = true
}
withAnimation(.spring(duration: 0.4, bounce: 0.3).delay(0.4)) {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(0.4)) {
showWinnings = true
}
}
@ -114,10 +119,13 @@ struct ConfettiPiece: View {
@State private var rotation: Double = 0
@State private var opacity: Double = 1
private let confettiWidth: CGFloat = 8
private let confettiHeight: CGFloat = 12
var body: some View {
Rectangle()
.fill(color)
.frame(width: 8, height: 12)
.frame(width: confettiWidth, height: confettiHeight)
.rotationEffect(.degrees(rotation))
.position(position)
.opacity(opacity)
@ -154,7 +162,7 @@ struct ConfettiView: View {
#Preview {
ZStack {
Color(red: 0.0, green: 0.3, blue: 0.15)
Color.Table.preview
.ignoresSafeArea()
ResultBannerView(result: .playerWins, winnings: 500)

View File

@ -11,28 +11,33 @@ import SwiftUI
struct RoadMapView: View {
let results: [RoundResult]
// MARK: - Scaled Fonts (Dynamic Type)
@ScaledMetric(relativeTo: .caption2) private var historyFontSize: CGFloat = Design.BaseFontSize.small
@ScaledMetric(relativeTo: .caption2) private var dotSize: CGFloat = 22
var body: some View {
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text("HISTORY")
.font(.system(size: 10, weight: .bold, design: .rounded))
.font(.system(size: historyFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.tracking(1)
ScrollView(.horizontal) {
HStack(spacing: 4) {
HStack(spacing: Design.Spacing.xSmall) {
ForEach(results) { result in
RoadDot(result: result.result)
RoadDot(result: result.result, dotSize: dotSize)
}
}
.padding(.vertical, 4)
.padding(.vertical, Design.Spacing.xSmall)
}
.scrollIndicators(.hidden)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.black.opacity(0.3))
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(Color.black.opacity(Design.Opacity.light))
)
}
}
@ -40,6 +45,11 @@ struct RoadMapView: View {
/// A single dot in the road display.
struct RoadDot: View {
let result: GameResult
let dotSize: CGFloat
// MARK: - Scaled Fonts (Dynamic Type)
@ScaledMetric(relativeTo: .caption2) private var labelFontSize: CGFloat = Design.BaseFontSize.small
private var color: Color {
switch result {
@ -61,14 +71,14 @@ struct RoadDot: View {
ZStack {
Circle()
.fill(color)
.frame(width: 22, height: 22)
.frame(width: dotSize, height: dotSize)
Circle()
.strokeBorder(Color.white.opacity(0.3), lineWidth: 1)
.frame(width: 22, height: 22)
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
.frame(width: dotSize, height: dotSize)
Text(label)
.font(.system(size: 10, weight: .bold))
.font(.system(size: labelFontSize, weight: .bold))
.foregroundStyle(.white)
}
}
@ -76,7 +86,7 @@ struct RoadDot: View {
#Preview {
ZStack {
Color(red: 0.0, green: 0.3, blue: 0.15)
Color.Table.preview
.ignoresSafeArea()
RoadMapView(results: [

View File

@ -19,11 +19,11 @@ struct SettingsView: View {
NavigationStack {
ZStack {
// Background
Color(red: 0.08, green: 0.12, blue: 0.08)
Color.Settings.background
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
VStack(spacing: Design.Spacing.xxLarge) {
// Table Limits Section (First!)
SettingsSection(title: "TABLE LIMITS", icon: "banknote") {
TableLimitsPicker(selection: $settings.tableLimits)
@ -57,7 +57,7 @@ struct SettingsView: View {
)
Divider()
.background(Color.white.opacity(0.1))
.background(Color.white.opacity(Design.Opacity.subtle))
SettingsToggle(
title: "Show History",
@ -76,7 +76,7 @@ struct SettingsView: View {
if settings.showAnimations {
Divider()
.background(Color.white.opacity(0.1))
.background(Color.white.opacity(Design.Opacity.subtle))
SpeedPicker(speed: $settings.dealingSpeed)
}
@ -91,24 +91,24 @@ struct SettingsView: View {
Image(systemName: "arrow.counterclockwise")
Text("Reset to Defaults")
}
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.red.opacity(0.8))
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.red.opacity(0.1))
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.red.opacity(Design.Opacity.subtle))
)
}
.padding(.horizontal)
.padding(.top, 8)
.padding(.top, Design.Spacing.small)
}
.padding(.vertical)
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(Color(red: 0.08, green: 0.12, blue: 0.08), for: .navigationBar)
.toolbarBackground(Color.Settings.background, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
@ -143,19 +143,19 @@ struct SettingsSection<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// Header
HStack(spacing: 8) {
HStack(spacing: Design.Spacing.small) {
Image(systemName: icon)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.yellow.opacity(0.8))
.font(.system(size: Design.BaseFontSize.body, weight: .semibold))
.foregroundStyle(.yellow.opacity(Design.Opacity.heavy))
Text(title)
.font(.system(size: 12, weight: .bold, design: .rounded))
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
.tracking(1)
.foregroundStyle(.white.opacity(0.6))
}
.padding(.horizontal, 4)
.padding(.horizontal, Design.Spacing.xSmall)
// Content card
VStack(spacing: 0) {
@ -163,7 +163,7 @@ struct SettingsSection<Content: View>: View {
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.white.opacity(0.05))
)
}
@ -176,44 +176,44 @@ struct DeckCountPicker: View {
@Binding var selection: DeckCount
var body: some View {
VStack(spacing: 12) {
VStack(spacing: Design.Spacing.medium) {
ForEach(DeckCount.allCases) { count in
Button {
selection = count
} label: {
HStack {
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(count.displayName)
.font(.system(size: 16, weight: .semibold))
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.white)
Text(count.description)
.font(.system(size: 12))
.foregroundStyle(.white.opacity(0.5))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
if selection == count {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22))
.font(.system(size: Design.Size.checkmark))
.foregroundStyle(.yellow)
} else {
Circle()
.strokeBorder(Color.white.opacity(0.3), lineWidth: 2)
.frame(width: 22, height: 22)
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
.frame(width: Design.Size.checkmark, height: Design.Size.checkmark)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(selection == count ? Color.yellow.opacity(0.1) : Color.clear)
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(selection == count ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 10)
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
selection == count ? Color.yellow.opacity(0.5) : Color.white.opacity(0.1),
lineWidth: 1
selection == count ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
lineWidth: Design.LineWidth.thin
)
)
}
@ -234,19 +234,19 @@ struct BalancePicker: View {
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 10) {
], spacing: Design.Spacing.small) {
ForEach(options, id: \.self) { amount in
Button {
balance = amount
} label: {
Text("$\(amount / 1000)K")
.font(.system(size: 14, weight: .bold))
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(balance == amount ? .black : .white)
.padding(.vertical, 12)
.padding(.vertical, Design.Spacing.medium)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(balance == amount ? Color.yellow : Color.white.opacity(0.1))
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(balance == amount ? Color.yellow : Color.white.opacity(Design.Opacity.subtle))
)
}
.buttonStyle(.plain)
@ -263,14 +263,14 @@ struct SettingsToggle: View {
var body: some View {
Toggle(isOn: $isOn) {
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(.white)
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(.white.opacity(0.5))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
.tint(.yellow)
@ -288,24 +288,24 @@ struct SpeedPicker: View {
]
var body: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Dealing Speed")
.font(.system(size: 15, weight: .medium))
.foregroundStyle(.white)
HStack(spacing: 8) {
HStack(spacing: Design.Spacing.small) {
ForEach(options, id: \.1) { option in
Button {
speed = option.1
} label: {
Text(option.0)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(speed == option.1 ? .black : .white.opacity(0.7))
.padding(.vertical, 8)
.foregroundStyle(speed == option.1 ? .black : .white.opacity(Design.Opacity.strong))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)
.background(
Capsule()
.fill(speed == option.1 ? Color.yellow : Color.white.opacity(0.1))
.fill(speed == option.1 ? Color.yellow : Color.white.opacity(Design.Opacity.subtle))
)
}
.buttonStyle(.plain)
@ -320,30 +320,30 @@ struct TableLimitsPicker: View {
@Binding var selection: TableLimits
var body: some View {
VStack(spacing: 10) {
VStack(spacing: Design.Spacing.small) {
ForEach(TableLimits.allCases) { limit in
Button {
selection = limit
} label: {
HStack {
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(limit.displayName)
.font(.system(size: 16, weight: .semibold))
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.white)
Text(limit.detailedDescription)
.font(.system(size: 12))
.foregroundStyle(.white.opacity(0.5))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
// Limits badge
Text(limit.description)
.font(.system(size: 12, weight: .bold, design: .rounded))
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
.foregroundStyle(selection == limit ? .black : .yellow)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
.fill(selection == limit ? Color.yellow : Color.yellow.opacity(0.2))
@ -351,24 +351,24 @@ struct TableLimitsPicker: View {
if selection == limit {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 20))
.font(.system(size: Design.Size.checkmark - 2))
.foregroundStyle(.yellow)
} else {
Circle()
.strokeBorder(Color.white.opacity(0.3), lineWidth: 2)
.frame(width: 20, height: 20)
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
.frame(width: Design.Size.checkmark - 2, height: Design.Size.checkmark - 2)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(selection == limit ? Color.yellow.opacity(0.1) : Color.clear)
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(selection == limit ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 10)
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
selection == limit ? Color.yellow.opacity(0.5) : Color.white.opacity(0.1),
lineWidth: 1
selection == limit ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
lineWidth: Design.LineWidth.thin
)
)
}