Phase 5: App-Shell mit responsivem Layout, Navigation und Theme
Zwei Layouts aus derselben Zusammensetzung: unter 1000px Tab-Leiste unten, darueber Seitenleiste links mit Brotkrume statt Zurueck-Pfeil. Navigation ist wie im alten Panel ein flacher Zustand mit fester ZURUECK-Tabelle statt eines Router-Pakets - die Verschachtelung ist hoechstens dreistufig. Theme und Tab-Beschriftung ueberleben einen Neustart (localStorage, abgesichert fuer den Privatmodus), Startwert folgt der Systemeinstellung. Symbole aus dem alten Panel uebernommen, damit App und Panel gleich aussehen. Neun Rendertests belegen die Abnahmekriterien: Layoutwechsel in beide Richtungen, Zurueck-Ziel ueber die Tabelle, Zahnrad statt Tab-Leiste fuer die Einstellungen, Theme-Persistenz.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Tag/Nacht-Umschaltung, übernommen aus dem alten Panel (A6 des Design-Audits):
|
||||
* gespeicherte Wahl schlägt Systemeinstellung, Standard ist "nacht".
|
||||
* Alle localStorage-Zugriffe sind abgesichert, weil sie im Privatmodus mancher
|
||||
* Browser werfen.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
|
||||
export type Theme = "nacht" | "tag"
|
||||
|
||||
const SCHLUESSEL = "dm360.theme"
|
||||
|
||||
function gespeichertLesen(): Theme | null {
|
||||
try {
|
||||
const wert = localStorage.getItem(SCHLUESSEL)
|
||||
return wert === "nacht" || wert === "tag" ? wert : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function gespeichertSchreiben(theme: Theme): void {
|
||||
try {
|
||||
localStorage.setItem(SCHLUESSEL, theme)
|
||||
} catch {
|
||||
// Privatmodus — die Wahl gilt dann nur für diese Sitzung
|
||||
}
|
||||
}
|
||||
|
||||
export function themeStartwert(): Theme {
|
||||
const gespeichert = gespeichertLesen()
|
||||
if (gespeichert) return gespeichert
|
||||
const hell =
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.matchMedia === "function" &&
|
||||
window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
return hell ? "tag" : "nacht"
|
||||
}
|
||||
|
||||
export function useTheme(): [Theme, (theme: Theme) => void] {
|
||||
const [theme, setzeTheme] = useState<Theme>(themeStartwert)
|
||||
|
||||
const setzen = useCallback((neu: Theme) => {
|
||||
setzeTheme(neu)
|
||||
gespeichertSchreiben(neu)
|
||||
}, [])
|
||||
|
||||
// Farbe der Systemleisten mitziehen, damit auf dem Handy kein heller
|
||||
// Balken über einer dunklen App steht.
|
||||
useEffect(() => {
|
||||
const marke = document.querySelector('meta[name="theme-color"]')
|
||||
if (marke) marke.setAttribute("content", theme === "nacht" ? "#161b23" : "#ffffff")
|
||||
}, [theme])
|
||||
|
||||
return [theme, setzen]
|
||||
}
|
||||
|
||||
/** Sichtbarkeit der Tab-Beschriftungen, ebenfalls dauerhaft gespeichert. */
|
||||
const BESCHRIFTUNG_SCHLUESSEL = "dm360.tabbeschriftung"
|
||||
|
||||
export function useTabBeschriftung(): [boolean, (an: boolean) => void] {
|
||||
const [an, setzeAn] = useState<boolean>(() => {
|
||||
try {
|
||||
return localStorage.getItem(BESCHRIFTUNG_SCHLUESSEL) !== "aus"
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
const setzen = useCallback((neu: boolean) => {
|
||||
setzeAn(neu)
|
||||
try {
|
||||
localStorage.setItem(BESCHRIFTUNG_SCHLUESSEL, neu ? "an" : "aus")
|
||||
} catch {
|
||||
// Privatmodus
|
||||
}
|
||||
}, [])
|
||||
|
||||
return [an, setzen]
|
||||
}
|
||||
Reference in New Issue
Block a user