feat: implement Dynamic Type with Apple text styles
Replace all custom Theme.FontSize values and hardcoded font sizes with Apple's built-in text styles (.largeTitle, .title2, .headline, .body, .subheadline, .caption, .caption2) to support accessibility scaling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,11 @@ struct AchievementsListView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text("\(earned) / \(total)")
|
||||
.font(.system(size: Theme.FontSize.heroTitle, weight: .bold, design: .rounded))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text("Achievements Earned")
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
if earned == total && total > 0 {
|
||||
@@ -77,7 +77,7 @@ struct AchievementsListView: View {
|
||||
Image(systemName: "star.fill")
|
||||
Text("All achievements unlocked!")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
}
|
||||
@@ -190,9 +190,9 @@ struct CategoryFilterButton: View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
Text(title)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.padding(.vertical, Theme.Spacing.sm)
|
||||
@@ -233,14 +233,14 @@ struct AchievementCard: View {
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
Text(achievement.definition.name)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(achievement.isEarned ? Theme.textPrimary(colorScheme) : Theme.textMuted(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
@@ -250,7 +250,7 @@ struct AchievementCard: View {
|
||||
if achievement.isEarned {
|
||||
if let earnedAt = achievement.earnedAt {
|
||||
Text(earnedAt.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
} else {
|
||||
@@ -260,7 +260,7 @@ struct AchievementCard: View {
|
||||
.progressViewStyle(AchievementProgressStyle())
|
||||
|
||||
Text(achievement.progressText)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -371,11 +371,11 @@ struct AchievementDetailSheet: View {
|
||||
// Title and description
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
Text(achievement.definition.name)
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(achievement.definition.description)
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -384,7 +384,7 @@ struct AchievementDetailSheet: View {
|
||||
Image(systemName: achievement.definition.category.iconName)
|
||||
Text(achievement.definition.category.displayName)
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(categoryColor)
|
||||
.padding(.horizontal, Theme.Spacing.sm)
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
@@ -401,7 +401,7 @@ struct AchievementDetailSheet: View {
|
||||
.foregroundStyle(.green)
|
||||
|
||||
Text("Earned on \(earnedAt.formatted(date: .long, time: .omitted))")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -409,7 +409,7 @@ struct AchievementDetailSheet: View {
|
||||
// Progress section
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
Text("Progress")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
|
||||
ProgressView(value: achievement.progressPercentage)
|
||||
@@ -417,7 +417,7 @@ struct AchievementDetailSheet: View {
|
||||
.frame(width: 200)
|
||||
|
||||
Text("\(achievement.currentProgress) / \(achievement.totalRequired)")
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold))
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ struct AchievementDetailSheet: View {
|
||||
Image(systemName: sport.iconName)
|
||||
Text(sport.displayName)
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(sport.themeColor)
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.padding(.vertical, Theme.Spacing.sm)
|
||||
|
||||
@@ -72,24 +72,24 @@ struct GameMatchConfirmationView: View {
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "photo.fill")
|
||||
.font(.system(size: 36))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
if let date = candidate.metadata.captureDate {
|
||||
Label(formatDate(date), systemImage: "calendar")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
if candidate.metadata.hasValidLocation {
|
||||
Label("Location data available", systemImage: "location.fill")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
} else {
|
||||
Label("No location data", systemImage: "location.slash")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
Text("Nearest Stadium")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Spacer()
|
||||
}
|
||||
@@ -116,11 +116,11 @@ struct GameMatchConfirmationView: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(match.stadium.name)
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold, design: .rounded))
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(match.stadium.fullAddress)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -129,11 +129,11 @@ struct GameMatchConfirmationView: View {
|
||||
// Distance badge
|
||||
VStack(spacing: 2) {
|
||||
Text(match.formattedDistance)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(confidenceColor(match.confidence))
|
||||
|
||||
Text(match.confidence.description)
|
||||
.font(.system(size: 10))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
Text(matchOptionsTitle)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Spacer()
|
||||
}
|
||||
@@ -204,7 +204,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
Text(reason.description)
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -216,7 +216,7 @@ struct GameMatchConfirmationView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(match.matchupDescription)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Image(systemName: match.game.sport.iconName)
|
||||
@@ -225,7 +225,7 @@ struct GameMatchConfirmationView: View {
|
||||
}
|
||||
|
||||
Text(match.gameDateTime)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Confidence
|
||||
@@ -234,7 +234,7 @@ struct GameMatchConfirmationView: View {
|
||||
.fill(combinedConfidenceColor(match.confidence.combined))
|
||||
.frame(width: 8, height: 8)
|
||||
Text(match.confidence.combined.description)
|
||||
.font(.system(size: 10))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -272,7 +272,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
Text("Confirm & Import")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -287,7 +287,7 @@ struct GameMatchConfirmationView: View {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Skip This Photo")
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ struct PhotoImportView: View {
|
||||
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
Text("Import from Photos")
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text("Select photos taken at stadiums to automatically log your visits. We'll use GPS and date data to match them to games.")
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, Theme.Spacing.xl)
|
||||
@@ -114,7 +114,7 @@ struct PhotoImportView: View {
|
||||
Image(systemName: "photo.stack")
|
||||
Text("Select Photos")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -137,7 +137,7 @@ struct PhotoImportView: View {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
Text("How it works")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
@@ -147,7 +147,7 @@ struct PhotoImportView: View {
|
||||
InfoRow(icon: "hand.tap", text: "You confirm or edit the rest")
|
||||
}
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
@@ -164,11 +164,11 @@ struct PhotoImportView: View {
|
||||
ThemedSpinner(size: 50, lineWidth: 4)
|
||||
|
||||
Text("Processing photos...")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
Text("\(viewModel.processedCount) of \(viewModel.totalCount) photos")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
|
||||
Spacer()
|
||||
@@ -222,7 +222,7 @@ struct PhotoImportView: View {
|
||||
Image(systemName: "plus.circle")
|
||||
Text("Add More Photos")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.padding(.top, Theme.Spacing.md)
|
||||
@@ -259,11 +259,11 @@ struct PhotoImportView: View {
|
||||
private func summaryBadge(count: Int, label: String, color: Color) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(count)")
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.font(.title2)
|
||||
.foregroundStyle(color)
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -283,10 +283,10 @@ struct PhotoImportView: View {
|
||||
.foregroundStyle(color)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text(subtitle)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
Spacer()
|
||||
@@ -372,7 +372,7 @@ struct PhotoImportCandidateCard: View {
|
||||
.foregroundStyle(isConfirmed ? .green : Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Match result
|
||||
@@ -407,10 +407,10 @@ struct PhotoImportCandidateCard: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("\(matches.count) possible games")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text("Tap to select the correct game")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
Spacer()
|
||||
@@ -424,7 +424,7 @@ struct PhotoImportCandidateCard: View {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundStyle(.red)
|
||||
Text(reason.description)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -434,7 +434,7 @@ struct PhotoImportCandidateCard: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(match.matchupDescription)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Image(systemName: match.game.sport.iconName)
|
||||
@@ -442,7 +442,7 @@ struct PhotoImportCandidateCard: View {
|
||||
}
|
||||
|
||||
Text("\(match.stadium.name) • \(match.gameDateTime)")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Confidence badge
|
||||
@@ -463,7 +463,7 @@ struct PhotoImportCandidateCard: View {
|
||||
}()
|
||||
|
||||
return Text(text)
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
@@ -496,7 +496,7 @@ struct GameMatchPickerSheet: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(match.fullMatchupDescription)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Spacer()
|
||||
@@ -506,7 +506,7 @@ struct GameMatchPickerSheet: View {
|
||||
}
|
||||
|
||||
Text("\(match.stadium.name) • \(match.gameDateTime)")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
@@ -178,21 +178,21 @@ struct ProgressTabView: View {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text("\(progress.visitedStadiums)")
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
.font(.title3)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
Text("/\(progress.totalStadiums)")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text(viewModel.selectedSport.displayName)
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold, design: .rounded))
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text("Stadium Quest")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
if progress.isComplete {
|
||||
@@ -200,11 +200,11 @@ struct ProgressTabView: View {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
Text("Complete!")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
} else {
|
||||
Text("\(progress.totalStadiums - progress.visitedStadiums) stadiums remaining")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ struct ProgressTabView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Text("Visited (\(viewModel.visitedStadiums.count))")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ struct ProgressTabView: View {
|
||||
Image(systemName: "circle.dotted")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
Text("Not Yet Visited (\(viewModel.unvisitedStadiums.count))")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ struct ProgressTabView: View {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
HStack {
|
||||
Text("Achievements")
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Spacer()
|
||||
@@ -319,7 +319,7 @@ struct ProgressTabView: View {
|
||||
Text("View All")
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
}
|
||||
@@ -341,11 +341,11 @@ struct ProgressTabView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Track Your Progress")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text("Earn badges for stadium visits")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ struct ProgressTabView: View {
|
||||
private var recentVisitsSection: some View {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Recent Visits")
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
ForEach(viewModel.recentVisits) { visitSummary in
|
||||
@@ -422,7 +422,7 @@ struct LeagueSelectorButton: View {
|
||||
}
|
||||
|
||||
Text(sport.rawValue)
|
||||
.font(.system(size: Theme.FontSize.micro, weight: isSelected ? .bold : .medium))
|
||||
.font(.caption)
|
||||
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : Theme.textMuted(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -451,14 +451,14 @@ struct ProgressStatPill: View {
|
||||
VStack(spacing: 4) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 12))
|
||||
.font(.caption)
|
||||
Text(value)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .bold))
|
||||
.font(.body)
|
||||
}
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -483,12 +483,12 @@ struct StadiumChip: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(stadium.name)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.lineLimit(1)
|
||||
|
||||
Text(stadium.city)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ struct RecentVisitRow: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(visit.stadium.name)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
@@ -534,7 +534,7 @@ struct RecentVisitRow: View {
|
||||
Text(matchup)
|
||||
}
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -545,7 +545,7 @@ struct RecentVisitRow: View {
|
||||
Image(systemName: "photo")
|
||||
Text("\(visit.photoCount)")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
|
||||
@@ -585,17 +585,17 @@ struct StadiumDetailSheet: View {
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: visitStatus.isVisited ? "checkmark.seal.fill" : sport.iconName)
|
||||
.font(.system(size: 36))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(visitStatus.isVisited ? .green : sport.themeColor)
|
||||
}
|
||||
|
||||
Text(stadium.name)
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold, design: .rounded))
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(stadium.fullAddress)
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
if visitStatus.isVisited {
|
||||
@@ -604,7 +604,7 @@ struct StadiumDetailSheet: View {
|
||||
.foregroundStyle(.green)
|
||||
Text("Visited \(visitStatus.visitCount) time\(visitStatus.visitCount == 1 ? "" : "s")")
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
@@ -613,23 +613,23 @@ struct StadiumDetailSheet: View {
|
||||
if case .visited(let visits) = visitStatus {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Visit History")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
ForEach(visits.sorted(by: { $0.visitDate > $1.visitDate })) { visit in
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(visit.shortDateDescription)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
if let matchup = visit.matchup {
|
||||
Text(matchup)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(visit.visitType.displayName)
|
||||
.font(.system(size: Theme.FontSize.micro))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
|
||||
@@ -156,7 +156,7 @@ struct StadiumVisitSheet: View {
|
||||
awayTeamFocused = false
|
||||
} label: {
|
||||
Text(team.name)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
@@ -192,7 +192,7 @@ struct StadiumVisitSheet: View {
|
||||
homeTeamFocused = false
|
||||
} label: {
|
||||
Text(team.name)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
@@ -409,11 +409,11 @@ struct StadiumPickerSheet: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(stadium.name)
|
||||
.font(.system(size: Theme.FontSize.body, weight: .medium))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(stadium.fullAddress)
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
|
||||
|
||||
@@ -147,23 +147,23 @@ struct VisitDetailView: View {
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: visit.sportEnum?.iconName ?? "sportscourt")
|
||||
.font(.system(size: 36))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(sportColor)
|
||||
}
|
||||
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
Text(stadium.name)
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold, design: .rounded))
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(stadium.fullAddress)
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Visit type badge
|
||||
Text(visit.visitType.displayName)
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, Theme.Spacing.sm)
|
||||
.padding(.vertical, 4)
|
||||
@@ -189,7 +189,7 @@ struct VisitDetailView: View {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.foregroundStyle(sportColor)
|
||||
Text("Game Info")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ struct VisitDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.padding(Theme.Spacing.lg)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
@@ -233,7 +233,7 @@ struct VisitDetailView: View {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
Text("Details")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ struct VisitDetailView: View {
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.padding(Theme.Spacing.lg)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
@@ -293,12 +293,12 @@ struct VisitDetailView: View {
|
||||
Image(systemName: "note.text")
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
Text("Notes")
|
||||
.font(.system(size: Theme.FontSize.body, weight: .semibold))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
Text(visit.notes ?? "")
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -318,7 +318,7 @@ struct VisitDetailView: View {
|
||||
// Date
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text("Date")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
DatePicker("", selection: $editVisitDate, displayedComponents: .date)
|
||||
@@ -328,7 +328,7 @@ struct VisitDetailView: View {
|
||||
// Visit Type
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text("Visit Type")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
Picker("", selection: $editVisitType) {
|
||||
@@ -343,7 +343,7 @@ struct VisitDetailView: View {
|
||||
if editVisitType == .game {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text("Game Info")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
HStack {
|
||||
@@ -374,7 +374,7 @@ struct VisitDetailView: View {
|
||||
// Seat location
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text("Seat Location")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
TextField("e.g., Section 120, Row 5", text: $editSeatLocation)
|
||||
@@ -384,7 +384,7 @@ struct VisitDetailView: View {
|
||||
// Notes
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
Text("Notes")
|
||||
.font(.system(size: Theme.FontSize.caption, weight: .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
TextEditor(text: $editNotes)
|
||||
|
||||
Reference in New Issue
Block a user