// // ZyquoTheme.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The design system. Every color, font, spacing, radius, and shadow in the app // comes from these tokens — views contain zero raw hex values or magic numbers. // The light theme is the flagship (Phase 4 spec); dark derives from the same // semantic tokens with a deep night-sky navy base. // import SwiftUI // MARK: - Colors /// Semantic color tokens. Resolved per appearance via dynamic NSColor so the /// system handles light/dark switching natively. enum ZyquoColor { /// Main chat canvas — airy, faintly cool off-white / deep night sky. static let background = dynamic(light: 0xFAFBFD, dark: 0x14161E) /// Cards, assistant bubbles, input bar. static let surface = dynamic(light: 0xFFFFFF, dark: 0x1C1F2A) /// Hover states, code block background. static let surfaceSecondary = dynamic(light: 0xF2F4F8, dark: 0x232734) /// Primary actions, selection, links, send button (sky-indigo). static let accent = dynamic(light: 0x4E6AF0, dark: 0x6D84F5) /// Selected conversation row, user bubble tint. static let accentSubtle = dynamic(light: 0xEBEFFD, dark: 0x28304C) static let textPrimary = dynamic(light: 0x1A1C22, dark: 0xE8EAF2) static let textSecondary = dynamic(light: 0x6B7080, dark: 0x9BA1B5) static let textTertiary = dynamic(light: 0x9EA3B0, dark: 0x6A7188) /// Hairline separators (draw at 0.5pt). static let border = dynamic(light: 0xE4E7EE, dark: 0x2C3040) static let success = dynamic(light: 0x2FA36B, dark: 0x43BD83) static let warning = dynamic(light: 0xD9822B, dark: 0xE59A4D) static let danger = dynamic(light: 0xD64545, dark: 0xE36363) /// Builds a dynamic color that resolves per appearance. private 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 system font; SF Mono for code). Chat body scales with the /// user's font size preference (12–18pt) via `ChatFontScale`. enum ZyquoFont { /// 20pt semibold — window/section titles. static let title = Font.system(size: 20, weight: .semibold) /// Message body at a given user-selected size (default 13.5). static func body(size: Double = 13.5) -> Font { .system(size: size, weight: .regular) } static func bodyEmphasis(size: Double = 13.5) -> Font { .system(size: size, weight: .medium) } /// 11pt — timestamps, token counts, captions. static let caption = Font.system(size: 11, weight: .regular) /// Code blocks and inline code, scaled slightly below body. static func code(size: Double = 12.5) -> Font { .system(size: size, weight: .regular, design: .monospaced) } /// Mandatory generous line height for message text (spec: 1.45). static let bodyLineSpacingFactor: Double = 0.45 } // MARK: - Spacing, radii, shadows, metrics /// 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: 6 (small controls), 10 (bubbles/cards), 14 (floating panels). enum ZyquoRadius { static let small: CGFloat = 6 static let medium: CGFloat = 10 static let large: CGFloat = 14 } /// Shadows: extremely soft, used sparingly (floating panels, popovers, input bar). 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 sidebarWidth: CGFloat = 260 static let chatHeaderHeight: CGFloat = 52 static let maxMessageColumnWidth: CGFloat = 760 static let contentInset: CGFloat = 16 static let hairline: CGFloat = 0.5 static let windowMinWidth: CGFloat = 980 static let windowMinHeight: CGFloat = 640 static let windowDefaultWidth: CGFloat = 1240 static let windowDefaultHeight: CGFloat = 800 static let quickChatWidth: CGFloat = 640 static let settingsWidth: CGFloat = 720 static let settingsHeight: CGFloat = 520 static let attachmentThumbnail: CGFloat = 56 static let verticalTurnRhythm: CGFloat = 16 } /// Motion tokens: hover 80ms ease, send 150ms ease-out, pressed scale 0.97. enum ZyquoMotion { static let hover = Animation.easeInOut(duration: 0.08) static let send = Animation.easeOut(duration: 0.15) static let pressedScale: CGFloat = 0.97 static let picker = Animation.snappy } // 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 ) } } /// 0.5pt hairline separator in the border token color. struct ZyquoHairline: View { var body: some View { Rectangle() .fill(ZyquoColor.border) .frame(height: ZyquoMetrics.hairline) } }