spb/zyquo-router Public MIT
One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).
Swift 95.7%
Python 2.3%
Shell 1.2%
Makefile 0.9%
1//2// ZyquoTheme.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The design system — Zyquo family DNA with the Router's graphite-cyan9// "control room" identity. Every color, font, spacing, radius, and shadow in10// the app comes from these tokens; views contain zero raw hex values or11// magic numbers. Light is the flagship; dark derives as a deep graphite12// "ops room". SF Mono everywhere data lives: endpoints, model IDs, logs,13// JSON, keys.14//1516import SwiftUI1718// MARK: - Colors1920enum ZyquoColor {21 /// Canvas — crisp cool off-white / deep graphite ops room.22 static let background = dynamic(light: 0xFAFBFC, dark: 0x15181C)23 /// Cards, panels.24 static let surface = dynamic(light: 0xFFFFFF, dark: 0x1D2126)25 /// Hover, log rows, code blocks.26 static let surfaceSecondary = dynamic(light: 0xF1F4F6, dark: 0x252A31)27 /// Signal cyan — Start button, active states, links, request charts.28 static let accent = dynamic(light: 0x0891B2, dark: 0x22B8D4)29 /// Graphite pair — secondary emphasis, latency charts.30 static let graphite = dynamic(light: 0x374151, dark: 0x9AA4B2)31 /// Selected rows, active-server tint.32 static let accentSubtle = dynamic(light: 0xE5F5F9, dark: 0x143A44)33 static let textPrimary = dynamic(light: 0x191C1F, dark: 0xE9ECEF)34 static let textSecondary = dynamic(light: 0x697077, dark: 0x9BA3AA)35 static let textTertiary = dynamic(light: 0x9BA3AA, dark: 0x6B7280)36 /// Hairline separators (draw at 0.5pt).37 static let border = dynamic(light: 0xE4E8EB, dark: 0x2E343B)38 /// Server running / degraded / stopped & errors.39 static let success = dynamic(light: 0x2FA36B, dark: 0x43BD83)40 static let warning = dynamic(light: 0xD9822B, dark: 0xE59A4D)41 static let danger = dynamic(light: 0xD64545, dark: 0xE36363)4243 /// Chart series tokens: cyan = requests, graphite = latency.44 static let chartRequests = accent45 static let chartLatency = graphite4647 /// Per-provider hue for breakdown bars/badges (stable, muted family).48 static func providerHue(_ provider: ProviderID) -> Color {49 switch provider {50 case .openai: return dynamic(light: 0x10A37F, dark: 0x2CBF9B)51 case .anthropic: return dynamic(light: 0xC96F4A, dark: 0xD98A66)52 case .xai: return dynamic(light: 0x30343B, dark: 0xAAB2BD)53 case .mistral: return dynamic(light: 0xE8722E, dark: 0xF08A4D)54 case .gemini: return dynamic(light: 0x4285F4, dark: 0x6BA1F7)55 case .qwen: return dynamic(light: 0x7B4DE0, dark: 0x9670E8)56 case .deepseek: return dynamic(light: 0x4D6BFE, dark: 0x7189FE)57 case .kimi: return dynamic(light: 0x1F7A67, dark: 0x359984)58 case .perplexity: return dynamic(light: 0x20808D, dark: 0x3D9AA7)59 case .together: return dynamic(light: 0x2E62D9, dark: 0x5581E3)60 case .deepinfra: return dynamic(light: 0x5B8DEF, dark: 0x7BA4F3)61 case .cerebras: return dynamic(light: 0xB0491F, dark: 0xC66A42)62 case .custom: return graphite63 }64 }6566 static func dynamic(light: UInt32, dark: UInt32) -> Color {67 Color(nsColor: NSColor(name: nil) { appearance in68 let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light69 return NSColor(hex: hex)70 })71 }72}7374extension NSColor {75 /// 0xRRGGBB → NSColor (sRGB).76 convenience init(hex: UInt32) {77 self.init(78 srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,79 green: CGFloat((hex >> 8) & 0xFF) / 255,80 blue: CGFloat(hex & 0xFF) / 255,81 alpha: 182 )83 }84}8586// MARK: - Typography8788/// Type scale (SF Pro; SF Mono for everything data-shaped).89enum ZyquoFont {90 /// 20pt semibold — window/section titles.91 static let title = Font.system(size: 20, weight: .semibold)92 /// 15pt semibold — card titles.93 static let heading = Font.system(size: 15, weight: .semibold)94 static func body(size: Double = 13) -> Font {95 .system(size: size, weight: .regular)96 }97 static func bodyEmphasis(size: Double = 13) -> Font {98 .system(size: size, weight: .medium)99 }100 /// 11pt — captions, timestamps, table headers.101 static let caption = Font.system(size: 11, weight: .regular)102 /// SF Mono — endpoints, model IDs, logs, JSON, keys, metrics.103 static func mono(size: Double = 12.5, weight: Font.Weight = .regular) -> Font {104 .system(size: size, weight: weight, design: .monospaced)105 }106 /// Big dashboard numbers.107 static let tileValue = Font.system(size: 22, weight: .semibold, design: .rounded)108}109110// MARK: - Spacing, radii, shadows, metrics, motion111112/// Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 / 32.113enum ZyquoSpacing {114 static let xxs: CGFloat = 4115 static let xs: CGFloat = 8116 static let sm: CGFloat = 12117 static let md: CGFloat = 16118 static let lg: CGFloat = 20119 static let xl: CGFloat = 24120 static let xxl: CGFloat = 32121}122123/// Corner radii — generous, modern rounding: 10 (small controls),124/// 16 (cards), 22 (floating panels/hero surfaces).125enum ZyquoRadius {126 static let small: CGFloat = 10127 static let medium: CGFloat = 16128 static let large: CGFloat = 22129}130131/// Ultra-soft shadows, floating panels only.132enum ZyquoShadow {133 static let soft = ShadowStyle(color: .black.opacity(0.06), radius: 12, y: 2)134135 struct ShadowStyle {136 let color: Color137 let radius: CGFloat138 var x: CGFloat = 0139 var y: CGFloat = 0140 }141}142143/// Fixed layout metrics from the Phase 4 spec.144enum ZyquoMetrics {145 static let navigatorWidth: CGFloat = 240146 static let hairline: CGFloat = 0.5147 static let windowMinWidth: CGFloat = 1020148 static let windowMinHeight: CGFloat = 660149 static let windowDefaultWidth: CGFloat = 1280150 static let windowDefaultHeight: CGFloat = 820151 static let settingsWidth: CGFloat = 720152 static let settingsHeight: CGFloat = 540153 static let contentInset: CGFloat = 20154 static let tileMinWidth: CGFloat = 148155}156157/// Motion tokens — the status pill transitions cleanly, tiles update smoothly.158enum ZyquoMotion {159 static let hover = Animation.easeInOut(duration: 0.08)160 static let state = Animation.easeOut(duration: 0.15)161 static let pressedScale: CGFloat = 0.97162 static let live = Animation.easeInOut(duration: 0.25)163}164165// MARK: - View helpers166167extension View {168 /// Standard soft shadow for floating panels/popovers only.169 func zyquoSoftShadow() -> some View {170 shadow(171 color: ZyquoShadow.soft.color,172 radius: ZyquoShadow.soft.radius,173 x: ZyquoShadow.soft.x,174 y: ZyquoShadow.soft.y175 )176 }177178 /// Standard card container: surface, generous continuous rounding,179 /// hairline border, whisper of depth.180 func zyquoCard() -> some View {181 background(182 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)183 .fill(ZyquoColor.surface)184 .overlay(185 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)186 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)187 )188 .shadow(color: .black.opacity(0.03), radius: 6, y: 2)189 )190 }191}192193/// 0.5pt hairline separator in the border token color.194struct ZyquoHairline: View {195 var body: some View {196 Rectangle()197 .fill(ZyquoColor.border)198 .frame(height: ZyquoMetrics.hairline)199 }200}201