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:
co-authored by
Claude Opus 5
parent
9b600de9a9
commit
edb327cabe
@@ -32,7 +32,7 @@ public struct CalendarSourceSettings: View {
|
||||
if model.kind != .appleCalendar {
|
||||
Section {
|
||||
LabeledContent {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
|
||||
statusText
|
||||
Text("calendar.source.readOnly", bundle: .module)
|
||||
.font(.caption)
|
||||
@@ -53,7 +53,7 @@ public struct CalendarSourceSettings: View {
|
||||
private var statusText: some View {
|
||||
switch calendarrState {
|
||||
case .ready(let window):
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
VStack(alignment: .leading, spacing: Onyx.Space.xxs) {
|
||||
Text("calendar.source.status.ready \(window.events.count.formatted()) \(asOfText(window.asOf))",
|
||||
bundle: .module)
|
||||
if let coverage = window.coverage {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user