Files
Jarvis/App/HUD/HUDBannerPanel.swift
james.maes 122153d544 feat(01-03): add JarvisHUDPanel, HUDBannerPanel+Coordinator, AppDelegate entitlement hard-block
- JarvisHUDPanel: borderless 720×720 NSPanel, .statusBar level, .canJoinAllSpaces/.fullScreenAuxiliary/.transient collectionBehavior, transparent WKWebView (drawsBackground=false via KVO), cancelOperation(_:) + performClose(_:) overrides for Escape/⌘W dismiss, Reduce Transparency fallback to windowBackgroundColor@95%, fresh-center on summon via NSScreen.screenWithMouse()
- HUDBannerPanel: .floating non-activating 360pt banner panel, canBecomeKey/canBecomeMain both false (never steals focus), top-trailing positioning 24pt/8pt inset
- HUDBannerCoordinator (@MainActor): priority queue with pre-emption (lower priority int = higher precedence), dismissed-this-launch dedup by id, 300ms gap to next banner per UI-SPEC Surface 5
- BannerContent with four presets: keychain-empty (1) > input-monitoring-denied (2) > hotkey-bind-failed (3) > ollama-url-rejected (4); Sendable/Equatable/Identifiable
- HUDBanner (SwiftUI): 4pt arc-reactor-glow leading stripe + title/body/action/dismiss-× layout on .hudWindow visual-effect background with separatorColor border
- AppDelegate: EntitlementGateProbe protocol + InfoPlistEntitlementGateProbe impl + injectable onEntitlementFailure (@MainActor) defaulting to presentEntitlementFailureAlert + NSApp.terminate, skipped under XCTest host; bootstrap chain installs menu-bar + HUD panel + banner panel (Plan 04 prepends logging and appends config/keychain/hotkey)
- 9 new tests in JarvisAppTests: 3 HUD panel (style flags, summon/dismiss, Escape), 4 banner coordinator (enqueue, priority pre-emption, dismiss-drain, dismissed-this-launch dedup), 2 AppDelegate (hard-block triggers / does-not-trigger with mocked probe)
2026-04-22 13:26:40 -05:00

54 lines
2.1 KiB
Swift

import AppKit
import SwiftUI
/// A `.floating` non-activating panel positioned top-trailing on the main
/// display, used for degraded-mode banners (Input Monitoring denial, Keychain
/// empty, hotkey-bind failure, Ollama URL rejected). Distinct from
/// `JarvisHUDPanel` per UI-SPEC Surface 5 "Critical decision" lines 452-456 —
/// banners must persist across HUD summon/dismiss and live on their own z-order.
@MainActor
public final class HUDBannerPanel: NSPanel {
public init() {
let initialFrame = NSRect(x: 0, y: 0, width: 360, height: 96)
super.init(
contentRect: initialFrame,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
self.level = .floating
self.isMovableByWindowBackground = false
self.isOpaque = false
self.backgroundColor = .clear
self.hasShadow = true
self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient]
self.hidesOnDeactivate = false
self.titleVisibility = .hidden
}
/// Banner panel never steals focus (UI-SPEC Surface 5 line 473).
public override var canBecomeKey: Bool { false }
public override var canBecomeMain: Bool { false }
/// Install a SwiftUI view as the panel's content and re-position.
public func host(_ view: some View) {
self.contentView = NSHostingView(rootView: view)
self.positionTopTrailing()
}
/// Position banner 24pt from right edge, 8pt below the system menu-bar
/// height on the main display (UI-SPEC Surface 5 line 460).
public func positionTopTrailing() {
guard let screen = NSScreen.main else { return }
let visibleFrame = screen.visibleFrame
let bannerWidth: CGFloat = 360
let bannerHeight: CGFloat = self.contentView?.fittingSize.height ?? 96
let x = visibleFrame.maxX - bannerWidth - 24
let y = visibleFrame.maxY - bannerHeight - 8
self.setFrame(
NSRect(x: x, y: y, width: bannerWidth, height: bannerHeight),
display: true
)
}
}