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
@@ -45,6 +45,18 @@ public final class CalendarModel {
/// Die Kalender, die zur Auswahl stehen.
public private(set) var availableCalendars: [OnyxCalendar] = []
/// Der Zustand der Calendarr-Quelle **allein**, für die Einstellungen.
///
/// Getrennt von `state`, weil das bei „beide" auch Apple-Termine enthält —
/// eine Zahl daraus sagt über Calendarr nichts.
///
/// Und getrennt geführt statt in den Einstellungen nachgeladen: dort stand
/// bisher ein zweiter, eigener Aufruf von `CalendarrSource`, also immer der
/// **Schnappschuss** — auch wenn längst der Server gefragt wurde. Der
/// Zeitstempel daneben war deshalb das Alter einer Datei, die niemand mehr
/// liest, und sah aus wie ein Kalender, der seit Wochen stillsteht.
public private(set) var calendarrState: CalendarSourceState?
private static let kindKey = "calendar.source"
private static let hiddenKey = "calendar.hiddenCalendars"
private static let gridKey = "calendar.showsMonthGrid"
@@ -194,6 +206,7 @@ public final class CalendarModel {
let interval = DateInterval(start: start, end: end)
var states: [CalendarSourceState] = []
let calendarr = kind == .appleCalendar ? nil : calendarrChoice as AnyObject
for source in activeSources {
guard !Task.isCancelled else { return }
// Hier wird NICHT nachgefragt. Ein Berechtigungsdialog aus einer
@@ -201,7 +214,9 @@ public final class CalendarModel {
// passiert — die Anfrage muss aus dem Einstellungsfenster kommen.
// Ohne Berechtigung ist der ehrliche Zustand `denied` und nicht
// eine leere Terminliste, die "nichts mehr heute" behaupten würde.
states.append(source.isAuthorized ? await source.load(interval) : .denied)
let loaded = source.isAuthorized ? await source.load(interval) : .denied
if let calendarr, source as AnyObject === calendarr { calendarrState = loaded }
states.append(loaded)
}
guard !Task.isCancelled else { return }
state = CalendarSourceKind.merge(states).filteringHidden(hiddenCalendarIDs)
@@ -266,41 +281,26 @@ private struct CalendarWidgetView: View {
AgendaList(events: model.upcoming, coverage: window.coverage)
}
case .denied:
CalendarNotice(symbol: "lock", text: "widget.calendar.denied")
OnyxNotice(symbol: "lock",
text: String(localized: "widget.calendar.denied", bundle: .module))
case .neverWritten:
CalendarNotice(symbol: "clock.arrow.circlepath", text: "widget.calendar.neverWritten")
OnyxNotice(symbol: "clock.arrow.circlepath",
text: String(localized: "widget.calendar.neverWritten", bundle: .module))
case .loggedOut:
CalendarNotice(symbol: "person.crop.circle.badge.xmark", text: "widget.calendar.loggedOut")
OnyxNotice(symbol: "person.crop.circle.badge.xmark",
text: String(localized: "widget.calendar.loggedOut", bundle: .module))
case .incompatible:
CalendarNotice(symbol: "exclamationmark.triangle", text: "widget.calendar.incompatible")
OnyxNotice(symbol: "exclamationmark.triangle",
text: String(localized: "widget.calendar.incompatible", bundle: .module))
case .unreadable:
CalendarNotice(symbol: "questionmark.folder", text: "widget.calendar.unreadable")
OnyxNotice(symbol: "questionmark.folder",
text: String(localized: "widget.calendar.unreadable", bundle: .module))
}
}
.task { model.refresh() }
.onDisappear { model.stop() }
}
}
/// Kein Platzhalter, sondern eine Antwort: jeder Zustand sagt, was zu tun ist.
private struct CalendarNotice: View {
let symbol: String
let text: LocalizedStringKey
var body: some View {
VStack(spacing: 6) {
Image(systemName: symbol)
.font(.system(size: 16))
.foregroundStyle(Onyx.Color.textTertiary)
Text(text, bundle: .module)
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Mini-Monat
private struct LargeCalendar: View {
@@ -310,9 +310,9 @@ private struct LargeCalendar: View {
private var calendar: Calendar { .autoupdatingCurrent }
var body: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: Onyx.Space.md) {
MiniMonth(window: window)
Divider().overlay(Onyx.Color.hairline)
OnyxHairline()
AgendaList(events: Array(upcoming.prefix(3)), coverage: window.coverage)
}
}
@@ -328,11 +328,11 @@ private struct MiniMonth: View {
let busy = window.events.busyDays(calendar: calendar)
let today = calendar.startOfDay(for: Date())
VStack(spacing: 3) {
VStack(spacing: Onyx.Space.xs) {
HStack(spacing: 0) {
ForEach(weekdaySymbols, id: \.self) { symbol in
Text(symbol)
.font(.system(size: 8, weight: .medium))
.font(Onyx.Font.micro)
.foregroundStyle(Onyx.Color.textTertiary)
.frame(maxWidth: .infinity)
}
@@ -370,7 +370,7 @@ private struct DayCell: View {
Circle().fill(Onyx.Color.accent.opacity(0.9)).frame(width: 15, height: 15)
}
Text("\(Calendar.autoupdatingCurrent.component(.day, from: day.date))")
.font(.system(size: 9, weight: isToday ? .semibold : .regular))
.font(Onyx.Font.micro.weight(isToday ? .semibold : .regular))
.foregroundStyle(textColor)
}
.frame(maxWidth: .infinity)
@@ -413,10 +413,10 @@ private struct AgendaList: View {
} else {
// Nach Tagen getrennt: „8:00" allein sagt nicht, an welchem.
let days = AgendaGrouping.group(events)
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
ForEach(Array(days.enumerated()), id: \.element.id) { index, day in
Text(AgendaGrouping.label(for: day.date))
.font(.system(size: 9, weight: .semibold))
.font(Onyx.Font.micro.weight(.semibold))
.foregroundStyle(Onyx.Color.textSecondary)
.textCase(.uppercase)
// Luft nur zwischen den Tagen, nicht über dem ersten.
@@ -435,18 +435,18 @@ private struct EventRow: View {
let event: OnyxEvent
var body: some View {
HStack(spacing: 6) {
HStack(spacing: Onyx.Space.sm) {
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
.fill(Color(hex: event.colorHex) ?? Onyx.Color.accent)
.frame(width: 3)
VStack(alignment: .leading, spacing: 1) {
VStack(alignment: .leading, spacing: Onyx.Space.xxs) {
Text(event.title)
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textPrimary)
.lineLimit(1)
Text(timeText)
.font(.system(size: 9))
.font(Onyx.Font.micro)
.monospacedDigit()
.foregroundStyle(Onyx.Color.textTertiary)
.lineLimit(1)