// // Components.swift // Zyquo Local // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import SwiftUI /// RAM verdict badge (Fits / Tight / Too large) for THIS Mac. struct VerdictBadge: View { let verdict: MemoryAdvisor.Verdict private var color: Color { switch verdict { case .fits: ZyquoTheme.success case .tight: ZyquoTheme.warning case .tooLarge: ZyquoTheme.danger } } var body: some View { Text(verdict.label) .font(ZyquoTheme.caption.weight(.medium)) .foregroundStyle(color) .padding(.horizontal, ZyquoTheme.Spacing.xs) .padding(.vertical, 2) .background(color.opacity(0.12), in: Capsule()) } } /// Small neutral badge for quantization / params ("4bit", "8B"…). struct InfoBadge: View { let text: String var body: some View { Text(text) .font(ZyquoTheme.caption) .foregroundStyle(ZyquoTheme.textSecondary) .padding(.horizontal, ZyquoTheme.Spacing.xs) .padding(.vertical, 2) .background(ZyquoTheme.surfaceSecondary, in: Capsule()) .overlay(Capsule().stroke(ZyquoTheme.border, lineWidth: ZyquoTheme.hairline)) } } /// 0.97 press scale + 80 ms hover ease, per the motion spec. struct PressableButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.97 : 1) .animation(.easeOut(duration: 0.08), value: configuration.isPressed) } } /// Row that highlights on hover with surfaceSecondary. struct HoverHighlight: ViewModifier { @State private var hovering = false var cornerRadius: CGFloat = ZyquoTheme.Radius.s func body(content: Content) -> some View { content .background( RoundedRectangle(cornerRadius: cornerRadius) .fill(hovering ? ZyquoTheme.surfaceSecondary : .clear) ) .onHover { inside in withAnimation(.easeOut(duration: 0.08)) { hovering = inside } } } } extension View { func hoverHighlight(cornerRadius: CGFloat = ZyquoTheme.Radius.s) -> some View { modifier(HoverHighlight(cornerRadius: cornerRadius)) } /// Ultra-soft floating-panel shadow from the spec. func floatingShadow() -> some View { shadow(color: ZyquoTheme.shadowColor, radius: ZyquoTheme.shadowRadius, y: ZyquoTheme.shadowY) } } /// The Z wordmark glyph drawn from the icon's geometry (used in the sidebar /// and empty states). struct ZyquoGlyph: View { var size: CGFloat = 20 var color: Color = ZyquoTheme.accent var body: some View { Canvas { context, canvasSize in let s = canvasSize.width / 1024 var path = Path() path.move(to: CGPoint(x: 300 * s, y: 265 * s)) path.addLine(to: CGPoint(x: 724 * s, y: 265 * s)) path.addLine(to: CGPoint(x: 300 * s, y: 759 * s)) path.addLine(to: CGPoint(x: 724 * s, y: 759 * s)) context.stroke( path, with: .color(color), style: StrokeStyle(lineWidth: 130 * s, lineCap: .round, lineJoin: .round) ) } .frame(width: size, height: size) } } /// Formats byte counts consistently. func formatBytes(_ bytes: Int64) -> String { ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) } /// "2.3 GB/s", "418 MB/s"… func formatSpeed(_ bytesPerSecond: Double) -> String { formatBytes(Int64(bytesPerSecond)) + "/s" } /// "3 min left", "12 s left"… func formatETA(_ seconds: TimeInterval) -> String { if seconds < 90 { return "\(Int(seconds)) s left" } if seconds < 5400 { return "\(Int(seconds / 60)) min left" } return "\(Int(seconds / 3600)) h \(Int(seconds.truncatingRemainder(dividingBy: 3600) / 60)) min left" }