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:
@@ -96,7 +96,7 @@ struct SettingsView: View {
|
||||
Section {
|
||||
ForEach(AppearanceMode.allCases) { mode in
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
AppearanceManager.shared.currentMode = mode
|
||||
AnalyticsManager.shared.track(.appearanceChanged(mode: mode.displayName))
|
||||
}
|
||||
@@ -109,8 +109,9 @@ struct SettingsView: View {
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
Image(systemName: mode.iconName)
|
||||
.font(.system(size: 16))
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@@ -128,11 +129,13 @@ struct SettingsView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.font(.title3)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(AppearanceManager.shared.currentMode == mode ? .isSelected : [])
|
||||
}
|
||||
} header: {
|
||||
Text("Appearance")
|
||||
@@ -148,7 +151,7 @@ struct SettingsView: View {
|
||||
Section {
|
||||
ForEach(AppTheme.allCases) { theme in
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
viewModel.selectedTheme = theme
|
||||
}
|
||||
} label: {
|
||||
@@ -181,11 +184,13 @@ struct SettingsView: View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.font(.title3)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(viewModel.selectedTheme == theme ? .isSelected : [])
|
||||
}
|
||||
} header: {
|
||||
Text("Theme")
|
||||
@@ -218,6 +223,7 @@ struct SettingsView: View {
|
||||
} icon: {
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -306,6 +312,7 @@ struct SettingsView: View {
|
||||
} icon: {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -328,15 +335,30 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
Link(destination: URL(string: "https://sportstime.88oakapps.com/privacy.html")!) {
|
||||
Label("Privacy Policy", systemImage: "hand.raised")
|
||||
Label {
|
||||
Text("Privacy Policy")
|
||||
} icon: {
|
||||
Image(systemName: "hand.raised")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
Link(destination: URL(string: "https://sportstime.88oakapps.com/eula.html")!) {
|
||||
Label("EULA", systemImage: "doc.text")
|
||||
Label {
|
||||
Text("EULA")
|
||||
} icon: {
|
||||
Image(systemName: "doc.text")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
Link(destination: URL(string: "mailto:support@88oakapps.com")!) {
|
||||
Label("Contact Support", systemImage: "envelope")
|
||||
Label {
|
||||
Text("Contact Support")
|
||||
} icon: {
|
||||
Image(systemName: "envelope")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("About")
|
||||
@@ -365,6 +387,7 @@ struct SettingsView: View {
|
||||
} icon: {
|
||||
Image(systemName: "photo.badge.plus")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -380,7 +403,12 @@ struct SettingsView: View {
|
||||
Button(role: .destructive) {
|
||||
showResetConfirmation = true
|
||||
} label: {
|
||||
Label("Reset to Defaults", systemImage: "arrow.counterclockwise")
|
||||
Label {
|
||||
Text("Reset to Defaults")
|
||||
} icon: {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Theme.cardBackground(colorScheme))
|
||||
@@ -682,6 +710,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -714,6 +743,7 @@ struct SettingsView: View {
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -741,6 +771,7 @@ struct SettingsView: View {
|
||||
guard !isSyncActionInProgress else { return }
|
||||
|
||||
isSyncActionInProgress = true
|
||||
AccessibilityAnnouncer.announce("Manual sync started.")
|
||||
|
||||
Task {
|
||||
defer { isSyncActionInProgress = false }
|
||||
@@ -748,8 +779,10 @@ struct SettingsView: View {
|
||||
do {
|
||||
let result = try await BackgroundSyncManager.shared.triggerManualSync()
|
||||
syncActionMessage = "Sync complete. Updated \(result.totalUpdated) records."
|
||||
AccessibilityAnnouncer.announce(syncActionMessage ?? "")
|
||||
} catch {
|
||||
syncActionMessage = "Sync failed: \(error.localizedDescription)"
|
||||
AccessibilityAnnouncer.announce(syncActionMessage ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -779,13 +812,13 @@ struct SyncStatusRow: View {
|
||||
// Status indicator
|
||||
Image(systemName: statusIcon)
|
||||
.foregroundStyle(statusColor)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.frame(width: 20)
|
||||
|
||||
// Entity icon and name
|
||||
Image(systemName: status.entityType.iconName)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.frame(width: 20)
|
||||
|
||||
Text(status.entityType.rawValue)
|
||||
@@ -812,6 +845,7 @@ struct SyncStatusRow: View {
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("View sync details")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user