spb/zyquo-local Public MIT
Native macOS AI chat that runs LLMs 100% locally on Apple Silicon with MLX — no cloud, no API keys.
Swift 97.2%
Shell 1.8%
Makefile 1%
1//2// Components.swift3// Zyquo Local4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011/// RAM verdict badge (Fits / Tight / Too large) for THIS Mac.12struct VerdictBadge: View {13 let verdict: MemoryAdvisor.Verdict1415 private var color: Color {16 switch verdict {17 case .fits: ZyquoTheme.success18 case .tight: ZyquoTheme.warning19 case .tooLarge: ZyquoTheme.danger20 }21 }2223 var body: some View {24 Text(verdict.label)25 .font(ZyquoTheme.caption.weight(.medium))26 .foregroundStyle(color)27 .padding(.horizontal, ZyquoTheme.Spacing.xs)28 .padding(.vertical, 2)29 .background(color.opacity(0.12), in: Capsule())30 }31}3233/// Small neutral badge for quantization / params ("4bit", "8B"…).34struct InfoBadge: View {35 let text: String3637 var body: some View {38 Text(text)39 .font(ZyquoTheme.caption)40 .foregroundStyle(ZyquoTheme.textSecondary)41 .padding(.horizontal, ZyquoTheme.Spacing.xs)42 .padding(.vertical, 2)43 .background(ZyquoTheme.surfaceSecondary, in: Capsule())44 .overlay(Capsule().stroke(ZyquoTheme.border, lineWidth: ZyquoTheme.hairline))45 }46}4748/// 0.97 press scale + 80 ms hover ease, per the motion spec.49struct PressableButtonStyle: ButtonStyle {50 func makeBody(configuration: Configuration) -> some View {51 configuration.label52 .scaleEffect(configuration.isPressed ? 0.97 : 1)53 .animation(.easeOut(duration: 0.08), value: configuration.isPressed)54 }55}5657/// Row that highlights on hover with surfaceSecondary.58struct HoverHighlight: ViewModifier {59 @State private var hovering = false60 var cornerRadius: CGFloat = ZyquoTheme.Radius.s6162 func body(content: Content) -> some View {63 content64 .background(65 RoundedRectangle(cornerRadius: cornerRadius)66 .fill(hovering ? ZyquoTheme.surfaceSecondary : .clear)67 )68 .onHover { inside in69 withAnimation(.easeOut(duration: 0.08)) { hovering = inside }70 }71 }72}7374extension View {75 func hoverHighlight(cornerRadius: CGFloat = ZyquoTheme.Radius.s) -> some View {76 modifier(HoverHighlight(cornerRadius: cornerRadius))77 }7879 /// Ultra-soft floating-panel shadow from the spec.80 func floatingShadow() -> some View {81 shadow(color: ZyquoTheme.shadowColor, radius: ZyquoTheme.shadowRadius, y: ZyquoTheme.shadowY)82 }83}8485/// The Z wordmark glyph drawn from the icon's geometry (used in the sidebar86/// and empty states).87struct ZyquoGlyph: View {88 var size: CGFloat = 2089 var color: Color = ZyquoTheme.accent9091 var body: some View {92 Canvas { context, canvasSize in93 let s = canvasSize.width / 102494 var path = Path()95 path.move(to: CGPoint(x: 300 * s, y: 265 * s))96 path.addLine(to: CGPoint(x: 724 * s, y: 265 * s))97 path.addLine(to: CGPoint(x: 300 * s, y: 759 * s))98 path.addLine(to: CGPoint(x: 724 * s, y: 759 * s))99 context.stroke(100 path,101 with: .color(color),102 style: StrokeStyle(lineWidth: 130 * s, lineCap: .round, lineJoin: .round)103 )104 }105 .frame(width: size, height: size)106 }107}108109/// Formats byte counts consistently.110func formatBytes(_ bytes: Int64) -> String {111 ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)112}113114/// "2.3 GB/s", "418 MB/s"…115func formatSpeed(_ bytesPerSecond: Double) -> String {116 formatBytes(Int64(bytesPerSecond)) + "/s"117}118119/// "3 min left", "12 s left"…120func formatETA(_ seconds: TimeInterval) -> String {121 if seconds < 90 { return "\(Int(seconds)) s left" }122 if seconds < 5400 { return "\(Int(seconds / 60)) min left" }123 return "\(Int(seconds / 3600)) h \(Int(seconds.truncatingRemainder(dividingBy: 3600) / 60)) min left"124}125