feat: add WCAG AA accessibility app-wide, fix CloudKit container config, remove debug logs
- Add VoiceOver labels, hints, and element grouping across all 60+ views - Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations - Replace fixed font sizes with semantic Dynamic Type styles - Hide decorative elements from VoiceOver with .accessibilityHidden(true) - Add .minimumHitTarget() modifier ensuring 44pt touch targets - Add AccessibilityAnnouncer utility for VoiceOver announcements - Improve color contrast values in Theme.swift for WCAG AA compliance - Extract CloudKitContainerConfig for explicit container identity - Remove PostHog debug console log from AnalyticsManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -115,14 +115,16 @@ struct AchievementsListView: View {
|
||||
.frame(width: 64, height: 64)
|
||||
|
||||
Image(systemName: selectedSport?.iconName ?? "trophy.fill")
|
||||
.font(.system(size: 28))
|
||||
.font(.title2)
|
||||
.foregroundStyle(earned > 0 ? completedGold : accentColor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text("\(earned)")
|
||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
||||
.font(.system(.largeTitle, design: .rounded).weight(.bold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(earned > 0 ? completedGold : Theme.textPrimary(colorScheme))
|
||||
Text("/ \(total)")
|
||||
.font(.title2)
|
||||
@@ -174,7 +176,7 @@ struct AchievementsListView: View {
|
||||
color: Theme.warmOrange,
|
||||
isSelected: selectedSport == nil
|
||||
) {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedSport = nil
|
||||
}
|
||||
}
|
||||
@@ -187,7 +189,7 @@ struct AchievementsListView: View {
|
||||
color: sport.themeColor,
|
||||
isSelected: selectedSport == sport
|
||||
) {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedSport = sport
|
||||
}
|
||||
}
|
||||
@@ -287,6 +289,8 @@ struct SportFilterButton: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +322,9 @@ struct AchievementCard: View {
|
||||
}
|
||||
|
||||
Image(systemName: achievement.definition.iconName)
|
||||
.font(.system(size: 28))
|
||||
.font(.title2)
|
||||
.foregroundStyle(badgeIconColor)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
if !achievement.isEarned {
|
||||
Circle()
|
||||
@@ -329,6 +334,7 @@ struct AchievementCard: View {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +352,7 @@ struct AchievementCard: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
if let earnedAt = achievement.earnedAt {
|
||||
Text(earnedAt.formatted(date: .abbreviated, time: .omitted))
|
||||
} else {
|
||||
@@ -376,6 +383,7 @@ struct AchievementCard: View {
|
||||
}
|
||||
.shadow(color: achievement.isEarned ? completedGold.opacity(0.3) : Theme.cardShadow(colorScheme), radius: achievement.isEarned ? 8 : 5, y: 2)
|
||||
.opacity(achievement.isEarned ? 1.0 : 0.7)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
private var badgeBackgroundColor: Color {
|
||||
@@ -492,8 +500,9 @@ struct AchievementDetailSheet: View {
|
||||
}
|
||||
|
||||
Image(systemName: achievement.definition.iconName)
|
||||
.font(.system(size: 56))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(badgeIconColor)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
if !achievement.isEarned {
|
||||
Circle()
|
||||
@@ -501,8 +510,9 @@ struct AchievementDetailSheet: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 24))
|
||||
.font(.title3)
|
||||
.foregroundStyle(.white)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,8 +548,9 @@ struct AchievementDetailSheet: View {
|
||||
if achievement.isEarned {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 32))
|
||||
.font(.title)
|
||||
.foregroundStyle(completedGold)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
if let earnedAt = achievement.earnedAt {
|
||||
Text("Earned on \(earnedAt.formatted(date: .long, time: .omitted))")
|
||||
@@ -575,6 +586,7 @@ struct AchievementDetailSheet: View {
|
||||
if let sport = achievement.definition.sport {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: sport.iconName)
|
||||
.accessibilityLabel(sport.displayName)
|
||||
Text(sport.displayName)
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
@@ -12,6 +12,7 @@ struct GamesHistoryRow: View {
|
||||
.font(.title3)
|
||||
.foregroundStyle(stadium.sport.themeColor)
|
||||
.frame(width: 32)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
// Visit info
|
||||
@@ -38,10 +39,12 @@ struct GamesHistoryRow: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
private func sportIcon(for sport: Sport) -> String {
|
||||
|
||||
@@ -8,7 +8,7 @@ struct VisitListCard: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header row (always visible)
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
@@ -37,11 +37,13 @@ struct VisitListCard: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.rotationEffect(.degrees(isExpanded ? 90 : 0))
|
||||
.accessibilityLabel(isExpanded ? "Collapse details" : "Expand details")
|
||||
}
|
||||
.padding()
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHint("Double-tap to expand visit details")
|
||||
|
||||
// Expanded content
|
||||
if isExpanded {
|
||||
@@ -115,6 +117,7 @@ private struct InfoRow: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 16)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
|
||||
@@ -107,6 +107,7 @@ struct GameMatchConfirmationView: View {
|
||||
HStack {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("Nearest Stadium")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -131,6 +132,7 @@ struct GameMatchConfirmationView: View {
|
||||
Text(match.formattedDistance)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(confidenceColor(match.confidence))
|
||||
.accessibilityLabel("\(match.formattedDistance), \(match.confidence.description) confidence")
|
||||
|
||||
Text(match.confidence.description)
|
||||
.font(.caption2)
|
||||
@@ -154,6 +156,7 @@ struct GameMatchConfirmationView: View {
|
||||
HStack {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text(matchOptionsTitle)
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -196,6 +199,9 @@ struct GameMatchConfirmationView: View {
|
||||
} label: {
|
||||
gameMatchRow(match, isSelected: selectedMatch?.id == match.id)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(selectedMatch?.id == match.id ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(selectedMatch?.id == match.id ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +228,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: match.game.sport.iconName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(match.game.sport.themeColor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Text(match.gameDateTime)
|
||||
@@ -233,6 +240,7 @@ struct GameMatchConfirmationView: View {
|
||||
Circle()
|
||||
.fill(combinedConfidenceColor(match.confidence.combined))
|
||||
.frame(width: 8, height: 8)
|
||||
.accessibilityLabel(confidenceAccessibilityLabel(match.confidence.combined))
|
||||
Text(match.confidence.combined.description)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
@@ -245,6 +253,7 @@ struct GameMatchConfirmationView: View {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.title2)
|
||||
.foregroundStyle(isSelected ? .green : Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(isSelected ? Theme.cardBackgroundElevated(colorScheme) : Color.clear)
|
||||
@@ -318,6 +327,14 @@ struct GameMatchConfirmationView: View {
|
||||
case .manualOnly: return .red
|
||||
}
|
||||
}
|
||||
|
||||
private func confidenceAccessibilityLabel(_ confidence: CombinedConfidence) -> String {
|
||||
switch confidence {
|
||||
case .autoSelect: return "High confidence"
|
||||
case .userConfirm: return "Medium confidence"
|
||||
case .manualOnly: return "Low confidence"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
@@ -57,6 +57,7 @@ private struct GamesHistoryContent: View {
|
||||
viewModel.clearFilters()
|
||||
}
|
||||
.font(.caption)
|
||||
.accessibilityHint("Clear all sport filters")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +120,7 @@ private struct SportChip: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: sportIcon)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(sport.rawValue)
|
||||
.font(.caption.bold())
|
||||
}
|
||||
@@ -131,6 +133,8 @@ private struct SportChip: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
|
||||
private var sportIcon: String {
|
||||
@@ -199,7 +203,7 @@ private struct EmptyGamesView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "ticket")
|
||||
.font(.system(size: 48))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("No games recorded yet")
|
||||
|
||||
@@ -91,7 +91,7 @@ struct PhotoImportView: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
.font(.system(size: 50))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ struct PhotoImportView: View {
|
||||
HStack {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("How it works")
|
||||
.font(.body)
|
||||
}
|
||||
@@ -374,6 +375,7 @@ struct PhotoImportCandidateCard: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(isConfirmed ? .green : Theme.textMuted(colorScheme))
|
||||
}
|
||||
.accessibilityLabel(isConfirmed ? "Deselect for import" : "Confirm import")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
@@ -419,6 +421,7 @@ struct PhotoImportCandidateCard: View {
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +429,7 @@ struct PhotoImportCandidateCard: View {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundStyle(.red)
|
||||
.accessibilityLabel("Error")
|
||||
Text(reason.description)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
@@ -538,6 +542,7 @@ private struct InfoRow: View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.frame(width: 16)
|
||||
.accessibilityHidden(true)
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ struct ProgressMapView: View {
|
||||
isVisited: isVisited(stadium),
|
||||
isSelected: selectedStadium?.id == stadium.id,
|
||||
onTap: {
|
||||
withAnimation(.spring(response: 0.3)) {
|
||||
Theme.Animation.withMotion(.spring(response: 0.3)) {
|
||||
if selectedStadium?.id == stadium.id {
|
||||
selectedStadium = nil
|
||||
} else {
|
||||
@@ -51,7 +51,7 @@ struct ProgressMapView: View {
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if mapViewModel.shouldShowResetButton {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.5)) {
|
||||
cameraPosition = .region(MapInteractionViewModel.defaultRegion)
|
||||
mapViewModel.resetToDefault()
|
||||
selectedStadium = nil
|
||||
@@ -108,6 +108,7 @@ struct StadiumMapPin: View {
|
||||
.fill(pinColor)
|
||||
.frame(width: 10, height: 6)
|
||||
.offset(y: -2)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// Stadium name (when selected)
|
||||
if isSelected {
|
||||
@@ -128,7 +129,10 @@ struct StadiumMapPin: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.animation(.spring(response: 0.3), value: isSelected)
|
||||
.accessibilityLabel("\(stadium.name), \(isVisited ? "visited" : "not visited")")
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
.animation(Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3), value: isSelected)
|
||||
}
|
||||
|
||||
private var pinColor: Color {
|
||||
|
||||
@@ -63,6 +63,7 @@ struct ProgressTabView: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.accessibilityLabel("Add stadium visit")
|
||||
}
|
||||
}
|
||||
.task {
|
||||
@@ -153,7 +154,7 @@ struct ProgressTabView: View {
|
||||
isSelected: viewModel.selectedSport == sport,
|
||||
progress: progressForSport(sport)
|
||||
) {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
viewModel.selectSport(sport)
|
||||
}
|
||||
}
|
||||
@@ -180,13 +181,18 @@ struct ProgressTabView: View {
|
||||
Circle()
|
||||
.stroke(Theme.warmOrange.opacity(0.2), lineWidth: 8)
|
||||
.frame(width: 80, height: 80)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress.progressFraction)
|
||||
.stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||
.frame(width: 80, height: 80)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.5), value: progress.progressFraction)
|
||||
.animation(
|
||||
Theme.Animation.prefersReducedMotion ? nil : .easeInOut(duration: 0.5),
|
||||
value: progress.progressFraction
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text("\(progress.visitedStadiums)")
|
||||
@@ -265,6 +271,7 @@ struct ProgressTabView: View {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.accessibilityHidden(true)
|
||||
Text("Visited (\(viewModel.visitedStadiums.count))")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -292,6 +299,7 @@ struct ProgressTabView: View {
|
||||
HStack {
|
||||
Image(systemName: "circle.dotted")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
Text("Not Yet Visited (\(viewModel.unvisitedStadiums.count))")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -331,6 +339,7 @@ struct ProgressTabView: View {
|
||||
HStack(spacing: 4) {
|
||||
Text("View All")
|
||||
Image(systemName: "chevron.right")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
@@ -348,8 +357,9 @@ struct ProgressTabView: View {
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Image(systemName: "trophy.fill")
|
||||
.font(.system(size: 24))
|
||||
.font(.title3)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -366,6 +376,7 @@ struct ProgressTabView: View {
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
@@ -396,6 +407,7 @@ struct ProgressTabView: View {
|
||||
HStack(spacing: 4) {
|
||||
Text("See All")
|
||||
Image(systemName: "chevron.right")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
@@ -432,6 +444,7 @@ struct ProgressStatPill: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(value)
|
||||
.font(.body)
|
||||
}
|
||||
@@ -460,6 +473,7 @@ struct StadiumChip: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.green)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@@ -495,6 +509,7 @@ struct StadiumChip: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +528,7 @@ struct RecentVisitRow: View {
|
||||
|
||||
Image(systemName: visit.sport.iconName)
|
||||
.foregroundStyle(visit.sport.themeColor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -538,6 +554,7 @@ struct RecentVisitRow: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
@@ -546,6 +563,7 @@ struct RecentVisitRow: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ struct StadiumVisitHistoryView: View {
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibilityLabel("Add visit to this stadium")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddVisit) {
|
||||
@@ -93,7 +94,7 @@ private struct EmptyVisitHistoryView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "calendar.badge.plus")
|
||||
.font(.system(size: 48))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("No visits recorded")
|
||||
|
||||
@@ -165,6 +165,7 @@ struct StadiumVisitSheet: View {
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.accessibilityLabel("Select team \(team.name)")
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
@@ -201,6 +202,7 @@ struct StadiumVisitSheet: View {
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.accessibilityLabel("Select team \(team.name)")
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
@@ -283,6 +285,7 @@ struct StadiumVisitSheet: View {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.accessibilityHidden(true)
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ struct VisitDetailView: View {
|
||||
Image(systemName: visit.sportEnum?.iconName ?? "sportscourt")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(sportColor)
|
||||
.accessibilityLabel(visit.sportEnum?.displayName ?? "Sport")
|
||||
}
|
||||
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
@@ -188,6 +189,7 @@ struct VisitDetailView: View {
|
||||
HStack {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.foregroundStyle(sportColor)
|
||||
.accessibilityHidden(true)
|
||||
Text("Game Info")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -232,6 +234,7 @@ struct VisitDetailView: View {
|
||||
HStack {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("Details")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -292,6 +295,7 @@ struct VisitDetailView: View {
|
||||
HStack {
|
||||
Image(systemName: "note.text")
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
.accessibilityHidden(true)
|
||||
Text("Notes")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Reference in New Issue
Block a user