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

@@ -61,7 +61,10 @@ struct PressableButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? scale : 1.0)
.animation(Theme.Animation.spring, value: configuration.isPressed)
.animation(
Theme.Animation.prefersReducedMotion ? nil : Theme.Animation.spring,
value: configuration.isPressed
)
}
}
@@ -71,6 +74,36 @@ extension View {
}
}
// MARK: - Minimum Hit Target Modifier
private struct MinimumHitTargetModifier: ViewModifier {
let size: CGFloat
func body(content: Content) -> some View {
content
.frame(minWidth: size, minHeight: size, alignment: .center)
.contentShape(Rectangle())
}
}
extension View {
/// Ensures interactive elements meet the recommended 44x44pt touch area.
func minimumHitTarget(_ size: CGFloat = 44) -> some View {
modifier(MinimumHitTargetModifier(size: size))
}
}
// MARK: - Accessibility Announcements
enum AccessibilityAnnouncer {
static func announce(_ message: String) {
guard !message.isEmpty else { return }
DispatchQueue.main.async {
UIAccessibility.post(notification: .announcement, argument: message)
}
}
}
// MARK: - Shimmer Effect Modifier
struct ShimmerEffect: ViewModifier {
@@ -79,22 +112,25 @@ struct ShimmerEffect: ViewModifier {
func body(content: Content) -> some View {
content
.overlay {
GeometryReader { geo in
LinearGradient(
colors: [
.clear,
Color.white.opacity(0.3),
.clear
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geo.size.width * 2)
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
if !Theme.Animation.prefersReducedMotion {
GeometryReader { geo in
LinearGradient(
colors: [
.clear,
Color.white.opacity(0.3),
.clear
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geo.size.width * 2)
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
}
.mask(content)
}
.mask(content)
}
.onAppear {
guard !Theme.Animation.prefersReducedMotion else { return }
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 1
}
@@ -120,8 +156,12 @@ struct StaggeredAnimation: ViewModifier {
.opacity(appeared ? 1 : 0)
.offset(y: appeared ? 0 : 20)
.onAppear {
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
if Theme.Animation.prefersReducedMotion {
appeared = true
} else {
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
appeared = true
}
}
}
}
@@ -184,7 +224,7 @@ struct ThemedBackground: ViewModifier {
func body(content: Content) -> some View {
content
.background {
if DesignStyleManager.shared.animationsEnabled {
if DesignStyleManager.shared.animationsEnabled && !Theme.Animation.prefersReducedMotion {
AnimatedSportsBackground()
.ignoresSafeArea()
} else {