spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// ZyquoTheme.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The design system. Every color, font, spacing, radius, and shadow in the app9// comes from these tokens — views contain zero raw hex values or magic numbers.10// Same token system as Zyquo Cloud/Local; the Agent identity is a confident11// violet/plum. The light theme is the flagship (Phase 4 spec); dark derives12// from the same semantic tokens with a deep plum-charcoal command-center base.13//1415import SwiftUI1617// MARK: - Colors1819/// Semantic color tokens. Resolved per appearance via dynamic NSColor so the20/// system handles light/dark switching natively.21enum ZyquoColor {22 /// Main canvas — cool off-white with a faint violet undertone / deep plum-charcoal.23 static let background = dynamic(light: 0xFBFAFD, dark: 0x161420)24 /// Cards, bubbles, input bar, panels.25 static let surface = dynamic(light: 0xFFFFFF, dark: 0x1E1B2A)26 /// Hover states, code/terminal block background.27 static let surfaceSecondary = dynamic(light: 0xF4F2F8, dark: 0x262233)28 /// Primary actions, selection, links, run button (confident violet by29 /// default). The dynamic provider reads the CURRENT accent choice from30 /// ZyquoAccentResolver at draw time, so the whole token API stays static31 /// while Settings › Appearance swaps the accent live.32 static let accent = Color(nsColor: NSColor(name: nil) { appearance in33 let (light, dark) = ZyquoAccentResolver.current.accentHex34 let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light35 return NSColor(hex: hex)36 })37 /// Selected task rows, user bubble tint (follows the accent choice).38 static let accentSubtle = Color(nsColor: NSColor(name: nil) { appearance in39 let (light, dark) = ZyquoAccentResolver.current.subtleHex40 let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light41 return NSColor(hex: hex)42 })43 static let textPrimary = dynamic(light: 0x1B1A20, dark: 0xEAE8F0)44 static let textSecondary = dynamic(light: 0x6E6B78, dark: 0x9C98AA)45 static let textTertiary = dynamic(light: 0xA09DAC, dark: 0x6B6878)46 /// Hairline separators (draw at 0.5pt).47 static let border = dynamic(light: 0xE7E4EE, dark: 0x2E2A3A)48 /// Tool succeeded / approval needed / destructive & errors.49 static let success = dynamic(light: 0x2FA36B, dark: 0x43BD83)50 static let warning = dynamic(light: 0xD9822B, dark: 0xE59A4D)51 static let danger = dynamic(light: 0xD64545, dark: 0xE36363)5253 /// Builds a dynamic color that resolves per appearance.54 private static func dynamic(light: UInt32, dark: UInt32) -> Color {55 Color(nsColor: NSColor(name: nil) { appearance in56 let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light57 return NSColor(hex: hex)58 })59 }60}6162/// Holds the accent choice ZyquoColor's dynamic providers read at draw time.63/// Written only by AppearanceStore on the main thread (init + user change);64/// scene roots re-identify on accent change so every view redraws with the65/// new value immediately.66enum ZyquoAccentResolver {67 static var current: AccentChoice = .violet68}6970extension NSColor {71 /// 0xRRGGBB → NSColor (sRGB).72 convenience init(hex: UInt32) {73 self.init(74 srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,75 green: CGFloat((hex >> 8) & 0xFF) / 255,76 blue: CGFloat(hex & 0xFF) / 255,77 alpha: 178 )79 }80}8182// MARK: - Typography8384/// Type scale (SF Pro system font; SF Mono for code/terminal). Body scales with85/// the user's font size preference (12–18pt).86enum ZyquoFont {87 /// 20pt semibold — window/section titles.88 static let title = Font.system(size: 20, weight: .semibold)89 /// Message body at a given user-selected size (default 13.5).90 static func body(size: Double = 13.5) -> Font {91 .system(size: size, weight: .regular)92 }93 static func bodyEmphasis(size: Double = 13.5) -> Font {94 .system(size: size, weight: .medium)95 }96 /// 11pt — timestamps, step counters, captions, status pills.97 static let caption = Font.system(size: 11, weight: .regular)98 /// Code blocks, commands, and terminal output (SF Mono), scaled slightly below body.99 static func code(size: Double = 12.5) -> Font {100 .system(size: size, weight: .regular, design: .monospaced)101 }102 /// Mandatory generous line height for message text (spec: 1.45).103 static let bodyLineSpacingFactor: Double = 0.45104}105106// MARK: - Spacing, radii, shadows, metrics107108/// Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 / 32.109enum ZyquoSpacing {110 static let xxs: CGFloat = 4111 static let xs: CGFloat = 8112 static let sm: CGFloat = 12113 static let md: CGFloat = 16114 static let lg: CGFloat = 20115 static let xl: CGFloat = 24116 static let xxl: CGFloat = 32117}118119/// Corner radii: 6 (small controls), 10 (cards/bubbles), 14 (floating panels).120enum ZyquoRadius {121 static let small: CGFloat = 6122 static let medium: CGFloat = 10123 static let large: CGFloat = 14124}125126/// Shadows: extremely soft, used sparingly (floating panels, popovers, input bar).127enum ZyquoShadow {128 static let soft = ShadowStyle(color: .black.opacity(0.06), radius: 12, y: 2)129130 struct ShadowStyle {131 let color: Color132 let radius: CGFloat133 var x: CGFloat = 0134 var y: CGFloat = 0135 }136}137138/// Fixed layout metrics from the Phase 4 spec (command-center window).139enum ZyquoMetrics {140 static let sidebarWidth: CGFloat = 260141 static let headerHeight: CGFloat = 52142 static let maxMessageColumnWidth: CGFloat = 760143 static let planPanelWidth: CGFloat = 300144 static let contentInset: CGFloat = 16145 static let hairline: CGFloat = 0.5146 static let windowMinWidth: CGFloat = 1040147 static let windowMinHeight: CGFloat = 680148 static let windowDefaultWidth: CGFloat = 1320149 static let windowDefaultHeight: CGFloat = 860150 static let quickTaskWidth: CGFloat = 640151 static let settingsWidth: CGFloat = 760152 static let settingsHeight: CGFloat = 560153 static let verticalTurnRhythm: CGFloat = 16154 /// Default height of the Activity/Terminal drawer when expanded.155 static let terminalDrawerHeight: CGFloat = 240156 /// Width of the empty-state suggestion grid.157 static let emptyStateGridWidth: CGFloat = 520158}159160/// Motion tokens: hover 80ms ease, appear 150ms ease-out fade+rise, pressed 0.97.161enum ZyquoMotion {162 static let hover = Animation.easeInOut(duration: 0.08)163 static let appear = Animation.easeOut(duration: 0.15)164 static let pressedScale: CGFloat = 0.97165 static let picker = Animation.snappy166 /// Gentle repeating pulse for approval cards awaiting a decision.167 static let pulse = Animation.easeInOut(duration: 1.2).repeatForever(autoreverses: true)168 /// Rise distance for appearing step cards (150ms fade+rise).169 static let appearRise: CGFloat = 8170}171172// MARK: - View helpers173174extension View {175 /// Standard soft shadow for floating panels/popovers only.176 func zyquoSoftShadow() -> some View {177 shadow(178 color: ZyquoShadow.soft.color,179 radius: ZyquoShadow.soft.radius,180 x: ZyquoShadow.soft.x,181 y: ZyquoShadow.soft.y182 )183 }184}185186/// 0.5pt hairline separator in the border token color.187struct ZyquoHairline: View {188 var body: some View {189 Rectangle()190 .fill(ZyquoColor.border)191 .frame(height: ZyquoMetrics.hairline)192 }193}194