diff --git a/ClaudeMeter/Models/AppSettings.swift b/ClaudeMeter/Models/AppSettings.swift index 2719c9c..186ed73 100644 --- a/ClaudeMeter/Models/AppSettings.swift +++ b/ClaudeMeter/Models/AppSettings.swift @@ -30,6 +30,11 @@ struct AppSettings: Codable, Equatable, Sendable { /// Menu bar icon display style var iconStyle: IconStyle + /// Tint the menu bar icon with the green/orange/red status colours. When false (default), + /// the icon is rendered as a template image so macOS tints it to match the system menu bar + /// (white in dark mode, black in light mode), aligning with the system tray's aesthetic. + var useColoredIcon: Bool + static let `default` = AppSettings( refreshInterval: 60, hasNotificationsEnabled: true, @@ -37,7 +42,8 @@ struct AppSettings: Codable, Equatable, Sendable { isFirstLaunch: true, cachedOrganizationId: nil, isSonnetUsageShown: false, - iconStyle: .battery + iconStyle: .battery, + useColoredIcon: false ) enum CodingKeys: String, CodingKey { @@ -48,6 +54,41 @@ struct AppSettings: Codable, Equatable, Sendable { case cachedOrganizationId = "cached_organization_id" case isSonnetUsageShown = "show_sonnet_usage" case iconStyle = "icon_style" + case useColoredIcon = "use_colored_icon" + } + + init( + refreshInterval: TimeInterval, + hasNotificationsEnabled: Bool, + notificationThresholds: NotificationThresholds, + isFirstLaunch: Bool, + cachedOrganizationId: UUID?, + isSonnetUsageShown: Bool, + iconStyle: IconStyle, + useColoredIcon: Bool + ) { + self.refreshInterval = refreshInterval + self.hasNotificationsEnabled = hasNotificationsEnabled + self.notificationThresholds = notificationThresholds + self.isFirstLaunch = isFirstLaunch + self.cachedOrganizationId = cachedOrganizationId + self.isSonnetUsageShown = isSonnetUsageShown + self.iconStyle = iconStyle + self.useColoredIcon = useColoredIcon + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let defaults = AppSettings.default + refreshInterval = try container.decodeIfPresent(TimeInterval.self, forKey: .refreshInterval) ?? defaults.refreshInterval + hasNotificationsEnabled = try container.decodeIfPresent(Bool.self, forKey: .hasNotificationsEnabled) ?? defaults.hasNotificationsEnabled + notificationThresholds = try container.decodeIfPresent(NotificationThresholds.self, forKey: .notificationThresholds) ?? defaults.notificationThresholds + isFirstLaunch = try container.decodeIfPresent(Bool.self, forKey: .isFirstLaunch) ?? defaults.isFirstLaunch + cachedOrganizationId = try container.decodeIfPresent(UUID.self, forKey: .cachedOrganizationId) + isSonnetUsageShown = try container.decodeIfPresent(Bool.self, forKey: .isSonnetUsageShown) ?? defaults.isSonnetUsageShown + iconStyle = try container.decodeIfPresent(IconStyle.self, forKey: .iconStyle) ?? defaults.iconStyle + // Backwards-compat: this key didn't exist before, so fall back to the default (off). + useColoredIcon = try container.decodeIfPresent(Bool.self, forKey: .useColoredIcon) ?? defaults.useColoredIcon } } diff --git a/ClaudeMeter/Views/MenuBar/IconCache.swift b/ClaudeMeter/Views/MenuBar/IconCache.swift index 48f67be..3dbbb5e 100644 --- a/ClaudeMeter/Views/MenuBar/IconCache.swift +++ b/ClaudeMeter/Views/MenuBar/IconCache.swift @@ -21,7 +21,8 @@ final class IconCache { isLoading: Bool, isStale: Bool, iconStyle: IconStyle, - weeklyPercentage: Double + weeklyPercentage: Double, + useColor: Bool ) -> NSImage? { cache.object(forKey: cacheKey( percentage: percentage, @@ -29,7 +30,8 @@ final class IconCache { isLoading: isLoading, isStale: isStale, iconStyle: iconStyle, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor )) } @@ -40,7 +42,8 @@ final class IconCache { isLoading: Bool, isStale: Bool, iconStyle: IconStyle, - weeklyPercentage: Double + weeklyPercentage: Double, + useColor: Bool ) { cache.setObject( image, @@ -50,7 +53,8 @@ final class IconCache { isLoading: isLoading, isStale: isStale, iconStyle: iconStyle, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor ) ) } @@ -61,10 +65,11 @@ final class IconCache { isLoading: Bool, isStale: Bool, iconStyle: IconStyle, - weeklyPercentage: Double + weeklyPercentage: Double, + useColor: Bool ) -> NSString { let percent = String(format: "%.2f", percentage) let weekly = String(format: "%.2f", weeklyPercentage) - return "\(percent)|\(weekly)|\(status.rawValue)|\(isLoading)|\(isStale)|\(iconStyle.rawValue)" as NSString + return "\(percent)|\(weekly)|\(status.rawValue)|\(isLoading)|\(isStale)|\(iconStyle.rawValue)|\(useColor)" as NSString } } diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/BatteryIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/BatteryIcon.swift index dc3bbaa..2058b12 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/BatteryIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/BatteryIcon.swift @@ -13,6 +13,7 @@ struct BatteryIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true private let capsuleWidth: CGFloat = 28 private let capsuleHeight: CGFloat = 10 @@ -22,14 +23,14 @@ struct BatteryIcon: View { if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 10, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { // Capsule with gradient fill using mask for proper rounded ends Capsule() - .fill(Color.gray.opacity(0.3)) + .fill(MenuBarIconColors.track(useColor: useColor)) .overlay(alignment: .leading) { GeometryReader { geo in - fillGradient + MenuBarIconColors.gradient(useColor: useColor, isStale: isStale) .frame(width: geo.size.width * min(percentage / 100, 1.0)) } .clipShape(Capsule()) @@ -37,9 +38,9 @@ struct BatteryIcon: View { .frame(width: capsuleWidth, height: capsuleHeight) // Percentage text - Text("\(Int(percentage))%") + Text("\(Int(percentage))%") .font(.system(size: 10, weight: .medium, design: .monospaced)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } if isStale && !isLoading { @@ -53,27 +54,6 @@ struct BatteryIcon: View { .accessibilityLabel("Usage: \(Int(percentage)) percent") .accessibilityValue(status.accessibilityDescription) } - - private var fillGradient: LinearGradient { - if isStale { - return LinearGradient( - colors: [.gray, .gray], - startPoint: .leading, - endPoint: .trailing - ) - } - - // Gradient shows current status position - return LinearGradient( - colors: [.green, .yellow, .orange, .red], - startPoint: .leading, - endPoint: .trailing - ) - } - - private var statusColor: Color { - isStale ? .gray : status.color - } } #Preview { @@ -84,6 +64,7 @@ struct BatteryIcon: View { BatteryIcon(percentage: 95, status: .critical, isLoading: false, isStale: false) BatteryIcon(percentage: 45, status: .safe, isLoading: true, isStale: false) BatteryIcon(percentage: 45, status: .safe, isLoading: false, isStale: true) + BatteryIcon(percentage: 75, status: .warning, isLoading: false, isStale: false, useColor: false) } .padding() } diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/CircularGaugeIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/CircularGaugeIcon.swift index 446750b..b11428b 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/CircularGaugeIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/CircularGaugeIcon.swift @@ -13,6 +13,7 @@ struct CircularGaugeIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true private let lineWidth: CGFloat = 3 private let size: CGFloat = 18 @@ -21,23 +22,23 @@ struct CircularGaugeIcon: View { ZStack { // Background circle Circle() - .stroke(Color.gray.opacity(0.3), lineWidth: lineWidth) + .stroke(MenuBarIconColors.track(useColor: useColor), lineWidth: lineWidth) // Progress arc Circle() .trim(from: 0, to: min(percentage / 100, 1.0)) - .stroke(statusColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .stroke(MenuBarIconColors.fill(useColor: useColor, status: status, isStale: isStale), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) // Center percentage or loading indicator if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 7, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { Text("\(Int(percentage))") .font(.system(size: 7, weight: .bold, design: .rounded)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } } .frame(width: size, height: size) @@ -54,10 +55,6 @@ struct CircularGaugeIcon: View { .accessibilityLabel("Usage: \(Int(percentage)) percent") .accessibilityValue(status.accessibilityDescription) } - - private var statusColor: Color { - isStale ? .gray : status.color - } } #Preview { diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/DualBarIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/DualBarIcon.swift index 278704e..437cc9c 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/DualBarIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/DualBarIcon.swift @@ -14,6 +14,7 @@ struct DualBarIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true private let barWidth: CGFloat = 32 private let barHeight: CGFloat = 5 @@ -24,23 +25,23 @@ struct DualBarIcon: View { if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 11, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { // Two stacked progress bars VStack(spacing: barSpacing) { - // Session bar (top) - blue/cyan + // Session bar (top): status colour in colour mode, black in monochrome ProgressBar( percentage: percentage, - color: sessionBarColor, - isStale: isStale + color: MenuBarIconColors.fill(useColor: useColor, status: status, isStale: isStale), + useColor: useColor ) .frame(width: barWidth, height: barHeight) - // Weekly bar (bottom) - purple + // Weekly bar (bottom): purple in colour mode, black in monochrome ProgressBar( percentage: weeklyPercentage, - color: weeklyBarColor, - isStale: isStale + color: MenuBarIconColors.secondary(useColor: useColor, isStale: isStale), + useColor: useColor ) .frame(width: barWidth, height: barHeight) } @@ -48,7 +49,7 @@ struct DualBarIcon: View { // Show session percentage (primary metric) Text("\(Int(percentage))%") .font(.system(size: 10, weight: .medium, design: .monospaced)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } if isStale && !isLoading { @@ -62,36 +63,20 @@ struct DualBarIcon: View { .accessibilityLabel("Session: \(Int(percentage)) percent, Weekly: \(Int(weeklyPercentage)) percent") .accessibilityValue(status.accessibilityDescription) } - - private var statusColor: Color { - isStale ? .gray : status.color - } - - private var sessionBarColor: Color { - if isStale { return .gray } - // Use status color for session bar - return status.color - } - - private var weeklyBarColor: Color { - if isStale { return .gray } - // Purple/violet for weekly to distinguish from session - return .purple - } } /// Individual progress bar component private struct ProgressBar: View { let percentage: Double let color: Color - let isStale: Bool + let useColor: Bool var body: some View { GeometryReader { geo in ZStack(alignment: .leading) { // Background RoundedRectangle(cornerRadius: 1.5) - .fill(Color.gray.opacity(0.3)) + .fill(MenuBarIconColors.track(useColor: useColor)) // Fill RoundedRectangle(cornerRadius: 1.5) diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/GaugeIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/GaugeIcon.swift index 539b15e..630fd7f 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/GaugeIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/GaugeIcon.swift @@ -13,18 +13,19 @@ struct GaugeIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true var body: some View { HStack(spacing: 4) { if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 14, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { Image(systemName: symbolName) .font(.system(size: 14, weight: .medium)) - .foregroundColor(statusColor) - .symbolRenderingMode(.hierarchical) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) + .symbolRenderingMode(useColor ? .hierarchical : .monochrome) } if isStale && !isLoading { @@ -59,10 +60,6 @@ struct GaugeIcon: View { return "gauge.with.dots.needle.100percent" } } - - private var statusColor: Color { - isStale ? .gray : status.color - } } #Preview { diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/MinimalIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/MinimalIcon.swift index a55e0e9..6cb44fc 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/MinimalIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/MinimalIcon.swift @@ -13,17 +13,18 @@ struct MinimalIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true var body: some View { HStack(spacing: 2) { if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 11, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { Text("\(Int(percentage))%") .font(.system(size: 13, weight: .semibold, design: .monospaced)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } if isStale && !isLoading { @@ -37,10 +38,6 @@ struct MinimalIcon: View { .accessibilityLabel("Usage: \(Int(percentage)) percent") .accessibilityValue(status.accessibilityDescription) } - - private var statusColor: Color { - isStale ? .gray : status.color - } } #Preview { diff --git a/ClaudeMeter/Views/MenuBar/IconStyles/SegmentedBarIcon.swift b/ClaudeMeter/Views/MenuBar/IconStyles/SegmentedBarIcon.swift index 2f571e8..df9120a 100644 --- a/ClaudeMeter/Views/MenuBar/IconStyles/SegmentedBarIcon.swift +++ b/ClaudeMeter/Views/MenuBar/IconStyles/SegmentedBarIcon.swift @@ -13,6 +13,7 @@ struct SegmentedBarIcon: View { let status: UsageStatus let isLoading: Bool let isStale: Bool + var useColor: Bool = true private let segmentCount = 5 private let segmentWidth: CGFloat = 4 @@ -23,7 +24,7 @@ struct SegmentedBarIcon: View { if isLoading { Image(systemName: "arrow.clockwise") .font(.system(size: 11, weight: .medium)) - .foregroundColor(statusColor) + .foregroundColor(MenuBarIconColors.text(useColor: useColor, status: status, isStale: isStale)) } else { HStack(spacing: segmentSpacing) { ForEach(0..= threshold - (100.0 / Double(segmentCount)) RoundedRectangle(cornerRadius: 1) - .fill(isActive ? segmentColor(for: index) : Color.gray.opacity(0.3)) + .fill(isActive ? segmentColor(for: index) : MenuBarIconColors.track(useColor: useColor)) .frame(width: segmentWidth, height: segmentHeight(for: index)) } } @@ -57,9 +58,8 @@ struct SegmentedBarIcon: View { } private func segmentColor(for index: Int) -> Color { - if isStale { - return .gray - } + if isStale { return .gray } + if !useColor { return .black } // Color segments by position to create a gradient effect (green → orange → red) // Uses Constants.Thresholds.Status for consistent color boundaries let segmentPercentage = Double(index + 1) / Double(segmentCount) * 100 @@ -71,10 +71,6 @@ struct SegmentedBarIcon: View { return .red } } - - private var statusColor: Color { - isStale ? .gray : status.color - } } #Preview { diff --git a/ClaudeMeter/Views/MenuBar/MenuBarIconRenderer.swift b/ClaudeMeter/Views/MenuBar/MenuBarIconRenderer.swift index cde2580..a5f4b65 100644 --- a/ClaudeMeter/Views/MenuBar/MenuBarIconRenderer.swift +++ b/ClaudeMeter/Views/MenuBar/MenuBarIconRenderer.swift @@ -17,7 +17,8 @@ struct MenuBarIconRenderer { isLoading: Bool, isStale: Bool, iconStyle: IconStyle, - weeklyPercentage: Double = 0 + weeklyPercentage: Double = 0, + useColor: Bool = true ) -> NSImage { let iconView = MenuBarIconView( percentage: percentage, @@ -25,7 +26,8 @@ struct MenuBarIconRenderer { isLoading: isLoading, isStale: isStale, iconStyle: iconStyle, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor ) let renderer = ImageRenderer(content: iconView) @@ -38,7 +40,9 @@ struct MenuBarIconRenderer { ) ?? NSImage() } - nsImage.isTemplate = false + // In monochrome mode the menu bar handles tinting (white on dark menu bars, black on + // light) via template rendering. In colour mode the original status palette is preserved. + nsImage.isTemplate = !useColor return nsImage } } diff --git a/ClaudeMeter/Views/MenuBar/MenuBarIconView.swift b/ClaudeMeter/Views/MenuBar/MenuBarIconView.swift index a6993c6..de0ee02 100644 --- a/ClaudeMeter/Views/MenuBar/MenuBarIconView.swift +++ b/ClaudeMeter/Views/MenuBar/MenuBarIconView.swift @@ -15,25 +15,68 @@ struct MenuBarIconView: View { let isStale: Bool let iconStyle: IconStyle var weeklyPercentage: Double = 0 // Optional, used by dualBar style + /// When false, the icon is drawn with pure black so the rendered NSImage can be used as a + /// template image and tinted by macOS (white on dark menu bars, black on light). + var useColor: Bool = true var body: some View { switch iconStyle { case .battery: - BatteryIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale) + BatteryIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) case .circular: - CircularGaugeIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale) + CircularGaugeIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) case .minimal: - MinimalIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale) + MinimalIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) case .segments: - SegmentedBarIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale) + SegmentedBarIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) case .dualBar: - DualBarIcon(percentage: percentage, weeklyPercentage: weeklyPercentage, status: status, isLoading: isLoading, isStale: isStale) + DualBarIcon(percentage: percentage, weeklyPercentage: weeklyPercentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) case .gauge: - GaugeIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale) + GaugeIcon(percentage: percentage, status: status, isLoading: isLoading, isStale: isStale, useColor: useColor) } } } +/// Centralised colour resolution for the menu bar icons. +/// +/// In monochrome mode every drawn pixel is black so the resulting NSImage can be rendered as a +/// template image (`NSImage.isTemplate = true`) and tinted by the system menu bar — white on +/// dark menu bars, black on light. In colour mode the original status palette is preserved. +enum MenuBarIconColors { + static func text(useColor: Bool, status: UsageStatus, isStale: Bool) -> Color { + if isStale { return .gray } + return useColor ? status.color : .black + } + + static func fill(useColor: Bool, status: UsageStatus, isStale: Bool) -> Color { + if isStale { return .gray } + return useColor ? status.color : .black + } + + /// Track / background. Matches the original gray track in colour mode (snapshot tests + /// are pixel-perfect). In monochrome mode it uses a black track so template rendering + /// tints it correctly. + static func track(useColor: Bool) -> Color { + useColor ? Color.gray.opacity(0.3) : Color.black.opacity(0.3) + } + + static func gradient(useColor: Bool, isStale: Bool) -> LinearGradient { + if isStale { + return LinearGradient(colors: [.gray, .gray], startPoint: .leading, endPoint: .trailing) + } + if useColor { + return LinearGradient(colors: [.green, .yellow, .orange, .red], startPoint: .leading, endPoint: .trailing) + } + return LinearGradient(colors: [.black, .black], startPoint: .leading, endPoint: .trailing) + } + + /// Secondary accent (e.g. weekly bar in DualBar). Purple in colour mode, black in monochrome. + static func secondary(useColor: Bool, isStale: Bool) -> Color { + if isStale { return .gray } + return useColor ? .purple : .black + } +} + // MARK: - Preview #Preview("All Styles") { @@ -49,6 +92,19 @@ struct MenuBarIconView: View { .padding() } +#Preview("Monochrome") { + VStack(alignment: .leading, spacing: 12) { + ForEach(IconStyle.allCases) { style in + HStack { + Text(style.displayName) + .frame(width: 80, alignment: .leading) + MenuBarIconView(percentage: 65, status: .warning, isLoading: false, isStale: false, iconStyle: style, weeklyPercentage: 45, useColor: false) + } + } + } + .padding() +} + #Preview("Battery States") { VStack(spacing: 20) { MenuBarIconView(percentage: 35, status: .safe, isLoading: false, isStale: false, iconStyle: .battery) diff --git a/ClaudeMeter/Views/MenuBar/MenuBarManager.swift b/ClaudeMeter/Views/MenuBar/MenuBarManager.swift index 7276352..efab097 100644 --- a/ClaudeMeter/Views/MenuBar/MenuBarManager.swift +++ b/ClaudeMeter/Views/MenuBar/MenuBarManager.swift @@ -99,6 +99,7 @@ final class MenuBarManager { _ = appModel.usageData _ = appModel.isLoading _ = appModel.settings.iconStyle + _ = appModel.settings.useColoredIcon } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } @@ -117,6 +118,7 @@ final class MenuBarManager { let isStale = appModel.usageData?.isStale ?? false let isLoading = appModel.isLoading let style = appModel.settings.iconStyle + let useColor = appModel.settings.useColoredIcon if let cachedImage = iconCache.get( percentage: percentage, @@ -124,7 +126,8 @@ final class MenuBarManager { isLoading: isLoading, isStale: isStale, iconStyle: style, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor ) { button.image = cachedImage return @@ -136,7 +139,8 @@ final class MenuBarManager { isLoading: isLoading, isStale: isStale, iconStyle: style, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor ) iconCache.set( @@ -146,7 +150,8 @@ final class MenuBarManager { isLoading: isLoading, isStale: isStale, iconStyle: style, - weeklyPercentage: weeklyPercentage + weeklyPercentage: weeklyPercentage, + useColor: useColor ) button.image = image diff --git a/ClaudeMeter/Views/Settings/SettingsView.swift b/ClaudeMeter/Views/Settings/SettingsView.swift index 01b06b8..9648441 100644 --- a/ClaudeMeter/Views/Settings/SettingsView.swift +++ b/ClaudeMeter/Views/Settings/SettingsView.swift @@ -69,6 +69,7 @@ struct SettingsView: View { refreshIntervalSection sonnetUsageSection iconStyleSection + coloredIconSection launchAtLoginSection } } @@ -209,6 +210,28 @@ struct SettingsView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } + // MARK: - Colored Icon Section + + private var coloredIconSection: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Use Colored Status Icon") + .font(.subheadline) + Text("Tint the menu bar icon green/orange/red based on usage. When off, the icon adapts to the system menu bar tint (white in dark mode).") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Toggle("", isOn: $appModel.settings.useColoredIcon) + .labelsHidden() + } + .padding() + .background(.quaternary.opacity(0.3)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + // MARK: - Launch at Login Section private var launchAtLoginSection: some View {