Kacheln und Fenster sprechen dieselbe Sprache

Alle Kacheln, Popovers und Fenster auf die Designschicht umgestellt: rund
hundert gesetzte Schriftgrößen und 159 gesetzte Abstände und Radien sind jetzt
Token. Übrig bleibt `spacing: 0` — das ist eine Aussage, keine geratene Zahl.

Die Einstellungen bekommen eine Seitenleiste statt sieben Reitern, deren
deutsche Beschriftungen zusammen 880 Punkte brauchten. Panel-Token stehen nicht
mehr in Fenstern: dort gilt Systemsemantik, damit der Hellmodus nicht schwarz
auf schwarz endet.

Neu ist ein Schaufenster, das alle Bauteile nebeneinander zeigt — ohne das
lässt sich eine Änderung an einem Token nur prüfen, indem man die App bedient,
bis jede Kachel einmal zu sehen war.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 21:05:51 +02:00
co-authored by Claude Opus 5
parent 9b600de9a9
commit edb327cabe
32 changed files with 3677 additions and 968 deletions
@@ -1,4 +1,5 @@
import AppKit
import OnyxDesign
import OnyxMenuBar
/// Der Akku, gezeichnet wie der von macOS.
@@ -259,9 +260,9 @@ public struct BatteryGlyphView: View {
private var width: CGFloat { BatteryGlyph.size.width * scale }
public var body: some View {
HStack(spacing: 1 * scale) {
HStack(spacing: Onyx.Space.xxs * scale) {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3 * scale, style: .continuous)
RoundedRectangle(cornerRadius: Onyx.Radius.bar * scale, style: .continuous)
.strokeBorder(.primary.opacity(0.45), lineWidth: 1)
GeometryReader { geometry in
@@ -296,7 +297,7 @@ public struct BatteryGlyphView: View {
}
.compositingGroup()
RoundedRectangle(cornerRadius: 1 * scale, style: .continuous)
RoundedRectangle(cornerRadius: Onyx.Radius.bar * scale, style: .continuous)
.fill(.primary.opacity(0.45))
.frame(width: 2 * scale, height: 5 * scale)
}
@@ -34,7 +34,7 @@ public struct ChargeLimitControl: View {
}
public var body: some View {
HStack(spacing: 6) {
HStack(spacing: Onyx.Space.sm) {
Text("charge.limit", bundle: .module)
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textSecondary)
@@ -62,7 +62,7 @@ public struct ChargeLimitControl: View {
URL(string: "x-apple.systempreferences:com.apple.Battery-Settings.extension")!)
} label: {
Image(systemName: "arrow.up.forward.app")
.font(.system(size: 10))
.font(.system(size: Onyx.Symbol.small))
.foregroundStyle(Onyx.Color.textTertiary)
}
.buttonStyle(.plain)
@@ -12,7 +12,7 @@ struct MetricPopoverContent: View {
let metric: MetricKind
var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: Onyx.Space.lg) {
// Ein Titel oben: bei drei gleich aussehenden Ringen sieht man
// sonst nicht, ob man vor dem CPU- oder dem GPU-Popover steht.
Text(String(localized: .init(metric.localizationKey), bundle: .module))
@@ -25,7 +25,7 @@ struct MetricPopoverContent: View {
case .temperature: SensorDetail(snapshot: model.snapshot)
}
}
.padding(14)
.padding(Onyx.Space.xl)
.frame(width: 280)
}
}
@@ -34,17 +34,6 @@ struct MetricPopoverContent: View {
/// Überschrift eines Abschnitts. Ohne sie ist eine lange Werteliste nur eine
/// lange Werteliste.
private struct SectionTitle: View {
let text: LocalizedStringKey
var body: some View {
Text(text, bundle: .module)
.font(.caption.weight(.semibold))
.foregroundStyle(Onyx.Color.accent)
.textCase(.uppercase)
}
}
private struct Row: View {
let label: String
let value: String
@@ -88,7 +77,7 @@ private struct Dial: View {
var body: some View {
// Sechs Punkte Abstand statt zwei: die Beschriftung klebte am Ring und
// sah aus, als gehörte sie noch hinein.
VStack(spacing: 6) {
VStack(spacing: Onyx.Space.sm) {
ZStack {
Circle().stroke(.quaternary, lineWidth: 5)
Circle()
@@ -96,13 +85,13 @@ private struct Dial: View {
.stroke(tint ?? Onyx.Color.accent,
style: .init(lineWidth: 5, lineCap: .round))
.rotationEffect(.degrees(-90))
Text(text).font(.system(size: 15, weight: .medium).monospacedDigit())
Text(text).font(Onyx.Font.headline.monospacedDigit())
}
.frame(width: 62, height: 62)
.alignmentGuide(.dialCenter) { $0[VerticalAlignment.center] }
Text(caption).font(.caption2).foregroundStyle(.secondary)
}
.padding(.bottom, 2)
.padding(.bottom, Onyx.Space.xxs)
}
}
@@ -115,7 +104,7 @@ private struct ProcessorDetail: View {
var body: some View {
let snapshot = model.snapshot
HStack(spacing: 14) {
HStack(spacing: Onyx.Space.xl) {
Dial(value: metric == .cpu ? snapshot.cpu.total : (snapshot.gpu ?? 0),
caption: metric == .cpu ? "CPU" : "GPU",
text: MetricFormat.percent(metric == .cpu ? snapshot.cpu.total
@@ -135,11 +124,11 @@ private struct ProcessorDetail: View {
.frame(maxWidth: .infinity)
if metric == .cpu, !model.snapshot.cpu.perCore.isEmpty {
SectionTitle(text: "popover.perCore")
OnyxPopoverTitle(String(localized: "popover.perCore", bundle: .module))
CoreGrid(values: model.snapshot.cpu.perCore)
}
SectionTitle(text: "popover.power")
OnyxPopoverTitle(String(localized: "popover.power", bundle: .module))
if let cpu = snapshot.power.cpu {
Row(label: String(localized: "popover.power.cpu", bundle: .module),
value: MetricFormat.watts(cpu))
@@ -154,7 +143,7 @@ private struct ProcessorDetail: View {
}
if !snapshot.fans.isEmpty {
SectionTitle(text: "popover.fans")
OnyxPopoverTitle(String(localized: "popover.fans", bundle: .module))
ForEach(snapshot.fans) { fan in
Row(label: fan.name, value: "\(Int(fan.rpm)) U/min")
}
@@ -181,13 +170,13 @@ private struct CoreGrid: View {
let values: [Double]
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 3), count: 8),
spacing: 3) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: Onyx.Space.xs), count: 8),
spacing: Onyx.Space.xs) {
ForEach(Array(values.enumerated()), id: \.offset) { _, value in
RoundedRectangle(cornerRadius: 2, style: .continuous)
RoundedRectangle(cornerRadius: Onyx.Radius.bar, style: .continuous)
.fill(Onyx.Color.accent.opacity(0.18))
.overlay(alignment: .bottom) {
RoundedRectangle(cornerRadius: 2, style: .continuous)
RoundedRectangle(cornerRadius: Onyx.Radius.bar, style: .continuous)
.fill(Onyx.Color.accent)
.frame(height: max(14 * value, 1.5))
}
@@ -205,12 +194,12 @@ private struct MemoryDetail: View {
var body: some View {
let memory = snapshot.memory
HStack(spacing: 14) {
HStack(spacing: Onyx.Space.xl) {
Dial(value: memory.usedFraction,
caption: String(localized: "popover.memory.used", bundle: .module),
text: MetricFormat.percent(memory.usedFraction),
tint: pressureColor)
VStack(alignment: .leading, spacing: 3) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
legend(Onyx.Color.accent, "popover.memory.app", memory.app)
legend(.purple, "popover.memory.wired", memory.wired)
legend(.teal, "popover.memory.compressed", memory.compressed)
@@ -218,7 +207,7 @@ private struct MemoryDetail: View {
}
}
SectionTitle(text: "popover.memory.swap")
OnyxPopoverTitle(String(localized: "popover.memory.swap", bundle: .module))
// Auslagerung ist der Wert, der zählt: solange nicht ausgelagert wird,
// ist ein voller Speicher kein Problem — macOS füllt ihn absichtlich.
Row(label: String(localized: "popover.memory.swapUsed", bundle: .module),
@@ -226,7 +215,7 @@ private struct MemoryDetail: View {
emphasis: memory.swapUsed > 512 * 1024 * 1024 ? Onyx.Color.warning : nil)
if !snapshot.topMemoryProcesses.isEmpty {
SectionTitle(text: "popover.processes")
OnyxPopoverTitle(String(localized: "popover.processes", bundle: .module))
ForEach(snapshot.topMemoryProcesses) { process in
Row(label: process.name, value: bytes(process.bytes))
}
@@ -242,7 +231,7 @@ private struct MemoryDetail: View {
}
private func legend(_ color: Color, _ key: LocalizedStringKey, _ value: UInt64) -> some View {
HStack(spacing: 5) {
HStack(spacing: Onyx.Space.sm) {
Circle().fill(color).frame(width: 7, height: 7)
Text(key, bundle: .module).font(.caption)
Spacer(minLength: 8)
@@ -263,7 +252,7 @@ private struct BatteryDetail: View {
var body: some View {
if let battery = snapshot.battery {
HStack(alignment: .dialCenter, spacing: 14) {
HStack(alignment: .dialCenter, spacing: Onyx.Space.xl) {
Dial(value: battery.charge,
caption: String(localized: "metric.battery", bundle: .module),
text: MetricFormat.percent(battery.charge),
@@ -275,7 +264,7 @@ private struct BatteryDetail: View {
// darunter waren zweimal dieselbe Auskunft an zwei Stellen —
// und die zweite stand allein unter dem Diagramm, wo sie
// aussah, als gehörte sie zu dessen Beschriftung.
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
if let remaining = battery.timeRemaining {
Text(duration(remaining)).font(.title3.monospacedDigit())
Text(battery.isCharging ? "popover.battery.untilFull"
@@ -302,11 +291,11 @@ private struct BatteryDetail: View {
// eine Akkusache, und man greift danach, während man auf den
// Ladestand schaut.
if let control {
Divider().overlay(Onyx.Color.hairline)
OnyxHairline()
ChargeLimitControl(control: control, roomy: true)
}
SectionTitle(text: "popover.battery.condition")
OnyxPopoverTitle(String(localized: "popover.battery.condition", bundle: .module))
if let health = battery.health {
Row(label: String(localized: "popover.battery.health", bundle: .module),
value: MetricFormat.percent(health))
@@ -320,7 +309,7 @@ private struct BatteryDetail: View {
value: MetricFormat.temperature(sensor.celsius))
}
SectionTitle(text: "popover.power")
OnyxPopoverTitle(String(localized: "popover.power", bundle: .module))
if let watts = battery.watts {
Row(label: String(localized: battery.isCharging ? "popover.battery.charging"
: "popover.battery.draining",
@@ -354,7 +343,7 @@ private struct BatteryDetail: View {
isPluggedIn: battery.isPluggedIn,
charge: battery.charge,
limit: control?.chargeLimit)
HStack(spacing: 6) {
HStack(spacing: Onyx.Space.sm) {
Image(systemName: symbol(for: activity))
.font(.caption)
.foregroundStyle(tint(for: activity))
@@ -401,7 +390,7 @@ private struct SensorDetail: View {
let snapshot: MetricsSnapshot
var body: some View {
SectionTitle(text: "popover.temperature")
OnyxPopoverTitle(String(localized: "popover.temperature", bundle: .module))
ForEach(snapshot.sensors) { sensor in
HStack {
Text(sensor.name).font(.callout)
@@ -421,9 +410,9 @@ private struct SensorDetail: View {
}
if !snapshot.fans.isEmpty {
SectionTitle(text: "popover.fans")
OnyxPopoverTitle(String(localized: "popover.fans", bundle: .module))
ForEach(snapshot.fans) { fan in
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: Onyx.Space.xxs) {
HStack {
Text(fan.name).font(.callout)
Spacer(minLength: 12)