From b950b41503352bf019b3070e84a44740750c2be1 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 16:44:10 -0400 Subject: [PATCH] fix(bar-app): settings as standalone window, real theme forcing, roomier layout Move Settings out of the menu-bar popover into a standalone resizable AppKit window so clicking it no longer steals focus and dismisses the bar (the .sheet inside MenuBarExtra(.window) was the root cause). Force the actual NSWindow appearance (aqua/darkAqua/system) on both the popover and the settings window so Light/Dark visibly flips materials and semantic colors, not just custom tokens. Replace the fragile popover quit dialog with an inline two-step arm/confirm. Widen the dropdown and increase spacing/type for readability. Fill the settings window responsively and make Done close it via the window controller (dismiss() is a no-op in a hosted NSWindow). --- .../Sources/CCSBarApp/BarAnalyticsView.swift | 16 +-- macos-bar/Sources/CCSBarApp/BarMenuView.swift | 108 ++++++++++------ .../CCSBarApp/BarPreferencesView.swift | 8 +- .../CCSBarApp/BarSubscriptionCard.swift | 16 +-- macos-bar/Sources/CCSBarApp/CCSBarApp.swift | 7 + .../CCSBarApp/SettingsWindowController.swift | 122 ++++++++++++++++++ .../CCSBarApp/WindowAppearanceForcer.swift | 51 ++++++++ 7 files changed, 266 insertions(+), 62 deletions(-) create mode 100644 macos-bar/Sources/CCSBarApp/SettingsWindowController.swift create mode 100644 macos-bar/Sources/CCSBarApp/WindowAppearanceForcer.swift diff --git a/macos-bar/Sources/CCSBarApp/BarAnalyticsView.swift b/macos-bar/Sources/CCSBarApp/BarAnalyticsView.swift index 83e515b5..d2a1ffc8 100644 --- a/macos-bar/Sources/CCSBarApp/BarAnalyticsView.swift +++ b/macos-bar/Sources/CCSBarApp/BarAnalyticsView.swift @@ -36,7 +36,7 @@ struct BarAnalyticsView: View { /// By-surface + top-models detail, tightened and subordinate. @ViewBuilder private var breakdown: some View { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 8) { // Surface breakdown: "how much Claude Code vs Codex" — only shown when // the backend supplies at least one surface entry. Top 5 keeps it compact. if !analytics.bySurface.isEmpty { @@ -67,7 +67,7 @@ struct BarAnalyticsView: View { /// The collapsed informational spend strip: a "SPEND" label, one muted caption /// line, and a thin inline 30-day sparkline when there is real spend. private var spendStrip: some View { - VStack(alignment: .leading, spacing: 3) { + VStack(alignment: .leading, spacing: 5) { SectionLabel("Spend") if analytics.hasRecentData { Text(spendCaption) @@ -75,7 +75,7 @@ struct BarAnalyticsView: View { .foregroundStyle(.secondary) if !sparklineIsEmpty { Sparkline(values: analytics.byDay.map(\.cost), accent: theme.accent) - .frame(height: 16) + .frame(height: 18) } } else { Text(idleCaption) @@ -134,10 +134,10 @@ private struct SurfaceBar: View { .foregroundStyle(.secondary) } } - .padding(.horizontal, 8) + .padding(.horizontal, 10) } } - .frame(height: 22) + .frame(height: 26) } } @@ -164,10 +164,10 @@ private struct ModelBar: View { .font(.system(.caption, design: .monospaced)) .foregroundStyle(.secondary) } - .padding(.horizontal, 8) + .padding(.horizontal, 10) } } - .frame(height: 22) + .frame(height: 26) } } @@ -177,7 +177,7 @@ struct SectionLabel: View { init(_ text: String) { self.text = text } var body: some View { Text(text.uppercased()) - .font(.system(size: 10, weight: .bold)) + .font(.system(size: 11, weight: .bold)) .foregroundStyle(.secondary) .padding(.top, 1) } diff --git a/macos-bar/Sources/CCSBarApp/BarMenuView.swift b/macos-bar/Sources/CCSBarApp/BarMenuView.swift index 427c932a..b80b190c 100644 --- a/macos-bar/Sources/CCSBarApp/BarMenuView.swift +++ b/macos-bar/Sources/CCSBarApp/BarMenuView.swift @@ -7,12 +7,15 @@ import CCSBarCore /// footer controls. struct BarMenuView: View { @ObservedObject var viewModel: BarViewModel - /// Drives the preferences sheet from the footer gear. - @State private var showingPrefs = false - @State private var confirmingQuit = false - /// The prefs adapter the sheet edits; shares the standard suite with the - /// view model so a write-through is visible on the next poll. - private let prefs = BarPreferences() + /// Resolved theme injected by ThemedRoot — used to tint the armed Quit control + /// with the themed red ramp so it matches the dropdown on both plates. + @Environment(\.barTheme) private var theme + /// Two-step inline quit confirm. First footer-Quit click arms it (icon swaps + /// hollow->filled, tints red); second click terminates. Reset on every popover + /// open via .onAppear so a stale armed state never carries across sessions — + /// no modal, no .confirmationDialog (those steal focus and dismiss the popover, + /// the exact fragility of BUG 1). + @State private var quitArmed = false var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -30,12 +33,12 @@ struct BarMenuView: View { // for the common 1-4 subscription setup; the scroll only engages for // genuine pool/model overflow. ScrollView { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 12) { // (1) ALERTS first — urgent quota crossings surface above everything. // Spend-cap alerts are opt-in OFF by default, so by default only // quota/reauth/cooldown conditions appear here. if !viewModel.activeAlerts.isEmpty { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 8) { SectionLabel("Alerts") ForEach(viewModel.activeAlerts) { alert in AlertRow(alert: alert) @@ -66,22 +69,24 @@ struct BarMenuView: View { // scroller at runtime (belt-and-suspenders with .scrollIndicators). ScrollerHider().frame(width: 0, height: 0) } - .padding(12) + .padding(14) } .scrollIndicators(.never) - // 580 fits the full reordered layout (subscription cards + spend strip + - // pool rows + surface/models) without wasted whitespace for a typical - // 1-4 account setup. Scroll engages gracefully only on real overflow. - .frame(maxHeight: 580) + // 620 fits the full reordered layout (subscription cards + spend strip + + // pool rows + surface/models) plus the added vertical breathing room + // before scroll engages, for a typical 1-4 account setup. Scroll engages + // gracefully only on real overflow. + .frame(maxHeight: 620) } Divider() footer } - .frame(width: 340) - .onAppear { viewModel.onOpen() } - .sheet(isPresented: $showingPrefs) { - BarPreferencesView(viewModel: viewModel, prefs: prefs) + .frame(width: 380) + .onAppear { + viewModel.onOpen() + // Disarm quit on every popover open so a stale armed state never persists. + quitArmed = false } } @@ -93,7 +98,7 @@ struct BarMenuView: View { /// present, preserving the single "Accounts" header for a CLIProxy-only setup. @ViewBuilder private var accountsSection: some View { let parts = BarFormatting.partitionSubscriptions(viewModel.rows) - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 8) { if let error = viewModel.lastError { ErrorBanner(message: error) } @@ -124,7 +129,7 @@ struct BarMenuView: View { @ViewBuilder private var poolSection: some View { let parts = BarFormatting.partitionSubscriptions(viewModel.rows) if !parts.subscriptions.isEmpty && !parts.pool.isEmpty { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 8) { SectionLabel("Pool accounts") ForEach(parts.pool) { row in BarRowView(row: row, viewModel: viewModel) @@ -199,7 +204,7 @@ struct BarMenuView: View { } private var footer: some View { - HStack(spacing: 10) { + HStack(spacing: 12) { Button { openDashboard() } label: { @@ -215,7 +220,11 @@ struct BarMenuView: View { } .help("Toggle the menu-bar icon between color and monochrome (does not change the bar theme)") Button { - showingPrefs = true + // Open Settings as a standalone AppKit NSWindow (NOT a .sheet on this + // popover). A sheet hosted in a .window-style MenuBarExtra popover pulls + // focus off the popover and auto-dismisses the whole bar (BUG 1). The + // window opens beside the popover and leaves it untouched. + SettingsWindowController.shared.show(viewModel: viewModel) } label: { Label("Settings", systemImage: "gearshape") } @@ -227,25 +236,38 @@ struct BarMenuView: View { Image(systemName: "arrow.clockwise") } .help("Refresh") - // Quit confirms first — a stray click here previously closed the whole - // app (removing the menu-bar item) with no easy way back. - Button { - confirmingQuit = true - } label: { - Image(systemName: "power") - } - .help("Quit CCS Bar") - .confirmationDialog("Quit CCS Bar?", isPresented: $confirmingQuit) { - Button("Quit", role: .destructive) { NSApplication.shared.terminate(nil) } - Button("Cancel", role: .cancel) {} - } message: { - Text("This closes the menu-bar app. Reopen it from Applications.") - } + // Quit confirms via a two-step INLINE arm/confirm — no modal, no sheet, no + // .confirmationDialog. Those all steal focus and auto-dismiss the popover + // (the exact fragility of BUG 1). A stray single click can no longer kill + // the app: the first click only arms; the popover stays open and responsive. + quitButton } .buttonStyle(.borderless) .font(.caption) .padding(.horizontal, 14) - .padding(.vertical, 9) + .padding(.vertical, 11) + } + + /// Two visual states in one footer slot. Disarmed: hollow power icon that arms + /// on click. Armed: filled power icon tinted themed red that terminates on + /// click. Reopening the popover disarms it (.onAppear on the root VStack). + @ViewBuilder private var quitButton: some View { + if !quitArmed { + Button { + quitArmed = true + } label: { + Image(systemName: "power") + } + .help("Quit CCS Bar (click again to confirm)") + } else { + Button { + NSApplication.shared.terminate(nil) + } label: { + Image(systemName: "power.circle.fill") + } + .help("Click to confirm quit") + .foregroundStyle(theme.bandRed) + } } private func openDashboard() { @@ -273,14 +295,14 @@ struct BarRowView: View { } var body: some View { - HStack(alignment: .top, spacing: 9) { + HStack(alignment: .top, spacing: 10) { Circle() .fill(healthColor) .frame(width: 8, height: 8) .padding(.top, 5) - VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 6) { + VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 7) { Text(row.displayName ?? row.accountId) .font(.system(.body, design: .default).weight(.medium)) .lineLimit(1) @@ -327,8 +349,8 @@ struct BarRowView: View { } } } - .padding(.vertical, 6) - .padding(.horizontal, 8) + .padding(.vertical, 8) + .padding(.horizontal, 10) .background(Color.primary.opacity(0.035), in: RoundedRectangle(cornerRadius: 8)) } @@ -433,7 +455,7 @@ struct QuotaGaugeView: View { .frame(width: max(2, geo.size.width * fill)) } } - .frame(width: 44, height: 5) + .frame(width: 54, height: 6) } private func color(for band: BarQuotaGauge.Band) -> Color { @@ -543,7 +565,7 @@ struct Chip: View { } var body: some View { Text(text) - .font(.system(size: 9.5, weight: .semibold)) + .font(.system(size: 10, weight: .semibold)) .padding(.horizontal, 5) .padding(.vertical, 1.5) .background(tint.opacity(0.22), in: Capsule()) diff --git a/macos-bar/Sources/CCSBarApp/BarPreferencesView.swift b/macos-bar/Sources/CCSBarApp/BarPreferencesView.swift index b0e4e438..e9e7da13 100644 --- a/macos-bar/Sources/CCSBarApp/BarPreferencesView.swift +++ b/macos-bar/Sources/CCSBarApp/BarPreferencesView.swift @@ -8,7 +8,6 @@ import CCSBarCore struct BarPreferencesView: View { @ObservedObject var viewModel: BarViewModel let prefs: BarPreferences - @Environment(\.dismiss) private var dismiss @Environment(\.barTheme) private var theme // Local editable mirror of the persisted prefs. Loaded on appear; each change @@ -34,7 +33,10 @@ struct BarPreferencesView: View { Divider() footer } - .frame(width: 360, height: 460) + // Fill the hosting window responsively (it is a real resizable NSWindow now, + // not a fixed 360x460 sheet) so there are no dead margins and resizing works. + .frame(minWidth: 420, idealWidth: 460, maxWidth: .infinity, + minHeight: 520, idealHeight: 600, maxHeight: .infinity) .onAppear(perform: hydrate) } @@ -156,7 +158,7 @@ struct BarPreferencesView: View { private var footer: some View { HStack { Spacer() - Button("Done") { commitLevels(); dismiss() } + Button("Done") { commitLevels(); SettingsWindowController.shared.close() } .keyboardShortcut(.defaultAction) } .padding(.horizontal, 14) diff --git a/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift b/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift index 021f4803..fc2a162a 100644 --- a/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift +++ b/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift @@ -33,7 +33,7 @@ struct BarSubscriptionCard: View { staleFootnote } } - .padding(.vertical, 9) + .padding(.vertical, 11) .padding(.horizontal, 10) .frame(maxWidth: .infinity, alignment: .leading) .background( @@ -46,7 +46,7 @@ struct BarSubscriptionCard: View { /// Health dot + product name + reauth chip + tier chip. No pause toggle — /// subscriptions are not routable pool accounts. private var titleRow: some View { - HStack(spacing: 6) { + HStack(spacing: 8) { Circle() .fill(healthColor) .frame(width: 8, height: 8) @@ -71,7 +71,7 @@ struct BarSubscriptionCard: View { let ordered = orderedWindows let bindingKey = binding?.key - return VStack(alignment: .leading, spacing: 3) { + return VStack(alignment: .leading, spacing: 5) { ForEach(ordered) { w in windowBarRow(w, isBinding: w.key == bindingKey) } @@ -118,7 +118,7 @@ struct BarSubscriptionCard: View { ? .system(.caption2, design: .monospaced).weight(.semibold) : .system(.caption2, design: .monospaced)) .foregroundStyle(isBinding ? .primary : .secondary) - .frame(width: 28, alignment: .leading) + .frame(width: 32, alignment: .leading) // Horizontal fill bar — wider than the old secondary thinBar so fine // gradations are visible. Remaining fraction fills from the left so a @@ -127,9 +127,9 @@ struct BarSubscriptionCard: View { Capsule().fill(Color.primary.opacity(isBinding ? 0.14 : 0.09)) Capsule() .fill(barColor) - .frame(width: max(2, 88 * fill)) + .frame(width: max(2, 110 * fill)) } - .frame(width: 88, height: isBinding ? 6 : 4) + .frame(width: 110, height: isBinding ? 7 : 5) Spacer(minLength: 5) @@ -137,7 +137,7 @@ struct BarSubscriptionCard: View { Text("\(Int(w.remainingPercent.rounded()))%") .font(.system(.caption2, design: .monospaced)) .foregroundStyle(isBinding ? barColor : .secondary) - .frame(width: 28, alignment: .trailing) + .frame(width: 32, alignment: .trailing) Spacer(minLength: 5) @@ -188,7 +188,7 @@ struct BarSubscriptionCard: View { .foregroundStyle(isBinding ? .secondary : .tertiary) } } - .frame(width: 42, alignment: .trailing) + .frame(width: 48, alignment: .trailing) } /// Extract the "~Th Mm" part from paceClause for the at-risk inline warning. diff --git a/macos-bar/Sources/CCSBarApp/CCSBarApp.swift b/macos-bar/Sources/CCSBarApp/CCSBarApp.swift index de57f800..e8875151 100644 --- a/macos-bar/Sources/CCSBarApp/CCSBarApp.swift +++ b/macos-bar/Sources/CCSBarApp/CCSBarApp.swift @@ -43,8 +43,15 @@ struct ThemedRoot: View { @ViewBuilder var content: Content var body: some View { + // Order matters: .preferredColorScheme first updates the SwiftUI \.colorScheme + // environment for descendants (so the token resolver + Color.primary/.secondary + // pick up the chosen scheme), THEN the .background WindowAppearanceForcer sets + // the actual host NSWindow.appearance so system materials + semantic-color + // inversions flip at the AppKit layer too — not just the custom RGB tokens. + // The two are complementary: env tokens + real window appearance. KEEP both. ResolvedThemeHost(content: content) .preferredColorScheme(appearance.forced) + .background(WindowAppearanceForcer(appearance: appearance)) } } diff --git a/macos-bar/Sources/CCSBarApp/SettingsWindowController.swift b/macos-bar/Sources/CCSBarApp/SettingsWindowController.swift new file mode 100644 index 00000000..2e966208 --- /dev/null +++ b/macos-bar/Sources/CCSBarApp/SettingsWindowController.swift @@ -0,0 +1,122 @@ +import SwiftUI +import AppKit +import CCSBarCore + +/// Opens the CCS Bar settings as a standalone AppKit `NSWindow`. +/// +/// MECHANISM: a singleton `NSWindowController`-style driver backed by a real +/// `NSWindow` + `NSWindowDelegate`, NOT the SwiftUI `Window` scene. AppKit is the +/// only path that gives deterministic control of the three things a +/// MenuBarExtra-only (no dock icon) app needs: +/// (a) forcing `window.appearance` so the theme flips at the AppKit layer, +/// (b) restoring `.accessory` activation policy on close (drop the dock icon), +/// (c) singleton reuse so a second Settings click focuses the existing window +/// instead of spawning a duplicate. +/// The SwiftUI `Window` scene leaves the app stuck in `.regular` with a lingering +/// dock icon and version-dependent focus behavior, so it is deliberately avoided. +/// +/// Crucially, opening this window does NOT touch the MenuBarExtra popover's own +/// NSWindow (a separate AppKit window), so the popover stays open and responsive +/// — the exact cross-window isolation `ScrollerHider` already proves works here. +/// This is what fixes BUG 1: the old `.sheet` presented inside the `.window` +/// popover stole focus and auto-dismissed the whole bar. +@MainActor +final class SettingsWindowController { + /// One window, max. A second show() reuses it rather than spawning a duplicate. + static let shared = SettingsWindowController() + + private var window: NSWindow? + /// Retained so the delegate isn't deallocated while the window lives. + private var delegate: SettingsWindowDelegate? + + private init() {} + + /// Show (or re-focus) the settings window, hosting the LIVE view model so the + /// appearance picker drives both this window and the menu-bar popover. + func show(viewModel: BarViewModel) { + if let existing = window { + // Reuse: bring the single window to front instead of opening another. + existing.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + return + } + + // SwiftUI root: the SAME view model + the SAME ThemedRoot pipeline as the + // popover, so the settings window themes identically and live-syncs. + let root = SettingsWindowRoot(viewModel: viewModel) + let hosting = NSHostingController(rootView: root) + + let window = NSWindow(contentViewController: hosting) + window.title = "CCS Bar Settings" + // Titled + closable only: it's a settings dialog, not a document window, so + // no miniaturize/zoom. Resizable (no .nonResizable) per the spec. + window.styleMask = [.titled, .closable, .resizable] + window.setContentSize(NSSize(width: 460, height: 600)) + window.minSize = NSSize(width: 420, height: 520) + // Reuse the instance on reopen instead of tearing it down on close. + window.isReleasedWhenClosed = false + window.center() + + let delegate = SettingsWindowDelegate(onClose: { [weak self] in self?.handleClose() }) + window.delegate = delegate + self.delegate = delegate + self.window = window + + // Accessory (menu-bar-only) apps can't take key focus; upgrade to .regular + // so the window focuses and shows in the app switcher while it's open. + NSApp.setActivationPolicy(.regular) + window.makeKeyAndOrderFront(nil) + // Force front even when invoked from another frontmost app. + NSApp.activate(ignoringOtherApps: true) + } + + /// Close the window programmatically (e.g. the Done button). Routes through + /// `performClose` so the delegate restores `.accessory` mode exactly like the + /// title-bar close button — `@Environment(\.dismiss)` is a no-op in a plain + /// NSHostingController window, so this is the reliable path. + func close() { window?.performClose(nil) } + + /// Delegate callback on window close: drop the dock icon back so we return to + /// menu-bar-only mode. The popover is untouched throughout. The window itself + /// is kept (isReleasedWhenClosed = false) for cheap reuse, but we clear our + /// reference so the next show() rebuilds a fresh, correctly-centered window. + private func handleClose() { + NSApp.setActivationPolicy(.accessory) + window = nil + delegate = nil + } +} + +/// Bridges `NSWindow` close back to the controller so it can restore the +/// `.accessory` activation policy (menu-bar-only mode). +final class SettingsWindowDelegate: NSObject, NSWindowDelegate { + private let onClose: () -> Void + init(onClose: @escaping () -> Void) { + self.onClose = onClose + } + func windowWillClose(_ notification: Notification) { + onClose() + } +} + +/// SwiftUI root hosted inside the settings `NSWindow`. Wraps the reused +/// `BarPreferencesView` in the SAME `ThemedRoot` token pipeline as the popover, +/// so the settings window themes identically. Observing the shared +/// `BarViewModel` means a theme pick here re-renders BOTH windows live. +struct SettingsWindowRoot: View { + @ObservedObject var viewModel: BarViewModel + /// The prefs adapter the view edits; shares the standard suite with the view + /// model so a write-through is visible on the next poll (same as the popover). + private let prefs = BarPreferences() + + var body: some View { + // ThemedRoot applies .preferredColorScheme + injects tokens, and its + // .background WindowAppearanceForcer forces THIS NSWindow's appearance so + // system materials + semantic colors flip too — single source of truth with + // the popover. Fill the window so the plate covers the full content area. + ThemedRoot(appearance: viewModel.appearance) { + BarPreferencesView(viewModel: viewModel, prefs: prefs) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} diff --git a/macos-bar/Sources/CCSBarApp/WindowAppearanceForcer.swift b/macos-bar/Sources/CCSBarApp/WindowAppearanceForcer.swift new file mode 100644 index 00000000..b9b8d6e8 --- /dev/null +++ b/macos-bar/Sources/CCSBarApp/WindowAppearanceForcer.swift @@ -0,0 +1,51 @@ +import SwiftUI +import AppKit +import CCSBarCore + +/// Zero-size bridge that walks up to the host `NSWindow` and forces its +/// `appearance` to match the user's chosen `BarAppearance`. +/// +/// Why this exists on top of `.preferredColorScheme`: that modifier only +/// rewrites the SwiftUI `\.colorScheme` environment for descendant views — it +/// does NOT change the host `NSWindow.effectiveAppearance`. So AppKit-level +/// surfaces keep reading the OS appearance and fight the chosen theme: +/// - system materials (the MenuBarExtra popover's backing material) +/// - semantic colors (`Color.primary` / `.secondary`, used by Chip text, +/// health dots, captions) which invert off the window appearance. +/// Setting `window.appearance` directly fixes the theme at the AppKit layer so +/// the whole surface flips, not just the custom RGB tokens. +/// +/// Modeled on the proven `ScrollerHider` pattern (which already reaches the host +/// window inside this popover), proving cross-window AppKit access works here. +struct WindowAppearanceForcer: NSViewRepresentable { + let appearance: BarAppearance + + func makeNSView(context: Context) -> NSView { + let probe = NSView(frame: .zero) + // Defer until the view is in the hierarchy; at make-time `view.window` is nil. + DispatchQueue.main.async { apply(to: probe) } + return probe + } + + func updateNSView(_ nsView: NSView, context: Context) { + // Re-apply on every update: the popover's NSWindow can be rebuilt on content + // changes, and the appearance pick itself changes mid-session. + DispatchQueue.main.async { apply(to: nsView) } + } + + /// Force the host window's appearance from the chosen theme. + /// .system -> nil (follow the OS) + /// .light -> aqua + /// .dark -> darkAqua + private func apply(to view: NSView) { + guard let window = view.window else { return } + switch appearance { + case .system: + window.appearance = nil + case .light: + window.appearance = NSAppearance(named: .aqua) + case .dark: + window.appearance = NSAppearance(named: .darkAqua) + } + } +}