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

@@ -21,6 +21,7 @@ struct PollCreationView: View {
Section {
TextField("Poll Title", text: $viewModel.title)
.textInputAutocapitalization(.words)
.accessibilityHint("Enter a descriptive name for your poll")
} header: {
Text("Title")
} footer: {
@@ -115,10 +116,13 @@ private struct TripSelectionRow: View {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.font(.title2)
.foregroundStyle(isSelected ? Theme.warmOrange : .secondary)
.accessibilityHidden(true)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityValue(isSelected ? "Selected" : "Not selected")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
private var tripSummary: String {

View File

@@ -77,6 +77,8 @@ struct PollDetailView: View {
}
} label: {
Image(systemName: "ellipsis.circle")
.minimumHitTarget()
.accessibilityLabel("More options")
}
}
}
@@ -172,7 +174,7 @@ struct PollDetailView: View {
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 56, height: 56)
Image(systemName: "link.circle.fill")
.font(.system(size: 28))
.font(.title2)
.foregroundStyle(Theme.warmOrange)
}
@@ -182,9 +184,11 @@ struct PollDetailView: View {
.foregroundStyle(Theme.textSecondary(colorScheme))
Text(poll.shareCode)
.font(.system(size: 36, weight: .bold, design: .monospaced))
.font(.system(.largeTitle, design: .monospaced).weight(.bold))
.foregroundStyle(Theme.warmOrange)
.tracking(4)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
// Copy button
@@ -221,6 +225,7 @@ struct PollDetailView: View {
Image(systemName: viewModel.hasVoted ? "checkmark.circle.fill" : "hand.raised.fill")
.font(.title3)
.foregroundStyle(viewModel.hasVoted ? Theme.mlsGreen : Theme.warmOrange)
.accessibilityLabel(viewModel.hasVoted ? "You have voted" : "You have not voted")
}
VStack(alignment: .leading, spacing: 2) {
@@ -231,6 +236,7 @@ struct PollDetailView: View {
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "person.2.fill")
.font(.caption2)
.accessibilityHidden(true)
Text("\(viewModel.votes.count) vote\(viewModel.votes.count == 1 ? "" : "s")")
.font(.subheadline)
}
@@ -263,6 +269,7 @@ struct PollDetailView: View {
HStack(spacing: Theme.Spacing.sm) {
Image(systemName: "chart.bar.fill")
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
Text("Results")
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
@@ -294,6 +301,7 @@ struct PollDetailView: View {
HStack(spacing: Theme.Spacing.sm) {
Image(systemName: "map.fill")
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
Text("Trip Options")
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
@@ -347,6 +355,27 @@ private struct ResultRow: View {
}
}
private var rankAccessibilityLabel: String {
switch rank {
case 1: return "First place"
case 2: return "Second place"
case 3: return "Third place"
default: return "\(rank)\(rankSuffix) place"
}
}
private var rankSuffix: String {
let ones = rank % 10
let tens = rank % 100
if tens >= 11 && tens <= 13 { return "th" }
switch ones {
case 1: return "st"
case 2: return "nd"
case 3: return "rd"
default: return "th"
}
}
private var rankColor: Color {
switch rank {
case 1: return Theme.warmOrange
@@ -366,6 +395,7 @@ private struct ResultRow: View {
Image(systemName: rankIcon)
.font(.subheadline)
.foregroundStyle(rankColor)
.accessibilityLabel(rankAccessibilityLabel)
}
VStack(alignment: .leading, spacing: 4) {
@@ -447,6 +477,7 @@ private struct TripPreviewCard: View {
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.tertiary)
.accessibilityHidden(true)
}
.padding()
.background(Theme.cardBackground(colorScheme))

View File

@@ -27,8 +27,13 @@ struct PollVotingView: View {
ForEach(Array(viewModel.rankings.enumerated()), id: \.element) { index, tripIndex in
RankingRow(
rank: index + 1,
trip: poll.tripSnapshots[tripIndex]
trip: poll.tripSnapshots[tripIndex],
canMoveUp: index > 0,
canMoveDown: index < viewModel.rankings.count - 1,
onMoveUp: { viewModel.moveTripUp(at: index) },
onMoveDown: { viewModel.moveTripDown(at: index) }
)
.accessibilityHint("Drag to change ranking position, or use move up and move down buttons")
}
.onMove { source, destination in
viewModel.moveTrip(from: source, to: destination)
@@ -79,11 +84,12 @@ struct PollVotingView: View {
Image(systemName: "arrow.up.arrow.down")
.font(.title2)
.foregroundStyle(Theme.warmOrange)
.accessibilityLabel("Drag to reorder")
Text("Drag to rank your preferences")
.font(.headline)
Text("Your top choice should be at the top")
Text("Your top choice should be at the top. You can drag, or use the move buttons.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
@@ -126,6 +132,10 @@ struct PollVotingView: View {
private struct RankingRow: View {
let rank: Int
let trip: Trip
let canMoveUp: Bool
let canMoveDown: Bool
let onMoveUp: () -> Void
let onMoveDown: () -> Void
var body: some View {
HStack(spacing: 12) {
@@ -147,6 +157,25 @@ private struct RankingRow: View {
}
Spacer()
VStack(spacing: 6) {
Button(action: onMoveUp) {
Image(systemName: "chevron.up")
.font(.caption.weight(.semibold))
}
.minimumHitTarget()
.disabled(!canMoveUp)
.accessibilityLabel("Move \(trip.displayName) up")
Button(action: onMoveDown) {
Image(systemName: "chevron.down")
.font(.caption.weight(.semibold))
}
.minimumHitTarget()
.disabled(!canMoveDown)
.accessibilityLabel("Move \(trip.displayName) down")
}
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}

View File

@@ -33,6 +33,7 @@ struct PollsListView: View {
showJoinPoll = true
} label: {
Image(systemName: "link.badge.plus")
.accessibilityLabel("Join a poll")
}
}
}