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:
@@ -29,7 +29,7 @@ struct CategoryPicker: View {
|
||||
isSelected: selectedCategory == category,
|
||||
colorScheme: colorScheme
|
||||
) {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedCategory = category
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ private struct CategoryPillButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
.animation(Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ struct PlaceSearchSheet: View {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
TextField(searchPlaceholder, text: $searchQuery)
|
||||
.textFieldStyle(.plain)
|
||||
@@ -82,6 +83,7 @@ struct PlaceSearchSheet: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear search")
|
||||
}
|
||||
}
|
||||
@@ -148,6 +150,7 @@ struct PlaceSearchSheet: View {
|
||||
Image(systemName: "mappin.slash")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("No places found")
|
||||
.font(.headline)
|
||||
@@ -180,6 +183,7 @@ struct PlaceSearchSheet: View {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.orange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("Search unavailable")
|
||||
.font(.headline)
|
||||
|
||||
@@ -87,7 +87,7 @@ struct QuickAddItemSheet: View {
|
||||
}
|
||||
.sheet(isPresented: $showLocationSearch) {
|
||||
PlaceSearchSheet(category: selectedCategory) { place in
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedPlace = place
|
||||
}
|
||||
// Use place name as title if empty
|
||||
@@ -209,6 +209,7 @@ struct QuickAddItemSheet: View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(inputBackground)
|
||||
@@ -255,7 +256,7 @@ struct QuickAddItemSheet: View {
|
||||
|
||||
// Remove button
|
||||
Button {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedPlace = nil
|
||||
}
|
||||
} label: {
|
||||
@@ -263,7 +264,9 @@ struct QuickAddItemSheet: View {
|
||||
.font(.title3)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.accessibilityLabel("Remove location")
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Remove \(place.name ?? "location")")
|
||||
.accessibilityHint("Double-tap to remove this location from the item")
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.warmOrange.opacity(0.08))
|
||||
@@ -272,9 +275,6 @@ struct QuickAddItemSheet: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.strokeBorder(Theme.warmOrange.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("\(place.name ?? "Location"), \(formatAddress(for: place) ?? "")")
|
||||
.accessibilityHint("Double-tap the remove button to clear this location")
|
||||
}
|
||||
|
||||
// MARK: - Section Header
|
||||
@@ -284,6 +284,7 @@ struct QuickAddItemSheet: View {
|
||||
Image(systemName: icon)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
@@ -440,7 +441,10 @@ private struct PressableStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
.animation(
|
||||
Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3, dampingFraction: 0.7),
|
||||
value: configuration.isPressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ struct AddItemSheet: View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
TextField("Search for a place...", text: $searchQuery)
|
||||
.textFieldStyle(.plain)
|
||||
.autocorrectionDisabled()
|
||||
@@ -156,6 +157,8 @@ struct AddItemSheet: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear search")
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
@@ -373,6 +376,7 @@ private struct PlaceResultRow: View {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
@@ -380,6 +384,8 @@ private struct PlaceResultRow: View {
|
||||
.background(isSelected ? Color.green.opacity(0.1) : Color.clear)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
|
||||
private var formattedAddress: String? {
|
||||
@@ -422,6 +428,8 @@ private struct CategoryButton: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,13 @@ struct CustomItemRow: View {
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.font(.title3)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityLabel("Drag to reorder")
|
||||
|
||||
// Icon and Title
|
||||
if let info = customInfo {
|
||||
Text(info.icon)
|
||||
.font(.title3)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(info.title)
|
||||
.font(.body)
|
||||
@@ -47,6 +49,7 @@ struct CustomItemRow: View {
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.sm)
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ struct DayHeaderRow: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Add item to this day")
|
||||
}
|
||||
|
||||
if isEmpty {
|
||||
|
||||
@@ -24,6 +24,7 @@ struct GameItemRow: View {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: game.game.sport.iconName)
|
||||
.font(.caption2)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.game.sport.rawValue)
|
||||
.font(.caption2)
|
||||
}
|
||||
@@ -44,6 +45,7 @@ struct GameItemRow: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "building.2")
|
||||
.font(.caption2)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.stadium.name)
|
||||
.font(.subheadline)
|
||||
}
|
||||
@@ -57,6 +59,7 @@ struct GameItemRow: View {
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
|
||||
@@ -24,6 +24,7 @@ struct TravelItemRow: View {
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.font(.title3)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityLabel("Drag to reorder")
|
||||
|
||||
// Car icon
|
||||
ZStack {
|
||||
@@ -35,12 +36,14 @@ struct TravelItemRow: View {
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.routeGold)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if let info = travelInfo {
|
||||
Text("\(info.fromCity) \u{2192} \(info.toCity)")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.accessibilityLabel("\(info.fromCity) to \(info.toCity)")
|
||||
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
if !info.formattedDistance.isEmpty {
|
||||
@@ -49,6 +52,7 @@ struct TravelItemRow: View {
|
||||
}
|
||||
if !info.formattedDistance.isEmpty && !info.formattedDuration.isEmpty {
|
||||
Text("\u{2022}")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
if !info.formattedDuration.isEmpty {
|
||||
Text(info.formattedDuration)
|
||||
@@ -61,6 +65,7 @@ struct TravelItemRow: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
|
||||
@@ -1145,6 +1145,7 @@ struct GameRowCompact: View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "building.2")
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(richGame.stadium.name)
|
||||
.font(.subheadline)
|
||||
}
|
||||
@@ -1175,6 +1176,7 @@ struct GameRowCompact: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Open \(richGame.stadium.name) in Maps")
|
||||
.accessibilityHint("Opens this stadium location in Apple Maps")
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackgroundElevated(colorScheme))
|
||||
@@ -1255,6 +1257,7 @@ struct TravelRowView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Get directions from \(segment.fromLocation.name) to \(segment.toLocation.name)")
|
||||
.accessibilityHint("Opens this stadium location in Apple Maps")
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
@@ -1304,6 +1307,7 @@ struct CustomItemRowView: View {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1336,12 +1340,14 @@ struct CustomItemRowView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Open \(info.title) in Maps")
|
||||
.accessibilityHint("Opens this stadium location in Apple Maps")
|
||||
}
|
||||
|
||||
// Chevron indicates this is tappable
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundStyle(.tertiary)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
.padding(.vertical, Theme.Spacing.sm)
|
||||
|
||||
@@ -66,12 +66,36 @@ struct RegionMapSelector: View {
|
||||
HStack(spacing: 0) {
|
||||
Button { onToggle(.west) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.west")
|
||||
.accessibilityLabel("West region")
|
||||
.accessibilityValue(selectedRegions.contains(.west) ? "Selected" : "Not selected")
|
||||
.accessibilityHint(
|
||||
selectedRegions.contains(.west)
|
||||
? "Double-tap to deselect this region"
|
||||
: "Double-tap to select this region"
|
||||
)
|
||||
.accessibilityAddTraits(selectedRegions.contains(.west) ? .isSelected : [])
|
||||
.frame(maxWidth: .infinity)
|
||||
Button { onToggle(.central) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.central")
|
||||
.accessibilityLabel("Central region")
|
||||
.accessibilityValue(selectedRegions.contains(.central) ? "Selected" : "Not selected")
|
||||
.accessibilityHint(
|
||||
selectedRegions.contains(.central)
|
||||
? "Double-tap to deselect this region"
|
||||
: "Double-tap to select this region"
|
||||
)
|
||||
.accessibilityAddTraits(selectedRegions.contains(.central) ? .isSelected : [])
|
||||
.frame(maxWidth: .infinity)
|
||||
Button { onToggle(.east) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.east")
|
||||
.accessibilityLabel("East region")
|
||||
.accessibilityValue(selectedRegions.contains(.east) ? "Selected" : "Not selected")
|
||||
.accessibilityHint(
|
||||
selectedRegions.contains(.east)
|
||||
? "Double-tap to deselect this region"
|
||||
: "Double-tap to select this region"
|
||||
)
|
||||
.accessibilityAddTraits(selectedRegions.contains(.east) ? .isSelected : [])
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -166,6 +190,7 @@ struct RegionMapSelector: View {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.5), lineWidth: isSelected ? 2 : 0)
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(region.shortName)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct TeamPickerView: View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
TextField("Search teams...", text: $searchText)
|
||||
.textFieldStyle(.plain)
|
||||
@@ -55,6 +56,8 @@ struct TeamPickerView: View {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear search")
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.sm)
|
||||
@@ -76,7 +79,7 @@ struct TeamPickerView: View {
|
||||
Spacer()
|
||||
|
||||
Button("Clear all") {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedTeamIds.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -102,7 +105,7 @@ struct TeamPickerView: View {
|
||||
}
|
||||
|
||||
private func toggleTeam(_ team: Team) {
|
||||
withAnimation(Theme.Animation.spring) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
if selectedTeamIds.contains(team.id) {
|
||||
selectedTeamIds.remove(team.id)
|
||||
} else {
|
||||
@@ -139,6 +142,7 @@ private struct TeamCard: View {
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +171,8 @@ private struct TeamCard: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
|
||||
private var teamColor: Color {
|
||||
|
||||
@@ -74,31 +74,34 @@ struct TimelineItemView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var itemIcon: some View {
|
||||
switch item {
|
||||
case .stop(let stop):
|
||||
if stop.hasGames {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
Group {
|
||||
switch item {
|
||||
case .stop(let stop):
|
||||
if stop.hasGames {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(Circle().fill(.blue))
|
||||
} else {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.font(.title2)
|
||||
}
|
||||
|
||||
case .travel(let segment):
|
||||
Image(systemName: segment.travelMode == .drive ? "car.fill" : "airplane")
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(Circle().fill(.blue))
|
||||
} else {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.font(.title2)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Circle().fill(.green))
|
||||
|
||||
case .rest:
|
||||
Image(systemName: "bed.double.fill")
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Circle().fill(.purple))
|
||||
}
|
||||
|
||||
case .travel(let segment):
|
||||
Image(systemName: segment.travelMode == .drive ? "car.fill" : "airplane")
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Circle().fill(.green))
|
||||
|
||||
case .rest:
|
||||
Image(systemName: "bed.double.fill")
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Circle().fill(.purple))
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
// MARK: - Item Content
|
||||
@@ -178,30 +181,34 @@ struct TravelItemContent: View {
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
|
||||
Text("•")
|
||||
Text("\u{2022}")
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(segment.formattedDistance)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("•")
|
||||
Text("\u{2022}")
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(segment.formattedDuration)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text("\(segment.fromLocation.name) → \(segment.toLocation.name)")
|
||||
Text("\(segment.fromLocation.name) \u{2192} \(segment.toLocation.name)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel("\(segment.fromLocation.name) to \(segment.toLocation.name)")
|
||||
|
||||
// EV Charging stops if applicable
|
||||
if !segment.evChargingStops.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundStyle(.green)
|
||||
.accessibilityHidden(true)
|
||||
Text("\(segment.evChargingStops.count) charging stop(s)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -263,6 +270,7 @@ struct TimelineGameRow: View {
|
||||
Image(systemName: richGame.game.sport.iconName)
|
||||
.foregroundStyle(richGame.game.sport.color)
|
||||
.frame(width: 20)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
// Matchup
|
||||
@@ -273,7 +281,8 @@ struct TimelineGameRow: View {
|
||||
// Time and venue (stadium local time)
|
||||
HStack(spacing: 4) {
|
||||
Text(richGame.localGameTimeShort)
|
||||
Text("•")
|
||||
Text("\u{2022}")
|
||||
.accessibilityHidden(true)
|
||||
Text(richGame.stadium.name)
|
||||
}
|
||||
.font(.caption)
|
||||
@@ -282,6 +291,7 @@ struct TimelineGameRow: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -308,7 +308,7 @@ struct TripOptionsView: View {
|
||||
hasAppliedDemoSelection = true
|
||||
// Auto-select "Most Games" sort after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
sortOption = DemoConfig.demoSortOption
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ struct TripOptionsView: View {
|
||||
Menu {
|
||||
ForEach(TripSortOption.allCases) { option in
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
sortOption = option
|
||||
}
|
||||
} label: {
|
||||
@@ -345,6 +345,7 @@ struct TripOptionsView: View {
|
||||
.font(.subheadline)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.padding(.horizontal, 16)
|
||||
@@ -397,6 +398,7 @@ struct TripOptionsView: View {
|
||||
.contentTransition(.identity)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption2)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundStyle(paceFilter == .all ? Theme.textPrimary(colorScheme) : Theme.warmOrange)
|
||||
.padding(.horizontal, 12)
|
||||
@@ -420,12 +422,12 @@ struct TripOptionsView: View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(CitiesFilter.allCases) { filter in
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
|
||||
citiesFilter = filter
|
||||
}
|
||||
} label: {
|
||||
Text(filter.displayName)
|
||||
.font(.system(size: 13, weight: citiesFilter == filter ? .semibold : .medium))
|
||||
.font(.caption.weight(citiesFilter == filter ? .semibold : .medium))
|
||||
.foregroundStyle(citiesFilter == filter ? .white : Theme.textPrimary(colorScheme))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
@@ -446,15 +448,16 @@ struct TripOptionsView: View {
|
||||
private var emptyFilterState: some View {
|
||||
VStack(spacing: Theme.Spacing.md) {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.font(.system(size: 48))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("No routes match your filters")
|
||||
.font(.body)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
Button {
|
||||
withAnimation {
|
||||
Theme.Animation.withMotion {
|
||||
citiesFilter = .noLimit
|
||||
paceFilter = .all
|
||||
}
|
||||
@@ -524,6 +527,7 @@ struct TripOptionCard: View {
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(uniqueCities.last ?? "")
|
||||
.font(.subheadline)
|
||||
@@ -560,7 +564,7 @@ struct TripOptionCard: View {
|
||||
// AI-generated description (after stats)
|
||||
if let description = aiDescription {
|
||||
Text(description)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.transition(.opacity)
|
||||
@@ -578,8 +582,9 @@ struct TripOptionCard: View {
|
||||
|
||||
// Right: Chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
@@ -607,7 +612,7 @@ struct TripOptionCard: View {
|
||||
let input = RouteDescriptionInput(from: option, games: games)
|
||||
|
||||
if let description = await RouteDescriptionGenerator.shared.generateDescription(for: input) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.3)) {
|
||||
aiDescription = description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -33,28 +33,46 @@ struct TeamFirstWizardStep: View {
|
||||
subtitle: "Select 2 or more teams to find optimal trip windows"
|
||||
)
|
||||
|
||||
// Selection button
|
||||
Button {
|
||||
showTeamPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
if !selectedTeams.isEmpty {
|
||||
// Show selected teams
|
||||
teamPreview
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTeamIds.removeAll()
|
||||
selectedSport = nil
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
if !selectedTeams.isEmpty {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Button {
|
||||
showTeamPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
teamPreview
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
// Empty state
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button {
|
||||
selectedTeamIds.removeAll()
|
||||
selectedSport = nil
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.minimumHitTarget()
|
||||
.accessibilityLabel("Clear all teams")
|
||||
}
|
||||
.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 teams")
|
||||
.font(.subheadline)
|
||||
@@ -65,17 +83,18 @@ struct TeamFirstWizardStep: 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(isValid ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isValid ? 2 : 1)
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Validation message
|
||||
if selectedTeamIds.isEmpty {
|
||||
@@ -139,6 +158,7 @@ struct TeamFirstWizardStep: View {
|
||||
.zIndex(Double(4 - index))
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("\(selectedTeamIds.count) teams")
|
||||
.font(.subheadline)
|
||||
@@ -279,14 +299,17 @@ private struct TeamMultiSelectListView: View {
|
||||
if selectedTeamIds.contains(team.id) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(Theme.textMuted(colorScheme).opacity(0.5))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedTeamIds.contains(team.id) ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
@@ -316,7 +339,7 @@ private struct TeamMultiSelectListView: View {
|
||||
}
|
||||
|
||||
private func toggleTeam(_ team: Team) {
|
||||
withAnimation(.easeInOut(duration: 0.15)) {
|
||||
Theme.Animation.withMotion(.easeInOut(duration: 0.15)) {
|
||||
if selectedTeamIds.contains(team.id) {
|
||||
selectedTeamIds.remove(team.id)
|
||||
} else {
|
||||
|
||||
@@ -133,7 +133,7 @@ struct TripWizardView: View {
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.frame(width: geometry.size.width)
|
||||
.animation(.easeInOut(duration: 0.2), value: viewModel.areStepsVisible)
|
||||
.animation(Theme.Animation.prefersReducedMotion ? .none : .easeInOut(duration: 0.2), value: viewModel.areStepsVisible)
|
||||
}
|
||||
}
|
||||
.themedBackground()
|
||||
|
||||
Reference in New Issue
Block a user