# Loading System Redesign ## Overview A complete overhaul of all loading views, progress indicators, and spinners to achieve visual consistency, a fresh premium aesthetic, and Apple-level polish. ## Design Decisions | Decision | Choice | |----------|--------| | Visual direction | Minimal + premium (Apple-level polish, no gimmicks) | | Animation style | Apple-style indeterminate (smooth arcs, gentle opacity) | | Loading text | Context-aware only (static labels, no clever messages) | | Colors | Theme-aware using existing `Theme.warmOrange` | | Skeleton animation | Gentle opacity pulse (no directional shimmer) | ## Component Hierarchy | Tier | Component | Use Case | |------|-----------|----------| | **Inline** | `LoadingSpinner` | Buttons, small areas, inline waits | | **Content** | `LoadingPlaceholder` | Skeleton shapes replacing content | | **Blocking** | `LoadingSheet` | Full-screen modal operations | ## Animation Principles - 1-second cycle duration (spinners), 1.2-second (skeletons) - Ease-in-out timing curves - Opacity range: 0.3 → 0.6 (subtle, never harsh) - No bouncing, no scaling, no playfulness - All shapes in a skeleton group pulse together (synchronized) ## Text Rules - Static, context-specific labels only - Format: "Loading [noun]..." (e.g., "Loading games...", "Loading schedule...") - No rotating messages, no AI-generated text - Text optional for spinners under 2 seconds --- ## Component: LoadingSpinner Replaces `ThemedSpinner`, `ThemedSpinnerCompact`, and all native `ProgressView()` usages. ### API ```swift LoadingSpinner(size: .medium) // .small, .medium, .large LoadingSpinner(size: .small, label: "Loading...") ``` ### Sizes | Size | Diameter | Stroke | Use Case | |------|----------|--------|----------| | `.small` | 16pt | 2pt | Inline with text, buttons | | `.medium` | 24pt | 3pt | Default, standalone | | `.large` | 40pt | 4pt | Prominent loading states | ### Visual Design - Single arc (270°) rotating smoothly - Background track at 15% opacity of accent color - Foreground arc at 100% accent color (`Theme.warmOrange`) - 1-second full rotation, linear timing - Round line caps ### Optional Label - Appears to the right of spinner (horizontal layout) - Uses `Theme.textSecondary` - Font: `.subheadline` for small/medium, `.body` for large ### Replacements | File | Line | New Code | |------|------|----------| | `SportsStep.swift` | 32 | `LoadingSpinner(size: .medium)` | | `ReviewStep.swift` | 50 | `LoadingSpinner(size: .medium)` | | `StadiumVisitHistoryView.swift` | 17 | `LoadingSpinner(size: .medium)` | | `GamesHistoryView.swift` | 17 | `LoadingSpinner(size: .medium, label: "Loading games...")` | | `StadiumVisitSheet.swift` | 235 | `LoadingSpinner(size: .small)` | --- ## Component: LoadingPlaceholder Replaces `PlaceholderCard` and shimmer rectangles in `LoadingTripsView`. ### API ```swift // Basic shapes LoadingPlaceholder.rectangle(width: 120, height: 16) LoadingPlaceholder.circle(diameter: 40) LoadingPlaceholder.capsule(width: 80, height: 24) // Pre-built composites LoadingPlaceholder.card // Trip card skeleton LoadingPlaceholder.listRow // Single list row skeleton ``` ### Animation - Gentle opacity pulse: 0.3 → 0.5 → 0.3 - 1.2-second cycle, ease-in-out - All shapes in a group pulse together (synchronized) - No directional movement, no shimmer sweep ### Colors - Light mode: `Theme.textMuted.opacity(0.2)` base - Dark mode: `Theme.cardBackgroundElevated` base - Pulse brightens by ~0.15 opacity at peak ### Card Skeleton Layout ``` ┌─────────────────────────┐ │ ▓▓▓▓▓▓ ▓▓▓ │ <- header row │ │ │ ▓▓▓▓▓▓▓▓▓▓ │ <- title │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ <- subtitle │ │ │ ▓▓▓▓▓ ▓▓▓▓▓ │ <- stats row └─────────────────────────┘ ``` --- ## Component: LoadingSheet Replaces `LoadingOverlay` and `PlanningProgressView`. ### API ```swift LoadingSheet(label: "Planning trip") LoadingSheet(label: "Exporting PDF", detail: "Generating maps...") ``` ### Visual Design ``` ┌─────────────────────────────┐ │ │ │ ╭─────────────────╮ │ │ │ │ │ │ │ (spinner) │ │ │ │ │ │ │ │ Planning trip │ │ │ │ │ │ │ ╰─────────────────╯ │ │ │ └─────────────────────────────┘ ↑ dimmed background ``` ### Specifications - Background: `Color.black.opacity(0.5)` - Card: `Theme.cardBackground` with `Theme.CornerRadius.large` - Spinner: `LoadingSpinner(size: .large)` centered - Label: `.headline` weight, `Theme.textPrimary` - Detail (optional): `.subheadline`, `Theme.textSecondary` - Card padding: `Theme.Spacing.xl` all sides - Shadow: `radius: 16, y: 8` ### Static Labels by Context | Context | Label | |---------|-------| | Trip planning | "Planning trip" | | PDF export | "Exporting PDF" | | Data sync | "Syncing data" | | Generic | "Loading" | --- ## Migration Plan ### Components to Deprecate | Current | Action | Replacement | |---------|--------|-------------| | `ThemedSpinner` | Remove | `LoadingSpinner` | | `ThemedSpinnerCompact` | Remove | `LoadingSpinner(size: .small)` | | `LoadingDots` | Remove | Not replaced | | `PlaceholderCard` | Remove | `LoadingPlaceholder.card` | | `PlanningProgressView` | Remove | `LoadingSheet` | | `LoadingOverlay` | Remove | `LoadingSheet` | | `LoadingTripsView` | Refactor | Use new skeletons | | `LoadingTextGenerator` | Remove | Static labels only | ### Components to Keep | Component | Reason | |-----------|--------| | `AnimatedRouteGraphic` | Used elsewhere, not a loading state | | `PulsingDot` | Used in maps, not loading | | `RoutePreviewStrip` | Display component, not loading | | `StatPill` | Display component, not loading | | `EmptyStateView` | Empty state, not loading | ### New File Structure ``` SportsTime/Core/Theme/ ├── Theme.swift (existing - no changes) ├── AnimatedComponents.swift (keep non-loading components) └── Loading/ ├── LoadingSpinner.swift (new) ├── LoadingPlaceholder.swift (new) └── LoadingSheet.swift (new) ``` ### Summary - 3 new files created - 6 components deprecated/removed - 5 native `ProgressView()` usages replaced - `LoadingTextGenerator.swift` deleted - `LoadingTripsView.swift` refactored to use new components