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:
@@ -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 */,
|
||||||
|
|||||||
131
Onyx/ColorSettingsView.swift
Normal file
131
Onyx/ColorSettingsView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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() } }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() } }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]),
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Je Programm"
|
"value": "Programme"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
106
Packages/OnyxKit/Sources/OnyxDesign/OnyxTheme.swift
Normal file
106
Packages/OnyxKit/Sources/OnyxDesign/OnyxTheme.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
76
Packages/OnyxKit/Tests/OnyxDesignTests/OnyxThemeTests.swift
Normal file
76
Packages/OnyxKit/Tests/OnyxDesignTests/OnyxThemeTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user