3babe2b3ab
New multiplatform SwiftUI app under apps/zacus-hub/ generated via
XcodeGen (project.yml). Three modes share a HubSession actor that
talks to tools/zacus-gateway over Tailscale-only HTTP/WS.
Game-master: live state stream (WebSocket /v1/state/ws), backend
health badges, hint trigger, journal.
Companion: push-to-talk via AVAudioRecorder (M4A AAC 16 kHz),
fallback file picker, hint request, transcript via voice-bridge
proxy.
Studio: YAML editor with validate/save (existing /v1/studio
routes), plus a Scratch tab hosting a Blockly + zelos-renderer
workspace in WKWebView. 38 custom Zacus block types across 10
categories (Scènes, NPC, Audio, LCD, Hardware ESP32, ESP-NOW,
BOX-3, M5, PLIP, Logique), .sb3 export/import via JSZip (procedure
calls + embedded YAML for lossless round-trip), Flasher sheet that
calls /v1/flash/{board} with auto/hot/cold strategies and shows
per-board step results including the cold-flash idf.py command.
ATS exception domains cover the Tailscale CGNAT range plus the
electron-server / studio / macm1 hostnames so the WebView and
URLSession can hit plain HTTP on the tailnet without TLS.
Token stays in the Keychain (cc.saillant.zacus.hub) — never
committed to source.
58 lines
1.8 KiB
Swift
58 lines
1.8 KiB
Swift
import SwiftUI
|
|
|
|
/// Sidebar listing block templates. Drag onto canvas, or click "+" for centre spawn.
|
|
struct BlockPaletteView: View {
|
|
var onAdd: (BlockKind) -> Void
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
ForEach(BlockCatalog.byCategory(), id: \.0) { category, specs in
|
|
section(category, specs: specs)
|
|
}
|
|
}
|
|
.padding(12)
|
|
}
|
|
.frame(width: 240)
|
|
.background(.thinMaterial)
|
|
}
|
|
|
|
private func section(_ cat: BlockCategory, specs: [BlockSpec]) -> some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(spacing: 8) {
|
|
Circle().fill(cat.color).frame(width: 10, height: 10)
|
|
Text(cat.label).font(.subheadline.bold())
|
|
}
|
|
ForEach(specs, id: \.kind) { spec in
|
|
tile(spec, color: cat.color)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func tile(_ spec: BlockSpec, color: Color) -> some View {
|
|
HStack {
|
|
Text(spec.title).font(.callout).foregroundStyle(.primary)
|
|
Spacer()
|
|
Button { onAdd(spec.kind) } label: {
|
|
Image(systemName: "plus.circle").foregroundStyle(color)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(.vertical, 6).padding(.horizontal, 8)
|
|
.background(RoundedRectangle(cornerRadius: 6).fill(color.opacity(0.12)))
|
|
.contentShape(Rectangle())
|
|
#if os(macOS)
|
|
.draggable(PaletteDrop(kind: spec.kind)) {
|
|
// drag preview
|
|
HStack(spacing: 6) {
|
|
Circle().fill(color).frame(width: 8, height: 8)
|
|
Text(spec.title).font(.caption.bold())
|
|
}
|
|
.padding(6)
|
|
.background(.thinMaterial)
|
|
.cornerRadius(6)
|
|
}
|
|
#endif
|
|
}
|
|
}
|