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,427 @@
|
||||
//
|
||||
// HomeContent_DarkIndustrial.swift
|
||||
// SportsTime
|
||||
//
|
||||
// DARK INDUSTRIAL: Steel, concrete, utility.
|
||||
// Stadium infrastructure vibes, warning stripes, exposed structure.
|
||||
// Functional brutalism with sports facility aesthetics.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_DarkIndustrial: 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]
|
||||
|
||||
// Industrial palette
|
||||
private let warningYellow = Color(red: 1.0, green: 0.8, blue: 0.0)
|
||||
private let steelGray = Color(red: 0.4, green: 0.45, blue: 0.5)
|
||||
private let concreteGray = Color(red: 0.3, green: 0.32, blue: 0.35)
|
||||
private let alertRed = Color(red: 0.9, green: 0.2, blue: 0.2)
|
||||
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark ? Color(red: 0.08, green: 0.09, blue: 0.1) : Color(red: 0.15, green: 0.16, blue: 0.18)
|
||||
}
|
||||
|
||||
private var textPrimary: Color {
|
||||
Color(white: 0.9)
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
Color(white: 0.55)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor.ignoresSafeArea()
|
||||
|
||||
// Industrial texture overlay
|
||||
industrialTexture
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// WARNING STRIPE HEADER
|
||||
warningStripeHeader
|
||||
|
||||
// INDUSTRIAL HERO
|
||||
industrialHero
|
||||
.padding(.top, 32)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// FEATURED TRIPS
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
featuredSection
|
||||
.padding(.top, 40)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// SAVED TRIPS
|
||||
if !savedTrips.isEmpty {
|
||||
savedSection
|
||||
.padding(.top, 40)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// INDUSTRIAL FOOTER
|
||||
industrialFooter
|
||||
.padding(.top, 48)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Industrial Texture
|
||||
|
||||
private var industrialTexture: some View {
|
||||
ZStack {
|
||||
// Grid pattern (exposed structure)
|
||||
GeometryReader { geo in
|
||||
Path { path in
|
||||
let spacing: CGFloat = 40
|
||||
for x in stride(from: CGFloat(0), to: geo.size.width, by: spacing) {
|
||||
path.move(to: CGPoint(x: x, y: 0))
|
||||
path.addLine(to: CGPoint(x: x, y: geo.size.height))
|
||||
}
|
||||
for y in stride(from: CGFloat(0), to: geo.size.height, by: spacing) {
|
||||
path.move(to: CGPoint(x: 0, y: y))
|
||||
path.addLine(to: CGPoint(x: geo.size.width, y: y))
|
||||
}
|
||||
}
|
||||
.stroke(steelGray.opacity(0.1), lineWidth: 0.5)
|
||||
}
|
||||
|
||||
// Corner rivets/bolts
|
||||
VStack {
|
||||
HStack {
|
||||
rivetCluster
|
||||
Spacer()
|
||||
rivetCluster
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
private var rivetCluster: some View {
|
||||
HStack(spacing: 8) {
|
||||
rivet
|
||||
rivet
|
||||
}
|
||||
}
|
||||
|
||||
private var rivet: some View {
|
||||
Circle()
|
||||
.fill(steelGray.opacity(0.3))
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Circle()
|
||||
.fill(steelGray.opacity(0.5))
|
||||
.frame(width: 4, height: 4)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Warning Stripe Header
|
||||
|
||||
private var warningStripeHeader: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Warning stripes
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<20, id: \.self) { i in
|
||||
Rectangle()
|
||||
.fill(i % 2 == 0 ? warningYellow : .black)
|
||||
.frame(width: 20, height: 8)
|
||||
}
|
||||
}
|
||||
|
||||
// System status bar
|
||||
HStack {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 6, height: 6)
|
||||
Text("SYSTEM ONLINE")
|
||||
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(Date.now.formatted(.dateTime.month().day().year()))
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(concreteGray.opacity(0.5))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Industrial Hero
|
||||
|
||||
private var industrialHero: some View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
// Sector label
|
||||
HStack(spacing: 8) {
|
||||
Rectangle()
|
||||
.fill(warningYellow)
|
||||
.frame(width: 4, height: 20)
|
||||
|
||||
Text("SECTOR A-1")
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(warningYellow)
|
||||
}
|
||||
|
||||
// Main title - stencil style
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("SPORTS")
|
||||
.font(.system(size: 42, weight: .black))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("TIME")
|
||||
.font(.system(size: 42, weight: .black))
|
||||
.foregroundStyle(warningYellow)
|
||||
}
|
||||
|
||||
// Description panel
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("// TRIP PLANNING SYSTEM")
|
||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(steelGray)
|
||||
|
||||
Text("Route optimization for stadium road trips. Multi-sport scheduling. Real-time coordination.")
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundStyle(textSecondary)
|
||||
.lineSpacing(4)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(concreteGray.opacity(0.3))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(steelGray.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
|
||||
// Industrial CTA
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
.font(.system(size: 12))
|
||||
|
||||
Text("INITIATE PLANNING")
|
||||
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("▶")
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
}
|
||||
.foregroundStyle(.black)
|
||||
.padding(18)
|
||||
.background(warningYellow)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(bgColor)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(steelGray.opacity(0.3), lineWidth: 2)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
// Section header
|
||||
HStack {
|
||||
HStack(spacing: 8) {
|
||||
Rectangle()
|
||||
.fill(alertRed)
|
||||
.frame(width: 4, height: 20)
|
||||
|
||||
Text("FEATURED ROUTES")
|
||||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 12))
|
||||
Text("REFRESH")
|
||||
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
}
|
||||
.foregroundStyle(steelGray)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(steelGray.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Industrial cards
|
||||
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
industrialTripCard(suggestedTrip.trip, index: index + 1)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func industrialTripCard(_ trip: Trip, index: Int) -> some View {
|
||||
HStack(spacing: 16) {
|
||||
// Index plate
|
||||
Text(String(format: "%02d", index))
|
||||
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.black)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(warningYellow)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(trip.name.uppercased())
|
||||
.font(.system(size: 13, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin")
|
||||
.font(.system(size: 10))
|
||||
Text("\(trip.stops.count) STOPS")
|
||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
||||
}
|
||||
.foregroundStyle(steelGray)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sportscourt")
|
||||
.font(.system(size: 10))
|
||||
Text("\(trip.totalGames) EVENTS")
|
||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
||||
}
|
||||
.foregroundStyle(steelGray)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(steelGray)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(concreteGray.opacity(0.2))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(steelGray.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Saved Section
|
||||
|
||||
private var savedSection: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
HStack {
|
||||
HStack(spacing: 8) {
|
||||
Rectangle()
|
||||
.fill(steelGray)
|
||||
.frame(width: 4, height: 20)
|
||||
|
||||
Text("SAVED ROUTES")
|
||||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("VIEW ALL ▶")
|
||||
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(steelGray)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(savedTrips.prefix(3)) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
HStack {
|
||||
Rectangle()
|
||||
.fill(steelGray.opacity(0.5))
|
||||
.frame(width: 3, height: 32)
|
||||
|
||||
Text(trip.name.uppercased())
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("[\(trip.stops.count)]")
|
||||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(warningYellow)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(steelGray.opacity(0.2))
|
||||
.frame(height: 1)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Industrial Footer
|
||||
|
||||
private var industrialFooter: some View {
|
||||
VStack(spacing: 12) {
|
||||
// Warning stripe
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<8, id: \.self) { i in
|
||||
Rectangle()
|
||||
.fill(i % 2 == 0 ? warningYellow.opacity(0.3) : .clear)
|
||||
.frame(width: 12, height: 4)
|
||||
}
|
||||
}
|
||||
|
||||
Text("// SPORTS TIME SYSTEMS")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(steelGray.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user