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

@@ -169,6 +169,7 @@ struct TripDetailView: View {
}
.foregroundStyle(Theme.warmOrange)
}
.accessibilityLabel("Export trip as PDF")
}
}
@@ -304,7 +305,10 @@ struct TripDetailView: View {
.stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round))
.frame(width: 80, height: 80)
.rotationEffect(.degrees(-90))
.animation(.easeInOut(duration: 0.3), value: exportProgress?.percentComplete)
.animation(
Theme.Animation.prefersReducedMotion ? nil : .easeInOut(duration: 0.3),
value: exportProgress?.percentComplete
)
Image(systemName: "doc.fill")
.font(.title2)
@@ -363,6 +367,7 @@ struct TripDetailView: View {
.shadow(color: .black.opacity(0.2), radius: 4, y: 2)
}
.accessibilityIdentifier("tripDetail.favoriteButton")
.accessibilityLabel(isSaved ? "Remove from favorites" : "Save to favorites")
.padding(.top, 12)
.padding(.trailing, 12)
}
@@ -556,7 +561,7 @@ struct TripDetailView: View {
set: { targeted in
// Only show as target if it's a valid drop location
let shouldShowTarget = targeted && (draggedTravelId == nil || isValidTravelTarget)
withAnimation(.easeInOut(duration: 0.2)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
if shouldShowTarget {
dropTargetId = sectionId
} else if dropTargetId == sectionId {
@@ -585,13 +590,13 @@ struct TripDetailView: View {
.onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
get: { dropTargetId == sectionId },
set: { targeted in
// Only accept custom items on travel, not other travel
let shouldShow = targeted && draggedItem != nil
withAnimation(.easeInOut(duration: 0.2)) {
if shouldShow {
dropTargetId = sectionId
} else if dropTargetId == sectionId {
dropTargetId = nil
// Only accept custom items on travel, not other travel
let shouldShow = targeted && draggedItem != nil
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
if shouldShow {
dropTargetId = sectionId
} else if dropTargetId == sectionId {
dropTargetId = nil
}
}
}
@@ -628,7 +633,7 @@ struct TripDetailView: View {
set: { targeted in
// Only accept custom items, not travel
let shouldShow = targeted && draggedItem != nil && draggedItem?.id != item.id
withAnimation(.easeInOut(duration: 0.2)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
if shouldShow {
dropTargetId = sectionId
} else if dropTargetId == sectionId {
@@ -654,7 +659,7 @@ struct TripDetailView: View {
set: { targeted in
// Only accept custom items, not travel
let shouldShow = targeted && draggedItem != nil
withAnimation(.easeInOut(duration: 0.2)) {
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
if shouldShow {
dropTargetId = sectionId
} else if dropTargetId == sectionId {
@@ -1323,7 +1328,7 @@ struct TripDetailView: View {
do {
try modelContext.save()
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = true
}
AnalyticsManager.shared.track(.tripSaved(
@@ -1348,7 +1353,7 @@ struct TripDetailView: View {
modelContext.delete(savedTrip)
}
try modelContext.save()
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = false
}
AnalyticsManager.shared.track(.tripDeleted(tripId: tripId.uuidString))
@@ -1818,7 +1823,7 @@ struct TravelSection: View {
.background(Theme.routeGold.opacity(0.2))
Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.8)) {
showEVChargers.toggle()
}
} label: {
@@ -1836,6 +1841,7 @@ struct TravelSection: View {
Image(systemName: showEVChargers ? "chevron.up" : "chevron.down")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
}
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.sm)