e9c16325cc
Musterseite entfernt (companion-app/muster, vite.muster.config.ts, /config/www/dmmuster). Ansage des Eigentuemers: sie fuehrt nur zu neuen Fehlerquellen. Ausloeser war ein Befund, den ich als App-Fehler gemeldet hatte - Ringe und Typenschild schwarz auf schwarz - und der nur dort bestand. Behoben: * Das Zahnrad im Autohaus-Kasten hatte seine Ecke verlassen (58/288 statt 8/8). Ursache war mein HIG-Durchgang von gestern: .zahnrad kam in die Gruppe fuer die 44x44-Trefferflaeche und bekam damit position:relative, was das absolute aus dem Basisblatt ueberschrieb. Die Auflage war ueberfluessig - .zahnrad ist dort schon 44x44. * Das Fotomenue hing mit position:absolute an der Kachel statt am Bildschirm und lag bei der hohen Raeder-Kachel ausserhalb des Sichtfelds; einen Abbrechen-Knopf hatte es nie. Das Panel benutzt jetzt dasselbe Aktionsblatt wie die App. .bildmenu ist samt Zustand, Markup, totem Handler und CSS entfernt. * Der Blattkopf las sich als dritte Auswahl (linksbuendig, 15px, --fg). Jetzt zentriert, 13px, gedaempft - Apples Muster, in beiden Codebasen. * "Anstehende Termine" stand 5px ueber "Oelwechsel" und wirkte als dessen Beischrift. Entfernt. * Wartungsplan-Formular: Art nach oben, Datum 124px statt Browser-Vorgabe, Werkstatt ueber die volle Zeile (218 -> 306px), Kosten 116px. Zwei Erklaertexte entfernt. * Thema folgt jetzt einer Systemumstellung im laufenden Betrieb. Vorher las es prefers-color-scheme genau einmal beim Laden. Drei Tests, die ohne die Behebung nachweislich fallen. Geprueft: 295 App-Tests, Panel node --check, 0 Tracebacks, alle Aenderungen live an der Testinstanz gemessen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
177 lines
6.9 KiB
TypeScript
177 lines
6.9 KiB
TypeScript
/**
|
|
* Ansichtseinstellungen: Tag/Nacht, Fahrbahn unter dem Fahrzeug, Beschriftung
|
|
* der Menüleiste. Übernommen aus dem alten Panel (A6 des Design-Audits):
|
|
* gespeicherte Wahl schlägt Systemeinstellung, Standard für das Thema ist
|
|
* „nacht".
|
|
*
|
|
* WARUM HIER EIN GETEILTER SPEICHER STEHT UND NICHT DREIMAL useState
|
|
* ------------------------------------------------------------------
|
|
* Bis 2026-08-31 legte jeder dieser Haken sein eigenes `useState` an. Wer ihn
|
|
* zweimal aufrief, bekam zwei voneinander unabhängige Kopien desselben Werts:
|
|
*
|
|
* useTheme() → App.tsx (setzt data-theme), Einstellungen.tsx (der Schalter),
|
|
* Typenschild.tsx (zweimal, für die Farbe des Schilds)
|
|
* useTabBeschriftung() → Einstellungen.tsx (der Schalter), App.tsx (die Leiste)
|
|
*
|
|
* Der Schalter in den Einstellungen änderte damit nur seine eigene Kopie und
|
|
* den Speicher der Ablage — die Kopie in App.tsx blieb stehen, `data-theme`
|
|
* wechselte nie, und die Umschaltung wirkte erst nach einem Neustart der App.
|
|
* Genau so gemeldet: „Tag/Nacht Umschaltung wirkungslos". Die übrigen
|
|
* Schalter hatten denselben Fehler, nur ist er dort weniger auffällig.
|
|
*
|
|
* `useSyncExternalStore` (in React eingebaut) löst das an der Wurzel: es gibt
|
|
* je Einstellung genau einen Wert, jeder Aufrufer sieht denselben, und ein
|
|
* Setzen benachrichtigt alle. Das ist der React-eigene Weg für einen Zustand,
|
|
* der außerhalb des Komponentenbaums lebt — kein Kontext-Anbieter nötig, und
|
|
* die Aufrufstellen bleiben unverändert.
|
|
*
|
|
* Alle Zugriffe auf die Ablage sind abgesichert: im Privatmodus mancher
|
|
* Browser wirft schon das Lesen.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useSyncExternalStore } from "react"
|
|
|
|
export type Theme = "nacht" | "tag"
|
|
|
|
function ablageLesen(schluessel: string): string | null {
|
|
try {
|
|
return localStorage.getItem(schluessel)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function ablageSchreiben(schluessel: string, wert: string): void {
|
|
try {
|
|
localStorage.setItem(schluessel, wert)
|
|
} catch {
|
|
// Privatmodus — die Wahl gilt dann nur für diese Sitzung.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ein Wert, den sich alle Aufrufer teilen.
|
|
*
|
|
* `wert` wird beim ersten Zugriff aus der Ablage gelesen und danach im
|
|
* Speicher gehalten — `useSyncExternalStore` verlangt, dass zwei Aufrufe von
|
|
* `stand()` ohne zwischenzeitliche Änderung dasselbe liefern. Bei jedem Mal
|
|
* neu aus der Ablage zu lesen genügt dem zwar auch, aber nur zufällig; ein
|
|
* gehaltener Wert macht die Zusage explizit.
|
|
*/
|
|
function einstellung<T>(
|
|
schluessel: string,
|
|
ausAblage: (roh: string | null) => T,
|
|
inAblage: (wert: T) => string,
|
|
) {
|
|
let wert = ausAblage(ablageLesen(schluessel))
|
|
const horcher = new Set<() => void>()
|
|
|
|
return {
|
|
stand: () => wert,
|
|
/** Ob überhaupt etwas in der Ablage steht - also eine bewusste Wahl. */
|
|
gewaehlt: () => ablageLesen(schluessel) !== null,
|
|
/* Setzt den gehaltenen Wert, OHNE ihn zu speichern. Für Werte, die dem
|
|
System folgen, solange niemand selbst gewählt hat: eine Systemumstellung
|
|
ist keine Entscheidung des Nutzers und darf deshalb keine ablegen. */
|
|
ausSystem: (neu: T) => {
|
|
if (neu === wert) return
|
|
wert = neu
|
|
for (const bei of horcher) bei()
|
|
},
|
|
horchen: (bei: () => void) => {
|
|
horcher.add(bei)
|
|
return () => horcher.delete(bei)
|
|
},
|
|
setzen: (neu: T) => {
|
|
if (neu === wert) return
|
|
wert = neu
|
|
ablageSchreiben(schluessel, inAblage(neu))
|
|
for (const bei of horcher) bei()
|
|
},
|
|
}
|
|
}
|
|
|
|
const THEMA = einstellung<Theme>(
|
|
"dm360.theme",
|
|
(roh) => {
|
|
if (roh === "nacht" || roh === "tag") return roh
|
|
// Ohne gespeicherte Wahl entscheidet das System; ohne beides: nacht.
|
|
const hell =
|
|
typeof window !== "undefined" &&
|
|
typeof window.matchMedia === "function" &&
|
|
window.matchMedia("(prefers-color-scheme: light)").matches
|
|
return hell ? "tag" : "nacht"
|
|
},
|
|
(wert) => wert,
|
|
)
|
|
|
|
/* Systemumstellung im laufenden Betrieb nachziehen.
|
|
|
|
Der Startwert oben liest `prefers-color-scheme` genau einmal, beim Laden des
|
|
Moduls. Stellt das Gerät danach um - etwa durch die automatische Umschaltung
|
|
bei Sonnenuntergang -, folgte das Stylesheet sofort (es hängt selbst an der
|
|
Medienabfrage), der React-Zustand aber nie. Alles, was sein Bild aus
|
|
`useTheme()` wählt statt aus CSS, blieb damit auf der alten Fassung stehen:
|
|
die Vier Ringe und das Typenschild sind `<img>` mit eingebettetem SVG und
|
|
je einer hellen und einer dunklen Fassung (siehe VierRinge in
|
|
Typenschild.tsx). Auf dunklem Grund stand dann die dunkle Fassung - schwarz
|
|
auf schwarz, vom Eigentümer am 05.09.2026 gemeldet. Ein Neuladen räumte es
|
|
auf, weshalb es sprunghaft wirkte.
|
|
|
|
Nur solange niemand selbst gewählt hat: eine gespeicherte Wahl in
|
|
"Einstellungen" bleibt unangetastet. */
|
|
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
const dunkel = window.matchMedia("(prefers-color-scheme: dark)")
|
|
const nachziehen = () => {
|
|
if (THEMA.gewaehlt()) return
|
|
THEMA.ausSystem(dunkel.matches ? "nacht" : "tag")
|
|
}
|
|
if (typeof dunkel.addEventListener === "function") dunkel.addEventListener("change", nachziehen)
|
|
else if (typeof dunkel.addListener === "function") dunkel.addListener(nachziehen)
|
|
}
|
|
|
|
|
|
const TABBESCHRIFTUNG = einstellung<boolean>(
|
|
"dm360.tabbeschriftung",
|
|
(roh) => roh !== "aus",
|
|
(an) => (an ? "an" : "aus"),
|
|
)
|
|
|
|
/** Nur für Tests: setzt den gehaltenen Wert auf den Stand der Ablage zurück. */
|
|
export function themeStartwert(): Theme {
|
|
return THEMA.stand()
|
|
}
|
|
|
|
export function useTheme(): [Theme, (theme: Theme) => void] {
|
|
const theme = useSyncExternalStore(THEMA.horchen, THEMA.stand, THEMA.stand)
|
|
const setzen = useCallback((neu: Theme) => THEMA.setzen(neu), [])
|
|
|
|
// Farbe der Systemleisten mitziehen, damit auf dem Handy kein heller
|
|
// Balken über einer dunklen App steht. Werte des Panels (--canvas in
|
|
// audi-dashboard-ios.css); hier standen einmal #161b23/#ffffff aus dem
|
|
// Basis-Stylesheet, das im Panel nie greift — die Systemleiste hatte damit
|
|
// einen anderen Ton als die App darunter.
|
|
useEffect(() => {
|
|
const marke = document.querySelector('meta[name="theme-color"]')
|
|
if (marke) marke.setAttribute("content", theme === "nacht" ? "#0C1014" : "#F2F2F7")
|
|
}, [theme])
|
|
|
|
return [theme, setzen]
|
|
}
|
|
|
|
/* useBoden() ist am 2026-08-31 entfallen: der Bodenschatten unter dem
|
|
Fahrzeug ist weg (Ansage des Eigentuemers), damit hatte der Schalter
|
|
nichts mehr zu schalten. Der abgelegte Wert unter "dm360.boden" bleibt
|
|
liegen und stoert nicht - er wird schlicht nicht mehr gelesen. */
|
|
|
|
/** Sichtbarkeit der Tab-Beschriftungen, ebenfalls dauerhaft gespeichert. */
|
|
export function useTabBeschriftung(): [boolean, (an: boolean) => void] {
|
|
const an = useSyncExternalStore(
|
|
TABBESCHRIFTUNG.horchen,
|
|
TABBESCHRIFTUNG.stand,
|
|
TABBESCHRIFTUNG.stand,
|
|
)
|
|
const setzen = useCallback((neu: boolean) => TABBESCHRIFTUNG.setzen(neu), [])
|
|
return [an, setzen]
|
|
}
|