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%
5.8 KB · 183 lines swift
Raw Blame History
1//2//  MainWindowView.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  App shell: 240pt translucent left navigator (sections, footer settings9//  gear, permanent status pill with Start/Stop) + detail area.10//1112import SwiftUI1314struct MainWindowView: View {15    @EnvironmentObject private var appearance: AppearanceStore16    // Stored so the ⌘1–6 menu commands can drive it from the Commands scene.17    @AppStorage("selectedSection") private var sectionRaw = AppSection.dashboard.rawValue1819    private var section: Binding<AppSection> {20        Binding(21            get: { AppSection(rawValue: sectionRaw) ?? .dashboard },22            set: { sectionRaw = $0.rawValue }23        )24    }2526    @State private var paletteShown = false2728    var body: some View {29        HStack(spacing: 0) {30            Navigator(section: section)31                .frame(width: ZyquoMetrics.navigatorWidth)32            Rectangle()33                .fill(ZyquoColor.border)34                .frame(width: ZyquoMetrics.hairline)35            detail36                .frame(maxWidth: .infinity, maxHeight: .infinity)37                .background(ZyquoColor.background)38        }39        .frame(40            minWidth: ZyquoMetrics.windowMinWidth,41            minHeight: ZyquoMetrics.windowMinHeight42        )43        .overlay(alignment: .top) {44            if paletteShown {45                CommandPalette(isPresented: $paletteShown)46                    .padding(.top, 90)47                    .transition(.opacity.combined(with: .scale(scale: 0.98, anchor: .top)))48            }49        }50        .background(51            Button("") {52                withAnimation(ZyquoMotion.state) { paletteShown.toggle() }53            }54            .keyboardShortcut("k", modifiers: .command)55            .hidden()56        )57        .preferredColorScheme(appearance.themeMode.colorScheme)58    }5960    @ViewBuilder61    private var detail: some View {62        switch section.wrappedValue {63        case .dashboard: DashboardView()64        case .models: ModelsView()65        case .requests: RequestsView()66        case .keys: KeysView()67        case .playground: PlaygroundView()68        case .docs: DocsView()69        }70    }71}7273// MARK: - Navigator7475private struct Navigator: View {76    @Binding var section: AppSection7778    var body: some View {79        VStack(alignment: .leading, spacing: 0) {80            // Wordmark81            HStack(spacing: ZyquoSpacing.xs) {82                RouterZGlyph(size: 20)83                Text("Zyquo Router")84                    .font(ZyquoFont.heading)85                    .foregroundStyle(ZyquoColor.textPrimary)86            }87            .padding(.horizontal, ZyquoSpacing.md)88            .padding(.top, ZyquoSpacing.xl)89            .padding(.bottom, ZyquoSpacing.lg)9091            ForEach(AppSection.allCases) { item in92                NavigatorRow(item: item, selected: section == item) {93                    withAnimation(ZyquoMotion.state) { section = item }94                }95            }9697            Spacer()9899            StatusPill()100                .padding(.horizontal, ZyquoSpacing.md)101                .padding(.bottom, ZyquoSpacing.sm)102103            HStack {104                Button {105                    SettingsOpener.open()106                } label: {107                    Image(systemName: "gearshape")108                        .font(.system(size: 13, weight: .medium))109                        .foregroundStyle(ZyquoColor.textSecondary)110                }111                .buttonStyle(.plain)112                .help("Settings")113                Spacer()114            }115            .padding(.horizontal, ZyquoSpacing.md)116            .padding(.bottom, ZyquoSpacing.md)117        }118        .background(NavigatorMaterial())119    }120}121122private struct NavigatorRow: View {123    let item: AppSection124    let selected: Bool125    let action: () -> Void126    @State private var hovering = false127128    var body: some View {129        Button(action: action) {130            HStack(spacing: ZyquoSpacing.xs) {131                Image(systemName: item.systemImage)132                    .font(.system(size: 13, weight: .medium))133                    .frame(width: 18)134                Text(item.rawValue)135                    .font(ZyquoFont.bodyEmphasis())136                Spacer()137            }138            .foregroundStyle(selected ? ZyquoColor.accent : ZyquoColor.textSecondary)139            .padding(.horizontal, ZyquoSpacing.sm)140            .padding(.vertical, 7)141            .background(142                RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)143                    .fill(selected ? ZyquoColor.accentSubtle : (hovering ? ZyquoColor.surfaceSecondary : .clear))144            )145        }146        .buttonStyle(.plain)147        .padding(.horizontal, ZyquoSpacing.xs)148        .onHover { hover in149            withAnimation(ZyquoMotion.hover) { hovering = hover }150        }151    }152}153154/// Translucent sidebar material behind the navigator.155private struct NavigatorMaterial: NSViewRepresentable {156    func makeNSView(context: Context) -> NSVisualEffectView {157        let view = NSVisualEffectView()158        view.material = .sidebar159        view.blendingMode = .behindWindow160        view.state = .followsWindowActiveState161        return view162    }163164    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}165}166167/// Mini Z-with-signal-dot wordmark glyph (Phase 5 derives the full icon).168struct RouterZGlyph: View {169    let size: CGFloat170171    var body: some View {172        ZStack(alignment: .bottomTrailing) {173            Text("Z")174                .font(.system(size: size, weight: .heavy, design: .rounded))175                .foregroundStyle(ZyquoColor.textPrimary)176            Circle()177                .fill(ZyquoColor.accent)178                .frame(width: size * 0.28, height: size * 0.28)179                .offset(x: size * 0.18, y: 0)180        }181    }182}183