import type { AppSettings } from '@/services/storage'; /** * Helles und dunkles Erscheinungsbild. * * Genau ein Schalter: das Attribut `data-erscheinungsbild` am Wurzelelement. * Alle Farben haengen an CSS-Variablen. Der Altbestand setzte teils Klassen, * teils Inline-Farben und teils ein zweites Stylesheet; einzelne Bereiche * blieben dadurch im dunklen Erscheinungsbild hell und unlesbar. */ export type ThemeSetting = AppSettings['theme']; /** Das Erscheinungsbild, das tatsaechlich gilt. */ export type Erscheinungsbild = 'hell' | 'dunkel'; type Abmeldung = () => void; let abmeldungen: Abmeldung[] = []; /** * Empfaenger fuer den Wechsel des geltenden Erscheinungsbilds. * * Das Attribut allein genuegt nicht: Alles, was in CSS-Variablen haengt, folgt * ihm von selbst - eine Zeichenflaeche nicht. Sie liest die Farben beim * Zeichnen und behaelt sie bis zum naechsten Mal. Wechselt das Erscheinungsbild * im Betrieb, stand die Zeichnung deshalb in den alten Farben in einer schon * umgestellten Oberflaeche. */ const wechselempfaenger = new Set<(erscheinungsbild: Erscheinungsbild) => void>(); /** Wendet die Einstellung an und folgt Systemvorgabe und Kontrastdesign. */ export function applyTheme(setting: ThemeSetting): void { loeseEmpfaenger(); const kontrastdesign = medienabfrage('(forced-colors: active)'); const systemDunkel = medienabfrage('(prefers-color-scheme: dark)'); const anwenden = (): void => { setAttribute( effektivesErscheinungsbild( setting, kontrastdesign?.matches === true, systemDunkel?.matches === true, ), ); }; anwenden(); // Beide Abfragen koennen sich im Betrieb aendern: das Erscheinungsbild des // Systems ohnehin, das Kontrastdesign beim Umschalten in den // Windows-Einstellungen. Beobachtet werden sie deshalb unabhaengig von der // gewaehlten Einstellung - bei "Hell" und "Dunkel" wirkt sich nur der Fall // Kontrastdesign aus, und genau der ist der Grund fuer diesen Umbau. beobachte(kontrastdesign, anwenden); beobachte(systemDunkel, anwenden); } /** * Meldet einen Empfaenger an, der bei jedem Wechsel gerufen wird. * * Gerufen wird nur, wenn sich das geltende Erscheinungsbild wirklich aendert - * ein Rundlauf von "Dunkel" auf "System" bei dunklem System aendert die * Einstellung, aber keine Farbe, und ein Neuzeichnen waere dort nur ein * unnoetiger Fokussprung. * * Die Rueckgabe meldet wieder ab. */ export function beiErscheinungsbildwechsel( empfaenger: (erscheinungsbild: Erscheinungsbild) => void, ): () => void { wechselempfaenger.add(empfaenger); return () => { wechselempfaenger.delete(empfaenger); }; } /** * Welches Erscheinungsbild tatsaechlich gilt. * * Befund L14 (EN 301 549 Kap. 11.7): Die einzige Eingangsgroesse fuer die * Farbwahl der Zeichenflaechen ist dieses Attribut (`planView`, * `simulationView`, `lageplanView` lesen `dataset['erscheinungsbild']`). Ein * `canvas` wird von `forced-colors: active` nicht erfasst - das Betriebssystem * ersetzt dort keine Farbe. Wer also ein dunkles Kontrastdesign benutzt und in * den Vorgaben ausdruecklich "Hell" gewaehlt hat, bekam eine weisse Zeichnung * mitten in einer schwarzen Oberflaeche. Umgekehrt ebenso. * * Bei aktivem Kontrastdesign uebersteuert das System deshalb die eigene Wahl: * Die Oberflaeche wird ohnehin vollstaendig in den Systemfarben gezeichnet, die * Wahl "Hell"/"Dunkel" ist dort ohne sichtbare Wirkung, und die Zeichenflaeche * soll zu dem passen, was daneben steht. Ob das Kontrastdesign hell oder dunkel * ist, sagt `prefers-color-scheme`: Bei aktivem Kontrastdesign richtet sich die * Abfrage nach der Hintergrundfarbe der Systempalette (CSS Color Adjust 1). * * Traefe diese Annahme einmal nicht zu, bliebe es beim heutigen Verhalten - die * Zeichnung waere dann so falsch wie zuvor, nicht falscher. */ export function effektivesErscheinungsbild( setting: ThemeSetting, kontrastdesign: boolean, systemDunkel: boolean, ): Erscheinungsbild { if (kontrastdesign) return systemDunkel ? 'dunkel' : 'hell'; if (setting === 'system') return systemDunkel ? 'dunkel' : 'hell'; return setting; } /** Naechste Einstellung im Rundlauf hell -> dunkel -> System. */ export function nextTheme(current: ThemeSetting): ThemeSetting { return current === 'hell' ? 'dunkel' : current === 'dunkel' ? 'system' : 'hell'; } export function themeLabel(setting: ThemeSetting): string { return setting === 'hell' ? 'Hell' : setting === 'dunkel' ? 'Dunkel' : 'System'; } /** * Setzt das Attribut und sagt Bescheid, wenn sich dabei etwas geaendert hat. * * Der Vorher-Wert wird am Attribut selbst abgelesen und nicht daneben * mitgeschrieben: Zwei Buchfuehrungen ueber dasselbe laufen auseinander, und * das Attribut ist die Quelle, aus der auch die Zeichenflaechen lesen. */ function setAttribute(value: Erscheinungsbild): void { const wurzel = document.documentElement; const vorher = wurzel.dataset['erscheinungsbild']; wurzel.dataset['erscheinungsbild'] = value; if (vorher === value) return; for (const empfaenger of [...wechselempfaenger]) { try { empfaenger(value); } catch (error) { // Ein Fehler in einer Ansicht darf die uebrigen nicht mitreissen - und // schon gar nicht den Programmstart, in dem `applyTheme` als eine der // ersten Anweisungen laeuft. console.error('Fehler beim Wechsel des Erscheinungsbilds:', error); } } } /** * Medienabfrage, sofern die Umgebung sie kennt. * * In der Testumgebung und in aelteren Anzeigen fehlt `matchMedia`. Ohne diese * Pruefung braeche das Setzen des Erscheinungsbilds dort mit einem Fehler ab - * und mit ihm der ganze Programmstart, denn `applyTheme` laeuft als eine der * ersten Anweisungen. */ function medienabfrage(abfrage: string): MediaQueryList | null { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null; try { return window.matchMedia(abfrage); } catch { return null; } } function beobachte(liste: MediaQueryList | null, anwenden: () => void): void { if (liste === null || typeof liste.addEventListener !== 'function') return; const empfaenger = (): void => { anwenden(); }; liste.addEventListener('change', empfaenger); abmeldungen.push(() => { liste.removeEventListener('change', empfaenger); }); } function loeseEmpfaenger(): void { for (const abmelden of abmeldungen) abmelden(); abmeldungen = []; }