Files
audi-app/companion-app/src/Shell.tsx
T
tobias e1ebe6d694 Vollständiger Panel-companion-app-Paritätsaudit (11 Kapitel)
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>
2026-08-30 12:21:58 +02:00

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>
)
}