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

This commit is contained in:
Matt Bruce 2025-12-16 17:54:46 -06:00
parent acd0064776
commit da7dcc1633
6 changed files with 685 additions and 742 deletions

View File

@ -205,6 +205,10 @@
knownRegions = ( knownRegions = (
en, en,
Base, Base,
"es-US",
es,
fr,
"fr-CA",
); );
mainGroup = EAD890AE2EF1E9CE006DBA80; mainGroup = EAD890AE2EF1E9CE006DBA80;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
@ -344,6 +348,7 @@
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
name = Debug; name = Debug;
@ -401,6 +406,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;

View File

@ -101,7 +101,7 @@ If SwiftData is configured to use CloudKit:
static let small: CGFloat = 8 static let small: CGFloat = 8
static let medium: CGFloat = 12 static let medium: CGFloat = 12
} }
enum FontSize { enum BaseFontSize {
static let body: CGFloat = 14 static let body: CGFloat = 14
static let title: CGFloat = 24 static let title: CGFloat = 24
} }
@ -125,7 +125,37 @@ If SwiftData is configured to use CloudKit:
} }
``` ```
- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`. - 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. - Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
## Dynamic Type instructions
- Always support Dynamic Type for accessibility; never use fixed font sizes without scaling.
- Use `@ScaledMetric` to scale custom font sizes and dimensions based on user accessibility settings:
```swift
struct MyView: View {
@ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .title) private var titleFontSize: CGFloat = 24
@ScaledMetric(relativeTo: .caption) private var chipTextSize: CGFloat = 11
var body: some View {
Text("Hello")
.font(.system(size: bodyFontSize, weight: .medium))
}
}
```
- Choose the appropriate `relativeTo` text style based on the semantic purpose:
- `.largeTitle`, `.title`, `.title2`, `.title3` for headings
- `.headline`, `.subheadline` for emphasized content
- `.body` for main content
- `.callout`, `.footnote`, `.caption`, `.caption2` for smaller text
- For constrained UI elements (chips, cards, badges) where overflow would break the design, you may use fixed sizes but document the reason:
```swift
// Fixed size: chip face has strict space constraints
private let chipValueFontSize: CGFloat = 11
```
- Prefer system text styles when possible: `.font(.body)`, `.font(.title)`, `.font(.caption)`.
- Test with accessibility settings: Settings > Accessibility > Display & Text Size > Larger Text.
## Project structure ## Project structure

File diff suppressed because it is too large Load Diff

View File

@ -34,9 +34,11 @@ enum Design {
static let xxxLarge: CGFloat = 28 static let xxxLarge: CGFloat = 28
} }
// MARK: - Font Sizes // MARK: - Base Font Sizes
// These are base values for use with @ScaledMetric in views.
// They will scale automatically based on user accessibility settings.
enum FontSize { enum BaseFontSize {
static let xxSmall: CGFloat = 7 static let xxSmall: CGFloat = 7
static let xSmall: CGFloat = 9 static let xSmall: CGFloat = 9
static let small: CGFloat = 10 static let small: CGFloat = 10

View File

@ -148,13 +148,16 @@ struct GameOverView: View {
@State private var showContent = false @State private var showContent = false
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .largeTitle) private var iconSize: CGFloat = Design.BaseFontSize.display
@ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle
@ScaledMetric(relativeTo: .body) private var messageFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .body) private var statsFontSize: CGFloat = 17
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.xLarge
// MARK: - Layout Constants // 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 modalCornerRadius = Design.CornerRadius.xxxLarge
private let statsCornerRadius = Design.CornerRadius.large private let statsCornerRadius = Design.CornerRadius.large
private let cardPadding = Design.Spacing.xxxLarge private let cardPadding = Design.Spacing.xxxLarge
@ -287,6 +290,10 @@ struct CardsDisplayArea: View {
let bankerIsWinner: Bool let bankerIsWinner: Bool
let isTie: Bool let isTie: Bool
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14
var body: some View { var body: some View {
HStack(spacing: 32) { HStack(spacing: 32) {
// Player side // Player side
@ -294,14 +301,14 @@ struct CardsDisplayArea: View {
// Label with value // Label with value
HStack(spacing: 8) { HStack(spacing: 8) {
Text("PLAYER") Text("PLAYER")
.font(.system(size: 14, weight: .bold, design: .rounded)) .font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white) .foregroundStyle(.white)
if !playerCards.isEmpty && playerCardsFaceUp.contains(true) { if !playerCards.isEmpty && playerCardsFaceUp.contains(true) {
ValueBadge(value: playerValue, color: .blue) ValueBadge(value: playerValue, color: .blue)
} }
} }
.frame(height: 30) .frame(minHeight: 30)
// Cards // Cards
CompactHandView( CompactHandView(
@ -316,14 +323,14 @@ struct CardsDisplayArea: View {
// Label with value // Label with value
HStack(spacing: 8) { HStack(spacing: 8) {
Text("BANKER") Text("BANKER")
.font(.system(size: 14, weight: .bold, design: .rounded)) .font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white) .foregroundStyle(.white)
if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) { if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) {
ValueBadge(value: bankerValue, color: .red) ValueBadge(value: bankerValue, color: .red)
} }
} }
.frame(height: 30) .frame(minHeight: 30)
// Cards // Cards
CompactHandView( CompactHandView(
@ -350,12 +357,21 @@ struct CompactHandView: View {
let cardsFaceUp: [Bool] let cardsFaceUp: [Bool]
let isWinner: Bool let isWinner: Bool
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10
// MARK: - Layout Constants
// Fixed size: cards have strict visual constraints
private let cardWidth: CGFloat = 45
var body: some View { var body: some View {
HStack(spacing: -12) { HStack(spacing: -12) {
if cards.isEmpty { if cards.isEmpty {
// Placeholders // Placeholders
ForEach(0..<2, id: \.self) { _ in ForEach(0..<2, id: \.self) { _ in
CardPlaceholderView(width: 45) CardPlaceholderView(width: cardWidth)
} }
} else { } else {
ForEach(cards.indices, id: \.self) { index in ForEach(cards.indices, id: \.self) { index in
@ -363,7 +379,7 @@ struct CompactHandView: View {
CardView( CardView(
card: cards[index], card: cards[index],
isFaceUp: isFaceUp, isFaceUp: isFaceUp,
cardWidth: 45 cardWidth: cardWidth
) )
.zIndex(Double(index)) .zIndex(Double(index))
} }
@ -380,7 +396,7 @@ struct CompactHandView: View {
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
if isWinner { if isWinner {
Text("WIN") Text("WIN")
.font(.system(size: 10, weight: .black)) .font(.system(size: winBadgeFontSize, weight: .black))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 2) .padding(.vertical, 2)
@ -399,11 +415,16 @@ struct ValueBadge: View {
let value: Int let value: Int
let color: Color let color: Color
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15
@ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = 26
var body: some View { var body: some View {
Text("\(value)") Text("\(value)")
.font(.system(size: 15, weight: .black, design: .rounded)) .font(.system(size: valueFontSize, weight: .black, design: .rounded))
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(width: 26, height: 26) .frame(width: badgeSize, height: badgeSize)
.background( .background(
Circle() Circle()
.fill(color) .fill(color)
@ -461,22 +482,30 @@ struct TopBarView: View {
let onReset: () -> Void let onReset: () -> Void
let onSettings: () -> Void let onSettings: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .caption2) private var labelFontSize: CGFloat = 9
@ScaledMetric(relativeTo: .body) private var currencyFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .title3) private var balanceFontSize: CGFloat = 20
@ScaledMetric(relativeTo: .caption) private var smallFontSize: CGFloat = 12
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = 16
var body: some View { var body: some View {
HStack { HStack {
// Balance display // Balance display
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("BALANCE") Text("BALANCE")
.font(.system(size: 9, weight: .medium, design: .rounded)) .font(.system(size: labelFontSize, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
.tracking(1) .tracking(1)
HStack(spacing: 4) { HStack(spacing: 4) {
Text("$") Text("$")
.font(.system(size: 14, weight: .bold)) .font(.system(size: currencyFontSize, weight: .bold))
.foregroundStyle(.yellow.opacity(0.8)) .foregroundStyle(.yellow.opacity(0.8))
Text(balance, format: .number) Text(balance, format: .number)
.font(.system(size: 20, weight: .black, design: .rounded)) .font(.system(size: balanceFontSize, weight: .black, design: .rounded))
.foregroundStyle(.white) .foregroundStyle(.white)
.contentTransition(.numericText()) .contentTransition(.numericText())
.animation(.spring(duration: 0.3), value: balance) .animation(.spring(duration: 0.3), value: balance)
@ -495,9 +524,9 @@ struct TopBarView: View {
if showCardsRemaining { if showCardsRemaining {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill") Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill")
.font(.system(size: 12)) .font(.system(size: smallFontSize))
Text("\(cardsRemaining)") Text("\(cardsRemaining)")
.font(.system(size: 12, weight: .medium)) .font(.system(size: smallFontSize, weight: .medium))
} }
.foregroundStyle(.white.opacity(0.5)) .foregroundStyle(.white.opacity(0.5))
@ -507,7 +536,7 @@ struct TopBarView: View {
// Settings button // Settings button
Button("Settings", systemImage: "gearshape.fill", action: onSettings) Button("Settings", systemImage: "gearshape.fill", action: onSettings)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: 16)) .font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
.padding(8) .padding(8)
.background( .background(
@ -517,7 +546,7 @@ struct TopBarView: View {
// Reset button // Reset button
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset) Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
.font(.system(size: 12, weight: .medium)) .font(.system(size: smallFontSize, weight: .medium))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
@ -538,25 +567,31 @@ struct ActionButtonsView: View {
let onClear: () -> Void let onClear: () -> Void
let onNewRound: () -> Void let onNewRound: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .body) private var clearButtonFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .headline) private var primaryButtonFontSize: CGFloat = 16
@ScaledMetric(relativeTo: .body) private var statusFontSize: CGFloat = 14
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
if gameState.currentPhase == .betting { if gameState.currentPhase == .betting {
// Clear bets button // Clear bets button
Button("Clear", systemImage: "xmark.circle", action: onClear) Button("Clear", systemImage: "xmark.circle", action: onClear)
.font(.system(size: 14, weight: .semibold)) .font(.system(size: clearButtonFontSize, weight: .semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.vertical, 12) .padding(.vertical, 12)
.background( .background(
Capsule() Capsule()
.fill(Color(red: 0.6, green: 0.2, blue: 0.2)) .fill(Color.Button.destructive)
) )
.opacity(gameState.currentBets.isEmpty ? 0.5 : 1.0) .opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
.disabled(gameState.currentBets.isEmpty) .disabled(gameState.currentBets.isEmpty)
// Deal button // Deal button
Button("Deal", systemImage: "play.fill", action: onDeal) Button("Deal", systemImage: "play.fill", action: onDeal)
.font(.system(size: 16, weight: .bold)) .font(.system(size: primaryButtonFontSize, weight: .bold))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, 32) .padding(.horizontal, 32)
.padding(.vertical, 12) .padding(.vertical, 12)
@ -564,22 +599,19 @@ struct ActionButtonsView: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [ colors: [Color.Button.goldLight, Color.Button.goldDark],
Color(red: 1.0, green: 0.85, blue: 0.3),
Color(red: 0.9, green: 0.7, blue: 0.2)
],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
) )
) )
.shadow(color: .yellow.opacity(0.3), radius: 6) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
.opacity(gameState.canDeal ? 1.0 : 0.5) .opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
.disabled(!gameState.canDeal) .disabled(!gameState.canDeal)
} else if gameState.currentPhase == .roundComplete { } else if gameState.currentPhase == .roundComplete {
// New round button // New round button
Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) Button("New Round", systemImage: "arrow.right.circle", action: onNewRound)
.font(.system(size: 16, weight: .bold)) .font(.system(size: primaryButtonFontSize, weight: .bold))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, 32) .padding(.horizontal, 32)
.padding(.vertical, 12) .padding(.vertical, 12)
@ -587,16 +619,13 @@ struct ActionButtonsView: View {
Capsule() Capsule()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [ colors: [Color.Button.goldLight, Color.Button.goldDark],
Color(red: 1.0, green: 0.85, blue: 0.3),
Color(red: 0.9, green: 0.7, blue: 0.2)
],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
) )
) )
.shadow(color: .yellow.opacity(0.3), radius: 6) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
} else { } else {
// Playing indicator // Playing indicator
HStack(spacing: 6) { HStack(spacing: 6) {
@ -604,8 +633,8 @@ struct ActionButtonsView: View {
.tint(.white) .tint(.white)
.scaleEffect(0.8) .scaleEffect(0.8)
Text("Dealing...") Text("Dealing...")
.font(.system(size: 14, weight: .medium)) .font(.system(size: statusFontSize, weight: .medium))
.foregroundStyle(.white.opacity(0.8)) .foregroundStyle(.white.opacity(Design.Opacity.heavy))
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.vertical, 12) .padding(.vertical, 12)

View File

@ -12,9 +12,12 @@ struct MiniBaccaratTableView: View {
@Bindable var gameState: GameState @Bindable var gameState: GameState
let selectedChip: ChipDenomination let selectedChip: ChipDenomination
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .caption) private var tableLimitsFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout Constants // MARK: - Layout Constants
private let tableLimitsFontSize = Design.FontSize.small
private let tieZoneHeight: CGFloat = 55 private let tieZoneHeight: CGFloat = 55
private let mainZoneHeight: CGFloat = 60 private let mainZoneHeight: CGFloat = 60
private let tieHorizontalPadding: CGFloat = 50 private let tieHorizontalPadding: CGFloat = 50
@ -198,11 +201,14 @@ struct TieBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
private let cornerRadius = Design.CornerRadius.small private let cornerRadius = Design.CornerRadius.small
private let titleFontSize = Design.FontSize.medium
private let subtitleFontSize = Design.FontSize.xSmall
private let chipTrailingPadding = Design.Spacing.small private let chipTrailingPadding = Design.Spacing.small
// MARK: - Computed Properties // MARK: - Computed Properties
@ -264,11 +270,14 @@ struct BankerBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
private let cornerRadius = Design.CornerRadius.medium 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 chipTrailingPadding = Design.Spacing.medium
private let selectionShadowRadius = Design.Shadow.radiusSmall private let selectionShadowRadius = Design.Shadow.radiusSmall
@ -346,11 +355,14 @@ struct PlayerBettingZone: View {
var isAtMax: Bool = false var isAtMax: Bool = false
let action: () -> Void let action: () -> Void
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var titleFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .caption2) private var subtitleFontSize: CGFloat = Design.BaseFontSize.xSmall
// MARK: - Layout Constants // MARK: - Layout Constants
private let cornerRadius = Design.CornerRadius.medium 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 chipTrailingPadding = Design.Spacing.medium
private let selectionShadowRadius = Design.Shadow.radiusSmall private let selectionShadowRadius = Design.Shadow.radiusSmall
@ -426,11 +438,12 @@ struct ChipOnTable: View {
var showMax: Bool = false var showMax: Bool = false
// MARK: - Layout Constants // MARK: - Layout Constants
// Fixed sizes: chip face has strict space constraints
private let chipSize = Design.Size.chipSmall private let chipSize = Design.Size.chipSmall
private let innerRingSize: CGFloat = 26 private let innerRingSize: CGFloat = 26
private let gradientEndRadius: CGFloat = 20 private let gradientEndRadius: CGFloat = 20
private let maxBadgeFontSize = Design.FontSize.xxSmall private let maxBadgeFontSize = Design.BaseFontSize.xxSmall
private let maxBadgeOffsetX: CGFloat = 6 private let maxBadgeOffsetX: CGFloat = 6
private let maxBadgeOffsetY: CGFloat = -4 private let maxBadgeOffsetY: CGFloat = -4
@ -451,7 +464,7 @@ struct ChipOnTable: View {
} }
private var textFontSize: CGFloat { private var textFontSize: CGFloat {
amount >= 1000 ? Design.FontSize.small : 11 amount >= 1000 ? Design.BaseFontSize.small : 11
} }
// MARK: - Body // MARK: - Body