SPB Git

spb/zyquo-mlx Public MIT

The local MLX foundry for your Mac — run, fine-tune, quantize, and ship models. Nothing leaves your machine.

Swift 93.4% Python 3.8% Makefile 2.2% Shell 0.5%
7.9 KB · 231 lines swift
Raw Blame History
1//2//  ZyquoTheme.swift3//  Zyquo MLX4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import SwiftUI1011/// The Zyquo MLX design system — single source of truth for every color,12/// font, spacing, radius, and motion value in the app (charter Phase 4).13/// Identity: molten copper on slate — a foundry, precise and Apple-clean.14/// Light theme is flagship; dark is the "deep slate forge" derivation.15enum ZyquoTheme {1617    // MARK: - Colors (light / dark pairs)1819    /// Canvas behind everything.20    static let background = adaptive(light: 0xFAFAF9, dark: 0x161A22)21    /// Cards, panels.22    static let surface = adaptive(light: 0xFFFFFF, dark: 0x1E2430)23    /// Hover states, code/log blocks.24    static let surfaceSecondary = adaptive(light: 0xF3F2F0, dark: 0x252C3A)2526    /// Molten copper — primary actions, run/train buttons, active states.27    static let accent = adaptive(light: 0xC2410C, dark: 0xEA6A2B)28    /// Bright end of the copper gradient.29    static let accentBright = adaptive(light: 0xEA6A2B, dark: 0xF0813F)30    /// Copper pair: slate for secondary emphasis.31    static let slate = adaptive(light: 0x334155, dark: 0x94A3B8)32    /// Selected rows, active-run tint.33    static let accentSubtle = adaptive(light: 0xFCEDE4, dark: 0x3A2A20)3435    static let textPrimary = adaptive(light: 0x1A1A1C, dark: 0xF2F2F4)36    static let textSecondary = adaptive(light: 0x6B6B72, dark: 0xA5A8B0)37    static let textTertiary = adaptive(light: 0x9E9EA6, dark: 0x71747E)38    static let border = adaptive(light: 0xE6E4E1, dark: 0x323A48)3940    static let success = adaptive(light: 0x2FA36B, dark: 0x3FBF81)41    static let warning = adaptive(light: 0xD9822B, dark: 0xE2953F)42    static let danger = adaptive(light: 0xD64545, dark: 0xE25A5A)4344    /// Chart tokens: copper = train loss, slate = val loss, teal = tokens/sec.45    static let chartTrain = accent46    static let chartVal = slate47    static let chartThroughput = adaptive(light: 0x0F8B8D, dark: 0x2BB3B6)4849    /// The copper gradient for hero actions.50    static var copperGradient: LinearGradient {51        LinearGradient(52            colors: [Color(hex: 0xC2410C), Color(hex: 0xEA6A2B)],53            startPoint: .top, endPoint: .bottom)54    }5556    // MARK: - Typography5758    static let titleFont = Font.system(size: 20, weight: .semibold)59    static let headlineFont = Font.system(size: 14, weight: .semibold)60    static let bodyFont = Font.system(size: 13)61    static let captionFont = Font.system(size: 11)62    static let captionMediumFont = Font.system(size: 11, weight: .medium)63    /// Logs, metrics, code — always SF Mono.64    static let monoFont = Font.system(size: 12, design: .monospaced)65    static let monoSmallFont = Font.system(size: 11, design: .monospaced)6667    // MARK: - Spacing (4pt grid)6869    static let spacing2: CGFloat = 270    static let spacing4: CGFloat = 471    static let spacing8: CGFloat = 872    static let spacing12: CGFloat = 1273    static let spacing16: CGFloat = 1674    static let spacing20: CGFloat = 2075    static let spacing24: CGFloat = 2476    static let spacing32: CGFloat = 327778    // MARK: - Radii & lines7980    static let radiusSmall: CGFloat = 681    static let radiusMedium: CGFloat = 1082    static let radiusLarge: CGFloat = 1483    /// Hairlines are always 0.5pt (family rule).84    static let hairline: CGFloat = 0.58586    // MARK: - Layout constants (charter §4.2)8788    static let sidebarWidth: CGFloat = 24089    static let headerHeight: CGFloat = 5290    static let defaultWindowSize = CGSize(width: 1360, height: 880)91    static let minWindowSize = CGSize(width: 1080, height: 700)9293    // MARK: - Motion9495    static let quickAnimation = Animation.easeOut(duration: 0.15)96    static let standardAnimation = Animation.easeInOut(duration: 0.25)9798    // MARK: - Helpers99100    private static func adaptive(light: UInt32, dark: UInt32) -> Color {101        Color(102            nsColor: NSColor(name: nil) { appearance in103                let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua104                return NSColor(Color(hex: isDark ? dark : light))105            })106    }107}108109extension Color {110    /// Token-internal hex initializer — views never use raw hex directly.111    init(hex: UInt32) {112        self.init(113            .sRGB,114            red: Double((hex >> 16) & 0xFF) / 255.0,115            green: Double((hex >> 8) & 0xFF) / 255.0,116            blue: Double(hex & 0xFF) / 255.0)117    }118}119120// MARK: - Shared components121122/// A 0.5pt hairline-bordered card surface.123struct ZyquoCard: ViewModifier {124    func body(content: Content) -> some View {125        content126            .background(ZyquoTheme.surface)127            .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusMedium))128            .overlay(129                RoundedRectangle(cornerRadius: ZyquoTheme.radiusMedium)130                    .stroke(ZyquoTheme.border, lineWidth: ZyquoTheme.hairline))131    }132}133134extension View {135    func zyquoCard() -> some View { modifier(ZyquoCard()) }136}137138/// Status pill for jobs and runs.139struct StatusPill: View {140    let text: String141    let color: Color142143    var body: some View {144        Text(text)145            .font(ZyquoTheme.captionMediumFont)146            .foregroundStyle(color)147            .padding(.horizontal, ZyquoTheme.spacing8)148            .padding(.vertical, ZyquoTheme.spacing2)149            .background(color.opacity(0.12))150            .clipShape(Capsule())151    }152}153154/// Model-type badge (LLM/VLM/Embed/Speech/Image).155struct TypeBadge: View {156    let type: ModelType157158    var body: some View {159        Text(type.displayName)160            .font(ZyquoTheme.captionMediumFont)161            .foregroundStyle(ZyquoTheme.slate)162            .padding(.horizontal, ZyquoTheme.spacing8)163            .padding(.vertical, ZyquoTheme.spacing2)164            .background(ZyquoTheme.surfaceSecondary)165            .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall))166    }167}168169/// Memory-verdict badge driven by MemoryAdvisor.170struct VerdictBadge: View {171    let verdict: MemoryVerdict172173    private var color: Color {174        switch verdict {175        case .comfortable: ZyquoTheme.success176        case .tight: ZyquoTheme.warning177        case .wontFit: ZyquoTheme.danger178        }179    }180181    var body: some View {182        StatusPill(text: verdict.displayName, color: color)183    }184}185186/// Instructive, intentional empty state (charter: App-Store-front-page quality).187struct EmptyStateView: View {188    let icon: String189    let title: String190    let message: String191    var actionLabel: String?192    var action: (() -> Void)?193194    var body: some View {195        VStack(spacing: ZyquoTheme.spacing16) {196            ZStack {197                Circle()198                    .fill(ZyquoTheme.accentSubtle)199                    .frame(width: 88, height: 88)200                Image(systemName: icon)201                    .font(.system(size: 36, weight: .light))202                    .foregroundStyle(ZyquoTheme.accent)203            }204            VStack(spacing: ZyquoTheme.spacing8) {205                Text(title)206                    .font(ZyquoTheme.titleFont)207                    .foregroundStyle(ZyquoTheme.textPrimary)208                Text(message)209                    .font(ZyquoTheme.bodyFont)210                    .foregroundStyle(ZyquoTheme.textSecondary)211                    .multilineTextAlignment(.center)212                    .frame(maxWidth: 420)213            }214            if let actionLabel, let action {215                Button(action: action) {216                    Text(actionLabel)217                        .font(ZyquoTheme.headlineFont)218                        .foregroundStyle(.white)219                        .padding(.horizontal, ZyquoTheme.spacing20)220                        .padding(.vertical, ZyquoTheme.spacing8)221                        .background(ZyquoTheme.copperGradient)222                        .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall))223                }224                .buttonStyle(.plain)225            }226        }227        .frame(maxWidth: .infinity, maxHeight: .infinity)228        .background(ZyquoTheme.background)229    }230}231