diff --git a/docs/plans/2026-01-12-loading-redesign-implementation.md b/docs/plans/2026-01-12-loading-redesign-implementation.md new file mode 100644 index 0000000..646ec28 --- /dev/null +++ b/docs/plans/2026-01-12-loading-redesign-implementation.md @@ -0,0 +1,1193 @@ +# Loading System Redesign Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace all loading indicators with a unified, Apple-style design system using three components: LoadingSpinner, LoadingPlaceholder, and LoadingSheet. + +**Architecture:** Create new Loading/ folder with three focused components. Deprecate 6 existing components in AnimatedComponents.swift. Replace 5 native ProgressView() usages. Refactor LoadingTripsView to use new skeletons. + +**Tech Stack:** SwiftUI, Swift Testing + +--- + +## Task 1: Create LoadingSpinner Component + +**Files:** +- Create: `SportsTime/Core/Theme/Loading/LoadingSpinner.swift` +- Test: `SportsTimeTests/Loading/LoadingSpinnerTests.swift` + +**Step 1: Create the Loading directory** + +```bash +mkdir -p SportsTime/Core/Theme/Loading +mkdir -p SportsTimeTests/Loading +``` + +**Step 2: Write the failing test** + +Create `SportsTimeTests/Loading/LoadingSpinnerTests.swift`: + +```swift +// +// LoadingSpinnerTests.swift +// SportsTimeTests +// + +import Testing +import SwiftUI +@testable import SportsTime + +struct LoadingSpinnerTests { + + @Test func smallSizeHasCorrectDimensions() { + let config = LoadingSpinner.Size.small + #expect(config.diameter == 16) + #expect(config.strokeWidth == 2) + } + + @Test func mediumSizeHasCorrectDimensions() { + let config = LoadingSpinner.Size.medium + #expect(config.diameter == 24) + #expect(config.strokeWidth == 3) + } + + @Test func largeSizeHasCorrectDimensions() { + let config = LoadingSpinner.Size.large + #expect(config.diameter == 40) + #expect(config.strokeWidth == 4) + } + + @Test func spinnerCanBeCreatedWithAllSizes() { + let small = LoadingSpinner(size: .small) + let medium = LoadingSpinner(size: .medium) + let large = LoadingSpinner(size: .large) + + #expect(small.size == .small) + #expect(medium.size == .medium) + #expect(large.size == .large) + } + + @Test func spinnerCanHaveOptionalLabel() { + let withLabel = LoadingSpinner(size: .medium, label: "Loading...") + let withoutLabel = LoadingSpinner(size: .medium) + + #expect(withLabel.label == "Loading...") + #expect(withoutLabel.label == nil) + } +} +``` + +**Step 3: Run test to verify it fails** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSpinnerTests test` + +Expected: FAIL with "Cannot find 'LoadingSpinner' in scope" + +**Step 4: Write the LoadingSpinner implementation** + +Create `SportsTime/Core/Theme/Loading/LoadingSpinner.swift`: + +```swift +// +// LoadingSpinner.swift +// SportsTime +// +// Apple-style indeterminate spinner with theme-aware colors. +// + +import SwiftUI + +struct LoadingSpinner: View { + enum Size { + case small, medium, large + + var diameter: CGFloat { + switch self { + case .small: return 16 + case .medium: return 24 + case .large: return 40 + } + } + + var strokeWidth: CGFloat { + switch self { + case .small: return 2 + case .medium: return 3 + case .large: return 4 + } + } + + var labelFont: Font { + switch self { + case .small, .medium: return .subheadline + case .large: return .body + } + } + } + + let size: Size + let label: String? + + @State private var rotation: Double = 0 + @Environment(\.colorScheme) private var colorScheme + + init(size: Size = .medium, label: String? = nil) { + self.size = size + self.label = label + } + + var body: some View { + HStack(spacing: Theme.Spacing.sm) { + spinnerView + + if let label { + Text(label) + .font(size.labelFont) + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + } + } + + private var spinnerView: some View { + ZStack { + // Background track + Circle() + .stroke(Theme.warmOrange.opacity(0.15), lineWidth: size.strokeWidth) + + // Rotating arc (270 degrees) + Circle() + .trim(from: 0, to: 0.75) + .stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: size.strokeWidth, lineCap: .round)) + .rotationEffect(.degrees(rotation)) + } + .frame(width: size.diameter, height: size.diameter) + .onAppear { + withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { + rotation = 360 + } + } + } +} + +// MARK: - Preview + +#Preview("Loading Spinner Sizes") { + VStack(spacing: 40) { + LoadingSpinner(size: .small, label: "Loading...") + LoadingSpinner(size: .medium, label: "Loading games...") + LoadingSpinner(size: .large, label: "Planning trip") + LoadingSpinner(size: .medium) + } + .padding(40) + .themedBackground() +} +``` + +**Step 5: Run tests to verify they pass** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSpinnerTests test` + +Expected: All 5 tests PASS + +**Step 6: Commit** + +```bash +git add SportsTime/Core/Theme/Loading/LoadingSpinner.swift SportsTimeTests/Loading/LoadingSpinnerTests.swift +git commit -m "$(cat <<'EOF' +feat: add LoadingSpinner component + +Apple-style indeterminate spinner with three sizes (small/medium/large), +optional label, and theme-aware colors. Uses 270-degree arc with +1-second rotation. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 2: Create LoadingPlaceholder Component + +**Files:** +- Create: `SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift` +- Test: `SportsTimeTests/Loading/LoadingPlaceholderTests.swift` + +**Step 1: Write the failing test** + +Create `SportsTimeTests/Loading/LoadingPlaceholderTests.swift`: + +```swift +// +// LoadingPlaceholderTests.swift +// SportsTimeTests +// + +import Testing +import SwiftUI +@testable import SportsTime + +struct LoadingPlaceholderTests { + + @Test func rectangleHasCorrectDimensions() { + let rect = LoadingPlaceholder.rectangle(width: 100, height: 20) + #expect(rect.width == 100) + #expect(rect.height == 20) + } + + @Test func circleHasCorrectDiameter() { + let circle = LoadingPlaceholder.circle(diameter: 40) + #expect(circle.diameter == 40) + } + + @Test func capsuleHasCorrectDimensions() { + let capsule = LoadingPlaceholder.capsule(width: 80, height: 24) + #expect(capsule.width == 80) + #expect(capsule.height == 24) + } + + @Test func animationCycleDurationIsCorrect() { + #expect(LoadingPlaceholder.animationDuration == 1.2) + } + + @Test func opacityRangeIsSubtle() { + #expect(LoadingPlaceholder.minOpacity == 0.3) + #expect(LoadingPlaceholder.maxOpacity == 0.5) + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingPlaceholderTests test` + +Expected: FAIL with "Cannot find 'LoadingPlaceholder' in scope" + +**Step 3: Write the LoadingPlaceholder implementation** + +Create `SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift`: + +```swift +// +// LoadingPlaceholder.swift +// SportsTime +// +// Skeleton placeholder shapes with gentle opacity pulse animation. +// + +import SwiftUI + +enum LoadingPlaceholder { + static let animationDuration: Double = 1.2 + static let minOpacity: Double = 0.3 + static let maxOpacity: Double = 0.5 + + static func rectangle(width: CGFloat, height: CGFloat) -> PlaceholderRectangle { + PlaceholderRectangle(width: width, height: height) + } + + static func circle(diameter: CGFloat) -> PlaceholderCircle { + PlaceholderCircle(diameter: diameter) + } + + static func capsule(width: CGFloat, height: CGFloat) -> PlaceholderCapsule { + PlaceholderCapsule(width: width, height: height) + } + + static var card: PlaceholderCard { + PlaceholderCard() + } + + static var listRow: PlaceholderListRow { + PlaceholderListRow() + } +} + +// MARK: - Placeholder Rectangle + +struct PlaceholderRectangle: View { + let width: CGFloat + let height: CGFloat + + @State private var isAnimating = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + RoundedRectangle(cornerRadius: 4) + .fill(placeholderColor) + .frame(width: width, height: height) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + .onAppear { + withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) { + isAnimating = true + } + } + } + + private var placeholderColor: Color { + colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme) + } +} + +// MARK: - Placeholder Circle + +struct PlaceholderCircle: View { + let diameter: CGFloat + + @State private var isAnimating = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Circle() + .fill(placeholderColor) + .frame(width: diameter, height: diameter) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + .onAppear { + withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) { + isAnimating = true + } + } + } + + private var placeholderColor: Color { + colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme) + } +} + +// MARK: - Placeholder Capsule + +struct PlaceholderCapsule: View { + let width: CGFloat + let height: CGFloat + + @State private var isAnimating = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Capsule() + .fill(placeholderColor) + .frame(width: width, height: height) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + .onAppear { + withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) { + isAnimating = true + } + } + } + + private var placeholderColor: Color { + colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme) + } +} + +// MARK: - Placeholder Card (Trip Card Skeleton) + +struct PlaceholderCard: View { + @State private var isAnimating = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + // Header row + HStack { + placeholderRect(width: 60, height: 20) + Spacer() + placeholderRect(width: 40, height: 16) + } + + // Title and subtitle + VStack(alignment: .leading, spacing: 4) { + placeholderRect(width: 120, height: 16) + placeholderRect(width: 160, height: 14) + } + + // Stats row + HStack(spacing: Theme.Spacing.sm) { + placeholderRect(width: 70, height: 14) + placeholderRect(width: 60, height: 14) + } + } + .padding(Theme.Spacing.md) + .frame(width: 200) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + .onAppear { + withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) { + isAnimating = true + } + } + } + + private func placeholderRect(width: CGFloat, height: CGFloat) -> some View { + RoundedRectangle(cornerRadius: 4) + .fill(placeholderColor) + .frame(width: width, height: height) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + } + + private var placeholderColor: Color { + colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme) + } +} + +// MARK: - Placeholder List Row + +struct PlaceholderListRow: View { + @State private var isAnimating = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(spacing: Theme.Spacing.md) { + placeholderCircle(diameter: 40) + + VStack(alignment: .leading, spacing: 6) { + placeholderRect(width: 140, height: 16) + placeholderRect(width: 100, height: 12) + } + + Spacer() + + placeholderRect(width: 50, height: 14) + } + .padding(Theme.Spacing.md) + .onAppear { + withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) { + isAnimating = true + } + } + } + + private func placeholderRect(width: CGFloat, height: CGFloat) -> some View { + RoundedRectangle(cornerRadius: 4) + .fill(placeholderColor) + .frame(width: width, height: height) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + } + + private func placeholderCircle(diameter: CGFloat) -> some View { + Circle() + .fill(placeholderColor) + .frame(width: diameter, height: diameter) + .opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity) + } + + private var placeholderColor: Color { + colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme) + } +} + +// MARK: - Preview + +#Preview("Loading Placeholders") { + VStack(spacing: 30) { + HStack(spacing: 20) { + LoadingPlaceholder.rectangle(width: 100, height: 20) + LoadingPlaceholder.circle(diameter: 40) + LoadingPlaceholder.capsule(width: 80, height: 24) + } + + LoadingPlaceholder.card + + LoadingPlaceholder.listRow + .background(Theme.cardBackground(.dark)) + } + .padding(20) + .themedBackground() +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingPlaceholderTests test` + +Expected: All 5 tests PASS + +**Step 5: Commit** + +```bash +git add SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift SportsTimeTests/Loading/LoadingPlaceholderTests.swift +git commit -m "$(cat <<'EOF' +feat: add LoadingPlaceholder component + +Skeleton placeholder shapes with gentle opacity pulse animation (0.3-0.5 +over 1.2s). Includes rectangle, circle, capsule primitives plus card and +listRow composites. Theme-aware colors. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 3: Create LoadingSheet Component + +**Files:** +- Create: `SportsTime/Core/Theme/Loading/LoadingSheet.swift` +- Test: `SportsTimeTests/Loading/LoadingSheetTests.swift` + +**Step 1: Write the failing test** + +Create `SportsTimeTests/Loading/LoadingSheetTests.swift`: + +```swift +// +// LoadingSheetTests.swift +// SportsTimeTests +// + +import Testing +import SwiftUI +@testable import SportsTime + +struct LoadingSheetTests { + + @Test func sheetRequiresLabel() { + let sheet = LoadingSheet(label: "Planning trip") + #expect(sheet.label == "Planning trip") + } + + @Test func sheetCanHaveOptionalDetail() { + let withDetail = LoadingSheet(label: "Exporting", detail: "Generating maps...") + let withoutDetail = LoadingSheet(label: "Loading") + + #expect(withDetail.detail == "Generating maps...") + #expect(withoutDetail.detail == nil) + } + + @Test func backgroundOpacityIsCorrect() { + #expect(LoadingSheet.backgroundOpacity == 0.5) + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSheetTests test` + +Expected: FAIL with "Cannot find 'LoadingSheet' in scope" + +**Step 3: Write the LoadingSheet implementation** + +Create `SportsTime/Core/Theme/Loading/LoadingSheet.swift`: + +```swift +// +// LoadingSheet.swift +// SportsTime +// +// Full-screen blocking loading overlay with centered card. +// + +import SwiftUI + +struct LoadingSheet: View { + static let backgroundOpacity: Double = 0.5 + + let label: String + let detail: String? + + @Environment(\.colorScheme) private var colorScheme + + init(label: String, detail: String? = nil) { + self.label = label + self.detail = detail + } + + var body: some View { + ZStack { + // Dimmed background + Color.black.opacity(Self.backgroundOpacity) + .ignoresSafeArea() + + // Centered card + VStack(spacing: Theme.Spacing.lg) { + LoadingSpinner(size: .large) + + VStack(spacing: Theme.Spacing.xs) { + Text(label) + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + if let detail { + Text(detail) + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + .multilineTextAlignment(.center) + } + } + } + .padding(Theme.Spacing.xl) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .shadow(color: .black.opacity(0.2), radius: 16, y: 8) + } + .transition(.opacity) + } +} + +// MARK: - Preview + +#Preview("Loading Sheet") { + ZStack { + Color.gray.ignoresSafeArea() + LoadingSheet(label: "Planning trip") + } +} + +#Preview("Loading Sheet with Detail") { + ZStack { + Color.gray.ignoresSafeArea() + LoadingSheet(label: "Exporting PDF", detail: "Generating maps...") + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSheetTests test` + +Expected: All 3 tests PASS + +**Step 5: Commit** + +```bash +git add SportsTime/Core/Theme/Loading/LoadingSheet.swift SportsTimeTests/Loading/LoadingSheetTests.swift +git commit -m "$(cat <<'EOF' +feat: add LoadingSheet component + +Full-screen blocking overlay with dimmed background (0.5 opacity), +centered card with large spinner, label, and optional detail text. +Replaces LoadingOverlay and PlanningProgressView. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 4: Replace ProgressView in SportsStep + +**Files:** +- Modify: `SportsTime/Features/Trip/Views/Wizard/Steps/SportsStep.swift:30-38` + +**Step 1: Replace ProgressView with LoadingSpinner** + +In `SportsStep.swift`, replace lines 30-38: + +```swift +// OLD (lines 30-38): + if isLoading { + HStack { + ProgressView() + .scaleEffect(0.8) + Text("Checking game availability...") + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + .padding(.vertical, Theme.Spacing.sm) + } +``` + +With: + +```swift + if isLoading { + LoadingSpinner(size: .small, label: "Checking availability...") + .padding(.vertical, Theme.Spacing.sm) + } +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Trip/Views/Wizard/Steps/SportsStep.swift +git commit -m "$(cat <<'EOF' +refactor: replace ProgressView with LoadingSpinner in SportsStep + +Use new LoadingSpinner(size: .small) for consistent loading UI. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 5: Replace ProgressView in ReviewStep + +**Files:** +- Modify: `SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift:48-53` + +**Step 1: Replace ProgressView with LoadingSpinner** + +In `ReviewStep.swift`, replace the button content (lines 48-56): + +```swift +// OLD: + Button(action: onPlan) { + HStack { + if isPlanning { + ProgressView() + .scaleEffect(0.8) + .tint(.white) + } + Text(isPlanning ? "Planning..." : "Plan My Trip") + .fontWeight(.semibold) + } +``` + +With: + +```swift + Button(action: onPlan) { + HStack(spacing: Theme.Spacing.sm) { + if isPlanning { + LoadingSpinner(size: .small) + .colorScheme(.dark) // Force white on orange button + } + Text(isPlanning ? "Planning..." : "Plan My Trip") + .fontWeight(.semibold) + } +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift +git commit -m "$(cat <<'EOF' +refactor: replace ProgressView with LoadingSpinner in ReviewStep + +Use LoadingSpinner(size: .small) in Plan My Trip button. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 6: Replace ProgressView in StadiumVisitHistoryView + +**Files:** +- Modify: `SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift:16-17` + +**Step 1: Replace ProgressView with LoadingSpinner** + +In `StadiumVisitHistoryView.swift`, replace line 17: + +```swift +// OLD: + if isLoading { + ProgressView() +``` + +With: + +```swift + if isLoading { + LoadingSpinner(size: .medium) +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift +git commit -m "$(cat <<'EOF' +refactor: replace ProgressView with LoadingSpinner in StadiumVisitHistoryView + +Use LoadingSpinner(size: .medium) for consistent loading UI. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 7: Replace ProgressView in GamesHistoryView + +**Files:** +- Modify: `SportsTime/Features/Progress/Views/GamesHistoryView.swift:17` + +**Step 1: Replace ProgressView with LoadingSpinner** + +In `GamesHistoryView.swift`, replace line 17: + +```swift +// OLD: + ProgressView("Loading games...") +``` + +With: + +```swift + LoadingSpinner(size: .medium, label: "Loading games...") +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Progress/Views/GamesHistoryView.swift +git commit -m "$(cat <<'EOF' +refactor: replace ProgressView with LoadingSpinner in GamesHistoryView + +Use LoadingSpinner with label for consistent loading UI. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 8: Replace ProgressView in StadiumVisitSheet + +**Files:** +- Modify: `SportsTime/Features/Progress/Views/StadiumVisitSheet.swift:234-236` + +**Step 1: Replace ProgressView with LoadingSpinner** + +In `StadiumVisitSheet.swift`, replace lines 234-236: + +```swift +// OLD: + HStack { + if isLookingUpGame { + ProgressView() + .scaleEffect(0.8) + } else { +``` + +With: + +```swift + HStack { + if isLookingUpGame { + LoadingSpinner(size: .small) + } else { +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Progress/Views/StadiumVisitSheet.swift +git commit -m "$(cat <<'EOF' +refactor: replace ProgressView with LoadingSpinner in StadiumVisitSheet + +Use LoadingSpinner(size: .small) in Look Up Game button. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 9: Replace planningOverlay in TripCreationView + +**Files:** +- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift:867-876` + +**Step 1: Replace PlanningProgressView with LoadingSheet** + +In `TripCreationView.swift`, replace lines 867-876: + +```swift +// OLD: + private var planningOverlay: some View { + ZStack { + Color.black.opacity(0.5) + .ignoresSafeArea() + + PlanningProgressView() + .padding(40) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24)) + } + } +``` + +With: + +```swift + private var planningOverlay: some View { + LoadingSheet(label: "Planning trip") + } +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Trip/Views/TripCreationView.swift +git commit -m "$(cat <<'EOF' +refactor: replace PlanningProgressView with LoadingSheet + +Use new LoadingSheet component for planning overlay. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 10: Refactor LoadingTripsView to Use New Placeholders + +**Files:** +- Modify: `SportsTime/Features/Home/Views/LoadingTripsView.swift` + +**Step 1: Rewrite LoadingTripsView to use LoadingPlaceholder** + +Replace the entire file with: + +```swift +// +// LoadingTripsView.swift +// SportsTime +// +// Loading state for suggested trips carousel using skeleton placeholders. +// + +import SwiftUI + +struct LoadingTripsView: View { + let message: String + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + // Header + HStack { + Text("Featured Trips") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + Spacer() + } + + // Loading indicator with message + LoadingSpinner(size: .small, label: message) + .padding(.vertical, Theme.Spacing.xs) + + // Placeholder cards + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: Theme.Spacing.md) { + ForEach(0..<3, id: \.self) { _ in + LoadingPlaceholder.card + } + } + } + } + } +} + +#Preview { + VStack { + LoadingTripsView(message: "Loading trips...") + .padding() + } + .themedBackground() +} +``` + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Features/Home/Views/LoadingTripsView.swift +git commit -m "$(cat <<'EOF' +refactor: simplify LoadingTripsView with new loading components + +Use LoadingSpinner and LoadingPlaceholder.card instead of custom +LoadingDots and PlaceholderCard with shimmer animation. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 11: Remove Deprecated Components from AnimatedComponents.swift + +**Files:** +- Modify: `SportsTime/Core/Theme/AnimatedComponents.swift` + +**Step 1: Remove deprecated loading components** + +Remove the following from `AnimatedComponents.swift`: +- `ThemedSpinner` (lines 106-136) +- `ThemedSpinnerCompact` (lines 138-164) +- `PlanningProgressView` (lines 245-279) +- `LoadingOverlay` (lines 348-417) +- Preview for "Themed Spinners" (lines 419-436) +- Preview for "Loading Overlay" (lines 459-467) +- Preview for "Loading Overlay with Progress" (lines 469-479) + +Keep: +- `AnimatedRouteGraphic` +- `PulsingDot` +- `RoutePreviewStrip` +- `StatPill` +- `EmptyStateView` +- Their previews + +**Step 2: Build to verify** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add SportsTime/Core/Theme/AnimatedComponents.swift +git commit -m "$(cat <<'EOF' +refactor: remove deprecated loading components from AnimatedComponents + +Remove ThemedSpinner, ThemedSpinnerCompact, PlanningProgressView, +and LoadingOverlay. These are replaced by the new Loading/ components. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 12: Delete LoadingTextGenerator.swift + +**Files:** +- Delete: `SportsTime/Core/Services/LoadingTextGenerator.swift` + +**Step 1: Remove the file** + +```bash +git rm SportsTime/Core/Services/LoadingTextGenerator.swift +``` + +**Step 2: Build to verify no references remain** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` + +Expected: Build succeeds (if there are errors, search for `LoadingTextGenerator` usages and remove them) + +**Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +refactor: remove LoadingTextGenerator + +Loading messages are now static context-specific labels, +no longer AI-generated or rotating. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Task 13: Delete LoadingDots and PlaceholderCard (Cleanup) + +**Files:** +- Modify: `SportsTime/Features/Home/Views/LoadingTripsView.swift` (if still contains old components) + +**Step 1: Verify LoadingDots and PlaceholderCard are no longer in codebase** + +```bash +grep -r "LoadingDots\|PlaceholderCard" SportsTime/ +``` + +If any usages remain, remove them. + +**Step 2: Build full test suite** + +Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test` + +Expected: All tests pass + +**Step 3: Final commit if needed** + +```bash +git add -A +git commit -m "$(cat <<'EOF' +chore: final cleanup of deprecated loading components + +Remove any remaining references to LoadingDots and PlaceholderCard. + +Co-Authored-By: Claude Opus 4.5 +EOF +)" +``` + +--- + +## Summary + +| Task | Description | Tests Added | +|------|-------------|-------------| +| 1 | Create LoadingSpinner | 5 | +| 2 | Create LoadingPlaceholder | 5 | +| 3 | Create LoadingSheet | 3 | +| 4 | Replace ProgressView in SportsStep | - | +| 5 | Replace ProgressView in ReviewStep | - | +| 6 | Replace ProgressView in StadiumVisitHistoryView | - | +| 7 | Replace ProgressView in GamesHistoryView | - | +| 8 | Replace ProgressView in StadiumVisitSheet | - | +| 9 | Replace planningOverlay in TripCreationView | - | +| 10 | Refactor LoadingTripsView | - | +| 11 | Remove deprecated components | - | +| 12 | Delete LoadingTextGenerator | - | +| 13 | Final cleanup and verification | - | + +**Total new tests:** 13 +**Files created:** 6 (3 components + 3 test files) +**Files modified:** 8 +**Files deleted:** 1 (LoadingTextGenerator.swift) +**Components removed:** 6 (ThemedSpinner, ThemedSpinnerCompact, LoadingDots, PlaceholderCard, PlanningProgressView, LoadingOverlay)