Add separate light/dark mode accent colors

Adds an option in Appearance settings to use different accent colors
in light and dark mode, with a toggle to keep a single shared color
(the default, preserving existing behavior).

- New synced settings keys: accentColorDark, customAccentColorDark,
  useSeparateDarkAccentColor; dark values fall back to the light
  selection until explicitly set, so no migration is needed
- resolvedAccentColor returns a dynamic platform color (UIColor trait
  provider / NSColor appearance provider) when the toggle is on, so
  the root tint and all direct readers adapt to light/dark
  automatically without consumer changes
- Accent Color section shows two stacked preset+custom grids with
  Light/Dark headers when enabled, extracted into AccentColorGrid;
  CustomAccentColorButton now takes bindings so the macOS color panel
  edits whichever target was last opened
- New DarkAccentColorTests covering sync contract, default-off state,
  fallbacks, and toggle-off resolution

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
This commit is contained in:
Arkadiusz Fal
2026-07-05 21:06:26 +02:00
parent b47888fe04
commit b632f11b2f
6 changed files with 250 additions and 28 deletions

View File

@@ -143,29 +143,68 @@ private struct AppIconPickerView: View {
// MARK: - Accent Color Section
private enum AccentColorTarget {
case light, dark
}
private struct AccentColorSection: View {
@Bindable var settings: SettingsManager
var body: some View {
SettingsFormSection("settings.appearance.accentColor.header") {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 50))], spacing: 16) {
ForEach(AccentColor.presets, id: \.self) { accentColor in
AccentColorButton(
accentColor: accentColor,
isSelected: settings.accentColor == accentColor,
onSelect: { settings.accentColor = accentColor }
)
}
#if !os(tvOS)
CustomAccentColorButton(settings: settings)
#endif
#if !os(tvOS)
Toggle(isOn: $settings.useSeparateDarkAccentColor) {
Text(String(localized: "settings.appearance.accentColor.separateColors"))
}
#endif
if settings.useSeparateDarkAccentColor {
Text(String(localized: "settings.appearance.accentColor.light"))
.font(.subheadline)
.foregroundStyle(.secondary)
AccentColorGrid(settings: settings, target: .light)
Text(String(localized: "settings.appearance.accentColor.dark"))
.font(.subheadline)
.foregroundStyle(.secondary)
AccentColorGrid(settings: settings, target: .dark)
} else {
AccentColorGrid(settings: settings, target: .light)
}
.padding(.vertical, 8)
}
}
}
private struct AccentColorGrid: View {
@Bindable var settings: SettingsManager
let target: AccentColorTarget
private var selection: Binding<AccentColor> {
target == .light ? $settings.accentColor : $settings.accentColorDark
}
private var customColor: Binding<Color> {
target == .light ? $settings.customAccentColor : $settings.customAccentColorDark
}
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 50))], spacing: 16) {
ForEach(AccentColor.presets, id: \.self) { accentColor in
AccentColorButton(
accentColor: accentColor,
isSelected: selection.wrappedValue == accentColor,
onSelect: { selection.wrappedValue = accentColor }
)
}
#if !os(tvOS)
CustomAccentColorButton(selection: selection, customColor: customColor)
#endif
}
.padding(.vertical, 8)
}
}
// MARK: - List Style Section
private struct ListStyleSection: View {
@@ -233,17 +272,18 @@ private struct AccentColorButton: View {
#if !os(tvOS)
private struct CustomAccentColorButton: View {
@Bindable var settings: SettingsManager
@Binding var selection: AccentColor
@Binding var customColor: Color
private var isSelected: Bool { settings.accentColor == .custom }
private var isSelected: Bool { selection == .custom }
#if os(macOS)
var body: some View {
Button {
settings.accentColor = .custom
ColorPanelBridge.shared.open(with: NSColor(settings.customAccentColor)) { nsColor in
settings.customAccentColor = Color(nsColor: nsColor)
settings.accentColor = .custom
selection = .custom
ColorPanelBridge.shared.open(with: NSColor(customColor)) { nsColor in
customColor = Color(nsColor: nsColor)
selection = .custom
}
} label: {
ZStack {
@@ -258,7 +298,7 @@ private struct CustomAccentColorButton: View {
.frame(width: 40, height: 40)
Circle()
.fill(settings.customAccentColor)
.fill(customColor)
.frame(width: 28, height: 28)
if isSelected {
@@ -274,10 +314,10 @@ private struct CustomAccentColorButton: View {
#else
private var pickedColor: Binding<Color> {
Binding(
get: { settings.customAccentColor },
get: { customColor },
set: { newColor in
settings.customAccentColor = newColor
settings.accentColor = .custom
customColor = newColor
selection = .custom
}
)
}
@@ -285,7 +325,7 @@ private struct CustomAccentColorButton: View {
var body: some View {
ZStack {
Circle()
.fill(settings.customAccentColor)
.fill(customColor)
.frame(width: 40, height: 40)
.opacity(isSelected ? 1 : 0)