Befundliste des Besitzers: drei echte Fehler, dazu Feinschliff (2026.8.30.9)

Fahrzeug-Pin auf der Standortkarte fehlte: die Marker-Effekte lesen Refs,
die erst im asynchronen Leaflet-Import gesetzt werden. Beim ersten Lauf leer,
Effekt steigt aus - und weil die Koordinaten sich danach nicht mehr aendern,
lief er nie wieder. Ein Bereitschaftsschalter in den Abhaengigkeiten behebt
das fuer Marker, Kachelebene und eigene Position zugleich.

Kachelpfeil auf "Mein Audi" sass auf halber Kachelhoehe statt oben neben der
Ueberschrift; Auswahlfelder hatten nach appearance:none ueberhaupt keinen
Aufklapp-Pfeil mehr.

SmartDeal: das Datumsfeld war fehlerhaft und die Abfrage, bis wann der Rabatt
gilt, fehlte ganz. Das Panel fragt beim Einschalten und zeigt danach nur an -
genau daran haengt, dass der Rabatt von selbst auslaufen kann. Portiert.

Adressen statt Koordinaten: Nominatim drosselt uns inzwischen. Beide Apps
fielen korrekt zurueck, merkten sich Treffer aber nur bis zum Neuladen. Jetzt
dauerhaft im selben Cache wie die Tankstellensuche.

Ziehen zum Aktualisieren auf dem Geraet: dem Scrollbehaelter fehlte
overscroll-behavior, das native Overscroll nahm die Geste weg - derselbe
Befund, den das Panel am 2026-08-17 hatte.

Ausserdem: Platzhalter wieder in voller Groesse, FIN statt Fahrgestellnummer,
Details als Hinweiszeile, Chevron an der Batteriespannung, Reifenzeile
"aktuell montiert:", Zahnrad statt Stift am Anzugsmoment, Radkilometer in der
Zahlenschrift, Layer-Icon zentriert, share-s beim Teilen, Schliessen-Knopf nur
bei aufgezogener Karte.

Nicht nachstellbar und zurueckgemeldet: "Fahrten/Tanken, grauer Hintergrund" -
Panel und App sind dort deckungsgleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 23:12:33 +02:00
parent f38907991d
commit a08922e9a0
16 changed files with 396 additions and 110 deletions
+55 -3
View File
@@ -23,15 +23,27 @@ export interface Koordinate {
const CACHE_SCHLUESSEL = "dm360.geocache"
function cacheLesen(): Record<string, Koordinate | null> {
function cacheLesen(): Record<string, Koordinate | string | null> {
try {
const roh = localStorage.getItem(CACHE_SCHLUESSEL)
return roh ? (JSON.parse(roh) as Record<string, Koordinate | null>) : {}
return roh ? (JSON.parse(roh) as Record<string, Koordinate | string | null>) : {}
} catch {
return {}
}
}
/** Adresstext im selben Ablagefach merken (Schlüssel mit "rev:"-Vorsatz). */
function adresseMerken(schluessel: string, adresse: string): void {
try {
const roh = localStorage.getItem(CACHE_SCHLUESSEL)
const cache = roh ? (JSON.parse(roh) as Record<string, unknown>) : {}
cache[schluessel] = adresse
localStorage.setItem(CACHE_SCHLUESSEL, JSON.stringify(cache))
} catch {
// Privatmodus oder volle Ablage — dann eben ohne Cache.
}
}
function cacheSchreiben(suche: string, treffer: Koordinate | null): void {
try {
const cache = cacheLesen()
@@ -42,11 +54,51 @@ function cacheSchreiben(suche: string, treffer: Koordinate | null): void {
}
}
/**
* Koordinaten → Adresse. Der Cache ist hier genauso wichtig wie bei der
* Vorwärtsrichtung, aus einem anderen Grund: Nominatim drosselt und sperrt
* bei häufigen Abrufen, und die Antwort kommt dann ohne CORS-Kopf zurück —
* im Browser ununterscheidbar von einem Netzfehler. Ohne dauerhaften Cache
* stand an einer längst aufgelösten Stelle plötzlich wieder nur ein
* Koordinatenpaar. Auf drei Nachkommastellen gerundet (~110 m), damit ein
* paar Meter Abweichung keinen neuen Abruf auslösen.
*/
export async function koordinatenAufloesen(lat: number, lon: number): Promise<string | null> {
const schluessel = `rev:${lat.toFixed(3)},${lon.toFixed(3)}`
const cache = cacheLesen()
const gemerkt = cache[schluessel]
if (typeof gemerkt === "string") return gemerkt
try {
const antwort = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`,
{ headers: { "Accept-Language": "de" } },
)
const daten = (await antwort.json()) as {
address?: Record<string, string>
display_name?: string
}
const a = daten.address ?? {}
const strasse = [a["road"], a["house_number"]].filter(Boolean).join(" ")
const ort = [a["postcode"], a["city"] ?? a["town"] ?? a["village"]].filter(Boolean).join(" ")
const adresse = [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null
// Nur echte Treffer merken: eine gedrosselte Antwort ist kein "hier gibt
// es keine Adresse", die soll beim nächsten Mal erneut versucht werden.
if (adresse) adresseMerken(schluessel, adresse)
return adresse
} catch {
return null
}
}
export async function adresseAufloesen(adresse: string | null | undefined): Promise<Koordinate | null> {
const suche = (adresse ?? "").trim()
if (!suche) return null
const cache = cacheLesen()
if (Object.prototype.hasOwnProperty.call(cache, suche)) return cache[suche] ?? null
if (Object.prototype.hasOwnProperty.call(cache, suche)) {
const gemerkt = cache[suche]
return typeof gemerkt === "object" ? gemerkt : null
}
let treffer: Koordinate | null = null
try {
+63 -22
View File
@@ -6,7 +6,7 @@
import { useEffect, useRef, useState } from "react"
import { ActionButton, Feld, Seg, Switch, Tile } from "@audi-dash/ui"
import { ActionButton, Feld, Popup, Seg, Switch, Tile } from "@audi-dash/ui"
import { DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api"
import type { Profil } from "../api"
@@ -14,7 +14,7 @@ import { eigeneVersion } from "../daten/appVersion"
import { useDaten } from "../daten/DatenKontext"
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
import { datumZeit, isoTag } from "../format"
import { datum, datumZeit, isoTag } from "../format"
import { useBoden, useTheme } from "../theme"
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
@@ -59,6 +59,10 @@ export function Einstellungen({
const [laeuft, setzeLaeuft] = useState(false)
const [gespeichert, setzeGespeichert] = useState(false)
const [importOffen, setzeImportOffen] = useState(false)
/* Abfrage "bis wann gilt der Rabatt", die das Einschalten des SmartDeals
ausloest (sdpopup im Panel). */
const [sdAbfrage, setzeSdAbfrage] = useState(false)
const [sdEntwurf, setzeSdEntwurf] = useState("")
const [otaLaeuft, setzeOtaLaeuft] = useState(false)
const [otaFehler, setzeOtaFehler] = useState<string | null>(null)
const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false)
@@ -401,35 +405,36 @@ export function Einstellungen({
>
<Switch
checked={fahrzeug.smartdeal.aktiv}
onChange={(an) =>
onChange={(an) => {
/* Einschalten fragt zuerst, bis wann der Rabatt gilt - ohne
dieses Datum kann die App ihn nie von selbst auslaufen
lassen, und genau daran haengt smartdeal.aktiv im
Profiladapter. Ausschalten braucht keine Rueckfrage.
Gleiches Verhalten wie data-sd/sdpopup im Panel. */
if (an) {
setzeSdEntwurf(fahrzeug.smartdeal.laeuftAb ?? "")
setzeSdAbfrage(true)
return
}
void profilSpeichern({
fahrzeug: {
...fahrzeug,
smartdeal: { ...fahrzeug.smartdeal, aktiv: an },
smartdeal: { ...fahrzeug.smartdeal, aktiv: false },
},
})
}
}}
aria-label="SmartDeal aktiv"
/>
</Feld>
{fahrzeug.smartdeal.aktiv ? (
<>
<Feld label="Gültig bis" last>
<input
className="dm-eingabe"
type="date"
value={fahrzeug.smartdeal.laeuftAb ?? ""}
onChange={(e) =>
void profilSpeichern({
fahrzeug: {
...fahrzeug,
smartdeal: { ...fahrzeug.smartdeal, laeuftAb: e.target.value || null },
},
})
}
/>
</Feld>
</>
<Feld label="Gültig bis" last>
{/* Reine Anzeige, wie im Panel: geaendert wird das Datum ueber den
Schalter, der die Abfrage oeffnet - so gibt es nur einen Weg
dorthin und keine zwei Eingabestellen fuer denselben Wert. */}
<span>
{fahrzeug.smartdeal.laeuftAb ? datum(fahrzeug.smartdeal.laeuftAb) : ""}
</span>
</Feld>
) : (
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
Ersparnis und Bildmarke werden ausgeblendet
@@ -437,6 +442,42 @@ export function Einstellungen({
)}
</Tile>
<Popup
open={sdAbfrage}
onClose={() => setzeSdAbfrage(false)}
variant="form"
anchor="center"
>
<span className="ads-eyebrow">SmartDeal</span>
<Feld label="Gültig bis" last>
<input
className="dm-eingabe"
type="date"
value={sdEntwurf}
onChange={(e) => setzeSdEntwurf(e.target.value)}
/>
</Feld>
<div className="dm-sd-knoepfe">
<ActionButton onClick={() => setzeSdAbfrage(false)}>Abbrechen</ActionButton>
<ActionButton
variant="primary"
disabled={!sdEntwurf}
onClick={() => {
if (!sdEntwurf) return
setzeSdAbfrage(false)
void profilSpeichern({
fahrzeug: {
...fahrzeug,
smartdeal: { aktiv: true, laeuftAb: sdEntwurf },
},
})
}}
>
Bestätigen
</ActionButton>
</div>
</Popup>
{/* „Nacht beginnt"/„Nacht endet" sind entfallen: das Panel fuehrt sie
in den Einstellungen nicht, die Nachtgrenze steht dort nur als
Erlaeuterung auf der Statistik-Seite. */}
+10 -9
View File
@@ -140,8 +140,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
<Werteliste
kinder={
<>
<Wertzeile label="Fahrgestellnummer" wert={fahrzeug.fin || ""} />
<Wertzeile label="Details" wert={fahrzeug.details || ""} />
<Wertzeile label="FIN" wert={fahrzeug.fin || ""} />
{/* Die technischen Daten stehen eine Ebene tiefer; hier nur der
Hinweis, dass es sie gibt, statt einer abgeschnittenen
Motorzeile. */}
<Wertzeile label="Details" wert="Informationen über das Auto" hinweis />
{/* Leerer Wert statt Strich, wie vAudi(): das Panel gibt hier
den Rohwert aus. */}
<Wertzeile label="Erstzulassung" wert={fahrzeug.erstzulassung} />
@@ -167,10 +170,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
/>
<button type="button" className="dm-wertzeile dm-wertzeile--knopf" onClick={() => geheZu("battverlauf")}>
<dt>Batteriespannung</dt>
{/* Ohne Chevron: das Panel gibt der Zeile nur den
Zeiger-Cursor, kein eigenes Pfeilsymbol. */}
<dd>
{fahrzeug.batteriespannung != null ? `${de(fahrzeug.batteriespannung, 1)} V` : "unbekannt"}
<svg className="dm-wertzeile__chevron" viewBox="0 0 6 10" aria-hidden="true">
<polyline points="1,1 5,5 1,9" />
</svg>
</dd>
</button>
</>
@@ -233,15 +237,12 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
<Werteliste
kinder={
<>
<Wertzeile
label={`montiert: ${fahrzeug.reifen.aktiv}räder`}
wert={(aktiverSatz["mass"] as string) || ""}
/>
<Wertzeile label="aktuell montiert:" wert={`${fahrzeug.reifen.aktiv}räder`} />
<Wertzeile label="Laufleistung" wert={deOderStrich(aktiverSatz.km) + " km"} />
<Wertzeile
label="Wechseltermin"
wert={datum(fahrzeug.reifen.wechsel["datum"] as string)}
zusatz={fahrzeug.reifen.wechsel["vereinbart"] ? "vereinbart" : ""}
zusatz={fahrzeug.reifen.wechsel["vereinbart"] ? "vereinbart" : undefined}
/>
</>
}
+3 -3
View File
@@ -15,7 +15,7 @@ import { DIENST_DOMAIN } from "../api"
import { useDaten } from "../daten/DatenKontext"
import type { Reifensatz, ReifenArchivEintrag } from "../daten/profilAdapter"
import { datum, de, deOderStrich } from "../format"
import { SymbolEdit, SymbolPaket } from "../symbole"
import { SymbolEdit, SymbolPaket, SymbolZahnradVoll } from "../symbole"
import { Wertzeile, Werteliste, bestaetigen } from "./bausteine"
import { Bild } from "./Bild"
import { kalenderDateiLaden } from "./kalender"
@@ -126,7 +126,7 @@ export function Reifen() {
<div className="dm-radkopf-row">
<div className="dm-radkopf">
<span className="dm-tile-headline">{satzLabel}</span>
<div className="dm-reifenzeile">
<div className="ads-fig dm-reifenzeile">
<IconButton
aria-label={`${satzLabel} Kilometerstand ändern`}
onClick={() => {
@@ -275,7 +275,7 @@ export function Reifen() {
setzeNmOffen((o) => !o)
}}
>
<SymbolEdit groesse={16} />
<SymbolZahnradVoll groesse={17} />
</IconButton>
</div>
{nmOffen && (
+56 -47
View File
@@ -11,6 +11,7 @@
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"
import { useDaten } from "../daten/DatenKontext"
import { koordinatenAufloesen } from "../daten/geokodierung"
import { de } from "../format"
import type { Fahrzeug } from "../daten/profilAdapter"
import { SymbolTanken } from "../symbole"
@@ -37,8 +38,10 @@ const CI = {
'M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z',
planRoute:
'm18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z',
upload:
'M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z',
// share-s aus dem Audi-CI-Satz (vom Nutzer geliefert, verbatim). Loest
// beim Teilen das upload-Icon ab, das dort nur ersatzweise stand.
shareS:
'm11.5 1.8 3.85 3.85-.7.7L12 3.71V15h-1V3.7L8.35 6.36l-.7-.7zM5 8h4v1H6v11h11V9h-3.02V8H18v13H5z',
parking:
'M0 2.5A2.5 2.5 0 0 1 2.5 0H23v20.5a2.5 2.5 0 0 1-2.5 2.5H0zM2.5 1C1.67 1 1 1.67 1 2.5V22h19.5c.83 0 1.5-.67 1.5-1.5V1zm11.59 6.35C13.6 7 13.02 7 12.5 7H8v6h4.5c.52 0 1.11-.01 1.59-.35.44-.3.91-1 .91-2.65 0-1.66-.47-2.34-.91-2.65M8 14v3H7V6h5.53c.5 0 1.38 0 2.13.53C15.47 7.09 16 8.16 16 10s-.53 2.9-1.34 3.47a3.6 3.6 0 0 1-2.13.53z',
// poi-car-l: Umriss (Ring) + Vollton-Silhouette dahinter, siehe
@@ -61,6 +64,11 @@ function ciIcon(d: string, px: number): string {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`
}
/* Wie ciIcon, aber mit der Korrektur fuer das Layer-Symbol (siehe dort). */
function ciIconZentriert(d: string, px: number): string {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true"><g transform="translate(1.44 1.44) scale(0.88) translate(-0.55 0.2)"><path fill="currentColor" d="${d}"/></g></svg>`
}
function fahrzeugMarkerHtml(px = 38): string {
// Groessenwechsel wie fahrzeugMarkerSVG() im Panel: ab 34px die grosse
// Zeichnung (48er-Raster), darunter die kleine (24er). Die Vorschaukarte der
@@ -118,34 +126,15 @@ function standortZustand(
}
}
/* Reverse-Geocoding über Nominatim (OpenStreetMap), wie standortAdresseAufloesen()
im Panel — Cache-Schlüssel auf 3 Nachkommastellen (~110 m) gerundet, damit
erneutes Öffnen an derselben Stelle keinen zweiten Abruf auslöst. Modulweiter
Cache (nicht in localStorage wie die Tankstellen-Adresssuche im Panel — die
Fahrzeugposition ändert sich laufend, ein dauerhafter Cache brächte nichts). */
let adressCacheKey: string | null = null
let adressCacheWert: string | null = null
async function standortAdresseAufloesen(lat: number, lon: number): Promise<string | null> {
const key = `${lat.toFixed(3)},${lon.toFixed(3)}`
if (adressCacheKey === key) return adressCacheWert
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`,
{ headers: { "Accept-Language": "de" } },
)
const daten = (await res.json()) as { address?: Record<string, string>; display_name?: string }
const a = daten.address ?? {}
const strasse = [a["road"], a["house_number"]].filter(Boolean).join(" ")
const ort = [a["postcode"], a["city"] ?? a["town"] ?? a["village"]].filter(Boolean).join(" ")
adressCacheWert = [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null
} catch {
// Bewusst NICHT den Schlüssel setzen — ein Netzfehler soll beim nächsten
// Öffnen erneut versucht werden, nicht dauerhaft als "erledigt" gelten.
return null
}
adressCacheKey = key
return adressCacheWert
}
/* Die Auflösung liegt jetzt in daten/geokodierung.ts, gemeinsam mit der
Vorwärtsrichtung und mit dauerhaftem Cache. Der frühere Cache hier hielt
nur einen einzigen Punkt und nur bis zum Neuladen — mit der Begründung, die
Fahrzeugposition ändere sich ohnehin laufend. Das stimmt fürs Fahren, nicht
fürs Parken: das Auto steht meist an denselben Stellen, und wenn Nominatim
gerade drosselt (Antwort ohne CORS-Kopf, im Browser nicht von einem
Netzfehler zu unterscheiden), stand an einer längst bekannten Stelle wieder
nur ein Koordinatenpaar. */
const standortAdresseAufloesen = koordinatenAufloesen
interface EigenePosition {
lat: number
@@ -284,6 +273,14 @@ export function Standort() {
else setzeBlattOffen((o) => !o)
}
/* Die Effekte weiter unten lesen leafletRef/kartenRef, die aber erst im
asynchron geladenen import()-Callback gesetzt werden. Beim ersten Lauf
waren sie leer, die Effekte stiegen aus - und weil ihre Abhaengigkeiten
(die Koordinaten) sich danach nicht mehr aenderten, liefen sie nie
wieder. Genau daran fehlte der Fahrzeug-Pin. Dieser Schalter bringt sie
erneut zum Laufen, sobald die Karte wirklich steht. */
const [karteBereit, setzeKarteBereit] = useState(false)
const bekannt = fahrzeug?.standortLat != null && fahrzeug?.standortLon != null
const fahrzeugPos = bekannt ? { lat: fahrzeug!.standortLat!, lon: fahrzeug!.standortLon! } : null
@@ -298,6 +295,7 @@ export function Standort() {
const karte = L.map(element, { attributionControl: false, zoomControl: false })
kartenRef.current = karte
kachelRef.current = L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte)
setzeKarteBereit(true)
if (fahrzeugPos) karte.setView([fahrzeugPos.lat, fahrzeugPos.lon], 15)
else karte.setView([51.16, 10.45], 5)
setTimeout(() => karte.invalidateSize(), 60)
@@ -322,7 +320,7 @@ export function Standort() {
kachelRef.current = satellit
? L.tileLayer(SAT_KACHEL_URL, { attribution: SAT_ZUSATZ, maxZoom: 19 }).addTo(karte)
: L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte)
}, [satellit])
}, [satellit, karteBereit])
// Fahrzeugmarker zeichnen/aktualisieren.
useEffect(() => {
@@ -344,7 +342,7 @@ export function Standort() {
marker.on("click", () => setzeBlattOffen(true))
fahrzeugMarkerRef.current = marker
}
}, [fahrzeugPos?.lat, fahrzeugPos?.lon])
}, [fahrzeugPos?.lat, fahrzeugPos?.lon, karteBereit])
// Eigene Position zeichnen/aktualisieren.
useEffect(() => {
@@ -367,7 +365,7 @@ export function Standort() {
zIndexOffset: -100,
}).addTo(karte)
}
}, [eigenePosition?.lat, eigenePosition?.lon])
}, [eigenePosition?.lat, eigenePosition?.lon, karteBereit])
// Anfangsausschnitt: beide Punkte zeigen, sobald beide bekannt sind.
useEffect(() => {
@@ -386,7 +384,7 @@ export function Standort() {
// (Fahrzeug bewegt sich) soll den Nutzer nicht aus einer manuellen
// Kartenposition herausreißen.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [!!fahrzeugPos, !!eigenePosition])
}, [!!fahrzeugPos, !!eigenePosition, karteBereit])
// Adresse nachladen, sobald/wann immer sich die Fahrzeugposition ändert.
useEffect(() => {
@@ -401,7 +399,7 @@ export function Standort() {
return () => {
abgebrochen = true
}
}, [fahrzeugPos?.lat, fahrzeugPos?.lon])
}, [fahrzeugPos?.lat, fahrzeugPos?.lon, karteBereit])
if (!fahrzeug || !einstellungen) return null
@@ -441,7 +439,13 @@ export function Standort() {
onClick={() => setzeSatellit((s) => !s)}
aria-pressed={satellit}
aria-label={satellit ? "Satellitenansicht, zur Straßenkarte wechseln" : "Straßenkarte, zur Satellitenansicht wechseln"}
dangerouslySetInnerHTML={{ __html: ciIcon(CI.mapLayer, 22) }}
/* Das Layer-Symbol ist als einziges der vier Kartenknoepfe nicht mittig in
seinem eigenen 24er-Raster (Rand links 2,1 / rechts 1,1, gemessen ueber
getBBox) und fuellt als gefuellte Form deutlich mehr Flaeche als die drei
Kontur-Nachbarn. Die Verschiebung setzt seinen Mittelpunkt genau auf
(12,12), der Faktor 0,88 nimmt ihm das Uebergewicht - dieselbe Behandlung
wie beim Statistik-Tabsymbol. */
dangerouslySetInnerHTML={{ __html: ciIconZentriert(CI.mapLayer, 22) }}
/>
<button
type="button"
@@ -506,16 +510,21 @@ export function Standort() {
aria-expanded={blattOffen}
aria-label={blattOffen ? "Fahrzeugdetails einklappen" : "Fahrzeugdetails aufklappen"}
/>
<button
type="button"
className="dm-standortmenu__schliessen"
onClick={() => setzeBlattOffen(false)}
aria-label="Schließen"
>
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<path d="M5 5l10 10M15 5 5 15" />
</svg>
</button>
{/* Nur im aufgezogenen Zustand: im halb verdeckten Ruhezustand hat
Schließen nichts zu schließen - der Knopf sah bedienbar aus und
tat nichts. */}
{blattOffen && (
<button
type="button"
className="dm-standortmenu__schliessen"
onClick={() => setzeBlattOffen(false)}
aria-label="Schließen"
>
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<path d="M5 5l10 10M15 5 5 15" />
</svg>
</button>
)}
<div className="dm-standortmenu__kopf">
<div className="dm-standortmenu__name">{einstellungen.fahrzeugtitel}</div>
<div className="dm-standortmenu__distanz">{distanzAnzeige}</div>
@@ -559,7 +568,7 @@ export function Standort() {
Route
</a>
<button type="button" className="dm-standort-pille" onClick={teilen} disabled={!bekannt}>
<span dangerouslySetInnerHTML={{ __html: ciIcon(CI.upload, 15) }} />
<span dangerouslySetInnerHTML={{ __html: ciIcon(CI.shareS, 15) }} />
Teilen
</button>
</div>
+6 -1
View File
@@ -226,15 +226,20 @@ export function Wertzeile({
label,
wert,
zusatz,
hinweis,
}: {
label: ReactNode
wert: ReactNode
zusatz?: ReactNode
/** Setzt den Wert wie einen Kommentar (klein, gedaempft) statt wie eine
Angabe - fuer Zeilen, deren rechte Seite nur beschreibt, was hinter der
Kachel liegt, statt selbst ein Messwert zu sein. */
hinweis?: boolean
}) {
return (
<div className="dm-wertzeile">
<dt>{label}</dt>
<dd>
<dd className={hinweis ? "dm-wertzeile__hinweis" : undefined}>
{wert}
{zusatz && <small>{zusatz}</small>}
</dd>
+10
View File
@@ -59,6 +59,16 @@ body {
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
/* Ohne das greift auf echten Touch-Geräten das native Overscroll des
Browsers, sobald am oberen Rand weiter nach unten gezogen wird - es nimmt
der eigenen "Ziehen zum Aktualisieren"-Geste die Kontrolle weg, obwohl
deren Logik für sich genommen richtig arbeitet. Genau derselbe Befund wie
im Panel am 2026-08-17 (main#view, overscroll-behavior-y:contain); am
Rechner ist er nicht nachstellbar, weil dort kein natives Overscroll
mitspielt. touch-action:pan-y sagt dem Browser zusätzlich, dass die
senkrechte Geste hier zu uns gehört. */
overscroll-behavior-y: contain;
touch-action: pan-y;
}
.dm-scroll::-webkit-scrollbar {
+44 -9
View File
@@ -50,8 +50,13 @@
input[type="date"].dm-eingabe {
-webkit-appearance: none;
appearance: none;
min-width: 0;
width: fit-content;
/* Kein width:fit-content mehr. Das stammte aus der Zeit, als .ads-feld input
eine feste min-width von 132px hatte - die ist beim Paritaetsabgleich auf
min-width:0 umgestellt worden, seither zwang fit-content das Feld in eine
Breite, die je Datum und Stelle anders ausfiel. Jetzt bemisst es sich wie
jedes andere Eingabefeld (flex:0 1 auto aus @audi-dash/ui), genau wie
.feld input im Panel. */
text-align: right;
}
/* Eine Zeile aus einer bearbeitbaren Liste (z. B. Selbstbeteiligung im
@@ -309,13 +314,13 @@ input[type="date"].dm-eingabe {
172px leerer Platzhalter vor der Reichweite, der wichtigsten Zahl der
Seite (`.bildbox.bild-fehlt.carfix.klein{height:120px}` und
`.szene.bleed:has(.bild-fehlt)` im Panel). */
/* 72px statt 120px: ohne hinterlegtes Foto stand hier eine leere Flaeche
direkt ueber der Reichweite, der wichtigsten Zahl der Seite. Der
Platzhalter bleibt sichtbar - er ist der einzige Hinweis, dass es diesen
Bildplatz gibt - nur eben schmal (Designpruefung 2026-08-30). */
/* Der Platzhalter belegt denselben Kasten wie ein echtes Foto (16:8). Er war
zwischenzeitlich auf 120 und dann 72px gestaucht, um die Reichweite
hoeher zu ruecken - auf Ansage des Besitzers steht er wieder in voller
Groesse, weil ein Streifen nicht erkennen laesst, was dort hingehoert. */
.dm-szene .ads-bildbox:has(.ads-platzhalter) {
aspect-ratio: auto;
height: 72px;
aspect-ratio: 16 / 8;
height: auto;
}
.dm-galerie-wechsel .ads-bildbox:has(.ads-platzhalter) {
@@ -940,9 +945,24 @@ button.dm-serviceblock:active {
align-items: center;
gap: 6px;
}
/* Gezeichneter Pfeil wie .chev im Panel - 6x10, Strichstaerke 1,4. Vorher
stand hier eine Schriftgroesse, weil an dieser Stelle einmal ein
Schriftzeichen als Pfeil diente. */
.dm-wertzeile__chevron {
width: 6px;
height: 10px;
flex: 0 0 auto;
color: var(--fg3);
fill: none;
stroke: currentColor;
stroke-width: 1.4;
}
/* Wert als Kommentar statt als Angabe (siehe Wertzeile hinweis). Groesse und
Farbe wie jede andere Nebenzeile der App. */
.dm-wertzeile__hinweis {
font-size: 12.5px;
color: var(--fg3);
font-size: 13px;
}
.dm-fussnote {
@@ -1360,11 +1380,15 @@ button.dm-listenzeile {
color: var(--fg);
display: block;
}
/* Der Kilometerstand des Radsatzes ist eine Kennzahl und traegt deshalb die
Zahlenschrift (.ads-fig, im Panel .fig mit font-size:32px) - er stand hier
bisher in der normalen Textschrift und Groesse. */
.dm-reifenzeile {
display: flex;
align-items: center;
gap: 8px;
margin-top: 14px;
font-size: 32px;
}
.dm-reifenzeile .ads-icon-button {
width: 24px;
@@ -1812,3 +1836,14 @@ button.dm-listenzeile {
height: 210px;
background: var(--tile-2);
}
/* Knopfzeile der SmartDeal-Abfrage - Abbrechen links, Bestaetigen rechts,
wie die .seg-Zeile im sdpopup des Panels. */
.dm-sd-knoepfe {
display: flex;
gap: var(--sp-2);
margin-top: 14px;
}
.dm-sd-knoepfe > * {
flex: 1;
}