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:
224
docs/plans/2026-01-12-loading-redesign-design.md
Normal file
224
docs/plans/2026-01-12-loading-redesign-design.md
Normal 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
|
||||
Reference in New Issue
Block a user