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,391 @@
|
||||
//
|
||||
// HomeContent_SoftPastel.swift
|
||||
// SportsTime
|
||||
//
|
||||
// SOFT PASTEL: Gentle, dreamy, calming.
|
||||
// Muted colors, soft shadows, rounded everything.
|
||||
// Cozy travel journal aesthetic.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_SoftPastel: 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]
|
||||
|
||||
// Soft pastel palette
|
||||
private let pastelPink = Color(red: 1.0, green: 0.85, blue: 0.88)
|
||||
private let pastelBlue = Color(red: 0.85, green: 0.92, blue: 1.0)
|
||||
private let pastelMint = Color(red: 0.85, green: 0.98, blue: 0.92)
|
||||
private let pastelLavender = Color(red: 0.92, green: 0.88, blue: 1.0)
|
||||
private let pastelPeach = Color(red: 1.0, green: 0.9, blue: 0.85)
|
||||
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color(red: 0.12, green: 0.12, blue: 0.14)
|
||||
: Color(red: 0.98, green: 0.97, blue: 0.99)
|
||||
}
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.9) : Color(red: 0.3, green: 0.28, blue: 0.35)
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.6) : Color(red: 0.5, green: 0.48, blue: 0.55)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor.ignoresSafeArea()
|
||||
|
||||
// Soft gradient clouds
|
||||
softClouds
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 28) {
|
||||
// PASTEL HERO
|
||||
pastelHero
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// FEATURED TRIPS
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
featuredSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// SAVED TRIPS
|
||||
if !savedTrips.isEmpty {
|
||||
savedSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// SOFT FOOTER
|
||||
softFooter
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Soft Clouds
|
||||
|
||||
private var softClouds: some View {
|
||||
ZStack {
|
||||
// Soft gradient blobs
|
||||
Ellipse()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [pastelPink.opacity(0.4), pastelPink.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 300, height: 200)
|
||||
.blur(radius: 50)
|
||||
.offset(x: -80, y: -150)
|
||||
|
||||
Ellipse()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [pastelBlue.opacity(0.4), pastelBlue.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 250, height: 180)
|
||||
.blur(radius: 45)
|
||||
.offset(x: 100, y: 150)
|
||||
|
||||
Ellipse()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [pastelMint.opacity(0.3), pastelMint.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 220, height: 160)
|
||||
.blur(radius: 40)
|
||||
.offset(x: -50, y: 450)
|
||||
|
||||
Ellipse()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [pastelLavender.opacity(0.3), pastelLavender.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 200, height: 150)
|
||||
.blur(radius: 35)
|
||||
.offset(x: 80, y: 600)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// MARK: - Pastel Hero
|
||||
|
||||
private var pastelHero: some View {
|
||||
VStack(spacing: 24) {
|
||||
// Soft icon cluster
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(pastelPink.opacity(colorScheme == .dark ? 0.3 : 0.6))
|
||||
.frame(width: 80, height: 80)
|
||||
.offset(x: -30, y: -10)
|
||||
|
||||
Circle()
|
||||
.fill(pastelBlue.opacity(colorScheme == .dark ? 0.3 : 0.6))
|
||||
.frame(width: 70, height: 70)
|
||||
.offset(x: 25, y: 5)
|
||||
|
||||
Circle()
|
||||
.fill(pastelMint.opacity(colorScheme == .dark ? 0.3 : 0.6))
|
||||
.frame(width: 60, height: 60)
|
||||
.offset(x: -5, y: 25)
|
||||
|
||||
Image(systemName: "car.fill")
|
||||
.font(.system(size: 28))
|
||||
.foregroundStyle(textPrimary.opacity(0.7))
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
|
||||
// Title
|
||||
VStack(spacing: 8) {
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 32, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("Plan your cozy road trip adventure")
|
||||
.font(.system(size: 15, weight: .regular, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
// Soft CTA
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Text("Start Planning")
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
}
|
||||
.foregroundStyle(textPrimary)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: colorScheme == .dark
|
||||
? [pastelPink.opacity(0.4), pastelLavender.opacity(0.4)]
|
||||
: [pastelPink, pastelLavender],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
)
|
||||
.shadow(color: pastelPink.opacity(0.3), radius: 15, y: 6)
|
||||
}
|
||||
}
|
||||
.padding(28)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 32)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.8))
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 20, y: 8)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
HStack {
|
||||
Text("Featured Trips")
|
||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(textSecondary)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.8))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
pastelTripCard(suggestedTrip.trip, colorIndex: index)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func pastelTripCard(_ trip: Trip, colorIndex: Int) -> some View {
|
||||
let colors = [pastelPink, pastelBlue, pastelMint, pastelLavender, pastelPeach]
|
||||
let accentColor = colors[colorIndex % colors.count]
|
||||
|
||||
return HStack(spacing: 16) {
|
||||
// Soft circle
|
||||
Circle()
|
||||
.fill(accentColor.opacity(colorScheme == .dark ? 0.3 : 0.6))
|
||||
.frame(width: 50, height: 50)
|
||||
.overlay(
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundStyle(textPrimary.opacity(0.7))
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 16, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("\(trip.stops.count) stops")
|
||||
.font(.system(size: 13, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("\(trip.totalGames) games")
|
||||
.font(.system(size: 13, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(textSecondary.opacity(0.6))
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.8))
|
||||
.shadow(color: accentColor.opacity(0.2), radius: 10, y: 4)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Saved Section
|
||||
|
||||
private var savedSection: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
HStack {
|
||||
Text("Your Trips")
|
||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("See all")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
let colors = [pastelPeach, pastelMint, pastelLavender]
|
||||
let accentColor = colors[index % colors.count]
|
||||
|
||||
HStack(spacing: 14) {
|
||||
// Soft dot
|
||||
Circle()
|
||||
.fill(accentColor.opacity(colorScheme == .dark ? 0.4 : 0.7))
|
||||
.frame(width: 12, height: 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 12, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(trip.stops.count)")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textPrimary.opacity(0.7))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(accentColor.opacity(colorScheme == .dark ? 0.2 : 0.4))
|
||||
)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.04) : Color.white.opacity(0.7))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Soft Footer
|
||||
|
||||
private var softFooter: some View {
|
||||
VStack(spacing: 12) {
|
||||
// Soft dots
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(pastelPink.opacity(0.5))
|
||||
.frame(width: 6, height: 6)
|
||||
Circle()
|
||||
.fill(pastelBlue.opacity(0.5))
|
||||
.frame(width: 6, height: 6)
|
||||
Circle()
|
||||
.fill(pastelMint.opacity(0.5))
|
||||
.frame(width: 6, height: 6)
|
||||
}
|
||||
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textSecondary.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user