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

@@ -27,6 +27,8 @@ struct ProGateModifier: ViewModifier {
.onTapGesture {
showPaywall = true
}
.accessibilityLabel("Pro feature locked")
.accessibilityHint("Double-tap to view upgrade options")
}
}
.sheet(isPresented: $showPaywall) {

View File

@@ -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")
}
}

View File

@@ -23,8 +23,9 @@ struct PaywallView: View {
SubscriptionStoreView(subscriptions: storeManager.products.filter { $0.subscription != nil }) {
VStack(spacing: Theme.Spacing.md) {
Image(systemName: "star.circle.fill")
.font(.system(size: 60))
.font(.system(.largeTitle, design: .default).weight(.regular))
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
Text("Upgrade to Pro")
.font(.largeTitle.bold())
@@ -79,10 +80,12 @@ struct PaywallView: View {
private func featurePill(icon: String, text: String) -> some View {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 11))
.font(.caption2)
.accessibilityHidden(true)
Text(text)
.font(.caption2)
}
.accessibilityElement(children: .combine)
.foregroundStyle(Theme.textMuted(colorScheme))
.padding(.horizontal, 10)
.padding(.vertical, 6)

View File

@@ -15,6 +15,7 @@ struct ProBadge: View {
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Theme.warmOrange, in: Capsule())
.accessibilityLabel("Pro feature")
}
}