// // ZyquoTheme.swift // Zyquo MLX // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import SwiftUI /// The Zyquo MLX design system — single source of truth for every color, /// font, spacing, radius, and motion value in the app (charter Phase 4). /// Identity: molten copper on slate — a foundry, precise and Apple-clean. /// Light theme is flagship; dark is the "deep slate forge" derivation. enum ZyquoTheme { // MARK: - Colors (light / dark pairs) /// Canvas behind everything. static let background = adaptive(light: 0xFAFAF9, dark: 0x161A22) /// Cards, panels. static let surface = adaptive(light: 0xFFFFFF, dark: 0x1E2430) /// Hover states, code/log blocks. static let surfaceSecondary = adaptive(light: 0xF3F2F0, dark: 0x252C3A) /// Molten copper — primary actions, run/train buttons, active states. static let accent = adaptive(light: 0xC2410C, dark: 0xEA6A2B) /// Bright end of the copper gradient. static let accentBright = adaptive(light: 0xEA6A2B, dark: 0xF0813F) /// Copper pair: slate for secondary emphasis. static let slate = adaptive(light: 0x334155, dark: 0x94A3B8) /// Selected rows, active-run tint. static let accentSubtle = adaptive(light: 0xFCEDE4, dark: 0x3A2A20) static let textPrimary = adaptive(light: 0x1A1A1C, dark: 0xF2F2F4) static let textSecondary = adaptive(light: 0x6B6B72, dark: 0xA5A8B0) static let textTertiary = adaptive(light: 0x9E9EA6, dark: 0x71747E) static let border = adaptive(light: 0xE6E4E1, dark: 0x323A48) static let success = adaptive(light: 0x2FA36B, dark: 0x3FBF81) static let warning = adaptive(light: 0xD9822B, dark: 0xE2953F) static let danger = adaptive(light: 0xD64545, dark: 0xE25A5A) /// Chart tokens: copper = train loss, slate = val loss, teal = tokens/sec. static let chartTrain = accent static let chartVal = slate static let chartThroughput = adaptive(light: 0x0F8B8D, dark: 0x2BB3B6) /// The copper gradient for hero actions. static var copperGradient: LinearGradient { LinearGradient( colors: [Color(hex: 0xC2410C), Color(hex: 0xEA6A2B)], startPoint: .top, endPoint: .bottom) } // MARK: - Typography static let titleFont = Font.system(size: 20, weight: .semibold) static let headlineFont = Font.system(size: 14, weight: .semibold) static let bodyFont = Font.system(size: 13) static let captionFont = Font.system(size: 11) static let captionMediumFont = Font.system(size: 11, weight: .medium) /// Logs, metrics, code — always SF Mono. static let monoFont = Font.system(size: 12, design: .monospaced) static let monoSmallFont = Font.system(size: 11, design: .monospaced) // MARK: - Spacing (4pt grid) static let spacing2: CGFloat = 2 static let spacing4: CGFloat = 4 static let spacing8: CGFloat = 8 static let spacing12: CGFloat = 12 static let spacing16: CGFloat = 16 static let spacing20: CGFloat = 20 static let spacing24: CGFloat = 24 static let spacing32: CGFloat = 32 // MARK: - Radii & lines static let radiusSmall: CGFloat = 6 static let radiusMedium: CGFloat = 10 static let radiusLarge: CGFloat = 14 /// Hairlines are always 0.5pt (family rule). static let hairline: CGFloat = 0.5 // MARK: - Layout constants (charter §4.2) static let sidebarWidth: CGFloat = 240 static let headerHeight: CGFloat = 52 static let defaultWindowSize = CGSize(width: 1360, height: 880) static let minWindowSize = CGSize(width: 1080, height: 700) // MARK: - Motion static let quickAnimation = Animation.easeOut(duration: 0.15) static let standardAnimation = Animation.easeInOut(duration: 0.25) // MARK: - Helpers private static func adaptive(light: UInt32, dark: UInt32) -> Color { Color( nsColor: NSColor(name: nil) { appearance in let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua return NSColor(Color(hex: isDark ? dark : light)) }) } } extension Color { /// Token-internal hex initializer — views never use raw hex directly. init(hex: UInt32) { self.init( .sRGB, red: Double((hex >> 16) & 0xFF) / 255.0, green: Double((hex >> 8) & 0xFF) / 255.0, blue: Double(hex & 0xFF) / 255.0) } } // MARK: - Shared components /// A 0.5pt hairline-bordered card surface. struct ZyquoCard: ViewModifier { func body(content: Content) -> some View { content .background(ZyquoTheme.surface) .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusMedium)) .overlay( RoundedRectangle(cornerRadius: ZyquoTheme.radiusMedium) .stroke(ZyquoTheme.border, lineWidth: ZyquoTheme.hairline)) } } extension View { func zyquoCard() -> some View { modifier(ZyquoCard()) } } /// Status pill for jobs and runs. struct StatusPill: View { let text: String let color: Color var body: some View { Text(text) .font(ZyquoTheme.captionMediumFont) .foregroundStyle(color) .padding(.horizontal, ZyquoTheme.spacing8) .padding(.vertical, ZyquoTheme.spacing2) .background(color.opacity(0.12)) .clipShape(Capsule()) } } /// Model-type badge (LLM/VLM/Embed/Speech/Image). struct TypeBadge: View { let type: ModelType var body: some View { Text(type.displayName) .font(ZyquoTheme.captionMediumFont) .foregroundStyle(ZyquoTheme.slate) .padding(.horizontal, ZyquoTheme.spacing8) .padding(.vertical, ZyquoTheme.spacing2) .background(ZyquoTheme.surfaceSecondary) .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall)) } } /// Memory-verdict badge driven by MemoryAdvisor. struct VerdictBadge: View { let verdict: MemoryVerdict private var color: Color { switch verdict { case .comfortable: ZyquoTheme.success case .tight: ZyquoTheme.warning case .wontFit: ZyquoTheme.danger } } var body: some View { StatusPill(text: verdict.displayName, color: color) } } /// Instructive, intentional empty state (charter: App-Store-front-page quality). struct EmptyStateView: View { let icon: String let title: String let message: String var actionLabel: String? var action: (() -> Void)? var body: some View { VStack(spacing: ZyquoTheme.spacing16) { ZStack { Circle() .fill(ZyquoTheme.accentSubtle) .frame(width: 88, height: 88) Image(systemName: icon) .font(.system(size: 36, weight: .light)) .foregroundStyle(ZyquoTheme.accent) } VStack(spacing: ZyquoTheme.spacing8) { Text(title) .font(ZyquoTheme.titleFont) .foregroundStyle(ZyquoTheme.textPrimary) Text(message) .font(ZyquoTheme.bodyFont) .foregroundStyle(ZyquoTheme.textSecondary) .multilineTextAlignment(.center) .frame(maxWidth: 420) } if let actionLabel, let action { Button(action: action) { Text(actionLabel) .font(ZyquoTheme.headlineFont) .foregroundStyle(.white) .padding(.horizontal, ZyquoTheme.spacing20) .padding(.vertical, ZyquoTheme.spacing8) .background(ZyquoTheme.copperGradient) .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall)) } .buttonStyle(.plain) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(ZyquoTheme.background) } }