e1ebe6d694
Systematischer Bildvergleich der companion-app gegen das HA-Panel (companion-app/AGENTS.md Kapitel 1-11): Farbtoken/Radien aus dem falschen Stylesheet gelesen (iOS-Overlay statt Basis-CSS, betraf fast jede Kachel/Farbe/Radius app-weit), vier app-weite @audi-dash/ui-Bugs (Switch/Seg/Feld rot statt neutral bzw. falsche Feldbreite), Reifen-Seite strukturell neu gebaut (beide Radsätze gleichzeitig statt Umschalter, editierbare Felder, Anzugsmoment-/km-Korrektur), Standort-Feature komplett neu (fehlte bisher ganz), sowie diverse Struktur-/Typografie-/Datenlücken in MeinAudi/Service/Versicherung/Sicherheit/Fahrten/Tanken/Statistik/Batterie/Einstellungen. Manifest auf 2026.8.30.2 angehoben, OTA-Bündel neu gebaut und in audi_ha_test verifiziert (sauberer Neustart, keine Tracebacks). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
/**
|
|
* 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 { VierRinge } from "./screens/Typenschild"
|
|
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>
|
|
{/* `kopf` wird nur in der schmalen Spalte gerendert (breit hat seinen
|
|
eigenen Seitenleisten-Kopf, siehe unten) - dort sind die Audi-Ringe
|
|
oben rechts der einzige Zugang zu Einstellungen, das Zahnrad
|
|
entfällt, "damit oben rechts nur ein Element sitzt" (Design-Audit
|
|
2026-08-13, siehe .rings/.profilbtn in audi-dashboard-ios.css).
|
|
Companion-app zeigte hier bisher immer ein Zahnrad, unabhängig von
|
|
der Bildschirmbreite - echte Abweichung, nicht nur Kosmetik: es gab
|
|
keinen Rings-Zugang in Telefonbreite. */}
|
|
<button
|
|
type="button"
|
|
className="dm-kopf__aktion"
|
|
aria-label="Einstellungen"
|
|
aria-current={route.name === "einst" ? "page" : undefined}
|
|
onClick={() => geheZu("einst")}
|
|
>
|
|
<VierRinge hoehe={17} />
|
|
</button>
|
|
</header>
|
|
)
|
|
|
|
if (breit) {
|
|
return (
|
|
<>
|
|
<nav className="dm-seitenleiste" aria-label="Hauptbereiche">
|
|
<div className="dm-seitenleiste__marke">
|
|
<VierRinge hoehe={18} />
|
|
<span className="ads-eyebrow">DataMetric360</span>
|
|
</div>
|
|
{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>
|
|
)
|
|
}
|