waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Gemeinsame Theme-Definitionen für Main-, Preload- und Renderer-Prozess. |
| 3 | * |
| 4 | * Es gibt drei tatsächlich gerenderte Themes (hell, dunkel, hochkontrast). |
| 5 | * Die Nutzerauswahl kennt zusätzlich `system`: dann entscheiden die |
| 6 | * Betriebssystem-Einstellungen (`prefers-color-scheme`, `forced-colors`, |
| 7 | * `prefers-contrast`). |
| 8 | */ |
| 9 | |
| 10 | /** Auswahlmöglichkeiten im Theme-Umschalter. */ |
| 11 | export const THEME_AUSWAHLEN = ['system', 'hell', 'dunkel', 'hochkontrast'] as const; |
| 12 | |
| 13 | /** Was die Nutzerin/der Nutzer eingestellt hat. */ |
| 14 | export type ThemeAuswahl = (typeof THEME_AUSWAHLEN)[number]; |
| 15 | |
| 16 | /** Was tatsächlich gerendert wird – `system` ist hier bereits aufgelöst. */ |
| 17 | export type ThemeAufgeloest = Exclude<ThemeAuswahl, 'system'>; |
| 18 | |
| 19 | /** Deutsche Beschriftungen für die Oberfläche. */ |
| 20 | export const THEME_BESCHRIFTUNGEN: Readonly<Record<ThemeAuswahl, string>> = Object.freeze({ |
| 21 | system: 'Systemvorgabe', |
| 22 | hell: 'Hell', |
| 23 | dunkel: 'Dunkel', |
| 24 | hochkontrast: 'Hoher Kontrast', |
| 25 | }); |
| 26 | |
| 27 | /** Ergänzende Erläuterung je Auswahl (für `aria-describedby`). */ |
| 28 | export const THEME_ERLAEUTERUNGEN: Readonly<Record<ThemeAuswahl, string>> = Object.freeze({ |
| 29 | system: 'Folgt automatisch den Anzeigeeinstellungen Ihres Betriebssystems.', |
| 30 | hell: 'Dunkle Schrift auf hellem Grund, ruhige Farbgebung.', |
| 31 | dunkel: 'Helle Schrift auf dunklem Grund, blendarm für Abendstunden.', |
| 32 | hochkontrast: 'Maximaler Kontrast mit kräftigen Rändern und Akzentfarben.', |
| 33 | }); |
| 34 | |
| 35 | /** |
| 36 | * Signale, die das Betriebssystem bzw. der Browser über Media Queries meldet. |
| 37 | * Werden separat übergeben, damit die Auflösungslogik rein und testbar bleibt. |
| 38 | */ |
| 39 | export interface SystemAnzeigeSignale { |
| 40 | /** `(prefers-color-scheme: dark)` */ |
| 41 | readonly bevorzugtDunkel: boolean; |
| 42 | /** `(forced-colors: active)` – z. B. Windows-Kontrastdesign */ |
| 43 | readonly erzwungeneFarben: boolean; |
| 44 | /** `(prefers-contrast: more)` */ |
| 45 | readonly bevorzugtMehrKontrast: boolean; |
| 46 | } |
| 47 | |
| 48 | export const SYSTEM_SIGNALE_STANDARD: SystemAnzeigeSignale = Object.freeze({ |
| 49 | bevorzugtDunkel: false, |
| 50 | erzwungeneFarben: false, |
| 51 | bevorzugtMehrKontrast: false, |
| 52 | }); |
| 53 | |
| 54 | /** Prüft zur Laufzeit, ob ein unbekannter Wert eine gültige Theme-Auswahl ist. */ |
| 55 | export function istThemeAuswahl(wert: unknown): wert is ThemeAuswahl { |
| 56 | return typeof wert === 'string' && (THEME_AUSWAHLEN as readonly string[]).includes(wert); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Löst die Nutzerauswahl gegen die Systemsignale zu einem konkreten Theme auf. |
| 61 | * |
| 62 | * Regeln: |
| 63 | * 1. Erzwungene Farben (Windows-Kontrastdesign) haben immer Vorrang – dann |
| 64 | * liefert das Betriebssystem die Farben und wir schalten auf `hochkontrast`. |
| 65 | * 2. Eine explizite Nutzerauswahl schlägt die restlichen Systemsignale. |
| 66 | * 3. Bei `system` entscheiden `prefers-contrast` und `prefers-color-scheme`. |
| 67 | */ |
| 68 | export function themeAufloesen( |
| 69 | auswahl: ThemeAuswahl, |
| 70 | signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, |
| 71 | ): ThemeAufgeloest { |
| 72 | if (signale.erzwungeneFarben) { |
| 73 | return 'hochkontrast'; |
| 74 | } |
| 75 | |
| 76 | if (auswahl !== 'system') { |
| 77 | return auswahl; |
| 78 | } |
| 79 | |
| 80 | if (signale.bevorzugtMehrKontrast) { |
| 81 | return 'hochkontrast'; |
| 82 | } |
| 83 | |
| 84 | return signale.bevorzugtDunkel ? 'dunkel' : 'hell'; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Passendes `color-scheme` für das aufgelöste Theme – steuert Scrollbalken, |
| 89 | * Formularelemente und die Fensterdekoration. |
| 90 | * |
| 91 | * Muss mit den `color-scheme`-Deklarationen in `tokens.css` übereinstimmen: |
| 92 | * Das eigene Thema „Hoher Kontrast“ zeichnet weiße Schrift auf Schwarz und |
| 93 | * zählt deshalb als `dark`. |
| 94 | * |
| 95 | * **Beim Windows-Kontrastdesign entscheidet aber das System.** Es gibt auch |
| 96 | * helle Kontrastdesigns (schwarz auf weiß). `hochkontrast` steht dann nur |
| 97 | * dafür, dass die Farben von aussen kommen – über hell oder dunkel sagt es |
| 98 | * nichts. Bis Fassung 0.24.1 lieferte diese Funktion dort unbedingt `dark` |
| 99 | * und stellte damit Scrollbalken und Formularelemente auf dunkel, während |
| 100 | * das System hell zeichnete. {@link startfarbeFuer} macht es seit jeher |
| 101 | * richtig; hier fehlte die Entsprechung. |
| 102 | */ |
| 103 | export function farbschemaFuer( |
| 104 | theme: ThemeAufgeloest, |
| 105 | signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, |
| 106 | ): 'light' | 'dark' { |
| 107 | if (signale.erzwungeneFarben) { |
| 108 | return signale.bevorzugtDunkel ? 'dark' : 'light'; |
| 109 | } |
| 110 | return theme === 'hell' ? 'light' : 'dark'; |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Grundfarbe je Theme – dieselben Werte wie `--farbe-grund` in `tokens.css`. |
| 115 | * |
| 116 | * Sie stehen hier ein zweites Mal, weil der Hauptprozess die Fensterfarbe |
| 117 | * setzen muss, bevor es ein Dokument gibt, das ein Stilblatt laden könnte. |
| 118 | * Dass beide Stellen übereinstimmen, rechnet `tests/token-kontraste.test.ts` |
| 119 | * gegen die CSS-Datei nach – eine Kopie, die niemand vergleicht, läuft |
| 120 | * auseinander. |
| 121 | */ |
| 122 | const STARTFARBEN: Readonly<Record<ThemeAufgeloest, string>> = Object.freeze({ |
| 123 | hell: '#f7f7f5', |
| 124 | dunkel: '#14161a', |
| 125 | hochkontrast: '#000000', |
| 126 | }); |
| 127 | |
| 128 | /** |
| 129 | * Hintergrundfarbe des Fensters für den Augenblick vor dem ersten Frame. |
| 130 | * |
| 131 | * `BrowserWindow` zeichnet diese Farbe, solange der Renderer noch nichts |
| 132 | * geliefert hat. Sie muss deshalb der **gespeicherten Wahl** folgen und nicht |
| 133 | * dem Systemdesign: Wer „Dunkel“ eingestellt hat, während das Betriebssystem |
| 134 | * hell läuft, bekäme sonst bei jedem Start ein helles Aufblitzen – dieselbe |
| 135 | * Falle, die bei der Anzeigegröße schon einmal zugeschlagen hat. |
| 136 | * |
| 137 | * Bei erzwungenen Farben (Windows-Kontrastdesign) gilt das nicht: Dort |
| 138 | * liefert das Betriebssystem die Farben, und `tokens.css` greift auf `Canvas` |
| 139 | * zurück. Ein helles Kontrastdesign ist weiß, ein dunkles schwarz – eine |
| 140 | * eigene Farbe wäre hier schlicht falsch. |
| 141 | */ |
| 142 | export function startfarbeFuer(auswahl: ThemeAuswahl, signale: SystemAnzeigeSignale): string { |
| 143 | if (signale.erzwungeneFarben) { |
| 144 | return signale.bevorzugtDunkel ? '#000000' : '#ffffff'; |
| 145 | } |
| 146 | return STARTFARBEN[themeAufloesen(auswahl, signale)]; |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Satz für die `aria-live`-Region nach einer Umschaltung. |
| 151 | * |
| 152 | * **Bei erzwungenen Farben wird nichts umgestellt, und das muss dastehen.** |
| 153 | * `themeAufloesen` gibt dem Windows-Kontrastdesign unbedingt Vorrang: Die |
| 154 | * Wahl wird gespeichert, sie wirkt aber erst, wenn das Kontrastdesign wieder |
| 155 | * aus ist. Bis Fassung 0.24.1 meldete die Ansage trotzdem „Darstellung |
| 156 | * umgestellt auf Dunkel“ – für jemanden, der die Umstellung nicht sehen |
| 157 | * kann, die denkbar irreführendste Auskunft. |
| 158 | */ |
| 159 | export function umschaltAnsage( |
| 160 | auswahl: ThemeAuswahl, |
| 161 | aufgeloest: ThemeAufgeloest, |
| 162 | signale: SystemAnzeigeSignale = SYSTEM_SIGNALE_STANDARD, |
| 163 | ): string { |
| 164 | if (signale.erzwungeneFarben) { |
| 165 | return ( |
| 166 | `Ihre Wahl ${THEME_BESCHRIFTUNGEN[auswahl]} ist gespeichert. ` + |
| 167 | 'Sichtbar wird sie erst, wenn das Kontrastdesign von Windows wieder ' + |
| 168 | 'ausgeschaltet ist – solange es läuft, bestimmt das Betriebssystem die Farben.' |
| 169 | ); |
| 170 | } |
| 171 | if (auswahl === 'system') { |
| 172 | return `Darstellung folgt der Systemvorgabe. Aktiv ist ${THEME_BESCHRIFTUNGEN[aufgeloest]}.`; |
| 173 | } |
| 174 | return `Darstellung umgestellt auf ${THEME_BESCHRIFTUNGEN[auswahl]}.`; |
| 175 | } |