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:
@@ -25,6 +25,7 @@ struct DateRangePicker: View {
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]
|
||||
private let daysOfWeekFull = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||
|
||||
private var monthYearString: String {
|
||||
let formatter = DateFormatter()
|
||||
@@ -96,13 +97,13 @@ struct DateRangePicker: View {
|
||||
if isDemoMode && !hasAppliedDemoSelection {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
// Navigate to demo month
|
||||
displayedMonth = DemoConfig.demoStartDate
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay + 0.5) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
startDate = DemoConfig.demoStartDate
|
||||
endDate = DemoConfig.demoEndDate
|
||||
selectionState = .complete
|
||||
@@ -119,7 +120,7 @@ struct DateRangePicker: View {
|
||||
let newYear = calendar.component(.year, from: newValue)
|
||||
|
||||
if oldMonth != newMonth || oldYear != newYear {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
displayedMonth = calendar.startOfDay(for: newValue)
|
||||
}
|
||||
}
|
||||
@@ -148,6 +149,7 @@ struct DateRangePicker: View {
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// End date
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
@@ -168,17 +170,18 @@ struct DateRangePicker: View {
|
||||
private var monthNavigation: some View {
|
||||
HStack {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
displayedMonth = calendar.date(byAdding: .month, value: -1, to: displayedMonth) ?? displayedMonth
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.frame(width: 36, height: 36)
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
.background(Theme.warmOrange.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.accessibilityLabel("Previous month")
|
||||
.accessibilityIdentifier("wizard.dates.previousMonth")
|
||||
|
||||
Spacer()
|
||||
@@ -191,28 +194,30 @@ struct DateRangePicker: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
displayedMonth = calendar.date(byAdding: .month, value: 1, to: displayedMonth) ?? displayedMonth
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.frame(width: 36, height: 36)
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
.background(Theme.warmOrange.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.accessibilityLabel("Next month")
|
||||
.accessibilityIdentifier("wizard.dates.nextMonth")
|
||||
}
|
||||
}
|
||||
|
||||
private var daysOfWeekHeader: some View {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Array(daysOfWeek.enumerated()), id: \.offset) { _, day in
|
||||
ForEach(Array(daysOfWeek.enumerated()), id: \.offset) { index, day in
|
||||
Text(day)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityLabel(daysOfWeekFull[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,6 +248,7 @@ struct DateRangePicker: View {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: "calendar.badge.clock")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("\(tripDuration) day\(tripDuration == 1 ? "" : "s")")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
@@ -348,7 +354,7 @@ struct DayCell: View {
|
||||
}
|
||||
|
||||
Text(dayNumber)
|
||||
.font(.system(size: 14, weight: (isStart || isEnd) ? .bold : .medium))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(
|
||||
isPast ? Theme.textMuted(colorScheme).opacity(0.5) :
|
||||
(isStart || isEnd) ? .white :
|
||||
|
||||
@@ -123,26 +123,53 @@ struct GamePickerStep: View {
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
Button {
|
||||
if isEnabled { onTap() }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(isEnabled ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
||||
if let value = value {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Button {
|
||||
if isEnabled { onTap() }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(isEnabled ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
if let value = value {
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.lineLimit(1)
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: onClear) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!isEnabled)
|
||||
|
||||
Button(action: onClear) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear \(label.lowercased()) selection")
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.warmOrange, lineWidth: 2)
|
||||
)
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
} else {
|
||||
Button {
|
||||
if isEnabled { onTap() }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(isEnabled ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(placeholder)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
@@ -152,19 +179,20 @@ struct GamePickerStep: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.textMuted(colorScheme).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(value != nil ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: value != nil ? 2 : 1)
|
||||
)
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!isEnabled)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +205,7 @@ struct GamePickerStep: View {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("\(selectedGameIds.count) game\(selectedGameIds.count == 1 ? "" : "s") selected")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
@@ -201,6 +230,8 @@ struct GamePickerStep: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Remove \(game.matchupDescription)")
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
@@ -236,6 +267,7 @@ struct GamePickerStep: View {
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("Trip Date Range")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
@@ -353,15 +385,18 @@ private struct SportsPickerSheet: View {
|
||||
if selectedSports.contains(sport) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedSports.contains(sport) ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
@@ -451,15 +486,18 @@ private struct TeamsPickerSheet: View {
|
||||
if selectedTeamIds.contains(team.id) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedTeamIds.contains(team.id) ? .isSelected : [])
|
||||
}
|
||||
} header: {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
@@ -555,15 +593,19 @@ private struct GamesPickerSheet: View {
|
||||
if selectedGameIds.contains(game.id) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedGameIds.contains(game.id) ? .isSelected : [])
|
||||
}
|
||||
} header: {
|
||||
Text(date, style: .date)
|
||||
|
||||
@@ -48,6 +48,7 @@ struct LocationSearchSheet: View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
TextField("Search cities, addresses, places...", text: $searchText)
|
||||
.textFieldStyle(.plain)
|
||||
.autocorrectionDisabled()
|
||||
@@ -61,6 +62,8 @@ struct LocationSearchSheet: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear search")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -85,6 +88,7 @@ struct LocationSearchSheet: View {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(.red)
|
||||
.font(.title2)
|
||||
.accessibilityHidden(true)
|
||||
VStack(alignment: .leading) {
|
||||
Text(result.name)
|
||||
.foregroundStyle(.primary)
|
||||
@@ -97,6 +101,7 @@ struct LocationSearchSheet: View {
|
||||
Spacer()
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundStyle(.blue)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -49,6 +49,7 @@ struct LocationsStep: View {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
Text("Round trip (return to start)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
@@ -107,6 +108,7 @@ struct LocationsStep: View {
|
||||
HStack {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(location.name)
|
||||
@@ -128,6 +130,8 @@ struct LocationsStep: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear location")
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
@@ -138,6 +142,7 @@ struct LocationsStep: View {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityLabel("Add location")
|
||||
|
||||
Text(placeholder)
|
||||
.font(.subheadline)
|
||||
@@ -148,6 +153,7 @@ struct LocationsStep: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
|
||||
@@ -25,6 +25,7 @@ struct MustStopsStep: View {
|
||||
HStack {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(location.name)
|
||||
.font(.subheadline)
|
||||
@@ -38,6 +39,8 @@ struct MustStopsStep: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Remove location")
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
@@ -56,6 +59,7 @@ struct MustStopsStep: View {
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.accessibilityLabel("Add must-see location")
|
||||
|
||||
Text("Skip this step if you don't have specific cities in mind")
|
||||
.font(.caption)
|
||||
|
||||
@@ -39,7 +39,7 @@ struct PlanningModeStep: View {
|
||||
.onAppear {
|
||||
if isDemoMode && selection == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
selection = DemoConfig.demoPlanningMode
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ private struct WizardModeCard: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme))
|
||||
.frame(width: 32)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(mode.displayName)
|
||||
@@ -79,6 +80,7 @@ private struct WizardModeCard: View {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -89,7 +91,11 @@ private struct WizardModeCard: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.accessibilityLabel("\(mode.displayName): \(mode.description)")
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
.accessibilityIdentifier("wizard.planningMode.\(mode.rawValue)")
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ private struct OptionButton: View {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
@@ -90,6 +91,8 @@ private struct OptionButton: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ struct ReviewStep: View {
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.accessibilityHidden(true)
|
||||
Text("Complete all required fields to continue")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
@@ -85,6 +86,7 @@ struct ReviewStep: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
}
|
||||
.accessibilityIdentifier("wizard.planTripButton")
|
||||
.accessibilityHint("Creates trip itinerary based on your selections")
|
||||
.disabled(!canPlanTrip || isPlanning)
|
||||
}
|
||||
.padding(Theme.Spacing.lg)
|
||||
@@ -155,6 +157,7 @@ private struct ReviewRow: View {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ private struct RoutePreferenceCard: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(isSelected ? Theme.warmOrange : Theme.textSecondary(colorScheme))
|
||||
.frame(width: 32)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(preference.displayName)
|
||||
@@ -79,6 +80,7 @@ private struct RoutePreferenceCard: View {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -89,7 +91,11 @@ private struct RoutePreferenceCard: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.accessibilityLabel(preference.displayName)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ struct SportsStep: View {
|
||||
if isDemoMode && !hasAppliedDemoSelection && selectedSports.isEmpty {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
_ = selectedSports.insert(DemoConfig.demoSport)
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ private struct SportCard: View {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title2)
|
||||
.foregroundStyle(cardColor)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(sport.rawValue)
|
||||
.font(.caption)
|
||||
@@ -111,7 +112,15 @@ private struct SportCard: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(borderColor, lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.accessibilityLabel(sport.rawValue)
|
||||
.accessibilityValue(
|
||||
isAvailable
|
||||
? (isSelected ? "Selected" : "Not selected")
|
||||
: "Unavailable"
|
||||
)
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
.accessibilityIdentifier("wizard.sports.\(sport.rawValue.lowercased())")
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isAvailable ? 1.0 : 0.5)
|
||||
|
||||
@@ -28,41 +28,60 @@ struct TeamPickerStep: View {
|
||||
subtitle: "See their home and away games"
|
||||
)
|
||||
|
||||
// Selection button
|
||||
Button {
|
||||
showTeamPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
if let team = selectedTeam {
|
||||
// Show selected team
|
||||
Circle()
|
||||
.fill(team.primaryColor.map { Color(hex: $0) } ?? team.sport.themeColor)
|
||||
.frame(width: 24, height: 24)
|
||||
if let team = selectedTeam {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Button {
|
||||
showTeamPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(team.primaryColor.map { Color(hex: $0) } ?? team.sport.themeColor)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(team.fullName)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(team.fullName)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(team.sport.rawValue)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
Text(team.sport.rawValue)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTeamId = nil
|
||||
selectedSport = nil
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
} else {
|
||||
// Empty state
|
||||
Button {
|
||||
selectedTeamId = nil
|
||||
selectedSport = nil
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear team selection")
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.warmOrange, lineWidth: 2)
|
||||
)
|
||||
} else {
|
||||
// Selection button
|
||||
Button {
|
||||
showTeamPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.2.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("Select a team")
|
||||
.font(.subheadline)
|
||||
@@ -73,17 +92,18 @@ struct TeamPickerStep: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(Theme.textMuted(colorScheme).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(selectedTeam != nil ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: selectedTeam != nil ? 2 : 1)
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(Theme.Spacing.lg)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
@@ -214,11 +234,14 @@ private struct TeamListView: View {
|
||||
if selectedTeamId == team.id {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedTeamId == team.id ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
||||
Reference in New Issue
Block a user