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>
199 lines
7.3 KiB
Swift
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))
|
|
}
|
|
}
|