/** * Gemeinsame Theme-Definitionen für Main-, Preload- und Renderer-Prozess. * * Es gibt drei tatsächlich gerenderte Themes (hell, dunkel, hochkontrast). * Die Nutzerauswahl kennt zusätzlich `system`: dann entscheiden die * Betriebssystem-Einstellungen (`prefers-color-scheme`, `forced-colors`, * `prefers-contrast`). */ /** Auswahlmöglichkeiten im Theme-Umschalter. */ export const THEME_AUSWAHLEN = ['system', 'hell', 'dunkel', 'hochkontrast'] as const; /** Was die Nutzerin/der Nutzer eingestellt hat. */ export type ThemeAuswahl = (typeof THEME_AUSWAHLEN)[number]; /** Was tatsächlich gerendert wird – `system` ist hier bereits aufgelöst. */ export type ThemeAufgeloest = Exclude; /** Deutsche Beschriftungen für die Oberfläche. */ export const THEME_BESCHRIFTUNGEN: Readonly> = Object.freeze({ system: 'Systemvorgabe', hell: 'Hell', dunkel: 'Dunkel', hochkontrast: 'Hoher Kontrast', }); /** Ergänzende Erläuterung je Auswahl (für `aria-describedby`). */ export const THEME_ERLAEUTERUNGEN: Readonly> = Object.freeze({ system: 'Folgt automatisch den Anzeigeeinstellungen Ihres Betriebssystems.', hell: 'Dunkle Schrift auf hellem Grund, ruhige Farbgebung.', dunkel: 'Helle Schrift auf dunklem Grund, blendarm für Abendstunden.', hochkontrast: 'Maximaler Kontrast mit kräftigen Rändern und Akzentfarben.', }); /** * Signale, die das Betriebssystem bzw. der Browser über Media Queries meldet. * Werden separat übergeben, damit die Auflösungslogik rein und testbar bleibt. */ export interface SystemAnzeigeSignale { /** `(prefers-color-scheme: dark)` */ readonly bevorzugtDunkel: boolean; /** `(forced-colors: active)` – z. B. Windows-Kontrastdesign */ readonly erzwungeneFarben: boolean; /** `(prefers-contrast: more)` */ readonly bevorzugtMehrKontrast: boolean; } export const SYSTEM_SIGNALE_STANDARD: SystemAnzeigeSignale = Object.freeze({ bevorzugtDunkel: false, erzwungeneFarben: false, bevorzugtMehrKontrast: false, }); /** Prüft zur Laufzeit, ob ein unbekannter Wert eine gültige Theme-Auswahl ist. */ export function istThemeAuswahl(wert: unknown): wert is ThemeAuswahl { return typeof wert === 'string' && (THEME_AUSWAHLEN as readonly string[]).includes(wert); } /** * Löst die Nutzerauswahl gegen die Systemsignale zu einem konkreten Theme auf. * * Regeln: * 1. Erzwungene Farben (Windows-Kontrastdesign) haben immer Vorrang – dann * liefert das Betriebssystem die Farben und wir schalten auf `hochkontrast`. * 2. Eine explizite Nutzerauswahl schlägt die restlichen Systemsignale. * 3. Bei `system` entscheiden `prefers-contrast` und `prefers-color-scheme`. */ export function themeAufloesen( auswahl: ThemeAuswahl, signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, ): ThemeAufgeloest { if (signale.erzwungeneFarben) { return 'hochkontrast'; } if (auswahl !== 'system') { return auswahl; } if (signale.bevorzugtMehrKontrast) { return 'hochkontrast'; } return signale.bevorzugtDunkel ? 'dunkel' : 'hell'; } /** * Passendes `color-scheme` für das aufgelöste Theme – steuert Scrollbalken, * Formularelemente und die Fensterdekoration. * * Muss mit den `color-scheme`-Deklarationen in `tokens.css` übereinstimmen: * Das eigene Thema „Hoher Kontrast“ zeichnet weiße Schrift auf Schwarz und * zählt deshalb als `dark`. * * **Beim Windows-Kontrastdesign entscheidet aber das System.** Es gibt auch * helle Kontrastdesigns (schwarz auf weiß). `hochkontrast` steht dann nur * dafür, dass die Farben von aussen kommen – über hell oder dunkel sagt es * nichts. Bis Fassung 0.24.1 lieferte diese Funktion dort unbedingt `dark` * und stellte damit Scrollbalken und Formularelemente auf dunkel, während * das System hell zeichnete. {@link startfarbeFuer} macht es seit jeher * richtig; hier fehlte die Entsprechung. */ export function farbschemaFuer( theme: ThemeAufgeloest, signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, ): 'light' | 'dark' { if (signale.erzwungeneFarben) { return signale.bevorzugtDunkel ? 'dark' : 'light'; } return theme === 'hell' ? 'light' : 'dark'; } /** * Grundfarbe je Theme – dieselben Werte wie `--farbe-grund` in `tokens.css`. * * Sie stehen hier ein zweites Mal, weil der Hauptprozess die Fensterfarbe * setzen muss, bevor es ein Dokument gibt, das ein Stilblatt laden könnte. * Dass beide Stellen übereinstimmen, rechnet `tests/token-kontraste.test.ts` * gegen die CSS-Datei nach – eine Kopie, die niemand vergleicht, läuft * auseinander. */ const STARTFARBEN: Readonly> = Object.freeze({ hell: '#f7f7f5', dunkel: '#14161a', hochkontrast: '#000000', }); /** * Hintergrundfarbe des Fensters für den Augenblick vor dem ersten Frame. * * `BrowserWindow` zeichnet diese Farbe, solange der Renderer noch nichts * geliefert hat. Sie muss deshalb der **gespeicherten Wahl** folgen und nicht * dem Systemdesign: Wer „Dunkel“ eingestellt hat, während das Betriebssystem * hell läuft, bekäme sonst bei jedem Start ein helles Aufblitzen – dieselbe * Falle, die bei der Anzeigegröße schon einmal zugeschlagen hat. * * Bei erzwungenen Farben (Windows-Kontrastdesign) gilt das nicht: Dort * liefert das Betriebssystem die Farben, und `tokens.css` greift auf `Canvas` * zurück. Ein helles Kontrastdesign ist weiß, ein dunkles schwarz – eine * eigene Farbe wäre hier schlicht falsch. */ export function startfarbeFuer(auswahl: ThemeAuswahl, signale: SystemAnzeigeSignale): string { if (signale.erzwungeneFarben) { return signale.bevorzugtDunkel ? '#000000' : '#ffffff'; } return STARTFARBEN[themeAufloesen(auswahl, signale)]; } /** * Satz für die `aria-live`-Region nach einer Umschaltung. * * **Bei erzwungenen Farben wird nichts umgestellt, und das muss dastehen.** * `themeAufloesen` gibt dem Windows-Kontrastdesign unbedingt Vorrang: Die * Wahl wird gespeichert, sie wirkt aber erst, wenn das Kontrastdesign wieder * aus ist. Bis Fassung 0.24.1 meldete die Ansage trotzdem „Darstellung * umgestellt auf Dunkel“ – für jemanden, der die Umstellung nicht sehen * kann, die denkbar irreführendste Auskunft. */ export function umschaltAnsage( auswahl: ThemeAuswahl, aufgeloest: ThemeAufgeloest, signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, ): string { if (signale.erzwungeneFarben) { return ( `Ihre Wahl ${THEME_BESCHRIFTUNGEN[auswahl]} ist gespeichert. ` + 'Sichtbar wird sie erst, wenn das Kontrastdesign von Windows wieder ' + 'ausgeschaltet ist – solange es läuft, bestimmt das Betriebssystem die Farben.' ); } if (auswahl === 'system') { return `Darstellung folgt der Systemvorgabe. Aktiv ist ${THEME_BESCHRIFTUNGEN[aufgeloest]}.`; } return `Darstellung umgestellt auf ${THEME_BESCHRIFTUNGEN[auswahl]}.`; }