Files
onyx/Onyx/AppearanceSettingsView.swift
T
ScarriffleandClaude Opus 5 edb327cabe 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>
2026-09-05 21:05:51 +02:00

199 lines
7.3 KiB
Swift

import SwiftUI
import OnyxDesign
/// Was am Aussehen einstellbar ist: die Farben und die Bewegung.
///
/// Drei Farben, nicht zehn: die Akzentfarbe, mit der Onyx sich selbst zeichnet,
/// und das Paar für Herunter- und Hochladen. Die Signalfarben stehen bewusst
/// nicht zur Wahl — Gelb heißt Warnung, Rot heißt kritisch, und wer das
/// umstellen kann, kann die Bedeutung zerstören.
///
/// Die Bewegung kommt als **Profil**, nicht als Federkonstanten. Ausfahren und
/// Einfahren müssen zueinander passen; diese Beziehung wählt man, nicht vier
/// Zahlen.
struct AppearanceSettingsView: View {
@Bindable var theme: OnyxTheme
/// Ein paar Vorschläge, damit man nicht im Farbwähler anfangen muss.
///
/// Alle gedämpft: Onyx ist eine dunkle Fläche, und eine volle Sättigung
/// darauf sticht, statt zu leiten.
private static let suggestions: [(name: LocalizedStringKey, hex: String)] = [
("color.suggestion.onyx", "#7FA8FF"),
("color.suggestion.mint", "#6FCF97"),
("color.suggestion.amber", "#E8B44A"),
("color.suggestion.coral", "#E5675C"),
("color.suggestion.lilac", "#B79CFF"),
("color.suggestion.slate", "#9AA6B2"),
]
var body: some View {
SettingsPage {
accent
network
material
motion
fixed
}
}
private var accent: some View {
OnyxFormCard(String(localized: "color.section.accent")) {
OnyxFormRow(String(localized: "color.accent"),
hint: String(localized: "color.accent.hint")) {
ColorPicker("", selection: $theme.accent, supportsOpacity: false)
.labelsHidden()
}
OnyxFormRow(String(localized: "color.suggestions")) {
HStack(spacing: Onyx.Space.row) {
ForEach(Self.suggestions, id: \.hex) { suggestion in
Swatch(hex: suggestion.hex,
name: suggestion.name,
isSelected: theme.accent.onyxHex == suggestion.hex) {
theme.accent = Color(onyxHex: suggestion.hex) ?? theme.accent
}
}
}
}
// Mit dem Farbfleck daneben: welche Farbe das System gerade führt,
// weiß man nicht auswendig — und dann wirkt das Ergebnis wie ein
// Fehler statt wie eine Übernahme.
OnyxFormRow(String(localized: "color.adoptSystem")) {
Button { theme.adoptSystemAccent() } label: {
HStack(spacing: Onyx.Space.sm) {
Circle()
.fill(Color(nsColor: .controlAccentColor))
.frame(width: 11, height: 11)
Text("settings.action.adopt")
}
}
}
}
}
private var network: some View {
OnyxFormCard(String(localized: "color.section.network")) {
OnyxFormRow(String(localized: "color.download"),
hint: String(localized: "color.network.hint")) {
ColorPicker("", selection: $theme.download, supportsOpacity: false)
.labelsHidden()
}
OnyxFormRow(String(localized: "color.upload")) {
ColorPicker("", selection: $theme.upload, supportsOpacity: false)
.labelsHidden()
}
OnyxFormRow(String(localized: "color.swap")) {
Button("settings.action.swap") {
let previous = theme.download
theme.download = theme.upload
theme.upload = previous
}
}
}
}
private var material: some View {
OnyxFormCard(String(localized: "settings.appearance.material"),
hint: String(localized: "settings.appearance.material.hint")) {
OnyxFormRow(String(localized: "settings.appearance.surface")) {
Picker("", selection: $theme.material) {
ForEach(NotchMaterial.allCases, id: \.self) { material in
Text(material.localizedName).tag(material)
}
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 240)
}
}
}
private var motion: some View {
OnyxFormCard(String(localized: "settings.appearance.motion"),
hint: String(localized: "settings.appearance.motion.hint")) {
OnyxFormRow(String(localized: "settings.appearance.profile")) {
Picker("", selection: $theme.motion) {
ForEach(MotionProfile.allCases, id: \.self) { profile in
Text(profile.localizedName).tag(profile)
}
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 240)
}
}
}
private var fixed: some View {
OnyxFormCard(String(localized: "color.section.fixed"),
hint: String(localized: "color.fixed.hint")) {
// Sichtbar, aber nicht bedienbar: sonst sucht man die Warnfarbe
// hier und findet sie nirgends.
OnyxFormRow(String(localized: "color.fixed.positive")) {
Circle().fill(Onyx.Color.positive).frame(width: 14, height: 14)
}
OnyxFormRow(String(localized: "color.fixed.warning")) {
Circle().fill(Onyx.Color.warning).frame(width: 14, height: 14)
}
OnyxFormRow(String(localized: "color.fixed.critical")) {
Circle().fill(Onyx.Color.critical).frame(width: 14, height: 14)
}
OnyxFormRow(String(localized: "color.reset")) {
Button("settings.action.reset") { theme.reset() }
.disabled(theme.isDefault)
}
}
}
}
extension NotchMaterial {
var localizedName: LocalizedStringKey {
switch self {
case .stone: "settings.material.stone"
case .blur: "settings.material.blur"
case .glass: "settings.material.glass"
}
}
}
extension MotionProfile {
var localizedName: LocalizedStringKey {
switch self {
case .snappy: "settings.motion.snappy"
case .bouncy: "settings.motion.bouncy"
case .calm: "settings.motion.calm"
}
}
}
/// Ein Farbfeld zum Anklicken.
private struct Swatch: View {
let hex: String
let name: LocalizedStringKey
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Circle()
.fill(Color(onyxHex: hex) ?? .gray)
.frame(width: 20, height: 20)
.overlay {
// Der Ring statt eines Hakens: ein Haken auf einer dunklen
// Farbe ist nicht zu sehen, auf einer hellen nicht auf
// einer dunklen Fläche.
Circle()
.strokeBorder(.primary, lineWidth: isSelected ? 2 : 0)
.padding(-3)
}
}
.buttonStyle(.plain)
.help(Text(name))
}
}