From fce95a67b610284edb4d7805b63da292a66d8f52 Mon Sep 17 00:00:00 2001 From: justice-hwan Date: Wed, 3 Jun 2026 02:31:18 +0900 Subject: [PATCH 1/2] feat: let users hide idle island logos Adds an Idle logos setting that affects only the compact rest state, preserving hover peek and expanded layouts while allowing the resting island to shrink to the notch width. Constraint: Issue #28 asks to reduce idle notch-adjacent logo footprint without changing hover, expanded, sync, or alert behavior. Rejected: Hide logos in peek or expanded states | those states still need provider context and existing geometry. Confidence: high Scope-risk: narrow Directive: Keep sync sweep and alert glow tied to compact model.size so hidden idle logos still fit the physical notch. Tested: ./scripts/verify.sh --- Resources/en.lproj/Localizable.strings | 2 ++ Resources/zh-Hans.lproj/Localizable.strings | 2 ++ Sources/Model/IslandModel.swift | 23 ++++++++++++++++++++- Sources/Model/LogoVisibilityStore.swift | 21 +++++++++++++++++++ Sources/Views/IslandRootView.swift | 16 ++++++++------ Sources/Views/SettingsView.swift | 19 +++++++++++++++++ 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 Sources/Model/LogoVisibilityStore.swift diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index d594f112..6fd36c9a 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -48,10 +48,12 @@ "Critical" = "Critical"; "Daily token usage in %@" = "Daily token usage in %@"; "Display" = "Display"; +"Show provider logos before hover or expand." = "Show provider logos before hover or expand."; "General" = "General"; "Follows macOS" = "Follows macOS"; "Glow only on refresh, hover, or limit alerts." = "Glow only on refresh, hover, or limit alerts."; "Idle" = "Idle"; +"Idle logos" = "Idle logos"; "idle" = "idle"; "Input + output" = "Input + output"; "Input + output only. Matches Anthropic's claude.ai stats." = "Input + output only. Matches Anthropic's claude.ai stats."; diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index 37141e75..63f13a4d 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -48,10 +48,12 @@ "Critical" = "危急"; "Daily token usage in %@" = "%@ 年每日 token 用量"; "Display" = "显示"; +"Show provider logos before hover or expand." = "在悬停或展开前显示服务图标。"; "General" = "通用"; "Follows macOS" = "跟随 macOS"; "Glow only on refresh, hover, or limit alerts." = "仅在刷新、悬停或限额提醒时显示辉光。"; "Idle" = "空闲"; +"Idle logos" = "空闲图标"; "idle" = "空闲"; "Input + output" = "输入 + 输出"; "Input + output only. Matches Anthropic's claude.ai stats." = "只统计输入 + 输出;与 Anthropic 的 claude.ai 统计一致。"; diff --git a/Sources/Model/IslandModel.swift b/Sources/Model/IslandModel.swift index 16785ee4..db8073ab 100644 --- a/Sources/Model/IslandModel.swift +++ b/Sources/Model/IslandModel.swift @@ -47,6 +47,7 @@ final class IslandModel: ObservableObject { private var rawNotch: NotchInfo private var activeScreen = ScreenPref.shared.screen private var overviewDayDetailVisible = false + private(set) var compactLogosVisible = LogoVisibilityStore.shared.visible private var subs: Set = [] @@ -55,6 +56,7 @@ final class IslandModel: ObservableObject { self.notch = Self.applyOverride(to: notch, width: IslandSpacingStore.shared.width) recomputeSize() subscribeToSpacingStore() + subscribeToLogoVisibilityStore() subscribeToScreenPref() } @@ -145,6 +147,21 @@ final class IslandModel: ObservableObject { .store(in: &subs) } + private func subscribeToLogoVisibilityStore() { + LogoVisibilityStore.shared.$visible + .dropFirst() + .sink { [weak self] visible in + guard let self else { return } + withAnimation(.openMorph) { + self.compactLogosVisible = visible + if self.state == .compact { + self.recomputeSize() + } + } + } + .store(in: &subs) + } + private func subscribeToScreenPref() { ScreenPref.shared.$screen .dropFirst() @@ -166,7 +183,7 @@ final class IslandModel: ObservableObject { switch state { case .compact: size = CGSize( - width: notch.width + tabWidth * 2, + width: notch.width + compactLogoTabWidth * 2, height: notch.height ) case .peek: @@ -182,6 +199,10 @@ final class IslandModel: ObservableObject { } } + private var compactLogoTabWidth: CGFloat { + compactLogosVisible ? tabWidth : 0 + } + private var expandedContentHeight: CGFloat { let baseHeight = activeScreen == .overview ? overviewBaseContentHeight diff --git a/Sources/Model/LogoVisibilityStore.swift b/Sources/Model/LogoVisibilityStore.swift new file mode 100644 index 00000000..4b4e5c58 --- /dev/null +++ b/Sources/Model/LogoVisibilityStore.swift @@ -0,0 +1,21 @@ +import Foundation + +/// User preference for showing the provider logos in the compact rest state. +/// +/// Default on preserves the current branded island. Turning it off affects +/// only `.compact`: the rest-state silhouette shrinks to the notch width, so +/// loading sweeps and alert glow stay fitted to the hardware notch. +@MainActor +final class LogoVisibilityStore: ObservableObject { + static let shared = LogoVisibilityStore() + + private static let key = "MacIsland.compactLogosVisible" + + @Published var visible: Bool { + didSet { UserDefaults.standard.set(visible, forKey: Self.key) } + } + + private init() { + self.visible = Pref.seededBool(key: Self.key, default: true) + } +} diff --git a/Sources/Views/IslandRootView.swift b/Sources/Views/IslandRootView.swift index e803e2eb..aec82a96 100644 --- a/Sources/Views/IslandRootView.swift +++ b/Sources/Views/IslandRootView.swift @@ -74,6 +74,8 @@ struct IslandRootView: View { image: claudeLogo, color: IslandColor.claude, provider: .claude, + state: model.state, + compactLogosVisible: model.compactLogosVisible, edgePadding: logoEdgePadding, topPadding: max(0, (model.notch.height - 20) / 2) ) @@ -83,6 +85,8 @@ struct IslandRootView: View { image: openaiLogo, color: IslandColor.codex, provider: .codex, + state: model.state, + compactLogosVisible: model.compactLogosVisible, edgePadding: logoEdgePadding, topPadding: max(0, (model.notch.height - 20) / 2) ) @@ -440,18 +444,17 @@ private struct LogoOverlay: View { let image: NSImage? let color: Color let provider: AlertEngine.Provider + let state: IslandModel.State + let compactLogosVisible: Bool let edgePadding: CGFloat let topPadding: CGFloat @ObservedObject private var visibility = ProviderVisibilityStore.shared var body: some View { - // Hidden providers fully drop out — header / peek pill / chrome - // are gated identically. `.opacity(isVisible ? 1 : 0)` keeps the - // view in the layout (so other overlays don't reflow) but makes - // it invisible, and the explicit `.animation(.openMorph, value:)` - // pairs the chrome fade with the panel layout swap when the user - // toggles a provider in Settings. + // The logo preference applies only to the compact rest state. + // Peek/expanded remain exactly as before so hover, pills, and panel + // chrome keep their current geometry. if let image { Image(nsImage: image) .resizable() @@ -470,6 +473,7 @@ private struct LogoOverlay: View { private var isVisible: Bool { visibility.effectiveVisible(provider: provider) + && (state != .compact || compactLogosVisible) } private var providerLabel: String { diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift index a79f681f..701dd80e 100644 --- a/Sources/Views/SettingsView.swift +++ b/Sources/Views/SettingsView.swift @@ -17,6 +17,7 @@ struct SettingsView: View { @ObservedObject private var alwaysShow = AlwaysShowUsageStore.shared @ObservedObject private var alertPrefs = AlertThresholdStore.shared @ObservedObject private var spacing = IslandSpacingStore.shared + @ObservedObject private var logos = LogoVisibilityStore.shared @ObservedObject private var targetDisplay = IslandTargetDisplayStore.shared @ObservedObject private var appLanguage = AppLanguageStore.shared @ObservedObject private var usage = UsageStore.shared @@ -138,6 +139,7 @@ struct SettingsView: View { chartSection costStyleSection targetDisplaySection + restChromeSection if spacingSectionVisible { spacingSection } @@ -659,6 +661,23 @@ struct SettingsView: View { .padding(.bottom, 14) } + private var restChromeSection: some View { + VStack(alignment: .leading, spacing: 0) { + sectionLabel("Idle") + SettingsRow( + title: "Idle logos", + subtitle: "Show provider logos before hover or expand." + ) { + SettingsToggle(isOn: logos.visible) { + logos.visible.toggle() + } + } + } + .padding(.horizontal, 14) + .padding(.top, 14) + .padding(.bottom, 14) + } + /// Default-on-the-left: Compact is the new default, so it sits left /// of Notch-style. private var spacingSegmented: some View { From da5ac951e305dbcba552ab83848c0434478de16f Mon Sep 17 00:00:00 2001 From: justice-hwan Date: Wed, 3 Jun 2026 18:34:16 +0900 Subject: [PATCH 2/2] fix: keep idle logo hiding notched-only Treat the idle-logo preference as effective only on physical-notch targets so external displays never collapse into an empty synthetic pill. Constraint: Maintainer review found the toggle was exposed and effective on non-notched displays, where hiding logos leaves an empty black pill. Rejected: Delay hover logo animations | review called it minor polish and it would widen scope. Confidence: high Scope-risk: narrow Directive: Keep hover peek and expanded geometry unchanged; only compact notched displays should shrink. Tested: ./scripts/verify.sh --- Sources/Model/IslandModel.swift | 8 ++++++-- Sources/Model/LogoVisibilityStore.swift | 8 +++++--- Sources/Views/SettingsView.swift | 11 ++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Sources/Model/IslandModel.swift b/Sources/Model/IslandModel.swift index db8073ab..4d1d251c 100644 --- a/Sources/Model/IslandModel.swift +++ b/Sources/Model/IslandModel.swift @@ -47,7 +47,7 @@ final class IslandModel: ObservableObject { private var rawNotch: NotchInfo private var activeScreen = ScreenPref.shared.screen private var overviewDayDetailVisible = false - private(set) var compactLogosVisible = LogoVisibilityStore.shared.visible + private var compactLogoPreferenceVisible = LogoVisibilityStore.shared.visible private var subs: Set = [] @@ -153,7 +153,7 @@ final class IslandModel: ObservableObject { .sink { [weak self] visible in guard let self else { return } withAnimation(.openMorph) { - self.compactLogosVisible = visible + self.compactLogoPreferenceVisible = visible if self.state == .compact { self.recomputeSize() } @@ -203,6 +203,10 @@ final class IslandModel: ObservableObject { compactLogosVisible ? tabWidth : 0 } + var compactLogosVisible: Bool { + !notch.hasNotch || compactLogoPreferenceVisible + } + private var expandedContentHeight: CGFloat { let baseHeight = activeScreen == .overview ? overviewBaseContentHeight diff --git a/Sources/Model/LogoVisibilityStore.swift b/Sources/Model/LogoVisibilityStore.swift index 4b4e5c58..ecc3b0a4 100644 --- a/Sources/Model/LogoVisibilityStore.swift +++ b/Sources/Model/LogoVisibilityStore.swift @@ -2,9 +2,11 @@ import Foundation /// User preference for showing the provider logos in the compact rest state. /// -/// Default on preserves the current branded island. Turning it off affects -/// only `.compact`: the rest-state silhouette shrinks to the notch width, so -/// loading sweeps and alert glow stay fitted to the hardware notch. +/// Default on preserves the current branded island. On notched displays, +/// turning it off affects only `.compact`: the rest-state silhouette shrinks +/// to the physical notch width, so loading sweeps and alert glow stay fitted +/// to the hardware notch. Non-notched displays ignore the hidden state to +/// avoid leaving an empty synthetic pill at top-center. @MainActor final class LogoVisibilityStore: ObservableObject { static let shared = LogoVisibilityStore() diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift index 701dd80e..0fd2380f 100644 --- a/Sources/Views/SettingsView.swift +++ b/Sources/Views/SettingsView.swift @@ -139,7 +139,9 @@ struct SettingsView: View { chartSection costStyleSection targetDisplaySection - restChromeSection + if restChromeSectionVisible { + restChromeSection + } if spacingSectionVisible { spacingSection } @@ -154,6 +156,13 @@ struct SettingsView: View { DisplayInfo.currentTarget()?.notch.hasNotch == false } + /// The idle-logo toggle only makes sense on a physical notch. On + /// non-notched displays, hiding the rest-state logos would leave an empty + /// synthetic pill floating at top-center. + private var restChromeSectionVisible: Bool { + DisplayInfo.currentTarget()?.notch.hasNotch == true + } + private var providersTab: some View { VStack(alignment: .leading, spacing: 0) { providersSection