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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user