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,144 @@
|
||||
/**
|
||||
* Rahmen der App: Kopfzeile, Navigation, Inhaltsfläche.
|
||||
*
|
||||
* Zwei Layouts aus derselben Zusammensetzung — die entscheidende Anforderung
|
||||
* aus dem Design-Auftrag: schmal (Handy) mit Tab-Leiste unten, breit (Tablet,
|
||||
* eingebettet in Home Assistant) mit Seitenleiste links. Der Umschaltpunkt
|
||||
* liegt bei 1000px.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { TabBar } from "@audi-dash/ui"
|
||||
|
||||
import { useBreiterBildschirm } from "./hooks/useBreiterBildschirm"
|
||||
import type { Hauptbereich, Route, SeitenName } from "./navigation"
|
||||
import { TITEL, ZURUECK, bereichVon, istHauptbereich } from "./navigation"
|
||||
import {
|
||||
SymbolAudi,
|
||||
SymbolFahrten,
|
||||
SymbolStatistik,
|
||||
SymbolTanken,
|
||||
SymbolUebersicht,
|
||||
SymbolZahnrad,
|
||||
SymbolZurueck,
|
||||
} from "./symbole"
|
||||
|
||||
export interface ShellProps {
|
||||
route: Route
|
||||
geheZu: (name: SeitenName, id?: string) => void
|
||||
tabBeschriftung: boolean
|
||||
/** Wird über der Kopfzeile eingeblendet (Offline-Hinweis, Warteschlange). */
|
||||
hinweis?: ReactNode | undefined
|
||||
/** Heißt `children`, weil JSX-Verschachtelung genau diesen Namen erwartet. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const BEREICHE: { key: Hauptbereich; label: string; icon: ReactNode }[] = [
|
||||
{ key: "home", label: "Übersicht", icon: <SymbolUebersicht /> },
|
||||
{ key: "audi", label: "Mein Audi", icon: <SymbolAudi /> },
|
||||
{ key: "trips", label: "Fahrten", icon: <SymbolFahrten /> },
|
||||
{ key: "stat", label: "Statistik", icon: <SymbolStatistik /> },
|
||||
{ key: "fuel", label: "Tanken", icon: <SymbolTanken /> },
|
||||
]
|
||||
|
||||
export function Shell({ route, geheZu, tabBeschriftung, hinweis, children }: ShellProps) {
|
||||
const breit = useBreiterBildschirm()
|
||||
const aktiverBereich = bereichVon(route.name)
|
||||
const zurueckZiel = ZURUECK[route.name]
|
||||
|
||||
const kopf = (
|
||||
<header className="dm-kopf">
|
||||
{zurueckZiel && !breit ? (
|
||||
<button
|
||||
type="button"
|
||||
className="dm-kopf__zurueck"
|
||||
aria-label="Zurück"
|
||||
onClick={() => geheZu(zurueckZiel)}
|
||||
>
|
||||
<SymbolZurueck groesse={22} />
|
||||
</button>
|
||||
) : null}
|
||||
<div className="dm-kopf__titel">
|
||||
<span className="ads-eyebrow">DataMetric360</span>
|
||||
<div className="ads-title">{TITEL[route.name]}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="dm-kopf__aktion"
|
||||
aria-label="Einstellungen"
|
||||
aria-current={route.name === "einst" ? "page" : undefined}
|
||||
onClick={() => geheZu("einst")}
|
||||
>
|
||||
<SymbolZahnrad groesse={22} />
|
||||
</button>
|
||||
</header>
|
||||
)
|
||||
|
||||
if (breit) {
|
||||
return (
|
||||
<>
|
||||
<nav className="dm-seitenleiste" aria-label="Hauptbereiche">
|
||||
<span className="ads-eyebrow dm-seitenleiste__marke">DataMetric360</span>
|
||||
{BEREICHE.map((bereich) => (
|
||||
<button
|
||||
key={bereich.key}
|
||||
type="button"
|
||||
className={
|
||||
bereich.key === aktiverBereich
|
||||
? "dm-seitenleiste__punkt dm-seitenleiste__punkt--an"
|
||||
: "dm-seitenleiste__punkt"
|
||||
}
|
||||
aria-current={bereich.key === aktiverBereich ? "page" : undefined}
|
||||
onClick={() => geheZu(bereich.key)}
|
||||
>
|
||||
{bereich.icon}
|
||||
<span>{bereich.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="dm-seitenleiste__fuss">
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
route.name === "einst"
|
||||
? "dm-seitenleiste__punkt dm-seitenleiste__punkt--an"
|
||||
: "dm-seitenleiste__punkt"
|
||||
}
|
||||
onClick={() => geheZu("einst")}
|
||||
>
|
||||
<SymbolZahnrad />
|
||||
<span>Einstellungen</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="dm-spalte">
|
||||
{hinweis}
|
||||
{!istHauptbereich(route.name) && zurueckZiel ? (
|
||||
<div className="dm-brotkrume">
|
||||
<button type="button" onClick={() => geheZu(zurueckZiel)}>
|
||||
<SymbolZurueck groesse={16} />
|
||||
<span>{TITEL[zurueckZiel]}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<main className="dm-scroll dm-inhalt dm-inhalt--breit">{children}</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dm-spalte">
|
||||
{hinweis}
|
||||
{kopf}
|
||||
<main className="dm-scroll dm-inhalt">{children}</main>
|
||||
<TabBar
|
||||
items={BEREICHE}
|
||||
activeKey={aktiverBereich}
|
||||
onChange={(schluessel) => geheZu(schluessel as Hauptbereich)}
|
||||
iconOnly={!tabBeschriftung}
|
||||
className="dm-tableiste"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user