From 7cb5aafd67f52c4298d6806a3a97ef4d0562b89d Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 12 Jan 2026 22:14:57 -0600 Subject: [PATCH] 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 --- .../2026-01-12-loading-redesign-design.md | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 docs/plans/2026-01-12-loading-redesign-design.md diff --git a/docs/plans/2026-01-12-loading-redesign-design.md b/docs/plans/2026-01-12-loading-redesign-design.md new file mode 100644 index 0000000..70dccb9 --- /dev/null +++ b/docs/plans/2026-01-12-loading-redesign-design.md @@ -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