SPB Git

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%
6.5 KB · 222 lines swift
Raw Blame History
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