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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user