Remove CBB (~5,000+ games per season) to reduce complexity. Changes: - Remove .cbb enum case from Sport - Remove CBB theme color (cbbMint) - Update documentation to reflect 7 supported leagues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
420 lines
13 KiB
Swift
420 lines
13 KiB
Swift
//
|
|
// Theme.swift
|
|
// SportsTime
|
|
//
|
|
// Central design system for colors, typography, spacing, and animations.
|
|
//
|
|
|
|
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: "808080"), Color(hex: "1A1A1A"), Color(hex: "FAFAFA")]
|
|
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 {
|
|
|
|
private static var current: AppTheme { ThemeManager.shared.currentTheme }
|
|
|
|
// MARK: - Primary Accent Color
|
|
|
|
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: "808080")
|
|
case .sunset: return Color(hex: "F97316")
|
|
case .midnight: return Color(hex: "3B82F6")
|
|
}
|
|
}
|
|
|
|
static var warmOrangeGlow: Color {
|
|
warmOrange.opacity(0.7)
|
|
}
|
|
|
|
// 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: "A0A0A0")
|
|
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: "505050")
|
|
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")
|
|
static let nhlBlue = Color(hex: "003087")
|
|
static let nflBrown = Color(hex: "8B5A2B")
|
|
static let mlsGreen = Color(hex: "00A651")
|
|
static let wnbaPurple = Color(hex: "FF6F20") // WNBA orange
|
|
static let nwslTeal = Color(hex: "009688") // NWSL teal
|
|
|
|
// MARK: - Dark Mode Colors
|
|
|
|
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: "121212")
|
|
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: "0A0A0A")
|
|
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: "1C1C1C")
|
|
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: "2A2A2A")
|
|
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: "FAFAFA")
|
|
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: "D0D0D0")
|
|
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: "707070")
|
|
case .sunset: return Color(hex: "9D8AA8")
|
|
case .midnight: return Color(hex: "64748B")
|
|
}
|
|
}
|
|
|
|
// MARK: - Light Mode Colors
|
|
|
|
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: "FAFAFA")
|
|
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: "F0F0F0")
|
|
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: "1A1A1A")
|
|
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: "404040")
|
|
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: "707070")
|
|
case .sunset: return Color(hex: "9A6A5A")
|
|
case .midnight: return Color(hex: "64748B")
|
|
}
|
|
}
|
|
|
|
// MARK: - Adaptive Gradients
|
|
|
|
static func backgroundGradient(_ colorScheme: ColorScheme) -> LinearGradient {
|
|
colorScheme == .dark
|
|
? LinearGradient(colors: [darkBackground1, darkBackground2], startPoint: .top, endPoint: .bottom)
|
|
: LinearGradient(colors: [lightBackground1, lightBackground2], startPoint: .top, endPoint: .bottom)
|
|
}
|
|
|
|
// MARK: - Adaptive Colors
|
|
|
|
static func cardBackground(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkCardBackground : lightCardBackground
|
|
}
|
|
|
|
static func cardBackgroundElevated(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkCardBackgroundLight : lightCardBackgroundElevated
|
|
}
|
|
|
|
static func textPrimary(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkTextPrimary : lightTextPrimary
|
|
}
|
|
|
|
static func textSecondary(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkTextSecondary : lightTextSecondary
|
|
}
|
|
|
|
static func textMuted(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkTextMuted : lightTextMuted
|
|
}
|
|
|
|
static func surfaceGlow(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? darkSurfaceGlow : lightSurfaceBorder
|
|
}
|
|
|
|
static func cardShadow(_ colorScheme: ColorScheme) -> Color {
|
|
colorScheme == .dark ? Color.black.opacity(0.3) : Color.black.opacity(0.08)
|
|
}
|
|
|
|
// MARK: - Typography
|
|
|
|
enum FontSize {
|
|
static let heroTitle: CGFloat = 34
|
|
static let sectionTitle: CGFloat = 24
|
|
static let cardTitle: CGFloat = 18
|
|
static let body: CGFloat = 16
|
|
static let caption: CGFloat = 14
|
|
static let micro: CGFloat = 12
|
|
}
|
|
|
|
// MARK: - Spacing
|
|
|
|
enum Spacing {
|
|
static let xxs: CGFloat = 4
|
|
static let xs: CGFloat = 8
|
|
static let sm: CGFloat = 12
|
|
static let md: CGFloat = 16
|
|
static let lg: CGFloat = 20
|
|
static let xl: CGFloat = 24
|
|
static let xxl: CGFloat = 32
|
|
}
|
|
|
|
// MARK: - Corner Radius
|
|
|
|
enum CornerRadius {
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
static let xlarge: CGFloat = 20
|
|
}
|
|
|
|
// MARK: - Animation
|
|
|
|
enum Animation {
|
|
static let springResponse: Double = 0.3
|
|
static let springDamping: Double = 0.7
|
|
static let staggerDelay: Double = 0.1
|
|
static let routeDrawDuration: Double = 2.0
|
|
|
|
static var spring: SwiftUI.Animation {
|
|
.spring(response: springResponse, dampingFraction: springDamping)
|
|
}
|
|
|
|
static var gentleSpring: SwiftUI.Animation {
|
|
.spring(response: 0.5, dampingFraction: 0.8)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Color Hex Extension
|
|
|
|
extension Color {
|
|
init(hex: String) {
|
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
var int: UInt64 = 0
|
|
Scanner(string: hex).scanHexInt64(&int)
|
|
let a, r, g, b: UInt64
|
|
switch hex.count {
|
|
case 3: // RGB (12-bit)
|
|
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
|
case 6: // RGB (24-bit)
|
|
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
|
case 8: // ARGB (32-bit)
|
|
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
|
default:
|
|
(a, r, g, b) = (255, 0, 0, 0)
|
|
}
|
|
self.init(
|
|
.sRGB,
|
|
red: Double(r) / 255,
|
|
green: Double(g) / 255,
|
|
blue: Double(b) / 255,
|
|
opacity: Double(a) / 255
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Environment Key for Theme
|
|
|
|
private struct ColorSchemeKey: EnvironmentKey {
|
|
static let defaultValue: ColorScheme = .light
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var themeColorScheme: ColorScheme {
|
|
get { self[ColorSchemeKey.self] }
|
|
set { self[ColorSchemeKey.self] = newValue }
|
|
}
|
|
}
|