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:
Trey t
2026-02-11 09:27:23 -06:00
parent e9c15d70b1
commit d63d311cab
77 changed files with 982 additions and 263 deletions

View File

@@ -308,7 +308,7 @@ struct TripOptionsView: View {
hasAppliedDemoSelection = true
// Auto-select "Most Games" sort after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
withAnimation(.easeInOut(duration: 0.3)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
sortOption = DemoConfig.demoSortOption
}
}
@@ -329,7 +329,7 @@ struct TripOptionsView: View {
Menu {
ForEach(TripSortOption.allCases) { option in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
sortOption = option
}
} label: {
@@ -345,6 +345,7 @@ struct TripOptionsView: View {
.font(.subheadline)
Image(systemName: "chevron.down")
.font(.caption)
.accessibilityHidden(true)
}
.foregroundStyle(Theme.textPrimary(colorScheme))
.padding(.horizontal, 16)
@@ -397,6 +398,7 @@ struct TripOptionsView: View {
.contentTransition(.identity)
Image(systemName: "chevron.down")
.font(.caption2)
.accessibilityHidden(true)
}
.foregroundStyle(paceFilter == .all ? Theme.textPrimary(colorScheme) : Theme.warmOrange)
.padding(.horizontal, 12)
@@ -420,12 +422,12 @@ struct TripOptionsView: View {
HStack(spacing: 8) {
ForEach(CitiesFilter.allCases) { filter in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
citiesFilter = filter
}
} label: {
Text(filter.displayName)
.font(.system(size: 13, weight: citiesFilter == filter ? .semibold : .medium))
.font(.caption.weight(citiesFilter == filter ? .semibold : .medium))
.foregroundStyle(citiesFilter == filter ? .white : Theme.textPrimary(colorScheme))
.padding(.horizontal, 12)
.padding(.vertical, 6)
@@ -446,15 +448,16 @@ struct TripOptionsView: View {
private var emptyFilterState: some View {
VStack(spacing: Theme.Spacing.md) {
Image(systemName: "line.3.horizontal.decrease.circle")
.font(.system(size: 48))
.font(.largeTitle)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
Text("No routes match your filters")
.font(.body)
.foregroundStyle(Theme.textSecondary(colorScheme))
Button {
withAnimation {
Theme.Animation.withMotion {
citiesFilter = .noLimit
paceFilter = .all
}
@@ -524,6 +527,7 @@ struct TripOptionCard: View {
.font(.caption2)
}
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
Text(uniqueCities.last ?? "")
.font(.subheadline)
@@ -560,7 +564,7 @@ struct TripOptionCard: View {
// AI-generated description (after stats)
if let description = aiDescription {
Text(description)
.font(.system(size: 13, weight: .regular))
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.fixedSize(horizontal: false, vertical: true)
.transition(.opacity)
@@ -578,8 +582,9 @@ struct TripOptionCard: View {
// Right: Chevron
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
@@ -607,7 +612,7 @@ struct TripOptionCard: View {
let input = RouteDescriptionInput(from: option, games: games)
if let description = await RouteDescriptionGenerator.shared.generateDescription(for: input) {
withAnimation(.easeInOut(duration: 0.3)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
aiDescription = description
}
}