Farben einstellbar — und erst mal überhaupt eine Farbe

Das Grün, das überall auftauchte, war die System-Akzentfarbe: `.tint` und
`.accentColor` werden ohne eigene Angabe dorthin durchgereicht, und das
betraf alle Popovers und das ganze Einstellungsfenster. Die Notch-Widgets
dagegen benutzten Onyx' eigenes Blau. Zwei Farben, ungewollt.

Jetzt eine, und die ist einstellbar. Ein neuer Reiter „Farben": Akzentfarbe
mit sechs Vorschlägen und einem Knopf, der die Systemfarbe übernimmt, dazu
das Paar für Herunter- und Hochladen samt Tauschknopf.

Die Signalfarben bleiben fest und stehen nur zur Ansicht dort. Gelb heißt
Warnung, Rot heißt kritisch, Grün heißt in Ordnung — bei Temperatur, Akku,
Lüfter und Fehlern. Wer sie umstellen kann, kann ihre Bedeutung zerstören;
ein rotes „alles gut" liest niemand richtig. Sichtbar sind sie trotzdem,
sonst sucht man die Warnfarbe im Reiter und findet sie nirgends.

Umgesetzt über die Tokens statt über vierzig Fundstellen: `Onyx.Color.accent`
ist keine Konstante mehr, sondern liest bei jedem Zugriff aus `OnyxTheme`.
Damit merkt SwiftUI die Abhängigkeit und zeichnet neu, sobald die Farbe sich
ändert. Dazu ein `.tint` an den vier Wurzeln — Panel, Popovers, Einstellungen,
Einrichtung —, damit auch Haken, Regler und Auswahlfelder mitziehen.

Nebenbei die Beschriftung: „Je Programm" heißt jetzt „Programme". Großgesetzt
las sich das als englisches „J-E".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-13 23:24:40 +02:00
parent 7a3d2c1c39
commit 2aa60eedf9
20 changed files with 757 additions and 45 deletions

View File

@@ -40,6 +40,7 @@
C5A4C9B4D21A951159C7D325 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A5510538846C8427AEB831 /* OnboardingView.swift */; }; C5A4C9B4D21A951159C7D325 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A5510538846C8427AEB831 /* OnboardingView.swift */; };
CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3FDFD387188D36AF8287DE /* FanControl.swift */; }; CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3FDFD387188D36AF8287DE /* FanControl.swift */; };
CD501EBAE39807F5D354C5B2 /* OnyxMenuBar in Frameworks */ = {isa = PBXBuildFile; productRef = A7FD7A4864E5ED4B95C211AF /* OnyxMenuBar */; }; CD501EBAE39807F5D354C5B2 /* OnyxMenuBar in Frameworks */ = {isa = PBXBuildFile; productRef = A7FD7A4864E5ED4B95C211AF /* OnyxMenuBar */; };
E8B839A850BE54F32410C2C8 /* ColorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76B112C2D5753B2600D42D /* ColorSettingsView.swift */; };
EA4D6312E8A96E177F10797F /* OnyxDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 3D680DD645941493A7D3559E /* OnyxDesign */; }; EA4D6312E8A96E177F10797F /* OnyxDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 3D680DD645941493A7D3559E /* OnyxDesign */; };
F2A3AD184864422EA14A8C64 /* graphic-card.svg in Resources */ = {isa = PBXBuildFile; fileRef = 1E6A1C208C7EA56971A055E1 /* graphic-card.svg */; }; F2A3AD184864422EA14A8C64 /* graphic-card.svg in Resources */ = {isa = PBXBuildFile; fileRef = 1E6A1C208C7EA56971A055E1 /* graphic-card.svg */; };
FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 055263008CB3306A8D09E6D4 /* AppIcon.icon */; }; FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 055263008CB3306A8D09E6D4 /* AppIcon.icon */; };
@@ -80,6 +81,7 @@
B442E8A39C37C8FD92B4CA83 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; B442E8A39C37C8FD92B4CA83 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
B8214C5599D1857E832447EC /* bolt-light.svg */ = {isa = PBXFileReference; path = "bolt-light.svg"; sourceTree = "<group>"; }; B8214C5599D1857E832447EC /* bolt-light.svg */ = {isa = PBXFileReference; path = "bolt-light.svg"; sourceTree = "<group>"; };
B83C4E3CB821F903F3977E88 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = "<group>"; }; B83C4E3CB821F903F3977E88 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = "<group>"; };
DB76B112C2D5753B2600D42D /* ColorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSettingsView.swift; sourceTree = "<group>"; };
F12041E330231599C6540C21 /* FanSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanSettingsView.swift; sourceTree = "<group>"; }; F12041E330231599C6540C21 /* FanSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanSettingsView.swift; sourceTree = "<group>"; };
F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnyxApp.swift; sourceTree = "<group>"; }; F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnyxApp.swift; sourceTree = "<group>"; };
FC3FDFD387188D36AF8287DE /* FanControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanControl.swift; sourceTree = "<group>"; }; FC3FDFD387188D36AF8287DE /* FanControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanControl.swift; sourceTree = "<group>"; };
@@ -169,6 +171,7 @@
7A866E72010907DF4BEF025C /* bolt-dark.svg */, 7A866E72010907DF4BEF025C /* bolt-dark.svg */,
B8214C5599D1857E832447EC /* bolt-light.svg */, B8214C5599D1857E832447EC /* bolt-light.svg */,
085D860E654E09E4CD6E535A /* chip.svg */, 085D860E654E09E4CD6E535A /* chip.svg */,
DB76B112C2D5753B2600D42D /* ColorSettingsView.swift */,
FC3FDFD387188D36AF8287DE /* FanControl.swift */, FC3FDFD387188D36AF8287DE /* FanControl.swift */,
F12041E330231599C6540C21 /* FanSettingsView.swift */, F12041E330231599C6540C21 /* FanSettingsView.swift */,
89309D9F5722FA1B91CF2905 /* FanWidget.swift */, 89309D9F5722FA1B91CF2905 /* FanWidget.swift */,
@@ -355,6 +358,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C041F171FB69F4381309D978 /* AppModel.swift in Sources */, C041F171FB69F4381309D978 /* AppModel.swift in Sources */,
E8B839A850BE54F32410C2C8 /* ColorSettingsView.swift in Sources */,
CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */, CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */,
0CEBFA01137EEED0774A089F /* FanSettingsView.swift in Sources */, 0CEBFA01137EEED0774A089F /* FanSettingsView.swift in Sources */,
6CD3F5666C39F54506B8430A /* FanWidget.swift in Sources */, 6CD3F5666C39F54506B8430A /* FanWidget.swift in Sources */,

View File

@@ -0,0 +1,131 @@
import SwiftUI
import OnyxDesign
/// Die Farbwahl.
///
/// Drei Farben, nicht zehn: die Akzentfarbe, mit der Onyx sich selbst zeichnet,
/// und das Paar für Herunter- und Hochladen. Die Signalfarben stehen bewusst
/// nicht hier Gelb heißt Warnung, Rot heißt kritisch, und wer das umstellen
/// kann, kann die Bedeutung zerstören.
struct ColorSettingsView: View {
@Bindable var theme: OnyxTheme
/// Ein paar Vorschläge, damit man nicht im Farbwähler anfangen muss.
///
/// Alle gedämpft: Onyx ist eine dunkle Fläche, und eine volle Sättigung
/// darauf sticht, statt zu leiten.
private static let suggestions: [(name: LocalizedStringKey, hex: String)] = [
("color.suggestion.onyx", "#7FA8FF"),
("color.suggestion.mint", "#6FCF97"),
("color.suggestion.amber", "#E8B44A"),
("color.suggestion.coral", "#E5675C"),
("color.suggestion.lilac", "#B79CFF"),
("color.suggestion.slate", "#9AA6B2"),
]
var body: some View {
Form {
Section("color.section.accent") {
ColorPicker(selection: $theme.accent, supportsOpacity: false) {
Text("color.accent")
}
HStack(spacing: 8) {
ForEach(Self.suggestions, id: \.hex) { suggestion in
Swatch(hex: suggestion.hex,
name: suggestion.name,
isSelected: theme.accent.onyxHex == suggestion.hex) {
theme.accent = Color(onyxHex: suggestion.hex) ?? theme.accent
}
}
Spacer(minLength: 0)
}
Button { theme.adoptSystemAccent() } label: {
Text("color.adoptSystem")
}
Text("color.accent.hint")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("color.section.network") {
ColorPicker(selection: $theme.download, supportsOpacity: false) {
Text("color.download")
}
ColorPicker(selection: $theme.upload, supportsOpacity: false) {
Text("color.upload")
}
Button {
let previous = theme.download
theme.download = theme.upload
theme.upload = previous
} label: {
Text("color.swap")
}
Text("color.network.hint")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("color.section.fixed") {
// Sichtbar, aber nicht bedienbar: sonst sucht man die
// Warnfarbe hier und findet sie nirgends.
FixedRow(color: Onyx.Color.positive, label: "color.fixed.positive")
FixedRow(color: Onyx.Color.warning, label: "color.fixed.warning")
FixedRow(color: Onyx.Color.critical, label: "color.fixed.critical")
Text("color.fixed.hint")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section {
Button { theme.reset() } label: {
Text("color.reset")
}
.disabled(theme.isDefault)
}
}
.formStyle(.grouped)
}
}
/// Ein Farbfeld zum Anklicken.
private struct Swatch: View {
let hex: String
let name: LocalizedStringKey
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Circle()
.fill(Color(onyxHex: hex) ?? .gray)
.frame(width: 20, height: 20)
.overlay {
// Der Ring statt eines Hakens: ein Haken auf einer dunklen
// Farbe ist nicht zu sehen, auf einer hellen nicht auf
// einer dunklen Fläche.
Circle()
.strokeBorder(.primary, lineWidth: isSelected ? 2 : 0)
.padding(-3)
}
}
.buttonStyle(.plain)
.help(Text(name))
}
}
private struct FixedRow: View {
let color: Color
let label: LocalizedStringKey
var body: some View {
HStack {
Text(label)
Spacer(minLength: 12)
Circle().fill(color).frame(width: 14, height: 14)
}
}
}

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import OnyxDesign
import MetricsProvider import MetricsProvider
import OnyxHelperProtocol import OnyxHelperProtocol
@@ -47,7 +48,7 @@ struct FanSettingsView: View {
// Das Risiko benennen, bevor jemand zustimmt nicht danach. // Das Risiko benennen, bevor jemand zustimmt nicht danach.
Label("fans.warning", systemImage: "exclamationmark.triangle.fill") Label("fans.warning", systemImage: "exclamationmark.triangle.fill")
.font(.callout) .font(.callout)
.foregroundStyle(.orange) .foregroundStyle(Onyx.Color.warning)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
Text("fans.safety") Text("fans.safety")
@@ -56,7 +57,7 @@ struct FanSettingsView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
if case .failed(let message) = control.installState { if case .failed(let message) = control.installState {
Text(message).font(.callout).foregroundStyle(.red) Text(message).font(.callout).foregroundStyle(Onyx.Color.critical)
} }
Button("fans.install") { control.install() } Button("fans.install") { control.install() }
@@ -85,7 +86,7 @@ struct FanSettingsView: View {
Spacer() Spacer()
Text("\(Int(hottest))°").monospacedDigit() Text("\(Int(hottest))°").monospacedDigit()
.foregroundStyle(hottest >= FanSafety.criticalCelsius .foregroundStyle(hottest >= FanSafety.criticalCelsius
? .red : .secondary) ? Onyx.Color.critical : .secondary)
} }
} }
ForEach(report.fans, id: \.index) { fan in ForEach(report.fans, id: \.index) { fan in
@@ -108,7 +109,7 @@ struct FanSettingsView: View {
Text("fans.connecting").foregroundStyle(.secondary) Text("fans.connecting").foregroundStyle(.secondary)
} }
if let error = control.lastError { if let error = control.lastError {
Text(error).font(.callout).foregroundStyle(.red) Text(error).font(.callout).foregroundStyle(Onyx.Color.critical)
} }
} }
} }
@@ -165,7 +166,7 @@ private struct FanRow: View {
if let reason = fan.automaticReason, reason != "noRequest" { if let reason = fan.automaticReason, reason != "noRequest" {
Label(explanation(for: reason), systemImage: "shield.lefthalf.filled") Label(explanation(for: reason), systemImage: "shield.lefthalf.filled")
.font(.caption) .font(.caption)
.foregroundStyle(.orange) .foregroundStyle(Onyx.Color.warning)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} }

View File

@@ -141,6 +141,342 @@
} }
} }
}, },
"color.accent": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Farbe"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Colour"
}
}
}
},
"color.accent.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Gilt überall, wo Onyx sich selbst zeichnet: Regler, Ringe, Abschnittstitel, Haken und Auswahl."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Applies everywhere Onyx draws itself: sliders, dials, section headings, check marks and selection."
}
}
}
},
"color.adoptSystem": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Systemfarbe übernehmen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Adopt system colour"
}
}
}
},
"color.download": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Herunterladen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Download"
}
}
}
},
"color.fixed.critical": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Kritisch"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Critical"
}
}
}
},
"color.fixed.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Diese drei bleiben, wie sie sind. Sie bedeuten etwas — bei Temperatur, Akku, Lüfter und Fehlern."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "These three stay as they are. They carry meaning — for temperature, battery, fans and errors."
}
}
}
},
"color.fixed.positive": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "In Ordnung"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Normal"
}
}
}
},
"color.fixed.warning": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Warnung"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Warning"
}
}
}
},
"color.network.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Welche Richtung grün ist und welche rosa, ist Gewohnheit — nicht Bedeutung."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Which direction is green and which is pink is habit, not meaning."
}
}
}
},
"color.reset": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Auf Standard zurücksetzen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Reset to defaults"
}
}
}
},
"color.section.accent": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Akzentfarbe"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Accent colour"
}
}
}
},
"color.section.fixed": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Feste Farben"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Fixed colours"
}
}
}
},
"color.section.network": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Netzwerk"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Network"
}
}
}
},
"color.suggestion.amber": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Bernstein"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Amber"
}
}
}
},
"color.suggestion.coral": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Koralle"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Coral"
}
}
}
},
"color.suggestion.lilac": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Flieder"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Lilac"
}
}
}
},
"color.suggestion.mint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Minze"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Mint"
}
}
}
},
"color.suggestion.onyx": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Onyx-Blau"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Onyx blue"
}
}
}
},
"color.suggestion.slate": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Schiefer"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Slate"
}
}
}
},
"color.swap": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Richtungen tauschen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Swap directions"
}
}
}
},
"color.upload": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Hochladen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Upload"
}
}
}
},
"fans.approval.needed": { "fans.approval.needed": {
"extractionState": "stale", "extractionState": "stale",
"localizations": { "localizations": {
@@ -2014,6 +2350,22 @@
} }
} }
}, },
"settings.tab.colors": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Farben"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Colours"
}
}
}
},
"settings.tab.display": { "settings.tab.display": {
"extractionState": "stale", "extractionState": "stale",
"localizations": { "localizations": {

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import OnyxDesign
import AppKit import AppKit
import CalendarProvider import CalendarProvider
import WeatherProvider import WeatherProvider
@@ -175,7 +176,7 @@ private struct OnboardingRow: View {
HStack(alignment: .top, spacing: 14) { HStack(alignment: .top, spacing: 14) {
Image(systemName: symbol) Image(systemName: symbol)
.font(.system(size: 15)) .font(.system(size: 15))
.foregroundStyle(state == .granted ? .green : .secondary) .foregroundStyle(state == .granted ? Onyx.Color.positive : .secondary)
.frame(width: 22, height: 22) .frame(width: 22, height: 22)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
@@ -190,7 +191,7 @@ private struct OnboardingRow: View {
switch state { switch state {
case .granted: case .granted:
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(Onyx.Color.positive)
.font(.system(size: 16)) .font(.system(size: 16))
case .undetermined: case .undetermined:
Button("onboarding.allow") { Task { await request() } } Button("onboarding.allow") { Task { await request() } }

View File

@@ -1,5 +1,6 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
import OnyxDesign
import CalendarProvider import CalendarProvider
import WeatherProvider import WeatherProvider
import AudioProvider import AudioProvider
@@ -43,7 +44,8 @@ final class OnboardingWindowController: NSObject, NSWindowDelegate {
rootView: OnboardingView( rootView: OnboardingView(
calendarModel: calendarModel, weatherModel: weatherModel, calendarModel: calendarModel, weatherModel: weatherModel,
mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin, mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin,
onFinish: { [weak self] in self?.finish() })) onFinish: { [weak self] in self?.finish() })
.tint(Onyx.Color.accent))
let window = NSWindow(contentViewController: hosting) let window = NSWindow(contentViewController: hosting)
window.title = "Onyx" window.title = "Onyx"

View File

@@ -76,7 +76,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
NotchHostingView( NotchHostingView(
rootView: NotchContainer(presentation: presentation) { rootView: NotchContainer(presentation: presentation) {
NotchPanelContent(model: model) NotchPanelContent(model: model)
}, }
.tint(Onyx.Color.accent),
presentation: presentation) presentation: presentation)
} }
coordinator.start() coordinator.start()

View File

@@ -10,7 +10,7 @@ import OnyxHelperProtocol
import WeatherProvider import WeatherProvider
enum SettingsTab: Hashable { enum SettingsTab: Hashable {
case widgets, display, sources, menubar, fans, permissions case widgets, display, colors, sources, menubar, fans, permissions
} }
struct SettingsView: View { struct SettingsView: View {
@@ -32,6 +32,9 @@ struct SettingsView: View {
DisplaySettings(model: model) DisplaySettings(model: model)
.tabItem { Label("settings.tab.display", systemImage: "macbook") } .tabItem { Label("settings.tab.display", systemImage: "macbook") }
.tag(SettingsTab.display) .tag(SettingsTab.display)
ColorSettingsView(theme: OnyxTheme.shared)
.tabItem { Label("settings.tab.colors", systemImage: "paintpalette") }
.tag(SettingsTab.colors)
WidgetOptionsView(calendarModel: calendarModel, weatherModel: weatherModel, WidgetOptionsView(calendarModel: calendarModel, weatherModel: weatherModel,
shelfStore: shelfStore) shelfStore: shelfStore)
.tabItem { Label("settings.tab.options", systemImage: "slider.horizontal.3") } .tabItem { Label("settings.tab.options", systemImage: "slider.horizontal.3") }
@@ -189,7 +192,7 @@ private struct PermissionRow: View {
switch state { switch state {
case .granted: case .granted:
Label("settings.permissions.granted", systemImage: "checkmark.circle.fill") Label("settings.permissions.granted", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(Onyx.Color.positive)
.labelStyle(.titleAndIcon) .labelStyle(.titleAndIcon)
case .undetermined: case .undetermined:
Button("settings.permissions.request") { Task { await request() } } Button("settings.permissions.request") { Task { await request() } }

View File

@@ -1,5 +1,6 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
import OnyxDesign
import CalendarProvider import CalendarProvider
import WeatherProvider import WeatherProvider
import MetricsProvider import MetricsProvider
@@ -53,7 +54,11 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
showOnboarding: showOnboarding, showOnboarding: showOnboarding,
selectedTab: Binding( selectedTab: Binding(
get: { [weak self] in self?.selectedTab ?? .widgets }, get: { [weak self] in self?.selectedTab ?? .widgets },
set: { [weak self] in self?.selectedTab = $0 }))) set: { [weak self] in self?.selectedTab = $0 }))
// Ohne das folgen Haken, Regler und Auswahlfelder der
// Systemfarbe statt der eingestellten. Genau daher kam das
// Grün, das in Onyx sonst nirgends vorkommt.
.tint(Onyx.Color.accent))
let window = NSWindow(contentViewController: hosting) let window = NSWindow(contentViewController: hosting)
window.title = "Onyx" window.title = "Onyx"

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import OnyxDesign
import CalendarProvider import CalendarProvider
import WeatherProvider import WeatherProvider
import ShelfProvider import ShelfProvider
@@ -116,9 +117,9 @@ private struct CalendarrStatus: View {
case .loggedOut: case .loggedOut:
Text("calendar.source.status.loggedOut").font(.callout).foregroundStyle(.secondary) Text("calendar.source.status.loggedOut").font(.callout).foregroundStyle(.secondary)
case .incompatible: case .incompatible:
Text("calendar.source.status.incompatible").font(.callout).foregroundStyle(.orange) Text("calendar.source.status.incompatible").font(.callout).foregroundStyle(Onyx.Color.warning)
case .unreadable(let reason): case .unreadable(let reason):
Text(reason).font(.callout).foregroundStyle(.orange) Text(reason).font(.callout).foregroundStyle(Onyx.Color.warning)
case .denied, .none: case .denied, .none:
ProgressView().controlSize(.small) ProgressView().controlSize(.small)
} }
@@ -148,7 +149,7 @@ private struct WeatherOptions: View {
// steht hier, statt dass das Widget stumm leer bleibt. // steht hier, statt dass das Widget stumm leer bleibt.
if model.locationIsDenied { if model.locationIsDenied {
Text("options.weather.needsLocation") Text("options.weather.needsLocation")
.font(.callout).foregroundStyle(.orange) .font(.callout).foregroundStyle(Onyx.Color.warning)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} else if case .fixed(let name, _, _) = model.place { } else if case .fixed(let name, _, _) = model.place {
@@ -172,7 +173,7 @@ private struct WeatherOptions: View {
} }
if notFound { if notFound {
Text("options.weather.notFound").font(.callout).foregroundStyle(.orange) Text("options.weather.notFound").font(.callout).foregroundStyle(Onyx.Color.warning)
} }
} }

View File

@@ -41,6 +41,8 @@ let package = Package(
resources: [.process("Localizable.xcstrings")]), resources: [.process("Localizable.xcstrings")]),
.testTarget(name: "OnyxNotchTests", dependencies: ["OnyxNotch"]), .testTarget(name: "OnyxNotchTests", dependencies: ["OnyxNotch"]),
.testTarget(name: "OnyxDesignTests", dependencies: ["OnyxDesign"]),
.target(name: "OnyxWidgetKit", dependencies: ["OnyxDesign"]), .target(name: "OnyxWidgetKit", dependencies: ["OnyxDesign"]),
.testTarget(name: "OnyxWidgetKitTests", dependencies: ["OnyxWidgetKit"]), .testTarget(name: "OnyxWidgetKitTests", dependencies: ["OnyxWidgetKit"]),

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import OnyxDesign
/// Die Wahl der Terminquelle samt ehrlicher Auskunft darüber, was Calendarr /// Die Wahl der Terminquelle samt ehrlicher Auskunft darüber, was Calendarr
/// gerade liefert. /// gerade liefert.
@@ -73,7 +74,7 @@ public struct CalendarSourceSettings: View {
notice("calendar.source.status.incompatible") notice("calendar.source.status.incompatible")
case .unreadable(let reason): case .unreadable(let reason):
Text("calendar.source.status.unreadable \(reason)", bundle: .module) Text("calendar.source.status.unreadable \(reason)", bundle: .module)
.font(.callout).foregroundStyle(.orange) .font(.callout).foregroundStyle(Onyx.Color.warning)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
case .denied, .none: case .denied, .none:
ProgressView().controlSize(.small) ProgressView().controlSize(.small)

View File

@@ -40,7 +40,7 @@ private struct SectionTitle: View {
var body: some View { var body: some View {
Text(text, bundle: .module) Text(text, bundle: .module)
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
.foregroundStyle(.tint) .foregroundStyle(Onyx.Color.accent)
.textCase(.uppercase) .textCase(.uppercase)
} }
} }
@@ -81,7 +81,9 @@ private struct Dial: View {
let value: Double let value: Double
let caption: String let caption: String
let text: String let text: String
var tint: Color = .accentColor /// Ohne Angabe die Akzentfarbe. Als Standardwert ginge sie nicht: der
/// wird außerhalb des Hauptthreads erzeugt, die Farbe liegt aber dort.
var tint: Color?
var body: some View { var body: some View {
// Sechs Punkte Abstand statt zwei: die Beschriftung klebte am Ring und // Sechs Punkte Abstand statt zwei: die Beschriftung klebte am Ring und
@@ -91,7 +93,8 @@ private struct Dial: View {
Circle().stroke(.quaternary, lineWidth: 5) Circle().stroke(.quaternary, lineWidth: 5)
Circle() Circle()
.trim(from: 0, to: min(max(value, 0), 1)) .trim(from: 0, to: min(max(value, 0), 1))
.stroke(tint, style: .init(lineWidth: 5, lineCap: .round)) .stroke(tint ?? Onyx.Color.accent,
style: .init(lineWidth: 5, lineCap: .round))
.rotationEffect(.degrees(-90)) .rotationEffect(.degrees(-90))
Text(text).font(.system(size: 15, weight: .medium).monospacedDigit()) Text(text).font(.system(size: 15, weight: .medium).monospacedDigit())
} }
@@ -182,10 +185,10 @@ private struct CoreGrid: View {
spacing: 3) { spacing: 3) {
ForEach(Array(values.enumerated()), id: \.offset) { _, value in ForEach(Array(values.enumerated()), id: \.offset) { _, value in
RoundedRectangle(cornerRadius: 2, style: .continuous) RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(Color.accentColor.opacity(0.18)) .fill(Onyx.Color.accent.opacity(0.18))
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
RoundedRectangle(cornerRadius: 2, style: .continuous) RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(Color.accentColor) .fill(Onyx.Color.accent)
.frame(height: max(14 * value, 1.5)) .frame(height: max(14 * value, 1.5))
} }
.frame(height: 14) .frame(height: 14)
@@ -208,7 +211,7 @@ private struct MemoryDetail: View {
text: MetricFormat.percent(memory.usedFraction), text: MetricFormat.percent(memory.usedFraction),
tint: pressureColor) tint: pressureColor)
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
legend(.accentColor, "popover.memory.app", memory.app) legend(Onyx.Color.accent, "popover.memory.app", memory.app)
legend(.purple, "popover.memory.wired", memory.wired) legend(.purple, "popover.memory.wired", memory.wired)
legend(.teal, "popover.memory.compressed", memory.compressed) legend(.teal, "popover.memory.compressed", memory.compressed)
legend(.gray, "popover.memory.free", memory.free) legend(.gray, "popover.memory.free", memory.free)
@@ -220,7 +223,7 @@ private struct MemoryDetail: View {
// ist ein voller Speicher kein Problem macOS füllt ihn absichtlich. // ist ein voller Speicher kein Problem macOS füllt ihn absichtlich.
Row(label: String(localized: "popover.memory.swapUsed", bundle: .module), Row(label: String(localized: "popover.memory.swapUsed", bundle: .module),
value: "\(bytes(memory.swapUsed)) / \(bytes(memory.swapTotal))", value: "\(bytes(memory.swapUsed)) / \(bytes(memory.swapTotal))",
emphasis: memory.swapUsed > 512 * 1024 * 1024 ? .orange : nil) emphasis: memory.swapUsed > 512 * 1024 * 1024 ? Onyx.Color.warning : nil)
if !snapshot.topMemoryProcesses.isEmpty { if !snapshot.topMemoryProcesses.isEmpty {
SectionTitle(text: "popover.processes") SectionTitle(text: "popover.processes")
@@ -232,9 +235,9 @@ private struct MemoryDetail: View {
private var pressureColor: Color { private var pressureColor: Color {
switch snapshot.memory.pressure { switch snapshot.memory.pressure {
case .normal: .green case .normal: Onyx.Color.positive
case .warning: .orange case .warning: Onyx.Color.warning
case .critical: .red case .critical: Onyx.Color.critical
} }
} }
@@ -264,8 +267,9 @@ private struct BatteryDetail: View {
Dial(value: battery.charge, Dial(value: battery.charge,
caption: String(localized: "metric.battery", bundle: .module), caption: String(localized: "metric.battery", bundle: .module),
text: MetricFormat.percent(battery.charge), text: MetricFormat.percent(battery.charge),
tint: battery.isCharging ? .green tint: battery.isCharging ? Onyx.Color.positive
: (battery.charge < 0.2 ? .red : .accentColor)) : (battery.charge < 0.2 ? Onyx.Color.critical
: Onyx.Color.accent))
// Beides gehört neben den Ring: die Schätzung und der Grund // Beides gehört neben den Ring: die Schätzung und der Grund
// dafür. Am Netzteil" über dem Ring und Am Netz, lädt nicht" // dafür. Am Netzteil" über dem Ring und Am Netz, lädt nicht"
// darunter waren zweimal dieselbe Auskunft an zwei Stellen // darunter waren zweimal dieselbe Auskunft an zwei Stellen
@@ -382,7 +386,7 @@ private struct BatteryDetail: View {
private func tint(for activity: ChargeActivity) -> Color { private func tint(for activity: ChargeActivity) -> Color {
// Farbe nur, wo sie etwas meldet: Laden ist ein Zustand, den man // Farbe nur, wo sie etwas meldet: Laden ist ein Zustand, den man
// sucht. Alles andere bleibt zurückhaltend. // sucht. Alles andere bleibt zurückhaltend.
activity == .charging ? .green : .secondary activity == .charging ? Onyx.Color.positive : .secondary
} }
private func duration(_ seconds: TimeInterval) -> String { private func duration(_ seconds: TimeInterval) -> String {

View File

@@ -199,7 +199,7 @@
"de": { "de": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "Je Programm" "value": "Programme"
} }
}, },
"en": { "en": {

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import MetricsProvider import MetricsProvider
import OnyxDesign
import OnyxMenuBar import OnyxMenuBar
@MainActor @MainActor
@@ -9,9 +10,9 @@ struct NetworkPopoverContent: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top) { HStack(alignment: .top) {
rate(model.snapshot.downloadRate, "network.download", .green) rate(model.snapshot.downloadRate, "network.download", Onyx.Color.download)
Spacer() Spacer()
rate(model.snapshot.uploadRate, "network.upload", .pink) rate(model.snapshot.uploadRate, "network.upload", Onyx.Color.upload)
} }
TrafficChart(history: model.history) TrafficChart(history: model.history)
@@ -91,9 +92,11 @@ struct NetworkPopoverContent: View {
Text(process.name).font(.callout).lineLimit(1) Text(process.name).font(.callout).lineLimit(1)
Spacer(minLength: 8) Spacer(minLength: 8)
Text(Throughput.formatted(process.downloadRate)) Text(Throughput.formatted(process.downloadRate))
.font(.caption.monospacedDigit()).foregroundStyle(.green) .font(.caption.monospacedDigit())
.foregroundStyle(Onyx.Color.download)
Text(Throughput.formatted(process.uploadRate)) Text(Throughput.formatted(process.uploadRate))
.font(.caption.monospacedDigit()).foregroundStyle(.pink) .font(.caption.monospacedDigit())
.foregroundStyle(Onyx.Color.upload)
} }
} }
} }
@@ -134,7 +137,7 @@ struct NetworkPopoverContent: View {
private func section(_ key: LocalizedStringKey) -> some View { private func section(_ key: LocalizedStringKey) -> some View {
Text(key, bundle: .module) Text(key, bundle: .module)
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
.foregroundStyle(.tint) .foregroundStyle(Onyx.Color.accent)
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -183,11 +186,11 @@ private struct TrafficChart: View {
ForEach(Array(points.enumerated()), id: \.offset) { _, snapshot in ForEach(Array(points.enumerated()), id: \.offset) { _, snapshot in
VStack(spacing: 1) { VStack(spacing: 1) {
Rectangle() Rectangle()
.fill(.pink) .fill(Onyx.Color.upload)
.frame(height: max(half * CGFloat(snapshot.uploadRate / peak), 0.5)) .frame(height: max(half * CGFloat(snapshot.uploadRate / peak), 0.5))
.frame(maxHeight: .infinity, alignment: .bottom) .frame(maxHeight: .infinity, alignment: .bottom)
Rectangle() Rectangle()
.fill(.green) .fill(Onyx.Color.download)
.frame(height: max(half * CGFloat(snapshot.downloadRate / peak), 0.5)) .frame(height: max(half * CGFloat(snapshot.downloadRate / peak), 0.5))
.frame(maxHeight: .infinity, alignment: .top) .frame(maxHeight: .infinity, alignment: .top)
} }

View File

@@ -145,7 +145,7 @@ private struct NetworkWidgetView: View {
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: model.snapshot.interfaceKind.symbolName) Image(systemName: model.snapshot.interfaceKind.symbolName)
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundStyle(Onyx.Color.accent) .foregroundStyle(Onyx.Color.download)
Text(connectionName) Text(connectionName)
.font(Onyx.Font.caption) .font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textSecondary) .foregroundStyle(Onyx.Color.textSecondary)
@@ -154,7 +154,7 @@ private struct NetworkWidgetView: View {
Rates(snapshot: model.snapshot, compact: false) Rates(snapshot: model.snapshot, compact: false)
Sparkline(values: model.series(download: true), tint: Onyx.Color.accent, Sparkline(values: model.series(download: true), tint: Onyx.Color.download,
capacity: NetworkModel.historyLength) capacity: NetworkModel.historyLength)
.frame(height: 18) .frame(height: 18)
@@ -190,8 +190,8 @@ private struct Rates: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 1) { VStack(alignment: .leading, spacing: 1) {
row("arrow.down", Throughput.formatted(snapshot.downloadRate), Onyx.Color.accent) row("arrow.down", Throughput.formatted(snapshot.downloadRate), Onyx.Color.download)
row("arrow.up", Throughput.formatted(snapshot.uploadRate), Onyx.Color.positive) row("arrow.up", Throughput.formatted(snapshot.uploadRate), Onyx.Color.upload)
} }
} }

View File

@@ -0,0 +1,106 @@
import SwiftUI
import AppKit
/// Die einstellbaren Farben.
///
/// **Nicht alle Farben sind einstellbar, und das ist Absicht.** Gelb heißt
/// Warnung, Rot heißt kritisch, Grün heißt in Ordnung bei Temperatur, Akku,
/// Lüfter und Fehlermeldungen. Wer sie umstellen kann, kann ihre Bedeutung
/// zerstören; ein rotes alles gut" liest niemand richtig. Einstellbar ist
/// deshalb die Akzentfarbe, mit der Onyx sich selbst zeichnet, und das Paar für
/// Herunter- und Hochladen dort ist die Zuordnung willkürlich und nur eine
/// Gewohnheit.
@MainActor
@Observable
public final class OnyxTheme {
/// Die Instanz, aus der `Onyx.Color` liest.
public static let shared = OnyxTheme()
/// Kühl und entsättigt Farbe ist in Onyx ein Signal, keine Dekoration.
public static let defaultAccent = Color(red: 0.498, green: 0.659, blue: 1.0)
/// Herunterladen grün, Hochladen rosa: die Zuordnung ist reine Gewohnheit,
/// aber eine verbreitete. Wer sie anders kennt, dreht sie um.
public static let defaultDownload = Color(red: 0.435, green: 0.812, blue: 0.592)
public static let defaultUpload = Color(red: 0.937, green: 0.427, blue: 0.612)
public var accent: Color {
didSet { store(accent, as: Keys.accent) }
}
public var download: Color {
didSet { store(download, as: Keys.download) }
}
public var upload: Color {
didSet { store(upload, as: Keys.upload) }
}
private let defaults: UserDefaults
private enum Keys {
static let accent = "onyx.theme.accent"
static let download = "onyx.theme.download"
static let upload = "onyx.theme.upload"
}
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
accent = Color(onyxHex: defaults.string(forKey: Keys.accent)) ?? Self.defaultAccent
download = Color(onyxHex: defaults.string(forKey: Keys.download)) ?? Self.defaultDownload
upload = Color(onyxHex: defaults.string(forKey: Keys.upload)) ?? Self.defaultUpload
}
/// Ob alles auf den Werkseinstellungen steht für den Zurücksetzen-Knopf,
/// der sonst auch dann anböte, etwas zu tun, wenn es nichts zu tun gibt.
public var isDefault: Bool {
accent.onyxHex == Self.defaultAccent.onyxHex
&& download.onyxHex == Self.defaultDownload.onyxHex
&& upload.onyxHex == Self.defaultUpload.onyxHex
}
public func reset() {
accent = Self.defaultAccent
download = Self.defaultDownload
upload = Self.defaultUpload
}
/// Übernimmt die Akzentfarbe des Systems.
///
/// Genau die stand hier vorher an vielen Stellen unfreiwillig, weil
/// `.tint` und `.accentColor` ohne eigene Angabe dorthin durchgereicht
/// werden. Wem das gefiel, soll es behalten können.
public func adoptSystemAccent() {
accent = Color(nsColor: .controlAccentColor)
}
private func store(_ color: Color, as key: String) {
defaults.set(color.onyxHex, forKey: key)
}
}
public extension Color {
/// `#RRGGBB`, immer in sRGB.
///
/// Der Umweg über `NSColor` ist nötig, weil eine `Color` aus dem Farbwähler
/// in einem beliebigen Farbraum liegen kann ohne Umrechnung ergäbe die
/// Zerlegung in Komponenten je nach Bildschirm etwas anderes.
var onyxHex: String {
guard let srgb = NSColor(self).usingColorSpace(.sRGB) else { return "#7FA8FF" }
let red = Int((srgb.redComponent * 255).rounded())
let green = Int((srgb.greenComponent * 255).rounded())
let blue = Int((srgb.blueComponent * 255).rounded())
return String(format: "#%02X%02X%02X", red, green, blue)
}
/// Liest `#RRGGBB` und `RRGGBB`.
init?(onyxHex: String?) {
guard let onyxHex else { return nil }
var text = onyxHex.trimmingCharacters(in: .whitespaces)
if text.hasPrefix("#") { text.removeFirst() }
guard text.count == 6, let value = UInt32(text, radix: 16) else { return nil }
self.init(.sRGB,
red: Double((value >> 16) & 0xFF) / 255,
green: Double((value >> 8) & 0xFF) / 255,
blue: Double(value & 0xFF) / 255)
}
}

View File

@@ -21,8 +21,22 @@ public enum Onyx {
public static let textSecondary = SwiftUI.Color.white.opacity(0.58) public static let textSecondary = SwiftUI.Color.white.opacity(0.58)
public static let textTertiary = SwiftUI.Color.white.opacity(0.34) public static let textTertiary = SwiftUI.Color.white.opacity(0.34)
/// Kühl und entsättigt. Farbe ist in Onyx ein Signal, keine Dekoration. /// Die Farbe, mit der Onyx sich selbst zeichnet einstellbar.
public static let accent = SwiftUI.Color(red: 0.498, green: 0.659, blue: 1.0) ///
/// Gelesen statt gespeichert: die Tokens holen den Wert bei jedem
/// Zugriff aus `OnyxTheme`. Damit merkt SwiftUI die Abhängigkeit und
/// zeichnet neu, sobald sich die Farbe ändert bei einer Konstanten
/// wären alle vierzig Fundstellen einzeln umzustellen gewesen.
@MainActor public static var accent: SwiftUI.Color { OnyxTheme.shared.accent }
/// Herunter- und Hochladen. Auch einstellbar: welche Richtung grün ist
/// und welche rosa, ist Gewohnheit, keine Bedeutung.
@MainActor public static var download: SwiftUI.Color { OnyxTheme.shared.download }
@MainActor public static var upload: SwiftUI.Color { OnyxTheme.shared.upload }
/// Die Signalfarben bleiben fest. Gelb heißt Warnung, Rot heißt
/// kritisch, Grün heißt in Ordnung wer das umstellen kann, kann die
/// Bedeutung zerstören.
public static let positive = SwiftUI.Color(red: 0.435, green: 0.812, blue: 0.592) public static let positive = SwiftUI.Color(red: 0.435, green: 0.812, blue: 0.592)
public static let warning = SwiftUI.Color(red: 0.910, green: 0.706, blue: 0.290) public static let warning = SwiftUI.Color(red: 0.910, green: 0.706, blue: 0.290)
public static let critical = SwiftUI.Color(red: 0.898, green: 0.404, blue: 0.361) public static let critical = SwiftUI.Color(red: 0.898, green: 0.404, blue: 0.361)

View File

@@ -1,4 +1,5 @@
import AppKit import AppKit
import OnyxDesign
import SwiftUI import SwiftUI
/// Verwaltet beliebig viele Menüleisten-Elemente, je eines pro aktiviertem Modul. /// Verwaltet beliebig viele Menüleisten-Elemente, je eines pro aktiviertem Modul.
@@ -139,8 +140,12 @@ public final class MenuBarController {
} }
closeOpenPopover() closeOpenPopover()
// Die Akzentfarbe hier und nicht in jedem Popover einzeln: `.tint`
// vererbt sich nach unten, und ohne sie fielen Haken, Regler und
// Auswahlfelder auf die Systemfarbe zurück das war der Grund, warum
// die Popovers grün waren und die Widgets blau.
entry.popover.contentViewController = NSHostingController( entry.popover.contentViewController = NSHostingController(
rootView: entry.module.makePopoverView()) rootView: entry.module.makePopoverView().tint(Onyx.Color.accent))
entry.popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .minY) entry.popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .minY)
// Ohne das bleibt das Popover hinter dem aktiven Fenster. // Ohne das bleibt das Popover hinter dem aktiven Fenster.
entry.popover.contentViewController?.view.window?.makeKey() entry.popover.contentViewController?.view.window?.makeKey()

View File

@@ -0,0 +1,76 @@
import Testing
import SwiftUI
@testable import OnyxDesign
@Suite("Farbwahl")
@MainActor
struct OnyxThemeTests {
private func freshDefaults(_ name: String) -> UserDefaults {
let suite = "onyx.theme.test.\(name)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
@Test("Ohne Einstellung stehen die Werkswerte")
func defaultsAreTheFactoryColours() {
let theme = OnyxTheme(defaults: freshDefaults(#function))
#expect(theme.accent.onyxHex == OnyxTheme.defaultAccent.onyxHex)
#expect(theme.isDefault)
}
@Test("Eine gewählte Farbe übersteht den Neustart")
func choiceSurvivesRestart() {
let defaults = freshDefaults(#function)
let theme = OnyxTheme(defaults: defaults)
theme.accent = Color(onyxHex: "#FF8800")!
let restarted = OnyxTheme(defaults: defaults)
#expect(restarted.accent.onyxHex == "#FF8800")
#expect(!restarted.isDefault)
}
@Test("Herunter und Hoch sind getrennt einstellbar")
func directionsAreIndependent() {
let theme = OnyxTheme(defaults: freshDefaults(#function))
theme.download = Color(onyxHex: "#112233")!
#expect(theme.download.onyxHex == "#112233")
#expect(theme.upload.onyxHex == OnyxTheme.defaultUpload.onyxHex)
}
@Test("Zurücksetzen stellt alles wieder her")
func resetRestoresEverything() {
let theme = OnyxTheme(defaults: freshDefaults(#function))
theme.accent = Color(onyxHex: "#000000")!
theme.download = Color(onyxHex: "#000000")!
theme.upload = Color(onyxHex: "#000000")!
theme.reset()
#expect(theme.isDefault)
}
}
@Suite("Farben als Text")
struct ColourHexTests {
@Test("Hin und zurück ergibt denselben Wert")
func roundTrip() {
for hex in ["#000000", "#FFFFFF", "#7FA8FF", "#6FCF97", "#EF6D9C"] {
#expect(Color(onyxHex: hex)?.onyxHex == hex)
}
}
@Test("Ohne Doppelkreuz geht es auch")
func hashIsOptional() {
#expect(Color(onyxHex: "7FA8FF")?.onyxHex == "#7FA8FF")
}
@Test("Unsinn ergibt keine Farbe")
func garbageIsRejected() {
// Sonst stünde nach einer verkorksten Einstellung Schwarz auf Schwarz.
#expect(Color(onyxHex: nil) == nil)
#expect(Color(onyxHex: "") == nil)
#expect(Color(onyxHex: "#12345") == nil)
#expect(Color(onyxHex: "#GGGGGG") == nil)
}
}