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>
225 lines
6.8 KiB
Markdown
225 lines
6.8 KiB
Markdown
# 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
|