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:
@@ -96,8 +96,10 @@ struct TripRoutesBackground: View {
|
||||
// Animated car/plane icon traveling along a route
|
||||
TravelingIcon(color: color, animate: animate)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
@@ -115,13 +117,15 @@ private struct TravelingIcon: View {
|
||||
Image(systemName: "car.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(color.opacity(0.3))
|
||||
.accessibilityHidden(true)
|
||||
.position(
|
||||
x: geo.size.width * (0.15 + position * 0.7),
|
||||
y: geo.size.height * (0.2 + sin(position * .pi * 2) * 0.15)
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
Theme.Animation.withMotion(.linear(duration: 8).repeatForever(autoreverses: false)) {
|
||||
position = 1
|
||||
}
|
||||
}
|
||||
@@ -176,6 +180,7 @@ struct DocumentsBackground: View {
|
||||
ForEach(0..<12, id: \.self) { index in
|
||||
documentIcon(index: index)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// Central PDF badge
|
||||
GeometryReader { geo in
|
||||
@@ -196,8 +201,10 @@ struct DocumentsBackground: View {
|
||||
.scaleEffect(animate ? 1.05 : 0.95)
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2.5).repeatForever(autoreverses: true)) {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 2.5).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
@@ -251,7 +258,7 @@ struct StadiumMapBackground: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Map grid canvas
|
||||
// Map grid canvas (decorative)
|
||||
Canvas { context, size in
|
||||
// Draw subtle grid lines like a map
|
||||
let gridSpacing: CGFloat = 35
|
||||
@@ -357,11 +364,13 @@ struct StadiumMapBackground: View {
|
||||
.position(x: geo.size.width * 0.5, y: geo.size.height * 0.92)
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2.5).repeatForever(autoreverses: true)) {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 2.5).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
withAnimation(.spring(response: 0.6, dampingFraction: 0.6).delay(0.3)) {
|
||||
Theme.Animation.withMotion(.spring(response: 0.6, dampingFraction: 0.6).delay(0.3)) {
|
||||
checkmarkScale = 1
|
||||
}
|
||||
}
|
||||
@@ -459,7 +468,10 @@ struct OnboardingPaywallView: View {
|
||||
.tag(pages.count)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.animation(.easeInOut, value: currentPage)
|
||||
.animation(
|
||||
Theme.Animation.prefersReducedMotion ? nil : .easeInOut,
|
||||
value: currentPage
|
||||
)
|
||||
|
||||
// Page indicator
|
||||
HStack(spacing: 8) {
|
||||
@@ -500,8 +512,9 @@ struct OnboardingPaywallView: View {
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
Image(systemName: page.icon)
|
||||
.font(.system(size: 44))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(page.color)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
@@ -521,6 +534,7 @@ struct OnboardingPaywallView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(page.color)
|
||||
.font(.body)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(bullet)
|
||||
.font(.body)
|
||||
@@ -582,6 +596,7 @@ struct OnboardingPaywallView: View {
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
.accessibilityHint("Page \(currentPage + 1) of \(pages.count + 1)")
|
||||
}
|
||||
|
||||
// Continue free (always visible)
|
||||
@@ -594,6 +609,7 @@ struct OnboardingPaywallView: View {
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.accessibilityHint("Skip and continue with free version")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user