// // AppearanceStore.swift // Zyquo Agent // // 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. Violet is the /// Agent flagship default; alternates keep the same subtle-tint relationship /// (graphite, sky, emerald, amber per the Phase 4 spec). enum AccentChoice: String, Codable, CaseIterable, Identifiable { case violet case graphite case sky case emerald case amber var id: String { rawValue } var displayName: String { switch self { case .violet: return "Violet" case .graphite: return "Graphite" case .sky: return "Sky" case .emerald: return "Emerald" case .amber: return "Amber" } } /// (light, dark) accent hex pair. var accentHex: (UInt32, UInt32) { switch self { case .violet: return (0x7A5AF0, 0x9B82F6) case .graphite: return (0x5B6270, 0x8A93A6) case .sky: return (0x4E6AF0, 0x6D84F5) case .emerald: return (0x1D9A6B, 0x3BB58A) case .amber: return (0xC77D1D, 0xDD9B45) } } /// (light, dark) subtle-tint hex pair (selected rows, user bubbles). var subtleHex: (UInt32, UInt32) { switch self { case .violet: return (0xEFEBFD, 0x2E2749) case .graphite: return (0xEEF0F4, 0x2A2E3A) case .sky: return (0xEBEFFD, 0x28304C) case .emerald: return (0xE6F6EF, 0x1F3A2F) case .amber: return (0xFBF1E3, 0x3D3222) } } } 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 = .violet var chatFontSize: Double = 13.5 } static let fileName = "appearance.json" @Published var themeMode: ThemeMode { didSet { save() } } @Published var accent: AccentChoice { didSet { // Feed the design-system resolver so the static ZyquoColor.accent // token picks up the new choice on the next draw. ZyquoAccentResolver.current = accent 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 ZyquoAccentResolver.current = stored.accent } /// 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 ) } }