SPB Git

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%
7.4 KB · 194 lines swift
Raw Blame History
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