Paritaetsrunden 2 und 3, Designpruefung umgesetzt (2026.8.30.8)
Drei zusammenhaengende Runden, alle live bei 375x812 gegen audi_ha_test geprueft und in beiden Codebasen angewandt. Paritaetsrunde 2: der gemeinsame Rahmen war das eigentliche Problem - Seitenrand, Kachelabstaende, Kopfleiste, Tableiste und vier Bausteine des Design-Systems trugen noch den Stand vor der iOS-Entscheidung. Dazu sieben Bildschirme neu aufgebaut. Zwei Panel-Fehler dabei mitbehoben: teaser() zeigte unter "Letzte Fahrt" den aeltesten Datensatz, und "Daten bearbeiten" war ein toter Knopf. Paritaetsrunde 3: das Panel hat nie Audi Type gerendert. @font-face in einem Shadow Root wird ignoriert - Schriftschnitte registriert der Browser pro Dokument, nie pro Shadow Tree. Die drei Schnitte liegen jetzt in audi-dashboard-schriften.css und werden ins Dokument gehaengt. Damit erledigt sich eine ganze Reihe von "die Schrift sieht anders aus"-Eindruecken: die beiden Anwendungen zeigten tatsaechlich verschiedene Schriften. Ausserdem "Daten bearbeiten" im Panel gebaut und portiert, das Zeilenmenue entfernt und die letzten neun Bildschirme angeglichen. Designpruefung: die fuenf Punkte der Reihenfolge. Beim vierten hat das Messen den Befund veraendert - gezaehlt waren die deklarierten Groessen, wirksam war laengst eine saubere Sieben-Schritt-Skala mit 27 Ausreissern; die sind jetzt auf den naechsten Schritt gezogen, keiner verschiebt sich um mehr als 1px. Zuletzt: die Standortvorschau zeichnete die falsche Nadel (das Panel wechselt den Icon-Satz ab 34px, die App nahm immer den grossen), und der Kopfabstand der App ist auf den sicheren Bereich reduziert - die 56px des Panels liegen dort unter der Kopfleiste von Home Assistant, in der nativen Huelle steht darueber nichts. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+129
-17
@@ -7,15 +7,18 @@
|
||||
* liegt bei 1000px.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import type { PointerEvent as ReactPointerEvent, ReactNode, RefObject } from "react"
|
||||
|
||||
import { TabBar } from "@audi-dash/ui"
|
||||
|
||||
import { standAlter } from "./format"
|
||||
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 {
|
||||
SymbolAktualisieren,
|
||||
SymbolAudi,
|
||||
SymbolFahrten,
|
||||
SymbolStatistik,
|
||||
@@ -31,10 +34,70 @@ export interface ShellProps {
|
||||
tabBeschriftung: boolean
|
||||
/** Wird über der Kopfzeile eingeblendet (Offline-Hinweis, Warteschlange). */
|
||||
hinweis?: ReactNode | undefined
|
||||
/** Zeitpunkt des letzten Datenstands — steht als „vor 3 Std." unter dem Titel. */
|
||||
statusStand?: string | null | undefined
|
||||
/** Neuabfrage beim Fahrzeug, ausgelöst durch Ziehen zum Aktualisieren. */
|
||||
beiAktualisieren?: (() => Promise<void>) | undefined
|
||||
/** Heißt `children`, weil JSX-Verschachtelung genau diesen Namen erwartet. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/** Ziehen zum Aktualisieren, 1:1 nach der ptr-Logik des Panels. */
|
||||
const PTR_SCHWELLE = 56
|
||||
|
||||
function useZiehenAktualisieren(
|
||||
inhalt: RefObject<HTMLElement | null>,
|
||||
beiAktualisieren: (() => Promise<void>) | undefined,
|
||||
) {
|
||||
const [zug, setzeZug] = useState(0)
|
||||
const [laedt, setzeLaedt] = useState(false)
|
||||
const start = useRef<number | null>(null)
|
||||
|
||||
if (!beiAktualisieren) {
|
||||
return { zug: 0, laedt: false, handler: {} as Record<string, never> }
|
||||
}
|
||||
|
||||
const beenden = () => {
|
||||
if (start.current === null) return
|
||||
start.current = null
|
||||
if (zug >= PTR_SCHWELLE) {
|
||||
setzeLaedt(true)
|
||||
void beiAktualisieren().finally(() => {
|
||||
setzeLaedt(false)
|
||||
setzeZug(0)
|
||||
})
|
||||
} else {
|
||||
setzeZug(0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
zug,
|
||||
laedt,
|
||||
handler: {
|
||||
onPointerDown: (e: ReactPointerEvent) => {
|
||||
if (!e.isPrimary || laedt) return
|
||||
if ((inhalt.current?.scrollTop ?? 0) > 0) return
|
||||
start.current = e.clientY
|
||||
setzeZug(0)
|
||||
},
|
||||
onPointerMove: (e: ReactPointerEvent) => {
|
||||
if (start.current === null || !e.isPrimary) return
|
||||
const dy = e.clientY - start.current
|
||||
if (dy > 0 && (inhalt.current?.scrollTop ?? 0) <= 0) {
|
||||
// Widerstand wie bei iOS: halbe Strecke, gedeckelt bei 90px.
|
||||
setzeZug(Math.min(90, dy * 0.5))
|
||||
} else {
|
||||
start.current = null
|
||||
setzeZug(0)
|
||||
}
|
||||
},
|
||||
onPointerUp: beenden,
|
||||
onPointerCancel: beenden,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const BEREICHE: { key: Hauptbereich; label: string; icon: ReactNode }[] = [
|
||||
{ key: "home", label: "Übersicht", icon: <SymbolUebersicht /> },
|
||||
{ key: "audi", label: "Mein Audi", icon: <SymbolAudi /> },
|
||||
@@ -43,13 +106,33 @@ const BEREICHE: { key: Hauptbereich; label: string; icon: ReactNode }[] = [
|
||||
{ key: "fuel", label: "Tanken", icon: <SymbolTanken /> },
|
||||
]
|
||||
|
||||
export function Shell({ route, geheZu, tabBeschriftung, hinweis, children }: ShellProps) {
|
||||
export function Shell({
|
||||
route,
|
||||
geheZu,
|
||||
tabBeschriftung,
|
||||
hinweis,
|
||||
statusStand,
|
||||
beiAktualisieren,
|
||||
children,
|
||||
}: ShellProps) {
|
||||
const breit = useBreiterBildschirm()
|
||||
const aktiverBereich = bereichVon(route.name)
|
||||
const zurueckZiel = ZURUECK[route.name]
|
||||
|
||||
const unterseite = !istHauptbereich(route.name)
|
||||
const inhaltRef = useRef<HTMLElement | null>(null)
|
||||
const ptr = useZiehenAktualisieren(inhaltRef, breit ? undefined : beiAktualisieren)
|
||||
|
||||
// Der Text tickt mit, wie standAlterTicken() im Panel (Sekundentakt).
|
||||
const [, neuZeichnen] = useState(0)
|
||||
useEffect(() => {
|
||||
if (route.name !== "home") return undefined
|
||||
const uhr = window.setInterval(() => neuZeichnen((n) => n + 1), 1000)
|
||||
return () => window.clearInterval(uhr)
|
||||
}, [route.name])
|
||||
|
||||
const kopf = (
|
||||
<header className="dm-kopf">
|
||||
<header className={unterseite ? "dm-kopf dm-kopf--unterseite" : "dm-kopf"}>
|
||||
{zurueckZiel && !breit ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -60,27 +143,38 @@ export function Shell({ route, geheZu, tabBeschriftung, hinweis, children }: She
|
||||
<SymbolZurueck groesse={22} />
|
||||
</button>
|
||||
) : null}
|
||||
{/* Kein Eyebrow: das Panel blendet seinen (`.eyebrow{display:none}` in
|
||||
audi-dashboard-ios.css) auf Telefonbreite vollständig aus - der feste
|
||||
Text "DataMetric360" stand hier über jedem Bildschirm, im Panel steht
|
||||
dort nichts. */}
|
||||
<div className="dm-kopf__titel">
|
||||
<span className="ads-eyebrow">DataMetric360</span>
|
||||
<div className="ads-title">{TITEL[route.name]}</div>
|
||||
{/* Datenstand nur auf der Übersicht, wie `#sync` im Panel. */}
|
||||
{route.name === "home" && (
|
||||
<div className="dm-stand">
|
||||
<SymbolAktualisieren groesse={13} />
|
||||
<span>{standAlter(statusStand)}</span>
|
||||
</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>
|
||||
Nur auf den Tab-Ebenen: auf Unterseiten blendet das Panel die Ringe
|
||||
aus (`#marke`.style.display in render()), dort steht links der
|
||||
Zurück-Pfeil. */}
|
||||
{!unterseite ? (
|
||||
<button
|
||||
type="button"
|
||||
className="dm-kopf__aktion"
|
||||
aria-label="Einstellungen"
|
||||
onClick={() => geheZu("einst")}
|
||||
>
|
||||
<VierRinge hoehe={17} />
|
||||
</button>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -143,7 +237,25 @@ export function Shell({ route, geheZu, tabBeschriftung, hinweis, children }: She
|
||||
<div className="dm-spalte">
|
||||
{hinweis}
|
||||
{kopf}
|
||||
<main className="dm-scroll dm-inhalt">{children}</main>
|
||||
{/* Ziehen zum Aktualisieren wie im Panel (`.ptr`): das Band wächst
|
||||
während der Geste aus der Höhe 0 heraus. */}
|
||||
<div
|
||||
className={ptr.laedt ? "dm-ptr dm-ptr--dreht" : "dm-ptr"}
|
||||
style={{ height: ptr.laedt ? "56px" : `${ptr.zug}px` }}
|
||||
aria-hidden={ptr.zug === 0 && !ptr.laedt}
|
||||
>
|
||||
<SymbolAktualisieren groesse={14} />
|
||||
<span>
|
||||
{ptr.laedt
|
||||
? "Aktualisieren …"
|
||||
: ptr.zug >= 56
|
||||
? "Loslassen zum Aktualisieren"
|
||||
: "Ziehen zum Aktualisieren"}
|
||||
</span>
|
||||
</div>
|
||||
<main className="dm-scroll dm-inhalt" ref={inhaltRef} {...ptr.handler}>
|
||||
{children}
|
||||
</main>
|
||||
<TabBar
|
||||
items={BEREICHE}
|
||||
activeKey={aktiverBereich}
|
||||
|
||||
Reference in New Issue
Block a user