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:
@@ -1,6 +1,8 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-08-30** (design-audit follow-through - the five priority items applied to
|
||||
**Last updated: 2026-08-30** (owner findings batch - vehicle pin, tile chevrons, select arrows,
|
||||
SmartDeal dialog, address cache and pull-to-refresh; manifest `2026.8.30.9`, see section AO.
|
||||
Before that: design-audit follow-through - the five priority items applied to
|
||||
both codebases, plus the location preview's wrong pin; manifest `2026.8.30.8`, see section AN.
|
||||
Before that: third parity round — the panel had never actually rendered Audi
|
||||
Type (@font-face inside a shadow root is ignored), "Daten bearbeiten" now works in both, and the
|
||||
@@ -5821,6 +5823,75 @@ pushed from elsewhere, no overlap).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## AO. Owner findings batch, 2026.8.30.9 - three of them real bugs with a single cause each
|
||||
|
||||
Seventeen findings in one go, all applied to both codebases unless noted.
|
||||
|
||||
**The three that were genuine defects, not drift:**
|
||||
|
||||
1. **No vehicle pin on the Standort map (app).** The marker effects read `leafletRef`/`kartenRef`,
|
||||
which are only filled inside the asynchronous `import("leaflet")` callback. On mount they were
|
||||
still null, the effects returned early - and since their dependencies (the coordinates) never
|
||||
changed afterwards, they never ran again. A `karteBereit` flag, set once the map exists and added
|
||||
to those dependency lists, brings them back. Same class of bug reached the tile-layer and
|
||||
own-position effects; all four were fixed together.
|
||||
2. **The chevron on the Mein-Audi tiles sat on half the tile's height (app).** `.ads-tile__go` was a
|
||||
flex child with `margin-left`, so on a tile with several value rows it pointed into the middle of
|
||||
a row instead of at the title that says where it leads. Now absolute at top 22px / right 18px -
|
||||
what the panel has done since 2026-08-13. Measured before: 132 / 106 / 106 / 98px from the top.
|
||||
3. **Select fields had no drop-down arrow at all (app).** `appearance:none` in `Feld.css` removed the
|
||||
native arrow without putting anything back, so a picker was indistinguishable from a plain text
|
||||
field. Reported at "Ansicht" and at the backup interval; the fix in `.ads-feld select` covers
|
||||
every select in the app and copies the panel's own chevron image and metrics one to one.
|
||||
|
||||
**SmartDeal: the date field was broken *and* the question behind it was missing.** The app carried an
|
||||
inline date input labelled "Gültig bis", the panel asks for the date in a dialog the moment the
|
||||
switch is turned on and afterwards only displays it. The panel's behaviour is the one that makes
|
||||
sense - `smartdeal.aktiv` is computed from that date (`laeuft_ab >= heute` in both codebases), so
|
||||
without it the discount can never expire on its own. The dialog is now ported; the inline field is
|
||||
gone, so there is exactly one way to set the value.
|
||||
|
||||
**Nominatim has started refusing us.** "Suddenly only GPS coordinates where an address used to be" is
|
||||
not a display bug: the reverse lookup comes back without a CORS header (that is what throttling looks
|
||||
like from a browser) and both apps fall back to coordinates as designed. What was missing is memory -
|
||||
the panel kept a single point in RAM, the app the same, both gone on reload, with the reasoning "the
|
||||
vehicle position changes constantly anyway". That holds for driving, not for parking: the car stands
|
||||
in the same few places. Both now cache resolved addresses in the same persistent store the fuel-station
|
||||
lookup already used (`rev:`-prefixed key, rounded to ~110 m); only real hits are stored, so a throttled
|
||||
answer is retried rather than remembered as "no address here".
|
||||
|
||||
**Pull-to-refresh did not work on the device.** The gesture logic was fine - the scroll container had
|
||||
no `overscroll-behavior`, so iOS's own rubber-band took the gesture before the threshold was reached.
|
||||
Exactly the finding the panel had on 2026-08-17 and the app never received; `.dm-scroll` now carries
|
||||
`overscroll-behavior-y: contain` and `touch-action: pan-y`. Not reproducible on a desktop, because
|
||||
there is no native overscroll to compete with.
|
||||
|
||||
**The rest, briefly:** the Übersicht placeholder is back at full 16:8 size (the audit had narrowed it
|
||||
to 72px; a strip does not show what belongs there); "Fahrgestellnummer" is now "FIN" and "Details"
|
||||
shows "Informationen über das Auto" as a comment-weight hint rather than a truncated engine line; the
|
||||
Batteriespannung row got its chevron back (an earlier note claiming the panel marks it by cursor only
|
||||
was wrong - the panel has a `.chev` there); the Reifen box leads with "aktuell montiert: /
|
||||
Sommerräder" and drops the dash under the change date; Anzugsmoment uses the gear, not the pencil (the
|
||||
pencil belongs to the kilometre correction); the wheel odometer is back in the figure typeface at
|
||||
32px; the map-layer icon is the only one of the four controls not centred in its own grid (2.1 left
|
||||
vs 1.1 right, measured via `getBBox`) and is now nudged onto centre and scaled to 0.88 so it stops
|
||||
outweighing its outline neighbours; Teilen uses the real `share-s` from the CI set; and the sheet's
|
||||
close button only exists while the sheet is raised - half-covered it looked operable and did nothing.
|
||||
|
||||
**One finding could not be reproduced:** "Fahrten/Tankfüllung: Auflistung/grauer Hintergrund ist
|
||||
falsch". Panel and app are identical there - same backgrounds (transparent rows on a white tile),
|
||||
same paddings, same indents, same text. Reported back rather than guessed at.
|
||||
|
||||
Verified: `node --check`, `design-system` rebuilt, `tsc --noEmit` clean, 147/147 green, deployed to
|
||||
`audi_ha_test` and confirmed against both frontends at 375x812 - chevrons at 22px on all four tiles,
|
||||
the Zustand row's chevron, the select arrow, the full-size placeholder, the SmartDeal dialog driven
|
||||
end to end (state restored afterwards), the Reifen page's gear/date/figure, and on Standort: one
|
||||
marker, the centred layer icon, the share glyph, and the close button appearing exactly when the sheet
|
||||
is raised.
|
||||
|
||||
---
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.8.30.8","sha256":"2754d426c7145d000fd2194d4dd85db096c2fcae008eb518584f189becfb0538","bytes":256098,"gebaut":"2026-08-30T16:30:51Z"}
|
||||
{"version":"2026.8.30.9","sha256":"63b67b2116bdc2970ebcb9c02dc8c2d1ec1f0156fd70c59f88db101307054474","bytes":256695,"gebaut":"2026-08-30T21:09:06Z"}
|
||||
Binary file not shown.
@@ -789,6 +789,20 @@ function standortErfassenFallsNoetig() {
|
||||
async function standortAdresseAufloesen(lat, lon) {
|
||||
const key = lat.toFixed(3) + "," + lon.toFixed(3);
|
||||
if (STANDORT_ADRESSE_KEY === key) return STANDORT_ADRESSE;
|
||||
/* Dauerhafter Cache im selben Fach wie die Tankstellensuche, Schluessel mit
|
||||
"rev:"-Vorsatz. Bis 2026-08-30 lag hier nur ein einziger Punkt im
|
||||
Arbeitsspeicher, weg beim naechsten Neuladen - mit der Begruendung, die
|
||||
Fahrzeugposition aendere sich ohnehin laufend. Das stimmt fuers Fahren,
|
||||
nicht fuers Parken: das Auto steht meist an denselben Stellen, und wenn
|
||||
Nominatim gerade drosselt, stand an einer laengst bekannten Stelle
|
||||
wieder nur ein Koordinatenpaar. */
|
||||
const cacheKey = "rev:" + key;
|
||||
const cache = geoCacheLesen();
|
||||
if (typeof cache[cacheKey] === "string") {
|
||||
STANDORT_ADRESSE = cache[cacheKey];
|
||||
STANDORT_ADRESSE_KEY = key;
|
||||
return STANDORT_ADRESSE;
|
||||
}
|
||||
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();
|
||||
@@ -804,6 +818,9 @@ async function standortAdresseAufloesen(lat, lon) {
|
||||
return null;
|
||||
}
|
||||
STANDORT_ADRESSE_KEY = key;
|
||||
// Nur echte Treffer merken - eine gedrosselte Antwort ist kein "hier gibt es
|
||||
// keine Adresse" und soll beim naechsten Mal erneut versucht werden.
|
||||
if (STANDORT_ADRESSE) geoCacheSchreiben(cacheKey, STANDORT_ADRESSE);
|
||||
return STANDORT_ADRESSE;
|
||||
}
|
||||
function standortAdresseLaden() {
|
||||
@@ -1081,6 +1098,11 @@ const CI = {
|
||||
selectAll: '<path fill="currentColor" d="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: '<path fill="currentColor" d="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: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
|
||||
/* share-l/-s aus dem Audi-CI-Satz (vom Nutzer als SVG geliefert, verbatim
|
||||
uebernommen) - l im 48er-, s im 24er-Raster wie poi-l/poi-s. Loest beim
|
||||
Teilen das upload-Icon ab, das dort nur ersatzweise stand. */
|
||||
shareL: '<path fill="currentColor" d="m23.5 3.8 6.85 6.85-.7.7L24 5.71V29h-1V5.7l-5.65 5.65-.7-.7 6.85-6.86ZM11 16h8v1h-7v23h23V17h-7v-1h8v25H11z"/>',
|
||||
shareS: '<path fill="currentColor" d="m11.5 1.8 3.85 3.85-.7.7L12 3.71V15h-1V3.7L8.35 6.36l-.7-.7zM5 8h4v1H6v11h11V9h-3.02V8H18v13H5z"/>',
|
||||
parking: '<path fill="currentColor" d="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"/>',
|
||||
/* battery-12v-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer als SVG geliefert,
|
||||
verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */
|
||||
@@ -1131,7 +1153,13 @@ function vStandort() {
|
||||
<div class="kartensteuerung">
|
||||
<button type="button" data-standort-layer aria-pressed="${SMAP_STIL === "satellit"}"
|
||||
aria-label="${SMAP_STIL === "satellit" ? "Satellitenansicht, zur Straßenkarte wechseln" : "Straßenkarte, zur Satellitenansicht wechseln"}">
|
||||
${ciSVG(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. -->
|
||||
${ciSVG(CI.mapLayer, 22).replace("<path", '<g transform="translate(1.44 1.44) scale(0.88) translate(-0.55 0.2)"><path').replace("</svg>", "</g></svg>")}
|
||||
</button>
|
||||
<button type="button" data-standort-fahrzeug aria-label="Fahrzeug auf der Karte suchen">
|
||||
${ciSVG(CI.carSearch, 22)}
|
||||
@@ -1151,9 +1179,13 @@ function vStandort() {
|
||||
<button type="button" class="standortmenu-griff" data-standort-umschalten
|
||||
aria-expanded="${STANDORTMENU_OFFEN}" aria-controls="standortmenu"
|
||||
aria-label="Fahrzeugdetails ${STANDORTMENU_OFFEN ? "einklappen" : "aufklappen"}"></button>
|
||||
${STANDORTMENU_OFFEN ? `
|
||||
<!-- Nur im aufgezogenen Zustand. Im halb verdeckten Ruhezustand hat
|
||||
Schließen nichts zu schließen - der Knopf sah bedienbar aus und
|
||||
tat nichts. -->
|
||||
<button type="button" class="standortmenu-schliessen" data-standort-schliessen aria-label="Schließen">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>
|
||||
</button>
|
||||
</button>` : ""}
|
||||
<div class="standortmenu-kopf">
|
||||
<div class="standortmenu-name">${esc(CONFIG.fahrzeugtitel)}</div>
|
||||
<div class="standortmenu-distanz" id="standortDistanz">${distanz}</div>
|
||||
@@ -1177,7 +1209,7 @@ function vStandort() {
|
||||
${ciSVG(CI.planRoute, 15)}Route
|
||||
</a>
|
||||
<button type="button" class="standort-pille" data-standort-teilen ${bekannt ? "" : "disabled"}>
|
||||
${ciSVG(CI.upload, 15)}Teilen
|
||||
${ciSVG(CI.shareS, 15)}Teilen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1642,8 +1674,10 @@ function vAudi() {
|
||||
<span class="label">Fahrzeug</span>
|
||||
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
|
||||
<dl class="rows" style="margin-top:10px">
|
||||
<div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
|
||||
<div class="row"><dt>Details</dt><dd>${esc(CAR.details)}</dd></div>
|
||||
<div class="row"><dt>FIN</dt><dd>${esc(CAR.fin)}</dd></div>
|
||||
<!-- Die technischen Daten stehen eine Ebene tiefer (Identität und Technik);
|
||||
hier nur der Hinweis darauf, statt einer abgeschnittenen Motorzeile. -->
|
||||
<div class="row"><dt>Details</dt><dd class="row-hinweis">Informationen über das Auto</dd></div>
|
||||
<div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
|
||||
<div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
|
||||
<div class="row" style="border-bottom:0"><dt>Ausstattung</dt>
|
||||
@@ -1703,10 +1737,10 @@ function vAudi() {
|
||||
<span class="label">Reifen</span>
|
||||
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
|
||||
<dl class="rows" style="margin-top:10px">
|
||||
<div class="row"><dt>montiert: ${r.aktiv}räder</dt><dd>${esc(a.mass)}</dd></div>
|
||||
<div class="row"><dt>aktuell montiert:</dt><dd>${r.aktiv}räder</dd></div>
|
||||
<div class="row"><dt>Laufleistung</dt><dd>${a.km != null ? de(a.km) + " km" : "unbekannt"}</dd></div>
|
||||
<div class="row" style="border-bottom:0"><dt>Wechseltermin</dt>
|
||||
<dd>${dedat(new Date(r.wechsel.datum))}<small>${r.wechsel.vereinbart ? "vereinbart" : "–"}</small></dd></div>
|
||||
<dd>${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "<small>vereinbart</small>" : ""}</dd></div>
|
||||
</dl></button>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +444,11 @@ button.sicherheitszeile:active { background: var(--tile-2); }
|
||||
.sicherheitsliste .sicherheitszeile:last-child { border-bottom: 0; }
|
||||
.sicherheitszeile-text { flex: 1; font-size: 14px; color: var(--fg); }
|
||||
.sicherheitszeile .chev { margin-left: auto; }
|
||||
/* Wert als Kommentar statt als Angabe - fuer Zeilen, deren rechte Seite nur
|
||||
beschreibt, was hinter der Kachel liegt (Mein Audi, "Details"). Groesse
|
||||
und Farbe wie jede andere Nebenzeile. */
|
||||
.row dd.row-hinweis { font-size: 12.5px; color: var(--fg3); }
|
||||
|
||||
.row-fussnote {
|
||||
flex: 1 0 100%;
|
||||
text-align: left;
|
||||
@@ -632,11 +637,11 @@ button.leaf:active { background: var(--tile-2); }
|
||||
260 px gestrichelte Leerfläche vor der Reichweite, der wichtigsten Zahl der
|
||||
App. Mit Foto gilt wieder das volle Seitenverhältnis. */
|
||||
.bildbox.bild-fehlt.carfix { aspect-ratio: auto; height: 132px; }
|
||||
/* 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). */
|
||||
.bildbox.bild-fehlt.carfix.klein { height: 72px; }
|
||||
/* 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. */
|
||||
.bildbox.bild-fehlt.carfix.klein { aspect-ratio: 16 / 8; height: auto; }
|
||||
.szene.bleed:has(.bild-fehlt)::before { display: none; }
|
||||
.szene.bleed:has(.bild-fehlt) { margin-left: 0; margin-right: 0; }
|
||||
.szene.bleed:has(.bild-fehlt) .carbox { padding: 0; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.30.8",
|
||||
"version": "2026.8.30.9",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": ["@paul"],
|
||||
|
||||
@@ -60,6 +60,21 @@
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* Aufklapp-Pfeil. appearance:none oben entfernt den nativen Pfeil des
|
||||
Browsers, ersatzlos - das Auswahlfeld war dadurch von einem reinen
|
||||
Textfeld nicht mehr zu unterscheiden (gemeldet an Ansicht und
|
||||
Auto-Backup). Dasselbe Bild und dieselben Masse wie .feld select in
|
||||
audi-dashboard-ios.css, dort seit 2026-08-16 nach Apples Vorgabe fuer
|
||||
Pull-down-Knoepfe. Farbe fest auf #8E8E93 - das ist in beiden Themes die
|
||||
Farbe aller uebrigen Chevrons. */
|
||||
.ads-feld select {
|
||||
padding-right: 28px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5l3.5 3.5 3.5-3.5' fill='none' stroke='%238E8E93' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 9px center;
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
|
||||
.ads-mit-einheit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -35,10 +35,18 @@ button.ads-tile--button:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Der Pfeil steht oben neben der Ueberschrift, nicht auf halber Kachelhoehe.
|
||||
Als Flex-Kind mit margin-left sass er mittig - bei einer Kachel mit
|
||||
mehreren Wertezeilen zeigt er dann irgendwo in die Zeilenmitte statt auf
|
||||
den Titel, der sagt, wohin es geht. Das Panel loest das seit 2026-08-13
|
||||
genau so: .go absolut, top 22px / right 18px (audi-dashboard.css und die
|
||||
.tilebtn-Regel der iOS-Auflage). */
|
||||
.ads-tile__go {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: 18px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
color: var(--fg3);
|
||||
flex: 0 0 auto;
|
||||
margin-left: var(--sp-3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user