feat: add component gallery with snapshot mode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:29:33 +02:00
co-authored by Claude Sonnet 5.5
parent d0f78f5165
commit 9ac8df7b9d
2 changed files with 139 additions and 0 deletions
+1
View File
@@ -20,6 +20,7 @@ let package = Package(
),
.executableTarget(name: "Sst", dependencies: ["SstCore", "SstWhisper"]),
.executableTarget(name: "SstProbe", dependencies: ["SstCore", "SstWhisper"]),
.executableTarget(name: "SstGallery", dependencies: ["SstUI"]),
.testTarget(name: "SstCoreTests", dependencies: ["SstCore"]),
.testTarget(name: "SstDataTests", dependencies: ["SstData", "SstCore"]),
.testTarget(name: "SstWhisperTests", dependencies: ["SstWhisper"]),
+138
View File
@@ -0,0 +1,138 @@
import SwiftUI
import SstUI
@main
struct GalleryApp: App {
init() {
// `SstGallery --snapshot out.png` renders both finishes to a PNG and exits (for checking without a screen grab).
let args = CommandLine.arguments
guard let flag = args.firstIndex(of: "--snapshot"), args.count > flag + 1 else { return }
let path = args[flag + 1]
Task { @MainActor in
let view = HStack(spacing: 0) {
Finish(scheme: .light, title: "Aluminium", frozen: true)
Finish(scheme: .dark, title: "Graphite", frozen: true)
}
.frame(width: Layout.windowDefaultWidth, height: Layout.windowDefaultHeight)
let renderer = ImageRenderer(content: view)
renderer.scale = 2
if let tiff = renderer.nsImage?.tiffRepresentation, let png = NSBitmapImageRep(data: tiff)?.representation(using: .png, properties: [:]) {
try? png.write(to: URL(fileURLWithPath: path))
}
exit(0)
}
}
var body: some Scene {
WindowGroup("Sst componenten") {
HStack(spacing: 0) {
Finish(scheme: .light, title: "Aluminium")
Finish(scheme: .dark, title: "Graphite")
}
.frame(minWidth: Layout.windowMinWidth, minHeight: Layout.windowMinHeight)
}
.defaultSize(width: Layout.windowDefaultWidth, height: Layout.windowDefaultHeight)
}
}
/// Fake speech level so the meter, LEDs and counter can be judged in motion.
private func simulatedDbfs(at t: Double) -> Double {
if t.truncatingRemainder(dividingBy: 9) > 6.2 { return -90 }
let syllable = floor(t * 4.2)
let r = abs(sin(syllable * 12.9898) * 43758.5453).truncatingRemainder(dividingBy: 1)
return r > 0.12 ? -38 + r * 22 : -90
}
struct Finish: View {
let scheme: ColorScheme
let title: String
var frozen = false
@State private var recording = true
@State private var started = Date()
@State private var query = ""
var body: some View {
Group {
if frozen {
content(elapsed: 12.3, dbfs: -26)
} else {
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in
let elapsed = timeline.date.timeIntervalSince(started)
let dbfs = recording ? simulatedDbfs(at: elapsed) : -120
content(elapsed: recording ? elapsed : 0, dbfs: dbfs)
}
}
}
.environment(\.colorScheme, scheme)
}
private func content(elapsed: Double, dbfs: Double) -> some View {
ZStack {
BrushedBackground()
VStack(alignment: .leading, spacing: Layout.panelGap) {
SilkLabel("Afwerking · \(title)")
VStack(alignment: .leading, spacing: Layout.panelGap) {
HStack(alignment: .top, spacing: Layout.panelGap) {
VUMeterView(levelDbfs: dbfs, isActive: recording, initialFraction: frozen ? VUScale.fraction(forDbfs: dbfs) : 0)
VStack(alignment: .leading, spacing: Layout.panelGap) {
SilkLabel("Tijd", style: Typography.labelSmall)
SegmentCounterView(Self.clock(elapsed))
SilkLabel("Niveau", style: Typography.labelSmall)
LEDBarView(fraction: VUScale.fraction(forDbfs: dbfs))
}
}
SeamDivider()
HStack(spacing: Layout.panelGap) {
Button { recording.toggle(); started = Date() } label: {
HStack { LampView(isOn: recording); Text("Rec") }
}
.buttonStyle(KeyButtonStyle(kind: .record, latched: recording))
Button("Stop") { recording = false }.buttonStyle(KeyButtonStyle())
Button("Kopieer") {}.buttonStyle(KeyButtonStyle())
Button("Uit") {}.buttonStyle(KeyButtonStyle()).disabled(true)
Spacer()
LampView(isOn: recording, large: true)
LampView(isOn: true, tint: .amber)
}
}
.padding(Layout.panelPadding)
.background(PanelBackground())
VStack(alignment: .leading, spacing: 0) {
row("14:02:11", "Ik wil de meeting verplaatsen naar tomorrow morning.", selected: false)
row("14:03:40", "Hij stuurt het door naar Claude Code vandaag.", selected: true)
}
.background(PaperBackground())
HStack {
SilkLabel("Zoek")
Group {
if frozen {
Text("claude").textStyle(Typography.body).frame(maxWidth: .infinity, alignment: .leading)
} else {
TextField("", text: $query).textFieldStyle(.plain).textStyle(Typography.body)
}
}
.padding(Space.s3).background(WellBackground())
}
}
.padding(Layout.panelPadding)
}
}
private func row(_ time: String, _ text: String, selected: Bool) -> some View {
HStack(alignment: .firstTextBaseline, spacing: Space.s4) {
Text(time).textStyle(Typography.counterSmall).foregroundStyle(Palette.inkSecondary)
Text(text).textStyle(Typography.reading).foregroundStyle(Palette.inkPrimary)
Spacer()
}
.padding(.horizontal, Space.s4).padding(.vertical, Space.s3)
.frame(minHeight: Size.rowHeight)
.background(selected ? Palette.surfaceSelection : Color.clear)
}
private static func clock(_ seconds: Double) -> String {
let total = Int(seconds * 10)
return String(format: "%02d:%02d.%d", (total / 600) % 100, (total / 10) % 60, total % 10)
}
}