Add theme picker with 7 color schemes
- Teal: Cool cyan and teal tones (default) - Orbit: Bold navy and orange - Retro: Classic columbia blue - Clutch: Championship red and gold - Monochrome: Clean grayscale aesthetic - Sunset: Warm oranges and purples - Midnight: Deep blues and gold Features: - Theme selection persisted via UserDefaults - ThemeManager singleton for app-wide theme state - All Theme colors now dynamically switch based on selection - Settings UI shows color preview circles for each theme 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,26 +7,119 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - App Theme Selection
|
||||
|
||||
enum AppTheme: String, CaseIterable, Identifiable {
|
||||
case teal = "Teal"
|
||||
case orbit = "Orbit"
|
||||
case retro = "Retro"
|
||||
case clutch = "Clutch"
|
||||
case monochrome = "Monochrome"
|
||||
case sunset = "Sunset"
|
||||
case midnight = "Midnight"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String { rawValue }
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .teal: return "Cool cyan and teal tones"
|
||||
case .orbit: return "Bold navy and orange"
|
||||
case .retro: return "Classic columbia blue"
|
||||
case .clutch: return "Championship red and gold"
|
||||
case .monochrome: return "Clean grayscale aesthetic"
|
||||
case .sunset: return "Warm oranges and purples"
|
||||
case .midnight: return "Deep blues and gold"
|
||||
}
|
||||
}
|
||||
|
||||
var previewColors: [Color] {
|
||||
switch self {
|
||||
case .teal: return [Color(hex: "4ECDC4"), Color(hex: "1A535C"), Color(hex: "FFE66D")]
|
||||
case .orbit: return [Color(hex: "EB6E1F"), Color(hex: "002D62"), Color(hex: "FFFFFF")]
|
||||
case .retro: return [Color(hex: "418FDE"), Color(hex: "C41E3A"), Color(hex: "FFFFFF")]
|
||||
case .clutch: return [Color(hex: "CE1141"), Color(hex: "FDB927"), Color(hex: "041E42")]
|
||||
case .monochrome: return [Color(hex: "6B7280"), Color(hex: "1F2937"), Color(hex: "F9FAFB")]
|
||||
case .sunset: return [Color(hex: "F97316"), Color(hex: "7C3AED"), Color(hex: "EC4899")]
|
||||
case .midnight: return [Color(hex: "3B82F6"), Color(hex: "1E3A5F"), Color(hex: "F59E0B")]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Manager
|
||||
|
||||
@Observable
|
||||
final class ThemeManager {
|
||||
static let shared = ThemeManager()
|
||||
|
||||
var currentTheme: AppTheme {
|
||||
didSet {
|
||||
UserDefaults.standard.set(currentTheme.rawValue, forKey: "selectedTheme")
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
if let saved = UserDefaults.standard.string(forKey: "selectedTheme"),
|
||||
let theme = AppTheme(rawValue: saved) {
|
||||
self.currentTheme = theme
|
||||
} else {
|
||||
self.currentTheme = .teal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme
|
||||
|
||||
enum Theme {
|
||||
|
||||
// MARK: - Accent Colors (Same in Light/Dark)
|
||||
private static var current: AppTheme { ThemeManager.shared.currentTheme }
|
||||
|
||||
static let warmOrange = Color(hex: "4ECDC4") // Strong Cyan (primary accent)
|
||||
static let warmOrangeGlow = Color(hex: "6ED9D1") // Lighter cyan glow
|
||||
// MARK: - Primary Accent Color
|
||||
|
||||
static let routeGold = Color(hex: "FFE66D") // Royal Gold
|
||||
static let routeAmber = Color(hex: "FF6B6B") // Grapefruit Pink
|
||||
static var warmOrange: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "4ECDC4")
|
||||
case .orbit: return Color(hex: "EB6E1F")
|
||||
case .retro: return Color(hex: "418FDE")
|
||||
case .clutch: return Color(hex: "CE1141")
|
||||
case .monochrome: return Color(hex: "6B7280")
|
||||
case .sunset: return Color(hex: "F97316")
|
||||
case .midnight: return Color(hex: "3B82F6")
|
||||
}
|
||||
}
|
||||
|
||||
// New palette colors
|
||||
static let primaryCyan = Color(hex: "4ECDC4") // Strong Cyan
|
||||
static let darkTeal = Color(hex: "1A535C") // Dark Teal
|
||||
static let mintCream = Color(hex: "F7FFF7") // Mint Cream
|
||||
static let grapefruit = Color(hex: "FF6B6B") // Grapefruit Pink
|
||||
static let royalGold = Color(hex: "FFE66D") // Royal Gold
|
||||
static var warmOrangeGlow: Color {
|
||||
warmOrange.opacity(0.7)
|
||||
}
|
||||
|
||||
// MARK: - Sport Colors
|
||||
// MARK: - Secondary Accent Colors
|
||||
|
||||
static var routeGold: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "FFE66D")
|
||||
case .orbit: return Color(hex: "002D62")
|
||||
case .retro: return Color(hex: "C41E3A")
|
||||
case .clutch: return Color(hex: "FDB927")
|
||||
case .monochrome: return Color(hex: "9CA3AF")
|
||||
case .sunset: return Color(hex: "7C3AED")
|
||||
case .midnight: return Color(hex: "F59E0B")
|
||||
}
|
||||
}
|
||||
|
||||
static var routeAmber: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "FF6B6B")
|
||||
case .orbit: return Color(hex: "EB6E1F")
|
||||
case .retro: return Color(hex: "418FDE")
|
||||
case .clutch: return Color(hex: "CE1141")
|
||||
case .monochrome: return Color(hex: "4B5563")
|
||||
case .sunset: return Color(hex: "EC4899")
|
||||
case .midnight: return Color(hex: "60A5FA")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sport Colors (constant across themes)
|
||||
|
||||
static let mlbRed = Color(hex: "E31937")
|
||||
static let nbaOrange = Color(hex: "F58426")
|
||||
@@ -36,25 +129,163 @@ enum Theme {
|
||||
|
||||
// MARK: - Dark Mode Colors
|
||||
|
||||
static let darkBackground1 = Color(hex: "1A535C") // Dark Teal
|
||||
static let darkBackground2 = Color(hex: "143F46") // Darker teal
|
||||
static let darkCardBackground = Color(hex: "1E5A64") // Slightly lighter teal
|
||||
static let darkCardBackgroundLight = Color(hex: "2A6B75") // Card elevated
|
||||
static let darkSurfaceGlow = Color(hex: "4ECDC4").opacity(0.15) // Cyan glow
|
||||
static let darkTextPrimary = Color(hex: "F7FFF7") // Mint Cream
|
||||
static let darkTextSecondary = Color(hex: "B8E8E4") // Light cyan-tinted
|
||||
static let darkTextMuted = Color(hex: "7FADA8") // Muted teal
|
||||
static var darkBackground1: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "1A535C")
|
||||
case .orbit: return Color(hex: "002D62")
|
||||
case .retro: return Color(hex: "1A3A5C")
|
||||
case .clutch: return Color(hex: "041E42")
|
||||
case .monochrome: return Color(hex: "111827")
|
||||
case .sunset: return Color(hex: "1F1033")
|
||||
case .midnight: return Color(hex: "0F172A")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkBackground2: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "143F46")
|
||||
case .orbit: return Color(hex: "001A3A")
|
||||
case .retro: return Color(hex: "0F2840")
|
||||
case .clutch: return Color(hex: "020E1F")
|
||||
case .monochrome: return Color(hex: "0D1117")
|
||||
case .sunset: return Color(hex: "150A24")
|
||||
case .midnight: return Color(hex: "020617")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkCardBackground: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "1E5A64")
|
||||
case .orbit: return Color(hex: "0A3A6E")
|
||||
case .retro: return Color(hex: "234B6E")
|
||||
case .clutch: return Color(hex: "0A2847")
|
||||
case .monochrome: return Color(hex: "1F2937")
|
||||
case .sunset: return Color(hex: "2D1B4E")
|
||||
case .midnight: return Color(hex: "1E3A5F")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkCardBackgroundLight: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "2A6B75")
|
||||
case .orbit: return Color(hex: "154A7E")
|
||||
case .retro: return Color(hex: "2E5A7E")
|
||||
case .clutch: return Color(hex: "153557")
|
||||
case .monochrome: return Color(hex: "374151")
|
||||
case .sunset: return Color(hex: "3D2B5E")
|
||||
case .midnight: return Color(hex: "2A4A6F")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkSurfaceGlow: Color {
|
||||
warmOrange.opacity(0.15)
|
||||
}
|
||||
|
||||
static var darkTextPrimary: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "F7FFF7")
|
||||
case .orbit: return Color(hex: "FFFFFF")
|
||||
case .retro: return Color(hex: "FFFFFF")
|
||||
case .clutch: return Color(hex: "FFFFFF")
|
||||
case .monochrome: return Color(hex: "F9FAFB")
|
||||
case .sunset: return Color(hex: "FFF7ED")
|
||||
case .midnight: return Color(hex: "F8FAFC")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkTextSecondary: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "B8E8E4")
|
||||
case .orbit: return Color(hex: "FFB380")
|
||||
case .retro: return Color(hex: "A8C8E8")
|
||||
case .clutch: return Color(hex: "FFD080")
|
||||
case .monochrome: return Color(hex: "D1D5DB")
|
||||
case .sunset: return Color(hex: "FED7AA")
|
||||
case .midnight: return Color(hex: "93C5FD")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkTextMuted: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "7FADA8")
|
||||
case .orbit: return Color(hex: "8090A0")
|
||||
case .retro: return Color(hex: "7898B8")
|
||||
case .clutch: return Color(hex: "8898A8")
|
||||
case .monochrome: return Color(hex: "6B7280")
|
||||
case .sunset: return Color(hex: "9D8AA8")
|
||||
case .midnight: return Color(hex: "64748B")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Light Mode Colors
|
||||
|
||||
static let lightBackground1 = Color(hex: "F7FFF7") // Mint Cream
|
||||
static let lightBackground2 = Color(hex: "E8F8F5") // Slightly darker mint
|
||||
static let lightCardBackground = Color.white
|
||||
static let lightCardBackgroundElevated = Color(hex: "F7FFF7") // Mint cream
|
||||
static let lightSurfaceBorder = Color(hex: "4ECDC4").opacity(0.3) // Cyan border
|
||||
static let lightTextPrimary = Color(hex: "1A535C") // Dark Teal
|
||||
static let lightTextSecondary = Color(hex: "2A6B75") // Medium teal
|
||||
static let lightTextMuted = Color(hex: "5A9A94") // Light teal
|
||||
static var lightBackground1: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "F7FFF7")
|
||||
case .orbit: return Color(hex: "FFF8F5")
|
||||
case .retro: return Color(hex: "F5F9FF")
|
||||
case .clutch: return Color(hex: "FFFAF5")
|
||||
case .monochrome: return Color(hex: "F9FAFB")
|
||||
case .sunset: return Color(hex: "FFFBF5")
|
||||
case .midnight: return Color(hex: "F8FAFC")
|
||||
}
|
||||
}
|
||||
|
||||
static var lightBackground2: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "E8F8F5")
|
||||
case .orbit: return Color(hex: "FFF0E8")
|
||||
case .retro: return Color(hex: "E8F0FF")
|
||||
case .clutch: return Color(hex: "FFF0E8")
|
||||
case .monochrome: return Color(hex: "F3F4F6")
|
||||
case .sunset: return Color(hex: "FFF3E8")
|
||||
case .midnight: return Color(hex: "EFF6FF")
|
||||
}
|
||||
}
|
||||
|
||||
static var lightCardBackground: Color { .white }
|
||||
|
||||
static var lightCardBackgroundElevated: Color { lightBackground1 }
|
||||
|
||||
static var lightSurfaceBorder: Color {
|
||||
warmOrange.opacity(0.3)
|
||||
}
|
||||
|
||||
static var lightTextPrimary: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "1A535C")
|
||||
case .orbit: return Color(hex: "002D62")
|
||||
case .retro: return Color(hex: "1A3A5C")
|
||||
case .clutch: return Color(hex: "041E42")
|
||||
case .monochrome: return Color(hex: "111827")
|
||||
case .sunset: return Color(hex: "431407")
|
||||
case .midnight: return Color(hex: "1E3A5F")
|
||||
}
|
||||
}
|
||||
|
||||
static var lightTextSecondary: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "2A6B75")
|
||||
case .orbit: return Color(hex: "1A4A7A")
|
||||
case .retro: return Color(hex: "2A5A7C")
|
||||
case .clutch: return Color(hex: "1A3A5A")
|
||||
case .monochrome: return Color(hex: "374151")
|
||||
case .sunset: return Color(hex: "7C2D12")
|
||||
case .midnight: return Color(hex: "2A4A6F")
|
||||
}
|
||||
}
|
||||
|
||||
static var lightTextMuted: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "5A9A94")
|
||||
case .orbit: return Color(hex: "5A7A9A")
|
||||
case .retro: return Color(hex: "5A8AAA")
|
||||
case .clutch: return Color(hex: "6A7A8A")
|
||||
case .monochrome: return Color(hex: "6B7280")
|
||||
case .sunset: return Color(hex: "9A6A5A")
|
||||
case .midnight: return Color(hex: "64748B")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Adaptive Gradients
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ final class SettingsViewModel {
|
||||
|
||||
// MARK: - User Preferences (persisted via UserDefaults)
|
||||
|
||||
var selectedTheme: AppTheme {
|
||||
didSet {
|
||||
ThemeManager.shared.currentTheme = selectedTheme
|
||||
}
|
||||
}
|
||||
|
||||
var selectedSports: Set<Sport> {
|
||||
didSet { savePreferences() }
|
||||
}
|
||||
@@ -41,6 +47,9 @@ final class SettingsViewModel {
|
||||
// Load from UserDefaults using local variables first
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
// Theme
|
||||
self.selectedTheme = ThemeManager.shared.currentTheme
|
||||
|
||||
// Selected sports
|
||||
if let sportStrings = defaults.stringArray(forKey: "selectedSports") {
|
||||
self.selectedSports = Set(sportStrings.compactMap { Sport(rawValue: $0) })
|
||||
@@ -93,6 +102,7 @@ final class SettingsViewModel {
|
||||
}
|
||||
|
||||
func resetToDefaults() {
|
||||
selectedTheme = .teal
|
||||
selectedSports = Set(Sport.supported)
|
||||
maxDrivingHoursPerDay = 8
|
||||
maxTripOptions = 10
|
||||
|
||||
@@ -11,6 +11,9 @@ struct SettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
// Theme Selection
|
||||
themeSection
|
||||
|
||||
// Sports Preferences
|
||||
sportsSection
|
||||
|
||||
@@ -37,6 +40,58 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Section
|
||||
|
||||
private var themeSection: some View {
|
||||
Section {
|
||||
ForEach(AppTheme.allCases) { theme in
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
viewModel.selectedTheme = theme
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
// Color preview circles
|
||||
HStack(spacing: -6) {
|
||||
ForEach(Array(theme.previewColors.enumerated()), id: \.offset) { _, color in
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 24, height: 24)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(theme.displayName)
|
||||
.font(.body)
|
||||
.foregroundStyle(.primary)
|
||||
Text(theme.description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if viewModel.selectedTheme == theme {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
.font(.title3)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} header: {
|
||||
Text("Theme")
|
||||
} footer: {
|
||||
Text("Choose a color scheme for the app.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sports Section
|
||||
|
||||
private var sportsSection: some View {
|
||||
|
||||
Reference in New Issue
Block a user