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
@@ -20,34 +20,72 @@ public struct OnyxVisualEffect: NSViewRepresentable {
public func updateNSView(_ view: NSVisualEffectView, context: Context) {}
}
/// Der Hintergrund des Panels: Vibrancy, Tint, ein sehr flacher Glanz von oben
/// und eine Haarlinie als Innenkante.
public struct OnyxSurface: View {
private let cornerRadius: CGFloat
/// Der Stein selbst — auf einer beliebigen Form.
///
/// Tint, Bänderung, Kante. Drei Lagen, die zusammen den Werkstoff ergeben, und
/// zwar unabhängig davon, welche Form sie annehmen.
///
/// **Warum generisch und nicht einfach ein abgerundetes Rechteck.** Das Panel
/// zeichnet auf ``NotchShape``, einer Form, die sich beim Aus- und Einfahren
/// verändert. Solange das Rezept an ein Rechteck gebunden war, konnte es dort
/// nicht mitbenutzt werden — also stand es zweimal da, einmal hier und einmal
/// im Panel. Zweimal heißt: eine Änderung an der Bänderung wirkt an einer
/// Stelle und an der anderen nicht, und das fällt erst auf, wenn beide
/// nebeneinander zu sehen sind.
public struct OnyxStone<S: InsettableShape>: View {
private let shape: S
public init(cornerRadius: CGFloat = Onyx.Metric.panelCornerRadius) {
self.cornerRadius = cornerRadius
public init(_ shape: S) {
self.shape = shape
}
public var body: some View {
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
// Aus dem Thema gelesen und nicht als Argument gereicht: sonst müsste
// jede Stelle, die eine Fläche zeichnet, den Werkstoff durchschleifen —
// bis hinunter zur Kachel, die ihn gar nicht kennt.
let material = OnyxTheme.shared.material
shape
.fill(Onyx.Color.surface.opacity(Onyx.Color.surfaceOpacity))
.background(OnyxVisualEffect().clipShape(shape))
.fill(Onyx.Color.surface.opacity(material.fillOpacity))
.overlay {
// Die Bänderung des Steins: oben ein Hauch heller, nach 40 %
// vollständig verlaufen. Mehr wirkt sofort billig.
// vollständig verlaufen. Mehr wirkt sofort billig — wie viel
// „ein Hauch" ist, hängt am Werkstoff.
shape.fill(
LinearGradient(
stops: [
.init(color: .white.opacity(0.02), location: 0),
.init(color: .white.opacity(material.sheen), location: 0),
.init(color: .clear, location: 0.4),
],
startPoint: .top, endPoint: .bottom))
}
.overlay {
shape.strokeBorder(Onyx.Color.hairline, lineWidth: Onyx.Metric.hairlineWidth)
// `strokeBorder` und nicht `stroke`: die Linie liegt dann
// vollständig **innen**. Mittig gezeichnet ragte sie zur Hälfte
// über die Kante hinaus und wurde an den Rundungen sichtbar
// dünner.
shape.strokeBorder(material.edge, lineWidth: Onyx.Metric.hairlineWidth)
}
}
}
/// Der Hintergrund des Panels: Vibrancy und darauf der Stein.
public struct OnyxSurface: View {
private let cornerRadius: CGFloat
public init(cornerRadius: CGFloat = Onyx.Radius.panel) {
self.cornerRadius = cornerRadius
}
public var body: some View {
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
OnyxStone(shape)
.background {
// Bei deckendem Stein trägt die Weichzeichnung nichts bei und
// wird auch nicht berechnet.
if OnyxTheme.shared.material.showsVibrancy {
OnyxVisualEffect().clipShape(shape)
}
}
}
}
@@ -61,10 +99,10 @@ public struct OnyxTile<Content: View>: View {
}
public var body: some View {
let shape = RoundedRectangle(cornerRadius: Onyx.Metric.tileCornerRadius, style: .continuous)
let shape = RoundedRectangle(cornerRadius: Onyx.Radius.tile, style: .continuous)
content
.padding(12)
.padding(Onyx.Space.lg)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(shape.fill(Onyx.Color.elevated))
.overlay(shape.strokeBorder(Onyx.Color.hairline, lineWidth: Onyx.Metric.hairlineWidth))