diff --git a/README.md b/README.md index a5382fdb..9d508cef 100644 --- a/README.md +++ b/README.md @@ -184,17 +184,19 @@ Settings is a custom `NSWindow`, not the system Settings scene. The app still runs as an accessory app with no Dock icon and no menu bar. - **General:** Launch at Login, 5m/15m/30m refresh interval, app language, - Always show usage, Low Power Mode, configurable limit alerts, and Sparkle - update controls. + light/dark/system appearance, Always show usage, Low Power Mode, + configurable limit alerts, and Sparkle update controls. - **Display:** used/remaining percentages, Usage and Cost visualization styles, - target display, and island width on non-notched screens. + target display, and island width on non-notched screens. Light appearance + applies a true semantic light palette to the expanded dashboard while the + compact and peek silhouettes remain black. - **Providers:** Claude/Codex visibility and status, token-counting mode, and a manual refresh for local cost data. Preferences are stored in `UserDefaults` under `MacIsland.*` keys (Sparkle manages its own `SU*` update keys, and Launch at Login uses -`SMAppService.mainApp`). Refresh, display, and provider changes apply live; -changing the app language offers to restart CodexIsland. +`SMAppService.mainApp`). Appearance, refresh, display, and provider changes +apply live; changing the app language offers to restart CodexIsland. ## Build from source diff --git a/README.zh-CN.md b/README.zh-CN.md index 9513e634..6eeba22b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -80,6 +80,7 @@ Claude: - 点击 `synced Xs ago` 状态可立即刷新。 - 点击展开面板左下角的齿轮打开设置。 - 在设置里可以开启登录启动、选择刷新间隔、切换低功耗模式、隐藏或显示 Claude / Codex、选择默认图表和成本视图、切换 token 统计口径、打开 GitHub / License,或退出应用。 +- 选择浅色外观后,展开面板会使用自适应的浅色文字与图表层级;收起和悬停预览状态仍保持黑色灵动岛外观。 服务可见性只影响显示。隐藏某个服务会移除它的 logo 和列,但应用仍会把最新用量保存在内存里,重新显示时不需要重置。 @@ -91,6 +92,7 @@ Claude: | 设置 | 存储 | UserDefaults key | 值 | | --- | --- | --- | --- | +| 外观 | `AppearanceStore` | `MacIsland.appearance` | `system`, `light`, `dark`,默认 `dark` | | 图表样式 | `StylePref` | `MacIsland.chartStyle` | `ring`, `bar`, `stepped`, `numeric`, `spark` | | 成本样式 | `CostStylePref` | `MacIsland.costStyle` | `dollar`, `multi`, `tokens`, `spark` | | Token 统计 | `TokenCountModeStore` | `MacIsland.tokenCountMode` | `all`, `billable` | @@ -100,7 +102,8 @@ Claude: | Codex 可见 | `ProviderVisibilityStore` | `MacIsland.codexVisible` | Boolean,默认 `true` | | 登录启动 | `LaunchAtLoginStore` | 由 `SMAppService.mainApp` 管理 | 系统登录项状态 | -刷新间隔会立即生效。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。 +外观和刷新间隔都会立即生效;选择“跟随系统”后,设置窗口会随 macOS +浅色/深色外观自动切换。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。 ## 从源码构建 diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index c2d3cf19..60280a2f 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -101,6 +101,11 @@ "Ring" = "Ring"; "scanning local logs…" = "scanning local logs…"; "Settings" = "Settings"; +"Appearance" = "Appearance"; +"Choose a light or dark skin, or follow macOS." = "Choose a light or dark skin, or follow macOS."; +"System" = "System"; +"Light" = "Light"; +"Dark" = "Dark"; "Switch to %@ (⌘%d)" = "Switch to %@ (⌘%d)"; "Show on" = "Show on"; "Sparkline" = "Sparkline"; diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index d96e12d3..92282186 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -101,6 +101,11 @@ "Ring" = "环形"; "scanning local logs…" = "正在扫描本地日志…"; "Settings" = "设置"; +"Appearance" = "外观"; +"Choose a light or dark skin, or follow macOS." = "选择浅色或深色皮肤,或跟随 macOS。"; +"System" = "跟随系统"; +"Light" = "浅色"; +"Dark" = "深色"; "Switch to %@ (⌘%d)" = "切换到%@(⌘%d)"; "Show on" = "显示在"; "Sparkline" = "折线"; diff --git a/Sources/Model/AppearanceStore.swift b/Sources/Model/AppearanceStore.swift new file mode 100644 index 00000000..f954ace5 --- /dev/null +++ b/Sources/Model/AppearanceStore.swift @@ -0,0 +1,40 @@ +import SwiftUI + +enum AppAppearance: String, CaseIterable, Hashable { + case system + case light + case dark + + var label: String { + switch self { + case .system: "System" + case .light: "Light" + case .dark: "Dark" + } + } + + var colorScheme: ColorScheme? { + switch self { + case .system: nil + case .light: .light + case .dark: .dark + } + } +} + +@MainActor +final class AppearanceStore: ObservableObject { + static let shared = AppearanceStore() + static let key = "MacIsland.appearance" + + @Published var appearance: AppAppearance { + didSet { + UserDefaults.standard.set(appearance.rawValue, forKey: Self.key) + } + } + + private init() { + let raw = UserDefaults.standard.string(forKey: Self.key) ?? "" + appearance = AppAppearance(rawValue: raw) ?? .dark + } +} diff --git a/Sources/Theme/Colors.swift b/Sources/Theme/Colors.swift index 85321f5b..275f9843 100644 --- a/Sources/Theme/Colors.swift +++ b/Sources/Theme/Colors.swift @@ -1,7 +1,19 @@ +import AppKit import SwiftUI /// Locked color tokens for CodexIsland. enum IslandColor { + static let settingsBackground = Color( + light: NSColor(calibratedWhite: 0.965, alpha: 1), + dark: NSColor(calibratedRed: 0.020, green: 0.020, blue: 0.027, alpha: 1) + ) + + /// Expanded island surface in Light appearance. Compact and peek states + /// deliberately stay black so the notch silhouette remains unchanged. + static let expandedLightBackground = Color( + nsColor: NSColor(calibratedWhite: 0.955, alpha: 1) + ) + /// #0047AB — loading sweep, glow halo. static let cobalt = Color(red: 0/255, green: 71/255, blue: 171/255) @@ -23,3 +35,11 @@ enum IslandColor { /// as "stop, you're cooked" without going full red-alert pure. static let alertRed = Color(red: 229/255, green: 72/255, blue: 77/255) } + +private extension Color { + init(light: NSColor, dark: NSColor) { + self.init(nsColor: NSColor(name: nil) { appearance in + appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : light + }) + } +} diff --git a/Sources/Theme/UrgencyColor.swift b/Sources/Theme/UrgencyColor.swift index 99deff38..66f028c9 100644 --- a/Sources/Theme/UrgencyColor.swift +++ b/Sources/Theme/UrgencyColor.swift @@ -13,6 +13,9 @@ enum UrgencyColor { let usedPercent = mode == .used ? percent : 100 - percent if usedPercent >= 90 { return red } if usedPercent >= 70 { return amber } - return .white + // Normal readings follow the active surface. Fixed white disappears + // on the expanded light panel; Color.primary resolves to black there + // and white in the dark expanded panel. + return Color.primary } } diff --git a/Sources/Views/Charts/BarChart.swift b/Sources/Views/Charts/BarChart.swift index 9200cd1a..b827d969 100644 --- a/Sources/Views/Charts/BarChart.swift +++ b/Sources/Views/Charts/BarChart.swift @@ -11,7 +11,7 @@ struct BarChart: View { ChartHead(value: value, label: label) GeometryReader { geo in ZStack(alignment: .leading) { - Capsule().fill(.white.opacity(0.06)).frame(height: 4) + Capsule().fill(Color.primary.opacity(0.06)).frame(height: 4) Capsule() .fill(color) .frame(width: geo.size.width * CGFloat(value / 100), height: 4) @@ -20,7 +20,7 @@ struct BarChart: View { // hint at scale without competing with the fill. ForEach([0.25, 0.5, 0.75], id: \.self) { p in Rectangle() - .fill(.white.opacity(0.12)) + .fill(Color.primary.opacity(0.12)) .frame(width: 1, height: 8) .offset(x: geo.size.width * p) } diff --git a/Sources/Views/Charts/ChartHead.swift b/Sources/Views/Charts/ChartHead.swift index 60e18fa0..a41d1b2d 100644 --- a/Sources/Views/Charts/ChartHead.swift +++ b/Sources/Views/Charts/ChartHead.swift @@ -10,7 +10,7 @@ struct ChartHead: View { HStack(alignment: .firstTextBaseline) { Text(label) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.78)) .textCase(.lowercase) Spacer() HStack(alignment: .firstTextBaseline, spacing: 1) { @@ -21,7 +21,7 @@ struct ChartHead: View { .animation(.strongEaseOut, value: value) Text("%") .font(Typography.label) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.62)) } } } @@ -33,7 +33,7 @@ struct ChartFoot: View { var body: some View { Text(caption) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.4)) + .foregroundStyle(Color.primary.opacity(0.64)) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Sources/Views/Charts/NoReadingChart.swift b/Sources/Views/Charts/NoReadingChart.swift index 82be6cb9..f6addb85 100644 --- a/Sources/Views/Charts/NoReadingChart.swift +++ b/Sources/Views/Charts/NoReadingChart.swift @@ -21,17 +21,17 @@ struct NoReadingChart: View { HStack(alignment: .firstTextBaseline) { Text(label) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) .textCase(.lowercase) Spacer() Text(verbatim: "—") .font(Typography.chartValue) - .foregroundStyle(.white.opacity(0.3)) + .foregroundStyle(Color.primary.opacity(0.3)) } // Empty track, no fill: the scale is still there, we just have // nothing to put on it. Capsule() - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) .frame(height: 4) .frame(height: 8) ChartFoot(caption: sub) diff --git a/Sources/Views/Charts/NumericChart.swift b/Sources/Views/Charts/NumericChart.swift index 58286650..1b9d077f 100644 --- a/Sources/Views/Charts/NumericChart.swift +++ b/Sources/Views/Charts/NumericChart.swift @@ -11,12 +11,12 @@ struct NumericChart: View { HStack(alignment: .firstTextBaseline) { Text(label) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.78)) .textCase(.lowercase) Spacer() Text(sub) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.4)) + .foregroundStyle(Color.primary.opacity(0.68)) } HStack(alignment: .firstTextBaseline, spacing: 2) { Text("\(Int(value))") @@ -26,14 +26,14 @@ struct NumericChart: View { .animation(.strongEaseOut, value: value) Text("%") .font(Typography.unit) - .foregroundStyle(.white.opacity(0.4)) + .foregroundStyle(Color.primary.opacity(0.58)) } .frame(maxWidth: .infinity, alignment: .leading) // Thin 3pt meter underneath echoes the value at a glance and // glows in the brand color so the big number doesn't sit alone. GeometryReader { geo in ZStack(alignment: .leading) { - Capsule().fill(.white.opacity(0.05)).frame(height: 3) + Capsule().fill(Color.primary.opacity(0.05)).frame(height: 3) Capsule() .fill(color) .frame(width: geo.size.width * CGFloat(value / 100), height: 3) diff --git a/Sources/Views/Charts/RingChart.swift b/Sources/Views/Charts/RingChart.swift index 6d7fb067..64b455e7 100644 --- a/Sources/Views/Charts/RingChart.swift +++ b/Sources/Views/Charts/RingChart.swift @@ -10,7 +10,7 @@ struct RingChart: View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 14) { ZStack { - Circle().stroke(.white.opacity(0.07), lineWidth: 3) + Circle().stroke(Color.primary.opacity(0.07), lineWidth: 3) Circle() .trim(from: 0, to: max(0.001, value / 100)) .stroke(color, style: StrokeStyle(lineWidth: 3, lineCap: .round)) @@ -26,7 +26,7 @@ struct RingChart: View { VStack(alignment: .leading, spacing: 4) { Text(label) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) .textCase(.lowercase) HStack(alignment: .firstTextBaseline, spacing: 1) { Text("\(Int(value))") @@ -36,14 +36,14 @@ struct RingChart: View { .animation(.strongEaseOut, value: value) Text("%") .font(Typography.label) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) } } Spacer() } Text(sub) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.4)) + .foregroundStyle(Color.primary.opacity(0.60)) .lineLimit(1) .truncationMode(.tail) } diff --git a/Sources/Views/Charts/SparkChart.swift b/Sources/Views/Charts/SparkChart.swift index 56d277f2..8f751013 100644 --- a/Sources/Views/Charts/SparkChart.swift +++ b/Sources/Views/Charts/SparkChart.swift @@ -93,7 +93,7 @@ private struct SparkSVG: View { path.move(to: CGPoint(x: 0, y: h * p)) path.addLine(to: CGPoint(x: w, y: h * p)) } - .stroke(.white.opacity(0.04), lineWidth: 1) + .stroke(Color.primary.opacity(0.04), lineWidth: 1) } // Dotted threshold at the current value — the line reading // "this is now" against the synthesized history. diff --git a/Sources/Views/Charts/SteppedChart.swift b/Sources/Views/Charts/SteppedChart.swift index 88db1684..61432dd7 100644 --- a/Sources/Views/Charts/SteppedChart.swift +++ b/Sources/Views/Charts/SteppedChart.swift @@ -14,7 +14,7 @@ struct SteppedChart: View { let filled = (value / 100) * Double(segments) ForEach(0.. some View { + if usesLightPalette { + content + .environment(\.colorScheme, .light) + } else { + content.environment(\.colorScheme, .dark) + } + } +} + /// Per-provider brand logo overlay. Observes only ProviderVisibilityStore /// so a UsageStore/CostStore tick doesn't re-render the logo image or /// re-evaluate its accessibility label. diff --git a/Sources/Views/OverviewView.swift b/Sources/Views/OverviewView.swift index 12160bea..b5ae1da7 100644 --- a/Sources/Views/OverviewView.swift +++ b/Sources/Views/OverviewView.swift @@ -104,26 +104,26 @@ struct OverviewView: View { Text(summaryLabel) .font(Typography.sectionLabel) .tracking(0.7) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) HStack(alignment: .firstTextBaseline, spacing: 4) { Text(Self.formatTokens(displayedTokens).value) .font(Typography.chartValue) - .foregroundStyle(.white) + .foregroundStyle(Color.primary) Text(Self.formatTokens(displayedTokens).unit) .font(Typography.unit) - .foregroundStyle(.white.opacity(0.40)) + .foregroundStyle(Color.primary.opacity(0.40)) } } HStack(alignment: .center, spacing: 10) { Text(summarySubline) .font(Typography.label) - .foregroundStyle(.white.opacity(0.50)) + .foregroundStyle(Color.primary.opacity(0.50)) if costStore.loading { Text(L10n.tr("Syncing")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.36)) + .foregroundStyle(Color.primary.opacity(0.36)) } } .padding(.bottom, 5) @@ -483,7 +483,7 @@ private struct MonthRail: View { ForEach(marks) { mark in Text(mark.label) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.30)) + .foregroundStyle(Color.primary.opacity(0.30)) .lineLimit(1) .fixedSize() .offset(x: mark.x, y: 0) @@ -498,10 +498,10 @@ private struct FutureContributionCell: View { var body: some View { RoundedRectangle(cornerRadius: cornerRadius) - .fill(.white.opacity(0.012)) + .fill(Color.primary.opacity(0.012)) .overlay { RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(.white.opacity(0.030), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.030), lineWidth: 0.5) } .frame(width: cellSize, height: cellSize) .accessibilityHidden(true) @@ -545,7 +545,7 @@ private struct ContributionCell: View { let opacity = day.totalTokens > 0 ? intensityScale.opacity(for: day.totalTokens) : 0.035 switch day.dominantProvider { case .none: - Color.white.opacity(opacity) + Color.primary.opacity(opacity) case .claude: IslandColor.claude.opacity(opacity) case .codex: @@ -569,10 +569,10 @@ private struct ContributionCell: View { } private var strokeColor: Color { - if isSelected { return .white.opacity(0.72) } - if hovering { return .white.opacity(0.22) } - guard day.totalTokens > 0 else { return .white.opacity(0.04) } - return .white.opacity(0.06 + Double(intensityScale.level(for: day.totalTokens)) * 0.012) + if isSelected { return Color.primary.opacity(0.72) } + if hovering { return Color.primary.opacity(0.22) } + guard day.totalTokens > 0 else { return Color.primary.opacity(0.04) } + return Color.primary.opacity(0.06 + Double(intensityScale.level(for: day.totalTokens)) * 0.012) } private var helpText: String { @@ -685,7 +685,7 @@ private struct DayDetailStrip: View { var body: some View { VStack(spacing: 8) { Rectangle() - .fill(.white.opacity(0.075)) + .fill(Color.primary.opacity(0.075)) .frame(height: 0.5) HStack(alignment: .center, spacing: 14) { @@ -693,12 +693,12 @@ private struct DayDetailStrip: View { Text(Self.detailFormatter.string(from: day.date).uppercased()) .font(Typography.sectionLabel) .tracking(0.6) - .foregroundStyle(.white.opacity(0.58)) + .foregroundStyle(Color.primary.opacity(0.58)) .lineLimit(1) Text(L10n.tr("All Tokens")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.36)) + .foregroundStyle(Color.primary.opacity(0.36)) .lineLimit(1) } .frame(width: 116, alignment: .leading) @@ -715,7 +715,7 @@ private struct DayDetailStrip: View { label: L10n.tr("TOTAL"), spokenLabel: L10n.tr("Total"), value: day.totalTokens, - color: .white.opacity(0.78), + color: Color.primary.opacity(0.78), dimmed: true ) @@ -749,7 +749,7 @@ private struct DayDetailStrip: View { Text(OverviewView.formatExactTokens(value)) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(Color.primary.opacity(0.76)) .lineLimit(1) .minimumScaleFactor(0.72) .allowsTightening(true) @@ -787,7 +787,7 @@ private struct TokenSplitMeter: View { GeometryReader { geo in ZStack(alignment: .leading) { Capsule() - .fill(.white.opacity(0.055)) + .fill(Color.primary.opacity(0.055)) if total > 0 { HStack(spacing: 0) { @@ -832,7 +832,7 @@ private struct ProviderSplitRow: View { if visibleCount == 0 { Text(L10n.tr("Providers Hidden")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.36)) + .foregroundStyle(Color.primary.opacity(0.36)) } else { HStack(spacing: 8) { if claudeVisible { @@ -860,7 +860,7 @@ private struct ProviderSplitRow: View { .frame(width: 5, height: 5) Text("\(label) \(share(value))") .font(Typography.caption) - .foregroundStyle(.white.opacity(0.46)) + .foregroundStyle(Color.primary.opacity(0.46)) .lineLimit(1) } } diff --git a/Sources/Views/PageIndicator.swift b/Sources/Views/PageIndicator.swift index 35bbe62e..ab1bac8b 100644 --- a/Sources/Views/PageIndicator.swift +++ b/Sources/Views/PageIndicator.swift @@ -23,7 +23,7 @@ struct PageIndicator: View { model.showScreen(screen) } label: { Circle() - .fill(.white.opacity(isActive ? 0.78 : 0.22)) + .fill(Color.primary.opacity(isActive ? 0.78 : 0.22)) .frame(width: 5, height: 5) // Visual stays 5pt; hit area expands ~6pt outward so the dot // is reachable without pixel-precise aim. diff --git a/Sources/Views/PanelFooter.swift b/Sources/Views/PanelFooter.swift index baf9e2a5..5ddfb127 100644 --- a/Sources/Views/PanelFooter.swift +++ b/Sources/Views/PanelFooter.swift @@ -25,7 +25,7 @@ struct PanelFooter: View { var body: some View { VStack(spacing: 0) { LinearGradient( - colors: [.clear, .white.opacity(0.06), .white.opacity(0.06), .clear], + colors: [.clear, Color.primary.opacity(0.08), Color.primary.opacity(0.08), .clear], startPoint: .leading, endPoint: .trailing ) .frame(height: 1) @@ -43,7 +43,7 @@ struct PanelFooter: View { Text(L10n.tr("click to cycle")) .font(Typography.label) } - .foregroundStyle(.white.opacity(0.42)) + .foregroundStyle(Color.primary.opacity(0.48)) .transition(.opacity.combined(with: .scale(scale: 0.92, anchor: .leading))) .accessibilityElement(children: .combine) .accessibilityLabel(cycleHintAccessibilityLabel) @@ -97,15 +97,15 @@ struct PanelFooter: View { Text(label) .font(Typography.chip) .tracking(0.8) - .foregroundStyle(.white.opacity(0.78)) + .foregroundStyle(Color.primary.opacity(0.78)) .padding(.horizontal, 6) .padding(.vertical, 2.5) .background( RoundedRectangle(cornerRadius: 4) - .fill(.white.opacity(0.08)) + .fill(Color.primary.opacity(0.08)) .overlay( RoundedRectangle(cornerRadius: 4) - .strokeBorder(.white.opacity(0.10), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5) ) ) .contentTransition(.opacity) @@ -139,25 +139,25 @@ struct PanelFooter: View { if activeLoading { Text(L10n.tr("Syncing…")) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } else if let updated = activeLastUpdated { Text(L10n.tr("Synced")) .font(Typography.label) - .foregroundStyle(.white.opacity(liveStatusHovered ? 0.85 : 0.55)) + .foregroundStyle(Color.primary.opacity(liveStatusHovered ? 0.85 : 0.55)) Text(relative(updated)) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(liveStatusHovered ? 0.95 : 0.72)) + .foregroundStyle(Color.primary.opacity(liveStatusHovered ? 0.95 : 0.72)) } else { Text(L10n.tr("Idle")) .font(Typography.label) - .foregroundStyle(.white.opacity(liveStatusHovered ? 0.7 : 0.4)) + .foregroundStyle(Color.primary.opacity(liveStatusHovered ? 0.70 : 0.40)) } } .padding(.horizontal, 6) .padding(.vertical, 3) .background( RoundedRectangle(cornerRadius: 5) - .fill(.white.opacity(liveStatusHovered && !activeLoading ? 0.05 : 0)) + .fill(Color.primary.opacity(liveStatusHovered && !activeLoading ? 0.05 : 0)) ) .contentShape(RoundedRectangle(cornerRadius: 5)) } diff --git a/Sources/Views/PanelHeader.swift b/Sources/Views/PanelHeader.swift index 5b4e9ec2..c4ad7f7d 100644 --- a/Sources/Views/PanelHeader.swift +++ b/Sources/Views/PanelHeader.swift @@ -56,20 +56,20 @@ struct PanelHeader: View { let content = HStack(spacing: 8) { Text(name) .font(Typography.providerTitle) - .foregroundStyle(.white) + .foregroundStyle(Color.primary) if let tag { Text(tag) .font(Typography.chip) .tracking(0.8) - .foregroundStyle(.white.opacity(0.6)) + .foregroundStyle(Color.primary.opacity(0.60)) .padding(.horizontal, 5) .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 3) - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) .overlay( RoundedRectangle(cornerRadius: 3) - .strokeBorder(.white.opacity(0.08), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5) ) ) } diff --git a/Sources/Views/ProviderBreakdown.swift b/Sources/Views/ProviderBreakdown.swift index b5fa4211..30a55a7c 100644 --- a/Sources/Views/ProviderBreakdown.swift +++ b/Sources/Views/ProviderBreakdown.swift @@ -194,7 +194,7 @@ struct PerModelBreakdown: View { Spacer(minLength: 0) Text(L10n.tr("no %@ activity in last 5h or this week", providerLowerLabel(provider))) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.4)) + .foregroundStyle(Color.primary.opacity(0.4)) Spacer(minLength: 0) } else { VStack(spacing: 5) { @@ -226,7 +226,7 @@ struct PerModelBreakdown: View { Text(L10n.tr("BY MODEL")) .font(Typography.sectionLabel) .tracking(0.6) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) Spacer(minLength: 0) HStack(spacing: 4) { Capsule() @@ -234,14 +234,14 @@ struct PerModelBreakdown: View { .frame(width: 8, height: 4) Text(L10n.tr("5h")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.50)) + .foregroundStyle(Color.primary.opacity(0.50)) .padding(.trailing, 4) Capsule() .fill(color.opacity(topWeight * dimFillMultiplier)) .frame(width: 8, height: 4) Text(L10n.tr("week")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.50)) + .foregroundStyle(Color.primary.opacity(0.50)) } } } @@ -276,7 +276,7 @@ private struct PerModelRow: View { HStack(spacing: 8) { Text(displayName) .font(Typography.label) - .foregroundStyle(.white.opacity(0.78)) + .foregroundStyle(Color.primary.opacity(0.78)) .frame(width: Self.nameWidth, alignment: .leading) .lineLimit(1) @@ -289,7 +289,7 @@ private struct PerModelRow: View { Text(trailingValue) .font(Typography.caption) - .foregroundStyle(.white.opacity(weekAbsolute > 0 ? 0.55 : 0.32)) + .foregroundStyle(Color.primary.opacity(weekAbsolute > 0 ? 0.55 : 0.32)) .frame(width: Self.trailingWidth, alignment: .trailing) } } @@ -338,7 +338,7 @@ private struct OverlapBar: View { : 0 ZStack(alignment: .leading) { Capsule() - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) .frame(height: Self.barHeight) if weekAbsolute > 0 { // Floor the dim opacity so the 4th-row track baseline @@ -382,10 +382,10 @@ struct BothHiddenPlaceholder: View { Spacer(minLength: 0) Text(L10n.tr("Both providers hidden")) .font(Typography.providerTitle) - .foregroundStyle(.white.opacity(0.45)) + .foregroundStyle(Color.primary.opacity(0.45)) Text(L10n.tr("Re-enable in Settings → Providers")) .font(Typography.caption) - .foregroundStyle(.white.opacity(0.32)) + .foregroundStyle(Color.primary.opacity(0.32)) Spacer(minLength: 0) } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Sources/Views/Settings/BrandHeader.swift b/Sources/Views/Settings/BrandHeader.swift index d7a98a0e..c3dd6215 100644 --- a/Sources/Views/Settings/BrandHeader.swift +++ b/Sources/Views/Settings/BrandHeader.swift @@ -24,21 +24,21 @@ struct BrandHeader: View { Text("CodexIsland") .font(Typography.brand) .tracking(-0.15) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) Text(L10n.tr("Your AI usage limits, living in your notch.")) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } Spacer(minLength: 8) Text("v\(version)") .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) .padding(.horizontal, 9) .padding(.vertical, 4) .background( - Capsule().fill(.white.opacity(0.04)) + Capsule().fill(Color.primary.opacity(0.04)) ) } .padding(.horizontal, 24) @@ -55,7 +55,7 @@ struct BrandHeader: View { .interpolation(.high) .aspectRatio(contentMode: .fit) .frame(width: 26, height: 26) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) .shadow(color: IslandColor.cobalt.opacity(0.35), radius: 6) } else { // Fallback if the resource is missing in the bundle: a plain diff --git a/Sources/Views/Settings/ChartStylePicker.swift b/Sources/Views/Settings/ChartStylePicker.swift index fb4cc890..ddf4120e 100644 --- a/Sources/Views/Settings/ChartStylePicker.swift +++ b/Sources/Views/Settings/ChartStylePicker.swift @@ -34,7 +34,7 @@ struct ChartStylePicker: View { case .ring: ZStack { Circle() - .stroke(.white.opacity(0.10), lineWidth: 3) + .stroke(Color.primary.opacity(0.10), lineWidth: 3) Circle() .trim(from: 0, to: 0.35) .stroke(claude, style: StrokeStyle(lineWidth: 3, lineCap: .round)) @@ -43,7 +43,7 @@ struct ChartStylePicker: View { .frame(width: 26, height: 26) case .bar: ZStack(alignment: .leading) { - Capsule().fill(.white.opacity(0.10)) + Capsule().fill(Color.primary.opacity(0.10)) Capsule().fill(claude) .frame(width: 28 * 0.35) } @@ -52,7 +52,7 @@ struct ChartStylePicker: View { HStack(spacing: 1.5) { ForEach(0..<8) { i in RoundedRectangle(cornerRadius: 0.75) - .fill(i < 3 ? claude : .white.opacity(0.10)) + .fill(i < 3 ? claude : Color.primary.opacity(0.10)) .frame(width: 2, height: 12) } } @@ -64,7 +64,7 @@ struct ChartStylePicker: View { .foregroundStyle(claude) Text("%") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) } case .spark: SparkPath() diff --git a/Sources/Views/Settings/CostStylePicker.swift b/Sources/Views/Settings/CostStylePicker.swift index 45e45a12..28df5566 100644 --- a/Sources/Views/Settings/CostStylePicker.swift +++ b/Sources/Views/Settings/CostStylePicker.swift @@ -34,14 +34,14 @@ struct CostStylePicker: View { HStack(alignment: .firstTextBaseline, spacing: 1) { Text("$") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) Text("87") .font(Typography.previewNumber) .foregroundStyle(claude) } case .multi: HStack(alignment: .bottom, spacing: 4) { - Capsule().fill(.white.opacity(0.20)) + Capsule().fill(Color.primary.opacity(0.20)) .frame(width: 8, height: 6) Capsule().fill(claude) .frame(width: 8, height: 18) @@ -54,7 +54,7 @@ struct CostStylePicker: View { .foregroundStyle(claude) Text("M") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) } case .spark: CostSparkPath() diff --git a/Sources/Views/Settings/SegmentedControl.swift b/Sources/Views/Settings/SegmentedControl.swift index f2f4e2bd..8e7ba0bd 100644 --- a/Sources/Views/Settings/SegmentedControl.swift +++ b/Sources/Views/Settings/SegmentedControl.swift @@ -21,16 +21,19 @@ struct SegmentedControl: View { Text(itemLabel) .font(Typography.bodyNumber) .foregroundStyle(isOn - ? Color.white.opacity(0.95) - : .white.opacity(0.55)) + ? Color.primary.opacity(0.95) + : Color.primary.opacity(0.55)) .padding(.horizontal, 10) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 5) - .fill(isOn ? .white.opacity(0.10) : .clear) + .fill(isOn ? Color.primary.opacity(0.10) : Color.clear) .overlay { RoundedRectangle(cornerRadius: 5) - .strokeBorder(.white.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) + .strokeBorder( + Color.primary.opacity(isOn ? 0.08 : 0), + lineWidth: 0.5 + ) } } } @@ -45,7 +48,7 @@ struct SegmentedControl: View { .padding(2) .background { RoundedRectangle(cornerRadius: 7) - .fill(.white.opacity(0.04)) + .fill(Color.primary.opacity(0.04)) } } } @@ -62,15 +65,15 @@ struct PillButton: View { Button(action: action) { Text(L10n.tr(label)) .font(Typography.button) - .foregroundStyle(.white.opacity(0.9)) + .foregroundStyle(Color.primary.opacity(0.9)) .padding(.horizontal, 12) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 6) - .fill(.white.opacity(0.10)) + .fill(Color.primary.opacity(0.10)) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(0.08), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5) } } } diff --git a/Sources/Views/Settings/SettingsFooter.swift b/Sources/Views/Settings/SettingsFooter.swift index cab372e8..467ce70b 100644 --- a/Sources/Views/Settings/SettingsFooter.swift +++ b/Sources/Views/Settings/SettingsFooter.swift @@ -22,15 +22,15 @@ struct SettingsFooter: View { } label: { Text(L10n.tr("Quit")) .font(Typography.label) - .foregroundStyle(.white.opacity(quitHovered ? 0.92 : 0.55)) + .foregroundStyle(Color.primary.opacity(quitHovered ? 0.92 : 0.55)) .padding(.horizontal, 11) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 6) - .fill(.white.opacity(quitHovered ? 0.06 : 0.03)) + .fill(Color.primary.opacity(quitHovered ? 0.06 : 0.03)) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(0.07), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.07), lineWidth: 0.5) } } } @@ -62,14 +62,14 @@ private struct DottedLink: View { HStack(spacing: 4) { Text(L10n.tr(title)) .font(Typography.label) - .foregroundStyle(.white.opacity(hovered ? 0.92 : 0.55)) + .foregroundStyle(Color.primary.opacity(hovered ? 0.92 : 0.55)) Text("↗") .font(Typography.micro) - .foregroundStyle(.white.opacity(hovered ? 0.6 : 0.3)) + .foregroundStyle(Color.primary.opacity(hovered ? 0.6 : 0.3)) } .overlay(alignment: .bottom) { Rectangle() - .fill(.white.opacity(hovered ? 0.32 : 0.18)) + .fill(Color.primary.opacity(hovered ? 0.32 : 0.18)) .frame(height: 0.5) .offset(y: 1) .mask( diff --git a/Sources/Views/Settings/SettingsRow.swift b/Sources/Views/Settings/SettingsRow.swift index 3c169409..4ab0abb0 100644 --- a/Sources/Views/Settings/SettingsRow.swift +++ b/Sources/Views/Settings/SettingsRow.swift @@ -42,17 +42,17 @@ struct SettingsRow: View { Text(L10n.tr(title)) .font(Typography.rowTitle) .tracking(-0.07) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) if let chip { Text(chip) .font(Typography.chip) .tracking(0.8) - .foregroundStyle(.white.opacity(0.6)) + .foregroundStyle(Color.primary.opacity(0.6)) .padding(.horizontal, 5) .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 3) - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) ) .accessibilityLabel(L10n.tr("Plan: %@", chip)) } @@ -60,7 +60,7 @@ struct SettingsRow: View { if let subtitle { Text(L10n.tr(subtitle)) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } } .accessibilityElement(children: .combine) @@ -73,7 +73,7 @@ struct SettingsRow: View { .padding(.vertical, 11) .background { RoundedRectangle(cornerRadius: 8) - .fill(.white.opacity(hovered ? 0.030 : 0)) + .fill(Color.primary.opacity(hovered ? 0.030 : 0)) } .contentShape(Rectangle()) .onHover { hovered = $0 } diff --git a/Sources/Views/Settings/SettingsToggle.swift b/Sources/Views/Settings/SettingsToggle.swift index ee4b4d6e..2bc7ddd8 100644 --- a/Sources/Views/Settings/SettingsToggle.swift +++ b/Sources/Views/Settings/SettingsToggle.swift @@ -16,16 +16,16 @@ struct SettingsToggle: View { Button(action: action) { ZStack(alignment: isOn ? .trailing : .leading) { Capsule() - .strokeBorder(.white.opacity(hovered ? 0.20 : 0.13), lineWidth: 1) + .strokeBorder(Color.primary.opacity(hovered ? 0.20 : 0.13), lineWidth: 1) .background { Capsule().fill(isOn ? IslandColor.cobalt.opacity(0.32) - : .white.opacity(0.07)) + : Color.primary.opacity(0.07)) } .frame(width: trackWidth, height: trackHeight) Circle() - .fill(isOn ? IslandColor.cobalt : Color.white.opacity(0.5)) + .fill(isOn ? IslandColor.cobalt : Color.primary.opacity(0.5)) .frame(width: dotSize, height: dotSize) .shadow( color: isOn ? IslandColor.cobalt.opacity(0.85) : .clear, diff --git a/Sources/Views/Settings/StyleTile.swift b/Sources/Views/Settings/StyleTile.swift index 3a48327f..5ab90623 100644 --- a/Sources/Views/Settings/StyleTile.swift +++ b/Sources/Views/Settings/StyleTile.swift @@ -19,8 +19,8 @@ struct StyleTile: View { Text(displayLabel) .font(Typography.micro) .foregroundStyle(isOn - ? Color(red: 0.58, green: 0.75, blue: 1.0) - : .white.opacity(0.55)) + ? Color.primary.opacity(0.90) + : Color.primary.opacity(0.55)) } .frame(maxWidth: .infinity) .padding(.top, 14) @@ -30,7 +30,7 @@ struct StyleTile: View { RoundedRectangle(cornerRadius: 9) .fill(isOn ? IslandColor.cobalt.opacity(0.14) - : .white.opacity(0.025)) + : Color.primary.opacity(0.025)) .overlay { RoundedRectangle(cornerRadius: 9) .strokeBorder(isOn diff --git a/Sources/Views/SettingsButton.swift b/Sources/Views/SettingsButton.swift index da09c9f2..4e0b5a36 100644 --- a/Sources/Views/SettingsButton.swift +++ b/Sources/Views/SettingsButton.swift @@ -15,12 +15,12 @@ struct SettingsButton: View { Image(systemName: "gearshape") .font(Typography.button) .symbolRenderingMode(.hierarchical) - .foregroundStyle(.white.opacity(hovered ? 0.64 : 0.34)) + .foregroundStyle(Color.primary.opacity(hovered ? 0.64 : 0.34)) .frame(width: 26, height: 26) .contentShape(Rectangle()) .background { Circle() - .fill(.white.opacity(hovered ? 0.08 : 0)) + .fill(Color.primary.opacity(hovered ? 0.08 : 0)) } } .buttonStyle(PressableButtonStyle()) diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift index 2b304f8c..96c5d1c5 100644 --- a/Sources/Views/SettingsView.swift +++ b/Sources/Views/SettingsView.swift @@ -20,6 +20,7 @@ struct SettingsView: View { @ObservedObject private var usageDisplay = UsageDisplayModeStore.shared @ObservedObject private var targetDisplay = IslandTargetDisplayStore.shared @ObservedObject private var appLanguage = AppLanguageStore.shared + @ObservedObject private var appearanceStore = AppearanceStore.shared @ObservedObject private var usage = UsageStore.shared @ObservedObject private var cost = CostStore.shared @ObservedObject private var updater = UpdaterController.shared @@ -67,8 +68,8 @@ struct SettingsView: View { SettingsFooter() } .frame(minWidth: 440, minHeight: 420) - .background(Color(red: 0.020, green: 0.020, blue: 0.027)) - .preferredColorScheme(.dark) + .background(IslandColor.settingsBackground) + .preferredColorScheme(appearanceStore.appearance.colorScheme) } // MARK: - Tabs @@ -105,16 +106,16 @@ struct SettingsView: View { Text(L10n.tr(tab.label)) .font(Typography.tabLabel) .foregroundStyle(isOn - ? .white.opacity(0.95) - : .white.opacity(0.50)) + ? Color.primary.opacity(0.95) + : Color.primary.opacity(0.50)) .padding(.horizontal, 12) .padding(.vertical, 6) .background { RoundedRectangle(cornerRadius: 6) - .fill(isOn ? .white.opacity(0.08) : .clear) + .fill(isOn ? Color.primary.opacity(0.08) : .clear) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) } } } @@ -166,7 +167,7 @@ struct SettingsView: View { private var hairline: some View { LinearGradient( - colors: [.clear, .white.opacity(0.055), .white.opacity(0.055), .clear], + colors: [.clear, Color.primary.opacity(0.055), Color.primary.opacity(0.055), .clear], startPoint: .leading, endPoint: .trailing ) .frame(height: 1) @@ -179,12 +180,12 @@ struct SettingsView: View { .font(Typography.sectionLabel) .tracking(1.05) .textCase(.uppercase) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) Spacer(minLength: 8) if let hint { Text(L10n.tr(hint)) .font(Typography.micro) - .foregroundStyle(.white.opacity(0.18)) + .foregroundStyle(Color.primary.opacity(0.18)) } } .padding(.horizontal, 10) @@ -214,6 +215,12 @@ struct SettingsView: View { ) { languagePicker } + SettingsRow( + title: "Appearance", + subtitle: "Choose a light or dark skin, or follow macOS." + ) { + appearanceSegmented + } SettingsRow( title: "Always show usage", subtitle: "Keep the percentage and time remaining visible without hovering." @@ -296,17 +303,17 @@ struct SettingsView: View { Button(action: action) { Text(L10n.tr(label)) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.85)) + .foregroundStyle(Color.primary.opacity(0.85)) .lineLimit(1) .fixedSize() .padding(.horizontal, 9) .padding(.vertical, 4) .background { RoundedRectangle(cornerRadius: 5) - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) .overlay { RoundedRectangle(cornerRadius: 5) - .strokeBorder(.white.opacity(0.10), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5) } } } @@ -371,7 +378,7 @@ struct SettingsView: View { Text(L10n.tr(label)) .font(Typography.rowTitle) .tracking(-0.07) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) Spacer(minLength: 8) thresholdStepper(value: value, range: range) } @@ -412,21 +419,21 @@ struct SettingsView: View { .textFieldStyle(.plain) .multilineTextAlignment(.center) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.95)) + .foregroundStyle(Color.primary.opacity(0.95)) .monospacedDigit() .frame(width: 22, height: 18) .clipped() Text("%") .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } .frame(width: 64, height: 28) .background { RoundedRectangle(cornerRadius: 7) - .fill(.white.opacity(0.05)) + .fill(Color.primary.opacity(0.05)) .overlay { RoundedRectangle(cornerRadius: 7) - .strokeBorder(.white.opacity(0.10), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5) } } } @@ -466,6 +473,15 @@ struct SettingsView: View { .accessibilityLabel(L10n.tr("Language")) } + private var appearanceSegmented: some View { + SegmentedControl( + items: AppAppearance.allCases, + selected: $appearanceStore.appearance, + label: \.label, + accessibilityPrefix: "Appearance" + ) + } + private var languageSelection: Binding { Binding( get: { appLanguage.language }, @@ -569,11 +585,11 @@ struct SettingsView: View { .font(Typography.sectionLabel) .tracking(1.05) .textCase(.uppercase) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) Text(costSubtitle()) .font(Typography.label) - .foregroundStyle(.white.opacity(0.42)) + .foregroundStyle(Color.primary.opacity(0.42)) .lineLimit(1) .truncationMode(.tail) diff --git a/Sources/Views/SettingsWindowController.swift b/Sources/Views/SettingsWindowController.swift index 85038069..af4c05c1 100644 --- a/Sources/Views/SettingsWindowController.swift +++ b/Sources/Views/SettingsWindowController.swift @@ -1,4 +1,5 @@ import AppKit +import Combine import SwiftUI /// Hand-rolled NSWindow for Settings instead of the SwiftUI `Settings` scene. @@ -9,6 +10,7 @@ import SwiftUI @MainActor final class SettingsWindowController: NSWindowController, NSWindowDelegate { static let shared = SettingsWindowController() + private var appearanceSubscription: AnyCancellable? private init() { let hosting = NSHostingController(rootView: SettingsView()) @@ -23,9 +25,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { window.titleVisibility = .hidden window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false - window.backgroundColor = NSColor( - calibratedRed: 0.075, green: 0.077, blue: 0.090, alpha: 1 - ) + window.backgroundColor = .windowBackgroundColor window.minSize = NSSize(width: 440, height: 420) // Hide the dock-stow button (we have no dock icon) but keep zoom // alongside resize handles so the user controls size. @@ -33,6 +33,14 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { window.center() super.init(window: window) window.delegate = self + appearanceSubscription = AppearanceStore.shared.$appearance + .sink { [weak window] appearance in + switch appearance { + case .system: window?.appearance = nil + case .light: window?.appearance = NSAppearance(named: .aqua) + case .dark: window?.appearance = NSAppearance(named: .darkAqua) + } + } } @available(*, unavailable) diff --git a/Sources/Views/UsageView.swift b/Sources/Views/UsageView.swift index c6b7ac9e..060d70ea 100644 --- a/Sources/Views/UsageView.swift +++ b/Sources/Views/UsageView.swift @@ -67,7 +67,7 @@ struct UsageView: View { private var hairline: some View { Rectangle() .fill(LinearGradient( - colors: [.clear, .white.opacity(0.06), .clear], + colors: [.clear, Color.primary.opacity(0.10), .clear], startPoint: .top, endPoint: .bottom )) .frame(width: 1) @@ -141,12 +141,12 @@ struct ReauthState: View { Text(L10n.tr(usage.fiveHour.error == ClaudeCredentials.reauthRequiredMessage ? "Claude re-login needed" : "Claude session expired")) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) ReauthButton() } else { Text(usage.fiveHour.error ?? ClaudeCredentials.tokenExpiredMessage) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) .multilineTextAlignment(.center) } } @@ -169,12 +169,12 @@ struct ReauthButton: View { } label: { Text(store.claudeReauthInProgress ? L10n.tr("waiting for browser…") : L10n.tr("Re-authenticate")) .font(Typography.label) - .foregroundStyle(.white.opacity(hovered && !store.claudeReauthInProgress ? 0.95 : 0.72)) + .foregroundStyle(Color.primary.opacity(hovered && !store.claudeReauthInProgress ? 0.95 : 0.72)) .padding(.horizontal, 8) .padding(.vertical, 3) .background( RoundedRectangle(cornerRadius: 5) - .fill(.white.opacity(hovered && !store.claudeReauthInProgress ? 0.08 : 0.04)) + .fill(Color.primary.opacity(hovered && !store.claudeReauthInProgress ? 0.08 : 0.04)) ) .contentShape(RoundedRectangle(cornerRadius: 5)) }