// // ZyquoTheme.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The design system. Every color, font, spacing, radius, and shadow in the app // comes from these tokens — views contain zero raw hex values or magic numbers. // Same token system as Zyquo Cloud/Local; the Agent identity is a confident // violet/plum. The light theme is the flagship (Phase 4 spec); dark derives // from the same semantic tokens with a deep plum-charcoal command-center base. // import SwiftUI // MARK: - Colors /// Semantic color tokens. Resolved per appearance via dynamic NSColor so the /// system handles light/dark switching natively. enum ZyquoColor { /// Main canvas — cool off-white with a faint violet undertone / deep plum-charcoal. static let background = dynamic(light: 0xFBFAFD, dark: 0x161420) /// Cards, bubbles, input bar, panels. static let surface = dynamic(light: 0xFFFFFF, dark: 0x1E1B2A) /// Hover states, code/terminal block background. static let surfaceSecondary = dynamic(light: 0xF4F2F8, dark: 0x262233) /// Primary actions, selection, links, run button (confident violet by /// default). The dynamic provider reads the CURRENT accent choice from /// ZyquoAccentResolver at draw time, so the whole token API stays static /// while Settings › Appearance swaps the accent live. static let accent = Color(nsColor: NSColor(name: nil) { appearance in let (light, dark) = ZyquoAccentResolver.current.accentHex let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) /// Selected task rows, user bubble tint (follows the accent choice). static let accentSubtle = Color(nsColor: NSColor(name: nil) { appearance in let (light, dark) = ZyquoAccentResolver.current.subtleHex let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) static let textPrimary = dynamic(light: 0x1B1A20, dark: 0xEAE8F0) static let textSecondary = dynamic(light: 0x6E6B78, dark: 0x9C98AA) static let textTertiary = dynamic(light: 0xA09DAC, dark: 0x6B6878) /// Hairline separators (draw at 0.5pt). static let border = dynamic(light: 0xE7E4EE, dark: 0x2E2A3A) /// Tool succeeded / approval needed / destructive & errors. static let success = dynamic(light: 0x2FA36B, dark: 0x43BD83) static let warning = dynamic(light: 0xD9822B, dark: 0xE59A4D) static let danger = dynamic(light: 0xD64545, dark: 0xE36363) /// Builds a dynamic color that resolves per appearance. private static func dynamic(light: UInt32, dark: UInt32) -> Color { Color(nsColor: NSColor(name: nil) { appearance in let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) } } /// Holds the accent choice ZyquoColor's dynamic providers read at draw time. /// Written only by AppearanceStore on the main thread (init + user change); /// scene roots re-identify on accent change so every view redraws with the /// new value immediately. enum ZyquoAccentResolver { static var current: AccentChoice = .violet } extension NSColor { /// 0xRRGGBB → NSColor (sRGB). convenience init(hex: UInt32) { self.init( srgbRed: CGFloat((hex >> 16) & 0xFF) / 255, green: CGFloat((hex >> 8) & 0xFF) / 255, blue: CGFloat(hex & 0xFF) / 255, alpha: 1 ) } } // MARK: - Typography /// Type scale (SF Pro system font; SF Mono for code/terminal). Body scales with /// the user's font size preference (12–18pt). enum ZyquoFont { /// 20pt semibold — window/section titles. static let title = Font.system(size: 20, weight: .semibold) /// Message body at a given user-selected size (default 13.5). static func body(size: Double = 13.5) -> Font { .system(size: size, weight: .regular) } static func bodyEmphasis(size: Double = 13.5) -> Font { .system(size: size, weight: .medium) } /// 11pt — timestamps, step counters, captions, status pills. static let caption = Font.system(size: 11, weight: .regular) /// Code blocks, commands, and terminal output (SF Mono), scaled slightly below body. static func code(size: Double = 12.5) -> Font { .system(size: size, weight: .regular, design: .monospaced) } /// Mandatory generous line height for message text (spec: 1.45). static let bodyLineSpacingFactor: Double = 0.45 } // MARK: - Spacing, radii, shadows, metrics /// Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 / 32. enum ZyquoSpacing { 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 } /// Corner radii: 6 (small controls), 10 (cards/bubbles), 14 (floating panels). enum ZyquoRadius { static let small: CGFloat = 6 static let medium: CGFloat = 10 static let large: CGFloat = 14 } /// Shadows: extremely soft, used sparingly (floating panels, popovers, input bar). enum ZyquoShadow { static let soft = ShadowStyle(color: .black.opacity(0.06), radius: 12, y: 2) struct ShadowStyle { let color: Color let radius: CGFloat var x: CGFloat = 0 var y: CGFloat = 0 } } /// Fixed layout metrics from the Phase 4 spec (command-center window). enum ZyquoMetrics { static let sidebarWidth: CGFloat = 260 static let headerHeight: CGFloat = 52 static let maxMessageColumnWidth: CGFloat = 760 static let planPanelWidth: CGFloat = 300 static let contentInset: CGFloat = 16 static let hairline: CGFloat = 0.5 static let windowMinWidth: CGFloat = 1040 static let windowMinHeight: CGFloat = 680 static let windowDefaultWidth: CGFloat = 1320 static let windowDefaultHeight: CGFloat = 860 static let quickTaskWidth: CGFloat = 640 static let settingsWidth: CGFloat = 760 static let settingsHeight: CGFloat = 560 static let verticalTurnRhythm: CGFloat = 16 /// Default height of the Activity/Terminal drawer when expanded. static let terminalDrawerHeight: CGFloat = 240 /// Width of the empty-state suggestion grid. static let emptyStateGridWidth: CGFloat = 520 } /// Motion tokens: hover 80ms ease, appear 150ms ease-out fade+rise, pressed 0.97. enum ZyquoMotion { static let hover = Animation.easeInOut(duration: 0.08) static let appear = Animation.easeOut(duration: 0.15) static let pressedScale: CGFloat = 0.97 static let picker = Animation.snappy /// Gentle repeating pulse for approval cards awaiting a decision. static let pulse = Animation.easeInOut(duration: 1.2).repeatForever(autoreverses: true) /// Rise distance for appearing step cards (150ms fade+rise). static let appearRise: CGFloat = 8 } // MARK: - View helpers extension View { /// Standard soft shadow for floating panels/popovers only. func zyquoSoftShadow() -> some View { shadow( color: ZyquoShadow.soft.color, radius: ZyquoShadow.soft.radius, x: ZyquoShadow.soft.x, y: ZyquoShadow.soft.y ) } } /// 0.5pt hairline separator in the border token color. struct ZyquoHairline: View { var body: some View { Rectangle() .fill(ZyquoColor.border) .frame(height: ZyquoMetrics.hairline) } }