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,299 @@
//
// HomeContent_Things3.swift
// SportsTime
//
// THINGS 3-INSPIRED: Ultra-clean task management aesthetic.
// Beautiful spacing, elegant typography, minimalist.
// Focus on clarity and completion.
//
import SwiftUI
import SwiftData
struct HomeContent_Things3: 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]
// Things 3-inspired colors
private var bgColor: Color {
colorScheme == .dark
? Color(red: 0.11, green: 0.11, blue: 0.12)
: Color.white
}
private let thingsBlue = Color(red: 0.35, green: 0.6, blue: 0.95)
private let thingsGray = Color(red: 0.55, green: 0.55, blue: 0.58)
private var textPrimary: Color {
colorScheme == .dark ? .white : Color(red: 0.15, green: 0.15, blue: 0.17)
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.5)
}
private var dividerColor: Color {
colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9)
}
var body: some View {
ScrollView {
VStack(spacing: 0) {
// Header
header
.padding(.horizontal, 24)
.padding(.top, 20)
.padding(.bottom, 28)
// New Trip action
newTripRow
.padding(.horizontal, 24)
Divider()
.background(dividerColor)
.padding(.horizontal, 24)
.padding(.vertical, 16)
// Your trips section
if !savedTrips.isEmpty {
tripsSection
.padding(.horizontal, 24)
}
// Suggestions section
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
suggestionsSection
.padding(.horizontal, 24)
.padding(.top, savedTrips.isEmpty ? 0 : 28)
}
Spacer(minLength: 60)
}
}
.background(bgColor.ignoresSafeArea())
}
// MARK: - Header
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Sports Time")
.font(.system(size: 34, weight: .bold))
.foregroundStyle(textPrimary)
Text(headerSubtitle)
.font(.system(size: 15))
.foregroundStyle(textSecondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var headerSubtitle: String {
if savedTrips.isEmpty {
return "Plan your first trip"
} else {
let upcoming = savedTrips.filter { ($0.trip?.startDate ?? Date()) > Date() }.count
if upcoming > 0 {
return "\(upcoming) upcoming trip\(upcoming == 1 ? "" : "s")"
}
return "\(savedTrips.count) trip\(savedTrips.count == 1 ? "" : "s") planned"
}
}
// MARK: - New Trip Row
private var newTripRow: some View {
Button {
showNewTrip = true
} label: {
HStack(spacing: 14) {
// Checkbox circle
ZStack {
Circle()
.stroke(thingsBlue, lineWidth: 2)
.frame(width: 22, height: 22)
Image(systemName: "plus")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(thingsBlue)
}
Text("New Trip")
.font(.system(size: 17))
.foregroundStyle(thingsBlue)
Spacer()
}
}
.buttonStyle(.plain)
}
// MARK: - Trips Section
private var tripsSection: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Your Trips")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(textSecondary)
.textCase(.uppercase)
.tracking(0.5)
Spacer()
if savedTrips.count > 3 {
Button {
selectedTab = 2
} label: {
Text("See All")
.font(.system(size: 13))
.foregroundStyle(thingsBlue)
}
}
}
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: {
tripRow(trip)
}
.buttonStyle(.plain)
if index < min(2, savedTrips.count - 1) {
Divider()
.background(dividerColor)
.padding(.leading, 36)
.padding(.vertical, 8)
}
}
}
}
}
}
private func tripRow(_ trip: Trip) -> some View {
HStack(spacing: 14) {
// Completion circle
Circle()
.stroke(thingsGray.opacity(0.5), lineWidth: 1.5)
.frame(width: 22, height: 22)
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.system(size: 17))
.foregroundStyle(textPrimary)
.lineLimit(1)
HStack(spacing: 12) {
if let sport = trip.uniqueSports.first {
HStack(spacing: 4) {
Image(systemName: sport.iconName)
.font(.system(size: 11))
Text(sport.displayName)
.font(.system(size: 13))
}
.foregroundStyle(sport.themeColor)
}
Text("")
.foregroundStyle(textSecondary)
Text(trip.formattedDateRange)
.font(.system(size: 13))
.foregroundStyle(textSecondary)
}
}
Spacer()
// Count badge
Text("\(trip.totalGames)")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(textSecondary)
}
}
// MARK: - Suggestions Section
private var suggestionsSection: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Suggestions")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(textSecondary)
.textCase(.uppercase)
.tracking(0.5)
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 13))
.foregroundStyle(thingsBlue)
}
}
VStack(spacing: 0) {
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
suggestionRow(suggestedTrip.trip)
}
.buttonStyle(.plain)
if index < min(3, suggestedTripsGenerator.suggestedTrips.count - 1) {
Divider()
.background(dividerColor)
.padding(.leading, 36)
.padding(.vertical, 8)
}
}
}
}
}
private func suggestionRow(_ trip: Trip) -> some View {
HStack(spacing: 14) {
// Light gray circle
Circle()
.fill(thingsGray.opacity(0.15))
.frame(width: 22, height: 22)
.overlay(
Image(systemName: "sparkles")
.font(.system(size: 10))
.foregroundStyle(thingsGray)
)
VStack(alignment: .leading, spacing: 3) {
Text(trip.name)
.font(.system(size: 17))
.foregroundStyle(textPrimary)
.lineLimit(1)
Text("\(trip.stops.count) stops • \(trip.totalGames) games")
.font(.system(size: 13))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(thingsGray.opacity(0.5))
}
}
}