spb/zyquo-router Public MIT
One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).
Swift 95.7%
Python 2.3%
Shell 1.2%
Makefile 0.9%
1//2// Components.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Shared design-system atoms: the server status pill, copyable mono fields,9// capability badges, stat tiles, section headers, empty states.10//1112import SwiftUI1314// MARK: - Status pill1516/// ● Running on :8787 / ○ Stopped — visible from every screen.17struct StatusPill: View {18 @EnvironmentObject private var server: ServerController1920 var body: some View {21 HStack(spacing: ZyquoSpacing.xs) {22 Circle()23 .fill(color)24 .frame(width: 7, height: 7)25 Text(label)26 .font(ZyquoFont.mono(size: 11, weight: .medium))27 .foregroundStyle(ZyquoColor.textPrimary)28 .lineLimit(1)29 Button(server.isRunning || server.state == .starting ? "Stop" : "Start") {30 server.toggle()31 }32 .buttonStyle(.plain)33 .font(ZyquoFont.caption.weight(.semibold))34 .foregroundStyle(ZyquoColor.accent)35 }36 .padding(.horizontal, ZyquoSpacing.sm)37 .padding(.vertical, 6)38 .background(39 Capsule()40 .fill(server.isRunning ? ZyquoColor.accentSubtle : ZyquoColor.surfaceSecondary)41 .overlay(Capsule().strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline))42 )43 .animation(ZyquoMotion.state, value: server.state)44 }4546 private var color: Color {47 switch server.state {48 case .running: return ZyquoColor.success49 case .starting: return ZyquoColor.warning50 case .failed: return ZyquoColor.danger51 case .stopped: return ZyquoColor.textTertiary52 }53 }5455 private var label: String {56 switch server.state {57 case .running(let port): return "Running on :\(port)"58 case .starting: return "Starting…"59 case .failed: return "Error"60 case .stopped: return "Stopped"61 }62 }63}6465// MARK: - Copyable mono value6667/// A mono value with a one-click copy button (endpoints, model IDs, keys).68struct CopyField: View {69 let value: String70 var display: String?71 var size: Double = 12.572 @State private var copied = false7374 var body: some View {75 HStack(spacing: ZyquoSpacing.xs) {76 Text(display ?? value)77 .font(ZyquoFont.mono(size: size))78 .foregroundStyle(ZyquoColor.textPrimary)79 .lineLimit(1)80 .truncationMode(.middle)81 .textSelection(.enabled)82 CopyButton(value: value)83 }84 }85}8687/// Small copy icon button with a transient "copied" checkmark.88struct CopyButton: View {89 let value: String90 @State private var copied = false9192 var body: some View {93 Button {94 NSPasteboard.general.clearContents()95 NSPasteboard.general.setString(value, forType: .string)96 withAnimation(ZyquoMotion.state) { copied = true }97 Task {98 try? await Task.sleep(nanoseconds: 1_200_000_000)99 withAnimation(ZyquoMotion.state) { copied = false }100 }101 } label: {102 Image(systemName: copied ? "checkmark" : "doc.on.doc")103 .font(.system(size: 10.5, weight: .medium))104 .foregroundStyle(copied ? ZyquoColor.success : ZyquoColor.textSecondary)105 }106 .buttonStyle(.plain)107 .help("Copy")108 }109}110111// MARK: - Badges112113/// vision / tools / reasoning capability chips on model rows.114struct CapabilityBadge: View {115 let label: String116 var tint: Color = ZyquoColor.graphite117118 var body: some View {119 Text(label)120 .font(.system(size: 9.5, weight: .semibold))121 .foregroundStyle(tint)122 .padding(.horizontal, 6)123 .padding(.vertical, 2.5)124 .background(125 Capsule()126 .fill(tint.opacity(0.12))127 )128 }129}130131/// Provider chip with its hue dot.132struct ProviderBadge: View {133 let provider: ProviderID134135 var body: some View {136 HStack(spacing: 5) {137 Circle()138 .fill(ZyquoColor.providerHue(provider))139 .frame(width: 6, height: 6)140 Text(provider.displayName)141 .font(ZyquoFont.caption)142 .foregroundStyle(ZyquoColor.textSecondary)143 }144 }145}146147// MARK: - Dashboard tile148149/// One live metric tile: label, big value, optional footnote.150struct StatTile: View {151 let label: String152 let value: String153 var footnote: String?154 var valueColor: Color = ZyquoColor.textPrimary155156 var body: some View {157 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {158 Text(label.uppercased())159 .font(.system(size: 9.5, weight: .semibold))160 .foregroundStyle(ZyquoColor.textTertiary)161 .kerning(0.4)162 Text(value)163 .font(ZyquoFont.tileValue)164 .foregroundStyle(valueColor)165 .contentTransition(.numericText())166 if let footnote {167 Text(footnote)168 .font(ZyquoFont.caption)169 .foregroundStyle(ZyquoColor.textSecondary)170 }171 }172 .frame(minWidth: ZyquoMetrics.tileMinWidth, alignment: .leading)173 .padding(ZyquoSpacing.md)174 .zyquoCard()175 }176}177178// MARK: - Section header179180struct SectionHeader: View {181 let title: String182 var subtitle: String?183184 var body: some View {185 VStack(alignment: .leading, spacing: 2) {186 Text(title)187 .font(ZyquoFont.title)188 .foregroundStyle(ZyquoColor.textPrimary)189 if let subtitle {190 Text(subtitle)191 .font(ZyquoFont.body())192 .foregroundStyle(ZyquoColor.textSecondary)193 }194 }195 }196}197198// MARK: - Empty state199200struct EmptyState: View {201 let systemImage: String202 let title: String203 let message: String204205 var body: some View {206 VStack(spacing: ZyquoSpacing.sm) {207 Image(systemName: systemImage)208 .font(.system(size: 28, weight: .light))209 .foregroundStyle(ZyquoColor.textTertiary)210 Text(title)211 .font(ZyquoFont.heading)212 .foregroundStyle(ZyquoColor.textPrimary)213 Text(message)214 .font(ZyquoFont.body())215 .foregroundStyle(ZyquoColor.textSecondary)216 .multilineTextAlignment(.center)217 .frame(maxWidth: 380)218 }219 .frame(maxWidth: .infinity, maxHeight: .infinity)220 }221}222