Eine Designschicht statt hundertfach gesetzter Zahlen

Token für Schrift, Symbolgröße, Abstand, Radius und Bewegung, dazu sieben
Bauteile, die vorher in jeder Kachel neu gebaut wurden — Karte, Abschnittstitel,
Hinweis, Pillenknopf, Regler, Ablegefläche, Haarlinie.

Symbole bekommen eine **eigene** Staffel und nicht die der Schrift: ein
9-Punkt-Symbol neben 9-Punkt-Text wirkt kleiner, nicht gleich groß. Beide in
eine Staffel zu zwingen hieße, an jeder Stelle gegen sie zu arbeiten.

Das Flächenrezept — Tint, Bänderung, Kante — stand zweimal da, einmal für das
Panel und einmal für Fenster. Es ist jetzt `OnyxStone`, generisch über die Form,
damit auch die animierende Notch-Silhouette es benutzen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 21:04:51 +02:00
co-authored by Claude Opus 5
parent 49a3ff0548
commit ecb39fbe5f
11 changed files with 1195 additions and 184 deletions
@@ -1,7 +1,8 @@
import SwiftUI
import AppKit
/// Die einstellbaren Farben.
/// Was am Aussehen von Onyx einstellbar ist: die Farben und das
/// Bewegungsprofil.
///
/// **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,
@@ -20,8 +21,17 @@ public final class 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)
/// aber eine verbreitete.
///
/// **Wärmer als das Signalgrün, und das ist der Punkt.** Vorher war dieser
/// Wert Zeichen für Zeichen `Onyx.Color.positive` — derselbe grüne Balken
/// hieß im Netzwerk „lädt herunter" und in den Messwerten „alles in
/// Ordnung". Zwei Bedeutungen in einer Farbe sind eine zu viel.
///
/// Wärmer und nicht kühler, weil das Gegenstück rosa bis cyan sein darf:
/// ein Grün, das ins Türkise zieht, wäre vom Hochladen kaum noch zu
/// unterscheiden. Nach Gelb hin ist von beidem Abstand.
public static let defaultDownload = Color(red: 0.561, green: 0.835, blue: 0.408)
public static let defaultUpload = Color(red: 0.937, green: 0.427, blue: 0.612)
public var accent: Color {
@@ -34,12 +44,31 @@ public final class OnyxTheme {
didSet { store(upload, as: Keys.upload) }
}
/// Wie sich das Panel bewegt.
///
/// Getrennt von den Farben gespeichert und **nicht** von `reset()` erfasst:
/// der Knopf dort heißt „Farben zurücksetzen", und ein Knopf, der nebenbei
/// die Animationen umstellt, ist eine Falle.
public var motion: MotionProfile = .snappy {
didSet { defaults.set(motion.rawValue, forKey: Keys.motion) }
}
/// Woraus die Panelfläche gemacht ist.
///
/// Wie `motion` von den Farben getrennt und **nicht** von `reset()`
/// erfasst: „Farben zurücksetzen" soll Farben zurücksetzen.
public var material: NotchMaterial = .stone {
didSet { defaults.set(material.rawValue, forKey: Keys.material) }
}
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"
static let motion = "onyx.theme.motion"
static let material = "onyx.theme.material"
}
public init(defaults: UserDefaults = .standard) {
@@ -47,10 +76,17 @@ public final class OnyxTheme {
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
// Unbekannte Profilnamen fallen auf die Voreinstellung zurück, statt die
// Bewegung ganz ausfallen zu lassen — eine App ohne Animation sieht aus,
// als hinge sie.
motion = MotionProfile(rawValue: defaults.string(forKey: Keys.motion) ?? "") ?? .snappy
material = NotchMaterial(rawValue: defaults.string(forKey: Keys.material) ?? "") ?? .stone
}
/// 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.
/// Ob **die Farben** auf den Werkseinstellungen stehen — für den
/// Zurücksetzen-Knopf, der sonst auch dann anböte, etwas zu tun, wenn es
/// nichts zu tun gibt. Das Bewegungsprofil zählt bewusst nicht mit; siehe
/// `motion`.
public var isDefault: Bool {
accent.onyxHex == Self.defaultAccent.onyxHex
&& download.onyxHex == Self.defaultDownload.onyxHex