feat(ui): add 23 home screen design variants with picker

Add design style system with 23 unique home screen aesthetics:
- Classic (original SportsTime design, now default)
- 12 experimental styles (Brutalist, Luxury Editorial, etc.)
- 10 polished app-inspired styles (Flighty, SeatGeek, Apple Maps,
  Things 3, Airbnb, Spotify, Nike Run Club, Fantastical, Strava,
  Carrot Weather)

Includes settings picker to switch between styles and persists
selection via UserDefaults.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 14:44:30 -06:00
parent 3d40145ffb
commit 56869ce479
27 changed files with 8636 additions and 27 deletions

View File

@@ -0,0 +1,360 @@
//
// HomeContent_SwissModernist.swift
// SportsTime
//
// SWISS MODERNIST: Grid perfection, Helvetica vibes, mathematical precision.
// Clean typography, strict alignment, minimalist color with bold accents.
//
import SwiftUI
import SwiftData
struct HomeContent_SwissModernist: View {
@Environment(\.colorScheme) private var colorScheme
@Binding var showNewTrip: Bool
@Binding var selectedTab: Int
@Binding var selectedSuggestedTrip: SuggestedTrip?
let savedTrips: [SavedTrip]
let suggestedTripsGenerator: SuggestedTripsGenerator
let displayedTips: [PlanningTip]
// Swiss design palette - restrained with bold accent
private let swissRed = Color(red: 1.0, green: 0.0, blue: 0.0)
private let swissBlack = Color(white: 0.0)
private var bgColor: Color {
colorScheme == .dark ? Color(white: 0.06) : Color(white: 0.98)
}
private var textPrimary: Color {
colorScheme == .dark ? .white : swissBlack
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.4)
}
private var gridLine: Color {
colorScheme == .dark ? Color(white: 0.15) : Color(white: 0.85)
}
var body: some View {
ZStack {
bgColor.ignoresSafeArea()
// Subtle grid overlay
gridOverlay
ScrollView {
VStack(spacing: 0) {
// HEADER - Strict typographic hierarchy
swissHeader
.padding(.top, 32)
.padding(.horizontal, 24)
// HERO - Mathematical precision
swissHero
.padding(.top, 48)
.padding(.horizontal, 24)
// FEATURED TRIPS
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
featuredSection
.padding(.top, 64)
.padding(.horizontal, 24)
}
// SAVED TRIPS
if !savedTrips.isEmpty {
savedSection
.padding(.top, 64)
.padding(.horizontal, 24)
}
// FOOTER
swissFooter
.padding(.top, 80)
.padding(.bottom, 40)
}
}
}
}
// MARK: - Grid Overlay
private var gridOverlay: some View {
GeometryReader { geo in
// Vertical grid lines
HStack(spacing: geo.size.width / 12) {
ForEach(0..<12, id: \.self) { _ in
Rectangle()
.fill(gridLine.opacity(0.3))
.frame(width: 0.5)
}
}
.padding(.horizontal, 24)
}
.allowsHitTesting(false)
}
// MARK: - Swiss Header
private var swissHeader: some View {
VStack(alignment: .leading, spacing: 12) {
// Date - small caps style
Text(Date.now.formatted(.dateTime.month(.wide).year()).uppercased())
.font(.system(size: 10, weight: .medium))
.tracking(3)
.foregroundStyle(textSecondary)
// Rule
Rectangle()
.fill(textPrimary)
.frame(height: 2)
.frame(maxWidth: 60)
}
}
// MARK: - Swiss Hero
private var swissHero: some View {
VStack(alignment: .leading, spacing: 32) {
// Title block - strict typographic scale
VStack(alignment: .leading, spacing: 8) {
Text("Sports")
.font(.system(size: 56, weight: .bold))
.foregroundStyle(textPrimary)
HStack(spacing: 0) {
Text("Time")
.font(.system(size: 56, weight: .bold))
.foregroundStyle(textPrimary)
// Red accent dot
Circle()
.fill(swissRed)
.frame(width: 14, height: 14)
.offset(y: 16)
}
}
.tracking(-1)
// Description - rational grid width
Text("Plan your perfect sports road trip with mathematical precision. Every route optimized. Every game aligned.")
.font(.system(size: 16, weight: .regular))
.foregroundStyle(textSecondary)
.lineSpacing(6)
.frame(maxWidth: 280, alignment: .leading)
// CTA - Swiss button
Button {
showNewTrip = true
} label: {
HStack(spacing: 16) {
Text("Begin")
.font(.system(size: 14, weight: .semibold))
Rectangle()
.fill(Color.white)
.frame(width: 24, height: 1)
Image(systemName: "arrow.right")
.font(.system(size: 12, weight: .medium))
}
.foregroundStyle(.white)
.padding(.horizontal, 32)
.padding(.vertical, 18)
.background(swissRed)
}
}
}
// MARK: - Featured Section
private var featuredSection: some View {
VStack(alignment: .leading, spacing: 32) {
// Section header - typographic hierarchy
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8) {
Text("01")
.font(.system(size: 11, weight: .medium, design: .monospaced))
.foregroundStyle(swissRed)
Text("Featured")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(textPrimary)
}
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14))
.foregroundStyle(textSecondary)
}
}
// Rule
Rectangle()
.fill(textPrimary)
.frame(height: 1)
// Grid of trips
VStack(spacing: 0) {
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
swissTripRow(suggestedTrip.trip, index: index + 1)
}
.buttonStyle(.plain)
}
}
}
}
private func swissTripRow(_ trip: Trip, index: Int) -> some View {
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 24) {
// Index number
Text(String(format: "%02d", index))
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(textSecondary)
.frame(width: 24)
// Trip name
Text(trip.name)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(textPrimary)
.lineLimit(1)
Spacer()
// Stats - tabular
HStack(spacing: 24) {
VStack(alignment: .trailing, spacing: 2) {
Text("\(trip.stops.count)")
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.foregroundStyle(textPrimary)
Text("cities")
.font(.system(size: 9, weight: .medium))
.foregroundStyle(textSecondary)
}
VStack(alignment: .trailing, spacing: 2) {
Text("\(trip.totalGames)")
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.foregroundStyle(textPrimary)
Text("games")
.font(.system(size: 9, weight: .medium))
.foregroundStyle(textSecondary)
}
}
// Arrow
Image(systemName: "arrow.right")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(swissRed)
}
.padding(.vertical, 20)
// Divider
Rectangle()
.fill(gridLine)
.frame(height: 0.5)
}
}
// MARK: - Saved Section
private var savedSection: some View {
VStack(alignment: .leading, spacing: 32) {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8) {
Text("02")
.font(.system(size: 11, weight: .medium, design: .monospaced))
.foregroundStyle(swissRed)
Text("Saved")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(textPrimary)
}
Spacer()
Button {
selectedTab = 2
} label: {
Text("All")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(textSecondary)
}
}
Rectangle()
.fill(textPrimary)
.frame(height: 1)
VStack(spacing: 0) {
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games)
} label: {
VStack(spacing: 0) {
HStack(spacing: 24) {
Text(String(format: "%02d", index + 1))
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(textSecondary)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(textPrimary)
Text(trip.formattedDateRange)
.font(.system(size: 11))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "arrow.right")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(textSecondary)
}
.padding(.vertical, 20)
Rectangle()
.fill(gridLine)
.frame(height: 0.5)
}
}
.buttonStyle(.plain)
}
}
}
}
}
// MARK: - Swiss Footer
private var swissFooter: some View {
VStack(spacing: 12) {
Rectangle()
.fill(textPrimary)
.frame(width: 40, height: 2)
Text("SPORTS TIME")
.font(.system(size: 9, weight: .medium))
.tracking(4)
.foregroundStyle(textSecondary)
}
}
}