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

@@ -47,7 +47,9 @@ struct PlaceholderRectangle: View {
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.accessibilityHidden(true)
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
@@ -72,7 +74,9 @@ struct PlaceholderCircle: View {
.fill(placeholderColor)
.frame(width: diameter, height: diameter)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.accessibilityHidden(true)
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
@@ -98,7 +102,9 @@ struct PlaceholderCapsule: View {
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.accessibilityHidden(true)
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
@@ -145,7 +151,10 @@ struct PlaceholderCard: View {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Loading content")
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
@@ -185,6 +194,7 @@ struct PlaceholderListRow: View {
}
.padding(Theme.Spacing.md)
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}

View File

@@ -25,10 +25,11 @@ struct LoadingSheet: View {
// Dimmed background
Color.black.opacity(Self.backgroundOpacity)
.ignoresSafeArea()
.accessibilityHidden(true)
// Centered card
VStack(spacing: Theme.Spacing.lg) {
LoadingSpinner(size: .large)
LoadingSpinner(size: .large, label: label)
VStack(spacing: Theme.Spacing.xs) {
Text(label)

View File

@@ -56,6 +56,7 @@ struct LoadingSpinner: View {
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
.accessibilityLabel(label ?? "Loading")
}
private var spinnerView: some View {
@@ -63,15 +64,18 @@ struct LoadingSpinner: View {
// Background track - subtle gray like Apple's native spinner
Circle()
.stroke(Color.secondary.opacity(0.2), lineWidth: size.strokeWidth)
.accessibilityHidden(true)
// Rotating arc (270 degrees) - gray like Apple's ProgressView
Circle()
.trim(from: 0, to: 0.75)
.stroke(Color.secondary, style: StrokeStyle(lineWidth: size.strokeWidth, lineCap: .round))
.rotationEffect(.degrees(rotation))
.accessibilityHidden(true)
}
.frame(width: size.diameter, height: size.diameter)
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotation = 360
}