Files
Sportstime/docs/plans/2026-01-12-loading-redesign-design.md
Trey t 7cb5aafd67 docs: add loading system redesign design
Complete design spec for overhauling all loading views, progress
indicators, and spinners. Covers LoadingSpinner, LoadingPlaceholder,
and LoadingSheet components with Apple-style minimal aesthetic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:14:57 -06:00

6.8 KiB

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

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

// 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

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