- 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)
54 lines
2.1 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|