// // AppearanceStore.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // User appearance preferences: theme mode, accent choice, chat font size. // Persisted to settings.json via PersistenceService. // import SwiftUI /// Accent color choices offered in Settings → Appearance. Sky-indigo is the /// flagship default; alternates keep the same subtle-tint relationship. enum AccentChoice: String, Codable, CaseIterable, Identifiable { case skyIndigo case graphite case teal case amber case rose var id: String { rawValue } var displayName: String { switch self { case .skyIndigo: return "Sky Indigo" case .graphite: return "Graphite" case .teal: return "Teal" case .amber: return "Amber" case .rose: return "Rose" } } /// (light, dark) accent hex pair. var accentHex: (UInt32, UInt32) { switch self { case .skyIndigo: return (0x4E6AF0, 0x6D84F5) case .graphite: return (0x5B6270, 0x8A93A6) case .teal: return (0x1D9A8F, 0x3BB5AA) case .amber: return (0xC77D1D, 0xDD9B45) case .rose: return (0xC94F7C, 0xDE7099) } } /// (light, dark) subtle-tint hex pair (selected rows, user bubbles). var subtleHex: (UInt32, UInt32) { switch self { case .skyIndigo: return (0xEBEFFD, 0x28304C) case .graphite: return (0xEEF0F4, 0x2A2E3A) case .teal: return (0xE6F6F4, 0x1F3A38) case .amber: return (0xFBF1E3, 0x3D3222) case .rose: return (0xFAEAF1, 0x3D2733) } } } enum ThemeMode: String, Codable, CaseIterable, Identifiable { case system, light, dark var id: String { rawValue } var displayName: String { switch self { case .system: return "System" case .light: return "Light" case .dark: return "Dark" } } var colorScheme: ColorScheme? { switch self { case .system: return nil case .light: return .light case .dark: return .dark } } } /// Observable appearance preferences, persisted as part of app settings. @MainActor final class AppearanceStore: ObservableObject { struct Stored: Codable { var themeMode: ThemeMode = .system var accent: AccentChoice = .skyIndigo var chatFontSize: Double = 13.5 } static let fileName = "appearance.json" @Published var themeMode: ThemeMode { didSet { save() } } @Published var accent: AccentChoice { didSet { save() } } /// Chat body font size, clamped to the spec's 12–18pt range. @Published var chatFontSize: Double { didSet { let clamped = min(18, max(12, chatFontSize)) if clamped != chatFontSize { chatFontSize = clamped } save() } } private let persistence: PersistenceService init(persistence: PersistenceService = .shared) { self.persistence = persistence let stored = persistence.load(Stored.self, from: Self.fileName) ?? Stored() themeMode = stored.themeMode accent = stored.accent chatFontSize = stored.chatFontSize } /// Current accent color resolved for the active appearance. var accentColor: Color { let (light, dark) = accent.accentHex return Color(nsColor: NSColor(name: nil) { appearance in let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) } /// Current subtle accent tint resolved for the active appearance. var accentSubtleColor: Color { let (light, dark) = accent.subtleHex return Color(nsColor: NSColor(name: nil) { appearance in let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) } private func save() { persistence.save( Stored(themeMode: themeMode, accent: accent, chatFontSize: chatFontSize), to: Self.fileName ) } }