// // DocsView.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // In-app rendering of docs/API.md — the same source of truth as the served // behavior, rendered as a polished reference: hero header, real tables, // code blocks with a header bar and one-click copy, generous rounding. // The file is bundled by the Makefile; a repo-relative fallback covers // `swift run` during development. // import SwiftUI struct DocsView: View { @State private var markdown: String? var body: some View { Group { if let markdown { ScrollView { VStack(alignment: .leading, spacing: ZyquoSpacing.md) { DocsHero() MarkdownText(markdown: markdown) } .padding(ZyquoSpacing.xl) .frame(maxWidth: 820, alignment: .leading) } .frame(maxWidth: .infinity) } else { EmptyState( systemImage: "book", title: "API reference unavailable", message: "docs/API.md was not found in the app bundle." ) } } .onAppear(perform: load) } private func load() { if let bundled = Bundle.main.url(forResource: "API", withExtension: "md"), let text = try? String(contentsOf: bundled, encoding: .utf8) { markdown = text return } // Development fallback: repo checkout next to the executable's cwd. let repoDocs = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent("docs/API.md") markdown = try? String(contentsOf: repoDocs, encoding: .utf8) } } /// Hero header card at the top of the reference. private struct DocsHero: View { @EnvironmentObject private var server: ServerController var body: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.xs) { HStack(spacing: ZyquoSpacing.xs) { RouterZGlyph(size: 22) Text("API Reference") .font(.system(size: 24, weight: .bold)) .foregroundStyle(ZyquoColor.textPrimary) } Text("One OpenAI-compatible endpoint for every provider. Point any SDK at your local base URL — this reference is generated from the same source of truth the server implements.") .font(ZyquoFont.body(size: 13.5)) .foregroundStyle(ZyquoColor.textSecondary) .lineSpacing(3) HStack(spacing: ZyquoSpacing.xs) { Text(server.isRunning ? server.endpointURL : "http://localhost:\(server.port)/v1") .font(ZyquoFont.mono(size: 12.5, weight: .medium)) .foregroundStyle(ZyquoColor.accent) CopyButton(value: server.isRunning ? server.endpointURL : "http://localhost:\(server.port)/v1") if !server.isRunning { CapabilityBadge(label: "SERVER STOPPED", tint: ZyquoColor.warning) } } .padding(.top, 2) } .padding(ZyquoSpacing.lg) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous) .fill( LinearGradient( colors: [ZyquoColor.accentSubtle, ZyquoColor.surface], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous) .strokeBorder(ZyquoColor.accent.opacity(0.18), lineWidth: ZyquoMetrics.hairline) ) ) } } // MARK: - Markdown renderer private struct MarkdownText: View { let markdown: String var body: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.sm) { ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in render(block) } } } private enum Block { case heading(level: Int, text: String) case code(String) case table(header: [String], rows: [[String]]) case paragraph(String) case bullet(String) case rule } private var blocks: [Block] { var result: [Block] = [] var paragraph: [String] = [] var codeBlock: [String]? var tableLines: [String] = [] func flushParagraph() { if !paragraph.isEmpty { result.append(.paragraph(paragraph.joined(separator: "\n"))) paragraph = [] } } func flushTable() { guard tableLines.count >= 2 else { tableLines = []; return } func cells(_ line: String) -> [String] { line.trimmingCharacters(in: CharacterSet(charactersIn: "| ")) .components(separatedBy: " | ") .map { $0.trimmingCharacters(in: .whitespaces) } } let header = cells(tableLines[0]) let rows = tableLines.dropFirst(2).map(cells) result.append(.table(header: header, rows: Array(rows))) tableLines = [] } for line in markdown.components(separatedBy: "\n") { if var code = codeBlock { if line.hasPrefix("```") { result.append(.code(code.joined(separator: "\n"))) codeBlock = nil } else { code.append(line) codeBlock = code } continue } if line.hasPrefix("```") { flushParagraph(); flushTable() codeBlock = [] continue } if line.hasPrefix("|") { flushParagraph() tableLines.append(line) continue } flushTable() if line.hasPrefix("#") { flushParagraph() let level = line.prefix(while: { $0 == "#" }).count result.append(.heading(level: level, text: line.drop(while: { $0 == "#" }).trimmingCharacters(in: .whitespaces))) } else if line.hasPrefix("---") { flushParagraph() result.append(.rule) } else if line.hasPrefix("- ") { flushParagraph() result.append(.bullet(String(line.dropFirst(2)))) } else if line.trimmingCharacters(in: .whitespaces).isEmpty { flushParagraph() } else if line.hasPrefix(" "), paragraph.isEmpty, case .bullet(let text) = result.last { // Hard-wrapped bullet continuation line. result[result.count - 1] = .bullet(text + " " + line.trimmingCharacters(in: .whitespaces)) } else { paragraph.append(line) } } flushParagraph(); flushTable() return result } @ViewBuilder private func render(_ block: Block) -> some View { switch block { case .heading(let level, let text): heading(level: level, text: text) case .code(let code): CodeCard(code: code) case .table(let header, let rows): TableCard(header: header, rows: rows) case .paragraph(let text): Text(attributed(text)) .font(ZyquoFont.body(size: 13.5)) .foregroundStyle(ZyquoColor.textPrimary) .lineSpacing(3.5) case .bullet(let text): HStack(alignment: .top, spacing: ZyquoSpacing.xs) { Circle() .fill(ZyquoColor.accent) .frame(width: 5, height: 5) .padding(.top, 7) Text(attributed(text)) .font(ZyquoFont.body(size: 13.5)) .foregroundStyle(ZyquoColor.textPrimary) .lineSpacing(3) } .padding(.leading, ZyquoSpacing.xxs) case .rule: ZyquoHairline() .padding(.vertical, ZyquoSpacing.xs) } } @ViewBuilder private func heading(level: Int, text: String) -> some View { if level <= 1 { EmptyView() // the hero replaces the H1 } else if level == 2 { HStack(spacing: ZyquoSpacing.xs) { RoundedRectangle(cornerRadius: 2, style: .continuous) .fill(ZyquoColor.accent) .frame(width: 4, height: 18) Text(attributed(text)) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(ZyquoColor.textPrimary) } .padding(.top, ZyquoSpacing.lg) } else { Text(attributed(text)) .font(ZyquoFont.heading) .foregroundStyle(ZyquoColor.textPrimary) .padding(.top, ZyquoSpacing.xs) } } private func attributed(_ text: String) -> AttributedString { (try? AttributedString( markdown: text, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace) )) ?? AttributedString(text) } } // MARK: - Code card private struct CodeCard: View { let code: String private var language: String { if code.contains("import OpenAI") || code.contains("await client") { return "javascript" } if code.contains("from openai") || code.contains("client =") { return "python" } if code.hasPrefix("curl") { return "bash" } if code.hasPrefix("{") || code.hasPrefix("[") { return "json" } return "code" } var body: some View { VStack(spacing: 0) { HStack(spacing: ZyquoSpacing.xs) { HStack(spacing: 4) { ForEach(0..<3, id: \.self) { _ in Circle() .fill(ZyquoColor.border) .frame(width: 7, height: 7) } } Text(language) .font(ZyquoFont.mono(size: 10)) .foregroundStyle(ZyquoColor.textTertiary) Spacer() CopyButton(value: code) } .padding(.horizontal, ZyquoSpacing.sm) .padding(.vertical, ZyquoSpacing.xs) .background(ZyquoColor.surfaceSecondary) ZyquoHairline() ScrollView(.horizontal, showsIndicators: false) { Text(code) .font(ZyquoFont.mono(size: 12)) .foregroundStyle(ZyquoColor.textPrimary) .lineSpacing(2.5) .textSelection(.enabled) .padding(ZyquoSpacing.sm) } .background(ZyquoColor.surface) } .clipShape(RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) } } // MARK: - Table card private struct TableCard: View { let header: [String] let rows: [[String]] var body: some View { VStack(spacing: 0) { row(cells: header, isHeader: true) ForEach(Array(rows.enumerated()), id: \.offset) { index, cells in ZyquoHairline() row(cells: cells, isHeader: false) .background(index.isMultiple(of: 2) ? ZyquoColor.surface : ZyquoColor.surfaceSecondary.opacity(0.5)) } } .clipShape(RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) } private func row(cells: [String], isHeader: Bool) -> some View { HStack(alignment: .top, spacing: ZyquoSpacing.sm) { ForEach(Array(cells.enumerated()), id: \.offset) { index, cell in Group { if isHeader { Text(cell.uppercased()) .font(.system(size: 9.5, weight: .semibold)) .foregroundStyle(ZyquoColor.textTertiary) .kerning(0.4) } else { Text(inline(cell)) .font(ZyquoFont.body(size: 12)) .foregroundStyle(ZyquoColor.textPrimary) .lineSpacing(2) } } .frame(maxWidth: index == 0 ? 190 : .infinity, alignment: .leading) } } .padding(.horizontal, ZyquoSpacing.sm) .padding(.vertical, ZyquoSpacing.xs) .background(isHeader ? ZyquoColor.surfaceSecondary : .clear) } private func inline(_ text: String) -> AttributedString { (try? AttributedString( markdown: text, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace) )) ?? AttributedString(text) } }