// // Components.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Shared design-system atoms: the server status pill, copyable mono fields, // capability badges, stat tiles, section headers, empty states. // import SwiftUI // MARK: - Status pill /// ● Running on :8787 / ○ Stopped — visible from every screen. struct StatusPill: View { @EnvironmentObject private var server: ServerController var body: some View { HStack(spacing: ZyquoSpacing.xs) { Circle() .fill(color) .frame(width: 7, height: 7) Text(label) .font(ZyquoFont.mono(size: 11, weight: .medium)) .foregroundStyle(ZyquoColor.textPrimary) .lineLimit(1) Button(server.isRunning || server.state == .starting ? "Stop" : "Start") { server.toggle() } .buttonStyle(.plain) .font(ZyquoFont.caption.weight(.semibold)) .foregroundStyle(ZyquoColor.accent) } .padding(.horizontal, ZyquoSpacing.sm) .padding(.vertical, 6) .background( Capsule() .fill(server.isRunning ? ZyquoColor.accentSubtle : ZyquoColor.surfaceSecondary) .overlay(Capsule().strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)) ) .animation(ZyquoMotion.state, value: server.state) } private var color: Color { switch server.state { case .running: return ZyquoColor.success case .starting: return ZyquoColor.warning case .failed: return ZyquoColor.danger case .stopped: return ZyquoColor.textTertiary } } private var label: String { switch server.state { case .running(let port): return "Running on :\(port)" case .starting: return "Starting…" case .failed: return "Error" case .stopped: return "Stopped" } } } // MARK: - Copyable mono value /// A mono value with a one-click copy button (endpoints, model IDs, keys). struct CopyField: View { let value: String var display: String? var size: Double = 12.5 @State private var copied = false var body: some View { HStack(spacing: ZyquoSpacing.xs) { Text(display ?? value) .font(ZyquoFont.mono(size: size)) .foregroundStyle(ZyquoColor.textPrimary) .lineLimit(1) .truncationMode(.middle) .textSelection(.enabled) CopyButton(value: value) } } } /// Small copy icon button with a transient "copied" checkmark. struct CopyButton: View { let value: String @State private var copied = false var body: some View { Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(value, forType: .string) withAnimation(ZyquoMotion.state) { copied = true } Task { try? await Task.sleep(nanoseconds: 1_200_000_000) withAnimation(ZyquoMotion.state) { copied = false } } } label: { Image(systemName: copied ? "checkmark" : "doc.on.doc") .font(.system(size: 10.5, weight: .medium)) .foregroundStyle(copied ? ZyquoColor.success : ZyquoColor.textSecondary) } .buttonStyle(.plain) .help("Copy") } } // MARK: - Badges /// vision / tools / reasoning capability chips on model rows. struct CapabilityBadge: View { let label: String var tint: Color = ZyquoColor.graphite var body: some View { Text(label) .font(.system(size: 9.5, weight: .semibold)) .foregroundStyle(tint) .padding(.horizontal, 6) .padding(.vertical, 2.5) .background( Capsule() .fill(tint.opacity(0.12)) ) } } /// Provider chip with its hue dot. struct ProviderBadge: View { let provider: ProviderID var body: some View { HStack(spacing: 5) { Circle() .fill(ZyquoColor.providerHue(provider)) .frame(width: 6, height: 6) Text(provider.displayName) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) } } } // MARK: - Dashboard tile /// One live metric tile: label, big value, optional footnote. struct StatTile: View { let label: String let value: String var footnote: String? var valueColor: Color = ZyquoColor.textPrimary var body: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) { Text(label.uppercased()) .font(.system(size: 9.5, weight: .semibold)) .foregroundStyle(ZyquoColor.textTertiary) .kerning(0.4) Text(value) .font(ZyquoFont.tileValue) .foregroundStyle(valueColor) .contentTransition(.numericText()) if let footnote { Text(footnote) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) } } .frame(minWidth: ZyquoMetrics.tileMinWidth, alignment: .leading) .padding(ZyquoSpacing.md) .zyquoCard() } } // MARK: - Section header struct SectionHeader: View { let title: String var subtitle: String? var body: some View { VStack(alignment: .leading, spacing: 2) { Text(title) .font(ZyquoFont.title) .foregroundStyle(ZyquoColor.textPrimary) if let subtitle { Text(subtitle) .font(ZyquoFont.body()) .foregroundStyle(ZyquoColor.textSecondary) } } } } // MARK: - Empty state struct EmptyState: View { let systemImage: String let title: String let message: String var body: some View { VStack(spacing: ZyquoSpacing.sm) { Image(systemName: systemImage) .font(.system(size: 28, weight: .light)) .foregroundStyle(ZyquoColor.textTertiary) Text(title) .font(ZyquoFont.heading) .foregroundStyle(ZyquoColor.textPrimary) Text(message) .font(ZyquoFont.body()) .foregroundStyle(ZyquoColor.textSecondary) .multilineTextAlignment(.center) .frame(maxWidth: 380) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }