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>
This commit is contained in:
Trey t
2026-01-12 22:14:57 -06:00
parent ba3ea6daeb
commit 7cb5aafd67

View File

@@ -0,0 +1,224 @@
# 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