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

View File

@ -101,7 +101,7 @@ If SwiftData is configured to use CloudKit:
static let small: CGFloat = 8
static let medium: CGFloat = 12
}
enum FontSize {
enum BaseFontSize {
static let body: CGFloat = 14
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`.
- 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

File diff suppressed because it is too large Load Diff

View File

@ -34,9 +34,11 @@ enum Design {
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 xSmall: CGFloat = 9
static let small: CGFloat = 10

View File

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

View File

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