// // ZyquoTheme.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The design system — Zyquo family DNA with the Router's graphite-cyan // "control room" identity. Every color, font, spacing, radius, and shadow in // the app comes from these tokens; views contain zero raw hex values or // magic numbers. Light is the flagship; dark derives as a deep graphite // "ops room". SF Mono everywhere data lives: endpoints, model IDs, logs, // JSON, keys. // import SwiftUI // MARK: - Colors enum ZyquoColor { /// Canvas — crisp cool off-white / deep graphite ops room. static let background = dynamic(light: 0xFAFBFC, dark: 0x15181C) /// Cards, panels. static let surface = dynamic(light: 0xFFFFFF, dark: 0x1D2126) /// Hover, log rows, code blocks. static let surfaceSecondary = dynamic(light: 0xF1F4F6, dark: 0x252A31) /// Signal cyan — Start button, active states, links, request charts. static let accent = dynamic(light: 0x0891B2, dark: 0x22B8D4) /// Graphite pair — secondary emphasis, latency charts. static let graphite = dynamic(light: 0x374151, dark: 0x9AA4B2) /// Selected rows, active-server tint. static let accentSubtle = dynamic(light: 0xE5F5F9, dark: 0x143A44) static let textPrimary = dynamic(light: 0x191C1F, dark: 0xE9ECEF) static let textSecondary = dynamic(light: 0x697077, dark: 0x9BA3AA) static let textTertiary = dynamic(light: 0x9BA3AA, dark: 0x6B7280) /// Hairline separators (draw at 0.5pt). static let border = dynamic(light: 0xE4E8EB, dark: 0x2E343B) /// Server running / degraded / stopped & 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) /// Chart series tokens: cyan = requests, graphite = latency. static let chartRequests = accent static let chartLatency = graphite /// Per-provider hue for breakdown bars/badges (stable, muted family). static func providerHue(_ provider: ProviderID) -> Color { switch provider { case .openai: return dynamic(light: 0x10A37F, dark: 0x2CBF9B) case .anthropic: return dynamic(light: 0xC96F4A, dark: 0xD98A66) case .xai: return dynamic(light: 0x30343B, dark: 0xAAB2BD) case .mistral: return dynamic(light: 0xE8722E, dark: 0xF08A4D) case .gemini: return dynamic(light: 0x4285F4, dark: 0x6BA1F7) case .qwen: return dynamic(light: 0x7B4DE0, dark: 0x9670E8) case .deepseek: return dynamic(light: 0x4D6BFE, dark: 0x7189FE) case .kimi: return dynamic(light: 0x1F7A67, dark: 0x359984) case .perplexity: return dynamic(light: 0x20808D, dark: 0x3D9AA7) case .together: return dynamic(light: 0x2E62D9, dark: 0x5581E3) case .deepinfra: return dynamic(light: 0x5B8DEF, dark: 0x7BA4F3) case .cerebras: return dynamic(light: 0xB0491F, dark: 0xC66A42) case .custom: return graphite } } 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) }) } } 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; SF Mono for everything data-shaped). enum ZyquoFont { /// 20pt semibold — window/section titles. static let title = Font.system(size: 20, weight: .semibold) /// 15pt semibold — card titles. static let heading = Font.system(size: 15, weight: .semibold) static func body(size: Double = 13) -> Font { .system(size: size, weight: .regular) } static func bodyEmphasis(size: Double = 13) -> Font { .system(size: size, weight: .medium) } /// 11pt — captions, timestamps, table headers. static let caption = Font.system(size: 11, weight: .regular) /// SF Mono — endpoints, model IDs, logs, JSON, keys, metrics. static func mono(size: Double = 12.5, weight: Font.Weight = .regular) -> Font { .system(size: size, weight: weight, design: .monospaced) } /// Big dashboard numbers. static let tileValue = Font.system(size: 22, weight: .semibold, design: .rounded) } // MARK: - Spacing, radii, shadows, metrics, motion /// 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 — generous, modern rounding: 10 (small controls), /// 16 (cards), 22 (floating panels/hero surfaces). enum ZyquoRadius { static let small: CGFloat = 10 static let medium: CGFloat = 16 static let large: CGFloat = 22 } /// Ultra-soft shadows, floating panels only. 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. enum ZyquoMetrics { static let navigatorWidth: CGFloat = 240 static let hairline: CGFloat = 0.5 static let windowMinWidth: CGFloat = 1020 static let windowMinHeight: CGFloat = 660 static let windowDefaultWidth: CGFloat = 1280 static let windowDefaultHeight: CGFloat = 820 static let settingsWidth: CGFloat = 720 static let settingsHeight: CGFloat = 540 static let contentInset: CGFloat = 20 static let tileMinWidth: CGFloat = 148 } /// Motion tokens — the status pill transitions cleanly, tiles update smoothly. enum ZyquoMotion { static let hover = Animation.easeInOut(duration: 0.08) static let state = Animation.easeOut(duration: 0.15) static let pressedScale: CGFloat = 0.97 static let live = Animation.easeInOut(duration: 0.25) } // 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 ) } /// Standard card container: surface, generous continuous rounding, /// hairline border, whisper of depth. func zyquoCard() -> some View { background( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .fill(ZyquoColor.surface) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) .shadow(color: .black.opacity(0.03), radius: 6, y: 2) ) } } /// 0.5pt hairline separator in the border token color. struct ZyquoHairline: View { var body: some View { Rectangle() .fill(ZyquoColor.border) .frame(height: ZyquoMetrics.hairline) } }