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,6 +1,6 @@
import SwiftUI
/// Die einzige Quelle für Farben, Maße und Bewegung in Onyx.
/// 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
@@ -10,8 +10,13 @@ public enum Onyx {
// MARK: - Farben
public enum Color {
/// Grundfläche des Panels.
public static let surface = SwiftUI.Color(red: 0.043, green: 0.043, blue: 0.051)
/// 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.
@@ -41,16 +46,107 @@ public enum Onyx {
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)
/// Deckkraft des Tints über der Vibrancy-Ebene. Darunter liest man den
/// Hintergrund durch, darüber wirkt das Panel wie eine schwarze Platte.
public static let surfaceOpacity: Double = 0.74
/// 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 panelCornerRadius: CGFloat = 26
public static let tileCornerRadius: CGFloat = 16
public static let panelPadding: CGFloat = 16
public static let tileSpacing: CGFloat = 10
public static let hairlineWidth: CGFloat = 1
@@ -58,31 +154,89 @@ public enum Onyx {
// 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: ein Spring mit einem Hauch Nachschwingen. Das gibt dem
/// Panel Masse, statt es nur größer werden zu lassen.
public static let expand = Animation.spring(response: 0.34, dampingFraction: 0.86)
/// Einfahren: kürzer und ohne Nachschwingen.
///
/// Schließen muss schneller sein als Öffnen. Beim Öffnen sieht man zu,
/// beim Schließen ist man schon woanders — dieselbe Kurve rückwärts
/// fühlt sich dort zäh an, und das Nachschwingen liest sich als Ruckler.
public static let collapse = Animation.spring(response: 0.24, dampingFraction: 1.0)
/// 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.
public static let value = Animation.easeOut(duration: 0.25)
@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 {
public static let title = SwiftUI.Font.system(size: 13, weight: .semibold)
public static let body = SwiftUI.Font.system(size: 12, weight: .regular)
// 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)
/// Für alle Messwerte — ohne feste Ziffernbreite zappeln die Zahlen.
public static let metric = SwiftUI.Font.system(size: 20, 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)
}
}