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

@@ -74,31 +74,34 @@ struct TimelineItemView: View {
@ViewBuilder
private var itemIcon: some View {
switch item {
case .stop(let stop):
if stop.hasGames {
Image(systemName: "sportscourt.fill")
Group {
switch item {
case .stop(let stop):
if stop.hasGames {
Image(systemName: "sportscourt.fill")
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.background(Circle().fill(.blue))
} else {
Image(systemName: "mappin.circle.fill")
.foregroundStyle(.orange)
.font(.title2)
}
case .travel(let segment):
Image(systemName: segment.travelMode == .drive ? "car.fill" : "airplane")
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.background(Circle().fill(.blue))
} else {
Image(systemName: "mappin.circle.fill")
.foregroundStyle(.orange)
.font(.title2)
.frame(width: 28, height: 28)
.background(Circle().fill(.green))
case .rest:
Image(systemName: "bed.double.fill")
.foregroundStyle(.white)
.frame(width: 28, height: 28)
.background(Circle().fill(.purple))
}
case .travel(let segment):
Image(systemName: segment.travelMode == .drive ? "car.fill" : "airplane")
.foregroundStyle(.white)
.frame(width: 28, height: 28)
.background(Circle().fill(.green))
case .rest:
Image(systemName: "bed.double.fill")
.foregroundStyle(.white)
.frame(width: 28, height: 28)
.background(Circle().fill(.purple))
}
.accessibilityHidden(true)
}
// MARK: - Item Content
@@ -178,30 +181,34 @@ struct TravelItemContent: View {
.font(.subheadline)
.fontWeight(.medium)
Text("")
Text("\u{2022}")
.foregroundStyle(.secondary)
.accessibilityHidden(true)
Text(segment.formattedDistance)
.font(.subheadline)
.foregroundStyle(.secondary)
Text("")
Text("\u{2022}")
.foregroundStyle(.secondary)
.accessibilityHidden(true)
Text(segment.formattedDuration)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Text("\(segment.fromLocation.name) \(segment.toLocation.name)")
Text("\(segment.fromLocation.name) \u{2192} \(segment.toLocation.name)")
.font(.caption)
.foregroundStyle(.secondary)
.accessibilityLabel("\(segment.fromLocation.name) to \(segment.toLocation.name)")
// EV Charging stops if applicable
if !segment.evChargingStops.isEmpty {
HStack(spacing: 4) {
Image(systemName: "bolt.fill")
.foregroundStyle(.green)
.accessibilityHidden(true)
Text("\(segment.evChargingStops.count) charging stop(s)")
.font(.caption)
.foregroundStyle(.secondary)
@@ -263,6 +270,7 @@ struct TimelineGameRow: View {
Image(systemName: richGame.game.sport.iconName)
.foregroundStyle(richGame.game.sport.color)
.frame(width: 20)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
// Matchup
@@ -273,7 +281,8 @@ struct TimelineGameRow: View {
// Time and venue (stadium local time)
HStack(spacing: 4) {
Text(richGame.localGameTimeShort)
Text("")
Text("\u{2022}")
.accessibilityHidden(true)
Text(richGame.stadium.name)
}
.font(.caption)
@@ -282,6 +291,7 @@ struct TimelineGameRow: View {
Spacer()
}
.accessibilityElement(children: .combine)
.padding(.vertical, 4)
}
}