spb/zyquo-cloud Public MIT
Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.
Swift 97.4%
Shell 1.7%
Makefile 1%
1//2// ZyquoTheme.swift3// Zyquo Cloud4//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// The light theme is the flagship (Phase 4 spec); dark derives from the same11// semantic tokens with a deep night-sky navy base.12//1314import SwiftUI1516// MARK: - Colors1718/// Semantic color tokens. Resolved per appearance via dynamic NSColor so the19/// system handles light/dark switching natively.20enum ZyquoColor {21 /// Main chat canvas — airy, faintly cool off-white / deep night sky.22 static let background = dynamic(light: 0xFAFBFD, dark: 0x14161E)23 /// Cards, assistant bubbles, input bar.24 static let surface = dynamic(light: 0xFFFFFF, dark: 0x1C1F2A)25 /// Hover states, code block background.26 static let surfaceSecondary = dynamic(light: 0xF2F4F8, dark: 0x232734)27 /// Primary actions, selection, links, send button (sky-indigo).28 static let accent = dynamic(light: 0x4E6AF0, dark: 0x6D84F5)29 /// Selected conversation row, user bubble tint.30 static let accentSubtle = dynamic(light: 0xEBEFFD, dark: 0x28304C)31 static let textPrimary = dynamic(light: 0x1A1C22, dark: 0xE8EAF2)32 static let textSecondary = dynamic(light: 0x6B7080, dark: 0x9BA1B5)33 static let textTertiary = dynamic(light: 0x9EA3B0, dark: 0x6A7188)34 /// Hairline separators (draw at 0.5pt).35 static let border = dynamic(light: 0xE4E7EE, dark: 0x2C3040)36 static let success = dynamic(light: 0x2FA36B, dark: 0x43BD83)37 static let warning = dynamic(light: 0xD9822B, dark: 0xE59A4D)38 static let danger = dynamic(light: 0xD64545, dark: 0xE36363)3940 /// Builds a dynamic color that resolves per appearance.41 private static func dynamic(light: UInt32, dark: UInt32) -> Color {42 Color(nsColor: NSColor(name: nil) { appearance in43 let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light44 return NSColor(hex: hex)45 })46 }47}4849extension NSColor {50 /// 0xRRGGBB → NSColor (sRGB).51 convenience init(hex: UInt32) {52 self.init(53 srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,54 green: CGFloat((hex >> 8) & 0xFF) / 255,55 blue: CGFloat(hex & 0xFF) / 255,56 alpha: 157 )58 }59}6061// MARK: - Typography6263/// Type scale (SF Pro system font; SF Mono for code). Chat body scales with the64/// user's font size preference (12–18pt) via `ChatFontScale`.65enum ZyquoFont {66 /// 20pt semibold — window/section titles.67 static let title = Font.system(size: 20, weight: .semibold)68 /// Message body at a given user-selected size (default 13.5).69 static func body(size: Double = 13.5) -> Font {70 .system(size: size, weight: .regular)71 }72 static func bodyEmphasis(size: Double = 13.5) -> Font {73 .system(size: size, weight: .medium)74 }75 /// 11pt — timestamps, token counts, captions.76 static let caption = Font.system(size: 11, weight: .regular)77 /// Code blocks and inline code, scaled slightly below body.78 static func code(size: Double = 12.5) -> Font {79 .system(size: size, weight: .regular, design: .monospaced)80 }81 /// Mandatory generous line height for message text (spec: 1.45).82 static let bodyLineSpacingFactor: Double = 0.4583}8485// MARK: - Spacing, radii, shadows, metrics8687/// Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 / 32.88enum ZyquoSpacing {89 static let xxs: CGFloat = 490 static let xs: CGFloat = 891 static let sm: CGFloat = 1292 static let md: CGFloat = 1693 static let lg: CGFloat = 2094 static let xl: CGFloat = 2495 static let xxl: CGFloat = 3296}9798/// Corner radii: 6 (small controls), 10 (bubbles/cards), 14 (floating panels).99enum ZyquoRadius {100 static let small: CGFloat = 6101 static let medium: CGFloat = 10102 static let large: CGFloat = 14103}104105/// Shadows: extremely soft, used sparingly (floating panels, popovers, input bar).106enum ZyquoShadow {107 static let soft = ShadowStyle(color: .black.opacity(0.06), radius: 12, y: 2)108109 struct ShadowStyle {110 let color: Color111 let radius: CGFloat112 var x: CGFloat = 0113 var y: CGFloat = 0114 }115}116117/// Fixed layout metrics from the Phase 4 spec.118enum ZyquoMetrics {119 static let sidebarWidth: CGFloat = 260120 static let chatHeaderHeight: CGFloat = 52121 static let maxMessageColumnWidth: CGFloat = 760122 static let contentInset: CGFloat = 16123 static let hairline: CGFloat = 0.5124 static let windowMinWidth: CGFloat = 980125 static let windowMinHeight: CGFloat = 640126 static let windowDefaultWidth: CGFloat = 1240127 static let windowDefaultHeight: CGFloat = 800128 static let quickChatWidth: CGFloat = 640129 static let settingsWidth: CGFloat = 720130 static let settingsHeight: CGFloat = 520131 static let attachmentThumbnail: CGFloat = 56132 static let verticalTurnRhythm: CGFloat = 16133}134135/// Motion tokens: hover 80ms ease, send 150ms ease-out, pressed scale 0.97.136enum ZyquoMotion {137 static let hover = Animation.easeInOut(duration: 0.08)138 static let send = Animation.easeOut(duration: 0.15)139 static let pressedScale: CGFloat = 0.97140 static let picker = Animation.snappy141}142143// MARK: - View helpers144145extension View {146 /// Standard soft shadow for floating panels/popovers only.147 func zyquoSoftShadow() -> some View {148 shadow(149 color: ZyquoShadow.soft.color,150 radius: ZyquoShadow.soft.radius,151 x: ZyquoShadow.soft.x,152 y: ZyquoShadow.soft.y153 )154 }155}156157/// 0.5pt hairline separator in the border token color.158struct ZyquoHairline: View {159 var body: some View {160 Rectangle()161 .fill(ZyquoColor.border)162 .frame(height: ZyquoMetrics.hairline)163 }164}165