mirror of https://github.com/garrytan/gstack.git
138 lines
4.6 KiB
Plaintext
138 lines
4.6 KiB
Plaintext
// AUTO-GENERATED from gstack/ios-qa/templates/DebugOverlay.swift.template
|
|
//
|
|
// DebugOverlay — on-device visual presence. Animated brand-colored border +
|
|
// agent attribution chip + (optional) recording watermark. Renders above
|
|
// sheets, alerts, and modals via a dedicated UIWindow with high windowLevel.
|
|
//
|
|
// Everything in this file is gated #if DEBUG and gone in Release.
|
|
|
|
#if DEBUG && canImport(UIKit)
|
|
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
@MainActor
|
|
public final class DebugOverlayWindow {
|
|
public static let shared = DebugOverlayWindow()
|
|
|
|
private var window: UIWindow?
|
|
|
|
public func install(recording: Bool = false) {
|
|
guard window == nil else { return }
|
|
guard let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first else { return }
|
|
|
|
let w = PassThroughWindow(windowScene: scene)
|
|
w.windowLevel = .alert + 1
|
|
w.backgroundColor = .clear
|
|
w.isUserInteractionEnabled = false
|
|
|
|
let host = UIHostingController(rootView: OverlayRoot(recording: recording))
|
|
host.view.backgroundColor = .clear
|
|
w.rootViewController = host
|
|
w.isHidden = false
|
|
|
|
window = w
|
|
}
|
|
|
|
public func setAttribution(_ identity: String) {
|
|
OverlayAttributionState.shared.identity = identity
|
|
}
|
|
}
|
|
|
|
/// A window that lets touches pass through to underlying windows.
|
|
private final class PassThroughWindow: UIWindow {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let view = super.hitTest(point, with: event)
|
|
return view == rootViewController?.view ? nil : view
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class OverlayAttributionState: ObservableObject {
|
|
static let shared = OverlayAttributionState()
|
|
@Published var identity: String = "Claude Code (local)"
|
|
}
|
|
|
|
private struct OverlayRoot: View {
|
|
@StateObject private var attribution = OverlayAttributionState.shared
|
|
@State private var phase: CGFloat = 0
|
|
let recording: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Animated brand border
|
|
BorderShape()
|
|
.stroke(
|
|
AngularGradient(
|
|
gradient: Gradient(colors: [
|
|
BrandColor.accent.opacity(0.0),
|
|
BrandColor.accent.opacity(0.8),
|
|
BrandColor.accent.opacity(0.0),
|
|
]),
|
|
center: .center,
|
|
angle: .degrees(phase * 360)
|
|
),
|
|
lineWidth: 4
|
|
)
|
|
.ignoresSafeArea()
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: false)) {
|
|
phase = 1.0
|
|
}
|
|
}
|
|
|
|
// Attribution chip (top safe area)
|
|
VStack {
|
|
HStack {
|
|
Spacer()
|
|
Text("Driven by \(attribution.identity)")
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
Capsule().fill(BrandColor.accent.opacity(0.85))
|
|
)
|
|
.padding(.trailing, 12)
|
|
.padding(.top, 8)
|
|
Spacer().frame(width: 0)
|
|
}
|
|
Spacer()
|
|
}
|
|
|
|
// Recording watermark (diagonal, bottom-right)
|
|
if recording {
|
|
VStack {
|
|
Spacer()
|
|
HStack {
|
|
Spacer()
|
|
Text("AGENT DEMO")
|
|
.font(.system(size: 10, weight: .heavy, design: .monospaced))
|
|
.foregroundColor(.red.opacity(0.7))
|
|
.rotationEffect(.degrees(-30))
|
|
.padding(.trailing, 16)
|
|
.padding(.bottom, 30)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
|
|
private struct BorderShape: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var p = Path()
|
|
p.addRoundedRect(in: rect.insetBy(dx: 2, dy: 2), cornerSize: CGSize(width: 16, height: 16))
|
|
return p
|
|
}
|
|
}
|
|
|
|
private enum BrandColor {
|
|
// gstack brand color — resolved from DESIGN.md when codegen runs.
|
|
// Default falls back to a deep blue.
|
|
static let accent = Color(red: 0.0, green: 0.46, blue: 1.0)
|
|
}
|
|
|
|
#endif // DEBUG && canImport(UIKit)
|