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>
243 lines
11 KiB
Swift
243 lines
11 KiB
Swift
import SwiftUI
|
||
|
||
/// Die einzige Quelle für Farben, Maße, Schrift und Bewegung in Onyx.
|
||
///
|
||
/// Onyx ist ein tiefschwarzer Stein mit feiner Bänderung und mattem Glanz — kein
|
||
/// Glas. Das Panel benutzt deshalb bewusst **nicht** die Liquid-Glass-APIs von
|
||
/// macOS 26, sondern eine dunkle Vibrancy-Ebene mit eigenem Tint darüber.
|
||
public enum Onyx {
|
||
|
||
// MARK: - Farben
|
||
|
||
public enum Color {
|
||
/// Grundfläche des Panels — **wirklich schwarz**.
|
||
///
|
||
/// Nicht sehr dunkles Grau: die Fläche grenzt oben unmittelbar an die
|
||
/// Notch, und die ist schwarzes Gehäuse. Jeder Grauwert daneben ist
|
||
/// eine sichtbare Kante genau dort, wo beides als ein Körper wirken
|
||
/// soll — vorher #0B0B0D bei 74 % Deckkraft, also grau auf grau.
|
||
public static let surface = SwiftUI.Color.black
|
||
/// Kacheln auf der Grundfläche.
|
||
public static let elevated = SwiftUI.Color(red: 0.078, green: 0.078, blue: 0.090)
|
||
/// Innenkante — der halbe „edle" Effekt steckt in dieser einen Linie.
|
||
public static let hairline = SwiftUI.Color.white.opacity(0.08)
|
||
|
||
public static let textPrimary = SwiftUI.Color(red: 0.949, green: 0.949, blue: 0.957)
|
||
public static let textSecondary = SwiftUI.Color.white.opacity(0.58)
|
||
public static let textTertiary = SwiftUI.Color.white.opacity(0.34)
|
||
|
||
/// Die Farbe, mit der Onyx sich selbst zeichnet — einstellbar.
|
||
///
|
||
/// 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 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)
|
||
|
||
/// Eine Karte **innerhalb** einer Kachel — etwas heller als die Kachel,
|
||
/// aber nicht so hell, dass sie selbst wie eine Kachel wirkt.
|
||
public static let card = SwiftUI.Color.white.opacity(0.035)
|
||
|
||
/// Die Schiene hinter einem Balken oder Regler.
|
||
///
|
||
/// **Nicht `hairline`.** Das sind 8 % Weiß und ein Wert für Kanten; auf
|
||
/// schwarzem Grund ist er praktisch nicht da. Als Schiene liest sich
|
||
/// das, als liefe der gefüllte Teil ins Nichts aus — man sieht dann nur
|
||
/// noch eine Länge, ohne zu wissen, wovon.
|
||
public static let track = SwiftUI.Color.white.opacity(0.22)
|
||
|
||
/// Text und Symbole **auf** der Akzentfarbe.
|
||
///
|
||
/// Bisher stand dafür an drei Stellen `Onyx.Color.surface` — was
|
||
/// zufällig richtig aussah und beim ersten hellen Akzent falsch würde.
|
||
public static let onAccent = SwiftUI.Color(red: 0.043, green: 0.043, blue: 0.051)
|
||
|
||
}
|
||
|
||
// MARK: - Abstände
|
||
|
||
/// Die Staffel für Abstände innerhalb einer Kachel oder eines Fensters.
|
||
///
|
||
/// **Zwei Punkte Schrittweite, nicht vier.** Nachgemessen standen im Baum
|
||
/// sechzehn verschiedene Abstände; die häufigsten waren 6, 4, 8 und 12. Ein
|
||
/// Vierer-Raster hätte davon drei verschoben — bei 180 × 200 Punkt großen
|
||
/// Kacheln ist das keine Feinheit, sondern eine sichtbare Umordnung ohne
|
||
/// Gegenwert. Die Staffel bildet also ab, was der dichten Fläche entspricht,
|
||
/// und schneidet stattdessen die Ausreißer weg: aus 3 und 5 wird 4, aus 7
|
||
/// und 9 wird 8, aus 10 wird 12, aus 14 wird 16.
|
||
public enum Space {
|
||
/// Zwischen Zeichen und dem, was direkt daneben klebt.
|
||
public static let xxs: CGFloat = 2
|
||
public static let xs: CGFloat = 4
|
||
public static let sm: CGFloat = 6
|
||
public static let md: CGFloat = 8
|
||
/// Das Innenpolster einer Zeile — zwischen `md` und `lg`.
|
||
///
|
||
/// Keine willkürliche Zwischenstufe: eine Zeile mit 8 Punkten wirkt
|
||
/// gedrängt, mit 12 fällt sie auseinander. Der Wert kam an zehn Stellen
|
||
/// unabhängig voneinander heraus, bevor er hier stand.
|
||
public static let row: CGFloat = 10
|
||
|
||
public static let lg: CGFloat = 12
|
||
public static let xl: CGFloat = 16
|
||
/// Der Rand eines Fensterinhalts — zwischen `xl` und `xxl`.
|
||
///
|
||
/// Fenster brauchen mehr Luft als Kacheln: eine Kachel sitzt in einem
|
||
/// Panel und teilt sich dessen Rand, ein Fenster hat nur den eigenen.
|
||
public static let window: CGFloat = 20
|
||
|
||
public static let xxl: CGFloat = 24
|
||
}
|
||
|
||
// MARK: - Rundungen
|
||
|
||
/// Rundungen sind nach dem benannt, was sie umschließen, nicht nach ihrer
|
||
/// Größe.
|
||
///
|
||
/// Eine Größenstaffel („sm", „md") lädt dazu ein, die nächstbeste zu nehmen.
|
||
/// Ein Name sagt dagegen, wo der Wert hingehört — und macht sofort sichtbar,
|
||
/// wenn eine Karte plötzlich so rund ist wie eine Kachel.
|
||
public enum Radius {
|
||
/// Vorschaubilder, Farbmarken, kleine Plättchen.
|
||
/// Ein Balken — Fortschritt, Pegel, Verlauf.
|
||
///
|
||
/// So klein, dass es nur darum geht, die Ecke nicht scharf stehen zu
|
||
/// lassen. Vorher standen hier 1, 2 und 3 nebeneinander, was an einem
|
||
/// 4 Punkte hohen Balken niemand unterscheiden kann.
|
||
public static let bar: CGFloat = 2
|
||
|
||
/// Ein Vorschaubild.
|
||
public static let thumbnail: CGFloat = 6
|
||
|
||
public static let chip: CGFloat = 4
|
||
/// Knöpfe, Felder, Regler.
|
||
public static let control: CGFloat = 8
|
||
/// Eine Karte **innerhalb** einer Kachel.
|
||
public static let card: CGFloat = 12
|
||
/// Die Kachel selbst.
|
||
public static let tile: CGFloat = 16
|
||
/// Das ausgefahrene Panel.
|
||
public static let panel: CGFloat = 26
|
||
/// Die Unterkante der zusammengezogenen Notch.
|
||
///
|
||
/// Kleiner als `panel`: ein fester Radius lässt die kleine Form
|
||
/// aufgeblasen aussehen.
|
||
public static let notch: CGFloat = 10
|
||
}
|
||
|
||
// MARK: - Maße
|
||
|
||
/// Was das Panel als Ganzes ausmisst — keine Rhythmusfrage, sondern
|
||
/// Geometrie.
|
||
///
|
||
/// `panelPadding` und `tileSpacing` gehen in die Größenrechnung von
|
||
/// `LayoutEngine` ein und stehen so in dessen Tests. Sie gehören deshalb
|
||
/// nicht in `Space`: wer dort die Staffel verschiebt, soll nicht aus
|
||
/// Versehen die Panelbreite ändern.
|
||
public enum Metric {
|
||
public static let panelPadding: CGFloat = 16
|
||
public static let tileSpacing: CGFloat = 10
|
||
public static let hairlineWidth: CGFloat = 1
|
||
}
|
||
|
||
// MARK: - Bewegung
|
||
|
||
/// Die Kurven — eingestellt über das Bewegungsprofil in `OnyxTheme`.
|
||
///
|
||
/// Gelesen statt gespeichert, aus demselben Grund wie bei der Akzentfarbe:
|
||
/// so kommt ein Profilwechsel an allen Fundstellen an, ohne dass eine
|
||
/// einzige davon davon weiß.
|
||
public enum Motion {
|
||
/// Ausfahren.
|
||
@MainActor public static var expand: Animation { OnyxTheme.shared.motion.expand }
|
||
/// Einfahren.
|
||
@MainActor public static var collapse: Animation { OnyxTheme.shared.motion.collapse }
|
||
/// Für Messwerte: sichtbar, aber ohne Pulsieren.
|
||
@MainActor public static var value: Animation { OnyxTheme.shared.motion.value }
|
||
}
|
||
|
||
// MARK: - Typografie
|
||
|
||
/// Die Schriftstaffel.
|
||
///
|
||
/// **Warum sie größer geworden ist.** Vorher gab es fünf Stufen, und zwei
|
||
/// davon benutzte niemand. Daneben standen rund hundert Aufrufe von
|
||
/// `.font(.system(size:))` — allein Größe 9 dreizehnmal. Das war kein
|
||
/// Ungehorsam: die Staffel hatte am unteren Ende schlicht nichts anzubieten,
|
||
/// und eine Kachel von 180 × 200 Punkt lebt dort unten.
|
||
/// Wie groß ein Symbol gezeichnet wird.
|
||
///
|
||
/// **Eine eigene Staffel, nicht die Schriftstaffel.** Symbole und Text
|
||
/// teilen sich zwar `.font(.system(size:))`, aber nicht die Frage: bei Text
|
||
/// entscheidet die Größe über Lesbarkeit und Rang, bei einem Symbol über
|
||
/// sein Gewicht im Bild. Ein 9-Punkt-Symbol neben 9-Punkt-Text wirkt
|
||
/// kleiner als der Text, nicht gleich groß. Sie in eine Staffel zu zwingen
|
||
/// hieße, an jeder Stelle gegen sie zu arbeiten.
|
||
///
|
||
/// Vorher standen hier zwölf verschiedene Werte von 7 bis 34 — 7, 8, 10, 12,
|
||
/// 15, 16, 18 waren keine Entscheidungen, sondern das, was gerade passte.
|
||
///
|
||
/// Nur die Größe, nicht das Gewicht: `.light` an einem großen Symbol und
|
||
/// `.bold` an einem winzigen sind Entscheidungen am Ort, keine Stufen.
|
||
public enum Symbol {
|
||
/// Marke neben kleinem Text — das Blatt am Energiesparmodus.
|
||
public static let caption: CGFloat = 9
|
||
/// Das Symbol einer Zeile.
|
||
public static let small: CGFloat = 11
|
||
/// Das Symbol eines Knopfes.
|
||
public static let medium: CGFloat = 13
|
||
/// Das Symbol eines leeren Zustands.
|
||
public static let large: CGFloat = 16
|
||
/// Das Symbol, das eine Kachel trägt.
|
||
public static let xlarge: CGFloat = 20
|
||
/// Das eine große Symbol — Wetterlage.
|
||
public static let hero: CGFloat = 34
|
||
}
|
||
|
||
public enum Font {
|
||
|
||
// Text
|
||
|
||
/// Der Arbeitswert in der Kachel: Beschriftungen an Werten, Wochentage,
|
||
/// Einheiten.
|
||
public static let micro = SwiftUI.Font.system(size: 9, weight: .medium)
|
||
public static let caption = SwiftUI.Font.system(size: 10, weight: .medium)
|
||
public static let footnote = SwiftUI.Font.system(size: 11, weight: .regular)
|
||
public static let body = SwiftUI.Font.system(size: 12, weight: .regular)
|
||
/// Kachelüberschriften und Abschnittstitel.
|
||
public static let title = SwiftUI.Font.system(size: 13, weight: .semibold)
|
||
public static let headline = SwiftUI.Font.system(size: 15, weight: .medium)
|
||
public static let display = SwiftUI.Font.system(size: 20, weight: .medium)
|
||
public static let displayLarge = SwiftUI.Font.system(size: 30, weight: .medium)
|
||
|
||
// Zahlen — ohne feste Ziffernbreite zappeln sie beim Zählen
|
||
|
||
public static let metricMicro = SwiftUI.Font.system(size: 9, weight: .medium)
|
||
.monospacedDigit()
|
||
public static let metricSmall = SwiftUI.Font.system(size: 11, weight: .medium)
|
||
.monospacedDigit()
|
||
public static let metric = SwiftUI.Font.system(size: 20, weight: .medium)
|
||
.monospacedDigit()
|
||
public static let metricLarge = SwiftUI.Font.system(size: 30, weight: .medium)
|
||
.monospacedDigit()
|
||
|
||
/// Das eine große Symbol einer Kachel — Wetterlage, Ladezustand.
|
||
///
|
||
/// Die einzige Größe, die kein Textgegenstück hat: hier steht ein Bild,
|
||
/// keine Zeile.
|
||
public static let symbolHero = SwiftUI.Font.system(size: 34, weight: .regular)
|
||
}
|
||
}
|