SPB Git

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