Pruefdurchgang ueber alle 26 Bildschirme - fuenf Fehler behoben, HIG nachgezogen

Gemessen statt geschaut: eine Pruefroutine lief auf jedem Bildschirm und
pruefte Ueberlauf, Tippziele, abgeschnittenen Text, Bilder ohne Quelle und
WCAG-Kontrast fuer jeden Textknoten. Dazu die Live-App mit echten Daten, die
Musterseite fuer Leerzustaende und das Panel zum Abgleich.

1. Deutsche Datumsangaben wurden falsch gelesen. new Date("01.03.2025")
   liest amerikanisch: Tage bis 12 wurden still vertauscht, ab Tag 13 fiel
   der Wert ganz weg. Die Erstzulassung stand auf zwei Bildschirmen
   unterschiedlich da. alsZeitpunkt() erkennt jetzt ISO, TT.MM.JJJJ und
   MM/JJJJ, weist unmoegliche Daten ab und fuehrt die Genauigkeit mit -
   aus "08/2026" wird "August 2026", kein erfundener Erster.

2. Die Hauptuntersuchung war in beiden Oberflaechen unerreichbar. Das Panel
   leitete sie aus der Erstzulassung ab, aber nur bei MM/JJJJ, und benutzte
   den eingetragenen Wert gar nicht. Jetzt beide: eingetragene Faelligkeit
   vor Servicebuch vor Erstzulassung + 24 Monate.

3. Die Tankstellenmarke stand zweimal da, wenn der Name kein Komma hat
   ("Shell München Ost"). ohneMarkeVorn() nimmt sie vorn ab.

4. Bilder wurden geholt, obwohl beide wussten, dass es sie nicht gibt -
   fuenf 404 fuer dieselbe Datei in einem Ladevorgang. Jetzt gar keine
   Adresse bei Stand 0; nachgemessen 4 Anfragen, alle 200.

5. Ein Bildelement ohne Quelle zeigte das kaputte Bildsymbol, und onerror
   greift dort nicht: ohne src gibt es keinen Ladeversuch.

Dazu: leere Klammern beim Steuersatz, leere graue Kachel bei leerer
Leistungsgruppe, Umbruch auf dem Einrichtungsbildschirm, drei
Leerdarstellungen in einer Kachel, fehlende Grundschrift.

Apple HIG: Kontrast war an einer Stelle 3,18 statt 4,5, weisse Schrift auf
der Loeschflaeche 3,41/3,55. Rot als Schrift und Rot als Flaeche sind jetzt
getrennte Token (--bad / --bad-flaeche), beide Apples systemRed fuer
erhoehten Kontrast. Tippziele: unsichtbares 44x44-Overlay, wo die sichtbare
Groesse Teil des Bildes ist, echte Mindesthoehe, wo das Element Flaeche ist -
Formulare werden dadurch sichtbar hoeher.

Neu auf Wunsch: Wischen in der Bildergalerie (beide Richtungen) und die
HIG-konforme Loeschgeste - ein voller Wisch loescht ohne zweiten Tipp, ab
55 % der Zeilenbreite, mit wachsender roter Flaeche und erhaltener
Rueckfrage. Dabei fiel auf, dass der Zugwert aus dem React-Zustand gelesen
wurde und ein schneller Wisch dadurch verlorenging; er liegt jetzt in einer
Referenz.

Paritaet: alle 26 Routen und Titel decken sich, Fahrzeugstatus zeilengleich,
zwei ungeplante Abweichungen (Datumsformatierung, Herkunft der
Hauptuntersuchung) geschlossen.

tsc sauber, 271 Tests, vite build sauber, Design-System gebaut, Panel als
Modul geparst, audi_ha_test auf 2026.9.4.23 ohne Traceback. Live nachgemessen:
Datum auf beiden Bildschirmen gleich, keine 404 mehr, Zurueck-Pfeil sichtbar
34x34 und treffbar 44x44, Galerie wischt in beide Richtungen, voller Wisch
loest die Rueckfrage aus, keine Kontrast-Unterschreitung mehr.
This commit is contained in:
2026-09-04 18:11:07 +02:00
parent ffaed3f544
commit b0ec8b4982
26 changed files with 1125 additions and 114 deletions
@@ -1 +1 @@
{"version":"2026.9.4.22","sha256":"66406934fb50aa810ff782ce877bc82d25ec8d338979d5ae9e28e5dc5c2ea521","bytes":323041,"gebaut":"2026-09-04T15:08:22Z"}
{"version":"2026.9.4.23","sha256":"7cc61f10ec846824ae34bc033115c795ab9026e7d56a72cdf9ec4cb6032e8559","bytes":324157,"gebaut":"2026-09-04T16:07:49Z"}
@@ -389,8 +389,17 @@ function bildStand(datei) {
// loest sich damit auf.
return BILD_STAENDE[datei] ?? 0;
}
/* null, wenn das Backend meldet, dass es dieses Foto nicht gibt (Stand 0).
Vorher wurde die Adresse trotzdem gebaut und der Browser holte bei JEDEM
Rendern eine 404 - am 04.09.2026 gemessen: fuenf Anfragen fuer
front-schraeg.webp in einem einzigen Ladevorgang, dazu heckansicht,
cockpit, sitze und seitenansicht-winter. Die App weiss laengst, dass es
sie nicht gibt; sie muss nicht danach fragen. */
function bildAdresse(datei) {
return `/local/bilder/${datei}?v=${bildStand(datei)}`;
const stand = bildStand(datei);
if (BILD_STAENDE && stand === 0) return null;
return `/local/bilder/${datei}?v=${stand}`;
}
/* Platzhalter statt kaputtem Bild-Symbol: bei Ladefehler (Bilder aus §7a
@@ -406,8 +415,15 @@ function bildAdresse(datei) {
eines Fotos nicht. */
function bildMitPlatzhalter(src, datei, label, klasse, imgId, dateiZeigen = false) {
const idTeil = imgId ? ` id="${imgId}"` : "";
return `<div class="bildbox ${klasse}" data-bildbox title="${esc(label)}">
<img class="${klasse}"${idTeil} src="${src}" alt="${esc(label)}" data-bildlabel="${esc(label)}" data-bilddatei="${esc(datei)}"
// Ohne Adresse KEIN src-Attribut - ein leeres src holt die Seite selbst
// erneut, und ein <img> ganz ohne src zeigt das kaputte Bildsymbol.
// Sichtbar bleibt es nicht: .bildbox.bild-fehlt img ist ausgeblendet.
// Das <img> selbst bleibt trotzdem stehen, weil die Galerie es weiter-
// schaltet (#carimg) - ohne das Element bliebe sie stehen.
const srcTeil = src ? ` src="${src}"` : "";
const fehltTeil = src ? "" : " bild-fehlt";
return `<div class="bildbox ${klasse}${fehltTeil}" data-bildbox title="${esc(label)}">
<img class="${klasse}"${idTeil}${srcTeil} alt="${esc(label)}" data-bildlabel="${esc(label)}" data-bilddatei="${esc(datei)}"
onerror="this.closest('[data-bildbox]').classList.add('bild-fehlt')">
<div class="platzhalter ${klasse}">
<svg viewBox="0 0 24 24" class="platzhalter-icon" fill="currentColor"><path fill-rule="evenodd" d="m23.35 1.35-22 22-.7-.7 22-22zM1 3h17.5v1H2v16H1zm21 11.85V4h1v17H6v-1h16v-3.77l-3.24-3.08-4.57 4.48-2.85-2.84.7-.7 2.15 2.14 4.56-4.47 3.25 3.1ZM4 8.5a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0M6.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3" clip-rule="evenodd"/></svg>
@@ -602,20 +618,29 @@ function markenlogo() {
szeneKlasse(), das je nach Bild und Reifensatz "strasse" oder "winter"
an .szene haengte; die zugehoerigen ::after-Regeln sind in
audi-dashboard.css beschrieben. */
function bildWeiter() {
/* Ein Bild weiter (richtung 1) oder zurueck (-1).
Die Richtung kam am 04.09.2026 dazu: der Eigentuemer wollte die Galerie
auch durch Wischen nach links/rechts durchschalten, nicht nur durch
Tippen (galerieWischenVerdrahten unten, wortgleich mit der App). */
function bildWeiter(richtung = 1) {
// Ueber die Galerie-Plaetze, nicht ueber alle: die Draufsicht wird
// uebersprungen (siehe nurStatus in BILDER).
const jetzt = GALERIE.findIndex((b) => b === BILDER[bild]);
const naechst = GALERIE[(Math.max(0, jetzt) + 1) % GALERIE.length];
const anzahl = GALERIE.length;
if (!anzahl) return;
const naechst = GALERIE[(Math.max(0, jetzt) + richtung + anzahl) % anzahl];
bild = BILDER.indexOf(naechst);
const img = ROOT.getElementById("carimg");
if (!img) return;
const info = bildInfo(bild);
img.src = info.src;
// Ohne Adresse das Attribut ENTFERNEN statt auf null zu setzen: src=""
// laesst den Browser die Seite selbst erneut holen.
if (info.src) img.src = info.src; else img.removeAttribute("src");
img.alt = info.label;
const box = img.closest("[data-bildbox]");
if (box) {
box.classList.remove("bild-fehlt"); // neues Bild kann laden, auch wenn das vorherige fehlte
box.classList.toggle("bild-fehlt", !info.src);
box.querySelector(".platzhalter-label").textContent = info.label;
}
const sz = ROOT.getElementById("szene");
@@ -623,6 +648,34 @@ function bildWeiter() {
[...ROOT.getElementById("dots").children].forEach((d, i) => d.classList.toggle("on", GALERIE[i] === BILDER[bild]));
}
/* Wischen in der Bildergalerie (Wunsch des Eigentuemers, 04.09.2026).
Zeigerereignisse statt Beruehrungsereignisse: dieselbe Verdrahtung traegt
Finger, Stift und Maus, und genau so macht es das Standort-Blatt schon
(standortMenuVerdrahten). Ein Wisch ab 40 px zaehlt als Wisch, alles
darunter bleibt ein Tipp - dieselbe Schwelle wie dort.
GEWISCHT SCHLUCKT DEN KLICK: ein Wisch loest anschliessend auch ein
click aus. Ohne diese Sperre schaltete die Galerie zweimal weiter. */
let galerieZug = null;
let galerieGewischt = false;
function galerieWischenVerdrahten() {
ROOT.addEventListener("pointerdown", (e) => {
const feld = e.target.closest && e.target.closest("[data-bildgalerie]");
galerieZug = feld ? { x0: e.clientX, y0: e.clientY } : null;
});
ROOT.addEventListener("pointerup", (e) => {
const zug = galerieZug;
galerieZug = null;
if (!zug) return;
const dx = e.clientX - zug.x0;
// Senkrecht ueberwiegt: das war ein Scrollen, kein Blaettern.
if (Math.abs(dx) <= 40 || Math.abs(e.clientY - zug.y0) > Math.abs(dx)) return;
galerieGewischt = true;
bildWeiter(dx < 0 ? 1 : -1);
});
}
/* ---------------------------------------------------- Termine/Vorhersage
heute() statt einer eingefrorenen Konstante - der Prototyp nutzte ein
festes Demo-Datum (new Date(2026,7,3)), das für den echten Betrieb durch
@@ -637,7 +690,72 @@ const gueltigesDatum = (d) => d instanceof Date && !Number.isNaN(d.getTime());
const dedat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
: "");
function dat(s) { const [d, m, j] = s.split(".").map(Number); return new Date(j, m - 1, d); }
function dat(s) {
// Nimmt seit dem 04.09.2026 auch ein fertiges Date entgegen: die
// Hauptuntersuchung reicht ihre Basis als Date herein (siehe
// datumAusText/hauptuntersuchungsTermin), monatePlus() ruft dat() darauf.
if (s instanceof Date) return new Date(s.getTime());
const [d, m, j] = String(s).split(".").map(Number);
return new Date(j, m - 1, d);
}
/* Freitext-Datum aus dem Profil - WORTGLEICH mit alsZeitpunkt() in
companion-app/src/format.ts.
Die Felder "Erstzulassung" und "Hauptuntersuchung" sind Eingabefelder, und
der Eigentuemer traegt dort ein, was ihm richtig scheint. Am 04.09.2026
gemessen: das Panel leitete die Hauptuntersuchung nur dann aus der
Erstzulassung ab, wenn diese ein "/" enthielt - bei "01.03.2025" (eine
voellig richtige Eingabe) blieb der Termin still leer.
`genau: "monat"` heisst: die Angabe nennt keinen Tag ("08/2026"). Daraus
den Ersten des Monats zu ZEIGEN waere eine Erfindung - deshalb die
Genauigkeit mitfuehren und in datumText() beruecksichtigen. */
const DATUM_ISO = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
const DATUM_TAG = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
const DATUM_MONAT = /^(\d{1,2})[./](\d{4})$/;
const DATUM_ISO_MONAT = /^(\d{4})-(\d{1,2})$/;
function datumBauen(jahr, monat, tag) {
if (monat < 1 || monat > 12 || tag < 1 || tag > 31) return null;
const d = new Date(jahr, monat - 1, tag);
// new Date(2025,1,31) waere der 3. Maerz - ein unmoegliches Datum soll "-"
// ergeben, nicht klammheimlich ein anderes.
if (d.getFullYear() !== jahr || d.getMonth() !== monat - 1 || d.getDate() !== tag) return null;
return d;
}
function datumAusText(wert) {
if (wert instanceof Date) return gueltigesDatum(wert) ? { zeit: wert, genau: "tag" } : null;
const text = String(wert == null ? "" : wert).trim();
if (!text) return null;
let m = DATUM_ISO.exec(text);
if (m) { const d = datumBauen(+m[1], +m[2], +m[3]); return d ? { zeit: d, genau: "tag" } : null; }
m = DATUM_TAG.exec(text);
if (m) { const d = datumBauen(+m[3], +m[2], +m[1]); return d ? { zeit: d, genau: "tag" } : null; }
m = DATUM_MONAT.exec(text);
if (m) { const d = datumBauen(+m[2], +m[1], 1); return d ? { zeit: d, genau: "monat" } : null; }
m = DATUM_ISO_MONAT.exec(text);
if (m) { const d = datumBauen(+m[1], +m[2], 1); return d ? { zeit: d, genau: "monat" } : null; }
return null;
}
/* Zeigt eine monatsgenaue Angabe als Monat statt als erfundenen Ersten. */
const demonat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" })
: "");
const datumText = (d, genau) => (genau === "monat" ? demonat(d) : dedat(d));
/* Die Erstzulassung, wie sie angezeigt wird: formatiert, wenn sie sich lesen
laesst, sonst der Rohwert, sonst ein Strich.
Bis zum 04.09.2026 stand hier immer der Rohwert und bei leerem Feld gar
nichts - waehrend die FIN daneben "" zeigte. Drei Leerdarstellungen in
einer Kachel. Wortgleich mit datum(fahrzeug.erstzulassung) in der App. */
function erstzulassungText() {
const z = datumAusText(CAR.erstzulassung);
if (z) return datumText(z.zeit, z.genau);
return (CAR.erstzulassung || "").trim() || "";
}
/* Datum + Uhrzeit in einer Zeile, für Start-/Zielzeitpunkt der Einzelfahrt. */
const dezeitpunkt = (d) => (gueltigesDatum(d)
? `${dedat(d)} · ${d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr`
@@ -1321,6 +1439,25 @@ const MARKEN = [
die Anzeige (Vorgabe des Eigentuemers, 04.09.2026).
Wortgrenzen sind Absicht: "Star" darf nicht in "Starnberg" treffen. */
/* Nimmt einem Namensteil die fuehrende Kopfmarke ab - wortgleich mit
ohneMarkeVorn() in companion-app/src/daten/tankstelle.ts.
Bis zum 04.09.2026 wurde ein Namensteil nur uebersprungen, wenn er FUER
SICH eine Marke ist. Ein von Hand eingetragener Name ohne Komma
("Shell München Ost") rutschte komplett durch und landete als ORT:
"Shell, Musterstr., Shell München Ost". Jetzt faellt die Marke vorn ab.
Steht die Marke NICHT am Anfang ("Autohof Shell Nord"), bleibt der Teil
unangetastet - dort ist nicht sicher, was Marke und was Ortsname ist. */
function ohneMarkeVorn(teil) {
const marke = markeErkennen(teil);
if (!marke) return teil;
// Kein Maskieren noetig: marke stammt immer aus MARKEN, und dort steht
// kein einziges Sonderzeichen der regulaeren Ausdruecke.
const muster = new RegExp(`^${marke}\\b[\\s,]*`, "i");
return muster.test(teil) ? teil.replace(muster, "").trim() : teil;
}
function markeErkennen(name) {
const roh = (name || "").trim();
if (!roh) return null;
@@ -1374,14 +1511,10 @@ function tankstelleTeile(name, adresse, marke) {
const teile = { marke: (marke || "").trim() || markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
const quellen = (adresse || "").split(",");
for (const stueck of (name || "").split(",")) {
const t = stueck.trim();
// Ein Teil, der fuer sich genommen eine Kopfmarke IST, taugt nie als Ort.
// Frueher stand hier nur der Vergleich mit der gewaehlten Marke; seit die
// gespeicherte Angabe vorgeht, koennen beide auseinanderfallen, und dann
// rutschte die Marke aus dem Namen in den Ort.
const eigen = t ? markeErkennen(t) : null;
if (eigen && eigen.toLowerCase() === t.toLowerCase()) continue;
quellen.push(t);
// Die Marke faellt vorn ab - sie steht schon fest und taugt nie als Ort.
// Bleibt nichts uebrig, war der Teil nur die Marke selbst.
const t = ohneMarkeVorn(stueck.trim());
if (t) quellen.push(t);
}
for (const stueck of quellen) {
const t = stueck.trim();
@@ -2136,7 +2269,9 @@ function prognoseZeile(art) {
function terminText(t) {
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
const restKmAbs = restKm !== null ? Math.abs(restKm) : null;
const haupt = t.zuerst === "km" && restKm !== null ? "in " + de(restKmAbs) + " km" : dedat(t.datum);
// datumText statt dedat: eine monatsgenaue Faelligkeit ("08/2026") darf nicht
// als erfundener Erster des Monats erscheinen.
const haupt = t.zuerst === "km" && restKm !== null ? "in " + de(restKmAbs) + " km" : datumText(t.datum, t.genau);
const neben = t.zuerst === "km" ? "voraussichtlich " + dedat(t.kmDatum) + " · " + dedat(t.datum)
: (t.km !== null ? "in " + de(restKmAbs) + " km" : (t.art === "Hauptuntersuchung" ? "" : "aus dem Wartungsplan fortgeschrieben"));
return { haupt, neben, restKm };
@@ -2352,7 +2487,7 @@ function vAudi() {
<!-- 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>Erstzulassung</dt><dd>${esc(erstzulassungText())}</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>
<dd>${CAR.ausstattung.reduce((n, g) => n + g.sonder.length, 0)} Positionen</dd></div>
@@ -2906,7 +3041,7 @@ function vIdent() {
<div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
<div class="row"><dt>FIN</dt><dd>${esc(CAR.fin)}</dd></div>
<div class="row"><dt>Motor und Leistung</dt><dd>${esc(CAR.details)}</dd></div>
<div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
<div class="row"><dt>Erstzulassung</dt><dd>${esc(erstzulassungText())}</dd></div>
<div class="row"><dt>Kennzeichen</dt><dd>${esc(CONFIG.kennzeichen)}</dd></div>
<div class="row" style="border-bottom:0"><dt>Zulassung</dt><dd>Deutschland</dd></div>
</dl></div>
@@ -2987,6 +3122,29 @@ function gewaehlterTermin() {
return auswahl[Math.min(svAuswahl, auswahl.length - 1)];
}
/* Die Hauptuntersuchung hat drei moegliche Quellen, in dieser Reihenfolge:
1. die im Profil EINGETRAGENE Faelligkeit (CAR.hu). Sie steht auf dem
Pruefbericht und ist damit die belastbarste Angabe - bis zum
04.09.2026 hat das Panel sie gar nicht benutzt, obwohl sie
gespeichert war (Befund des Eigentuemers).
2. der letzte Servicebuch-Eintrag plus 24 Monate.
3. die Erstzulassung plus 24 Monate.
Zu 3.: das ging vorher NUR mit einer Erstzulassung im Format MM/JJJJ.
Ein vollstaendiges Datum (01.03.2025) - eine voellig richtige Eingabe -
liess den Termin still leer. datumAusText() nimmt jetzt beide Formen. */
function hauptuntersuchungsTermin(bauen) {
const gesetzt = datumAusText(CAR.hu);
if (gesetzt) {
return { art: "Hauptuntersuchung", km: null, datum: gesetzt.zeit, kmDatum: null,
zuerst: "zeit", basis: null, ziel: gesetzt.zeit, genau: gesetzt.genau };
}
const erst = datumAusText(CAR.erstzulassung);
return bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
erst ? erst.zeit : null);
}
function termine() {
const oel = CAR.oel;
const oelKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
@@ -3032,8 +3190,7 @@ function termine() {
return [
oelTermin,
inspTermin,
bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
CAR.erstzulassung && CAR.erstzulassung.includes("/") ? `01.${CAR.erstzulassung.replace("/", ".")}` : null),
hauptuntersuchungsTermin(bauen),
];
}
@@ -3143,7 +3300,7 @@ function vVers() {
${ciSVG(CI.settingsS, 17)}
</button>
<div class="fig" style="font-size:46px;margin-top:12px">${eur(v.beitrag)}<span class="unit"></span></div>
<span class="label" style="margin-top:14px">${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} Versicherungsteuer (${esc(v.steuersatz)})</span>
<span class="label" style="margin-top:14px">${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} Versicherungsteuer${(v.steuersatz || "").trim() ? ` (${esc(v.steuersatz)})` : ""}</span>
<dl class="rows" style="margin-top:16px">
${v.teile.map((t, i) => `<div class="row" ${i === v.teile.length - 1 ? 'style="border-bottom:0"' : ""}>
<dt>${esc(t.bez)}${t.sf ? `<br><span style="color:var(--fg3);font-size:11px">${esc(t.sf)} · Beitragssatz ${esc(t.satz)}</span></dt>` : "</dt>"}
@@ -3211,7 +3368,7 @@ function vVertragsdetails() {
<div style="font-size:14px;margin-top:12px">Panne, Unfall, Diebstahl, Fahrerausfall</div>
<span class="label" style="margin-top:10px">Nummern und Leistungen für den Ernstfall</span>
</button>
${v.leistungen.map((g) => `<div class="tile"><span class="label">${esc(g.gruppe)}</span>
${v.leistungen.filter((g) => (g.gruppe || "").trim() || (g.posten || []).length).map((g) => `<div class="tile"><span class="label">${esc(g.gruppe)}</span>
<dl class="rows" style="margin-top:8px">
${g.posten.map((x, i) => `<div class="row" ${i === g.posten.length - 1 ? 'style="border-bottom:0"' : ""}>
<dt style="color:var(--fg)">${esc(x)}</dt><dd></dd></div>`).join("")}
@@ -4479,6 +4636,20 @@ let belegPopup = null;
let swipeAktivWrap = null, swipeStartX = 0, swipeStartY = 0, swipeGeste = null, swipeDX = 0;
let swipeWarGezogen = false;
const SWIPE_BREITE = 84;
/* Ab diesem Anteil der Zeilenbreite loescht das Loslassen sofort, ohne
zweiten Tipp - so macht es iOS, und genau das fehlte: ein voller Wisch
blieb wie ein kurzer an der Raste stehen (Wunsch des Eigentuemers,
04.09.2026). 0,55 haelt die Geste absichtsvoll: eine halbe Zeile ist beim
seitlichen Scrollen leicht versehentlich erreicht, eine klare Mehrheit
nicht. Wortgleich mit WISCH_VOLL_* in bausteine.tsx und
FULL_SWIPE_FRACTION in SwipeRow (Design-System). */
const SWIPE_VOLL_ANTEIL = 0.55;
const SWIPE_VOLL_MAX = 220;
const SWIPE_VOLL_MIN = 120;
function swipeVollAb(wrap) {
const breite = wrap ? wrap.offsetWidth : 0;
return -Math.min(SWIPE_VOLL_MAX, Math.max(SWIPE_VOLL_MIN, breite * SWIPE_VOLL_ANTEIL));
}
/* Ziehen zum Aktualisieren (nur Übersicht) - ptrY0 ist die Y-Position des
Fingers/der Maus beim Start, null wenn gerade keine Geste läuft. Eigene
@@ -5865,19 +6036,40 @@ function ereignisseVerdrahten() {
// audi-dashboard.css) - sonst blitzt er dauerhaft an den Zeilenraendern durch.
swipeAktivWrap.classList.add("wischt");
const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0;
swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx));
// Nicht mehr bei SWIPE_BREITE abschneiden: ohne den weiteren Weg gibt es
// keinen vollen Wisch und keine Rueckmeldung, dass er loescht.
swipeDX = Math.min(0, basis + dx);
const flaeche = swipeAktivWrap.querySelector(".swipe-delete");
if (flaeche) {
// Je weiter gezogen, desto mehr Rot - dieselbe Lesart wie auf iOS.
flaeche.style.width = Math.max(SWIPE_BREITE, -swipeDX) + "px";
flaeche.classList.toggle("swipe-delete--scharf", swipeDX <= swipeVollAb(swipeAktivWrap));
}
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
inhalt.style.transition = "none";
inhalt.style.transform = `translateX(${swipeDX}px)`;
}, { passive: false });
function swipeBeenden() {
if (!swipeAktivWrap) return;
const wrapJetzt = swipeAktivWrap;
const flaecheJetzt = wrapJetzt.querySelector(".swipe-delete");
if (flaecheJetzt) { flaecheJetzt.style.width = ""; flaecheJetzt.classList.remove("swipe-delete--scharf"); }
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
inhalt.style.transition = ""; inhalt.style.transform = "";
swipeAktivWrap.classList.remove("wischt");
if (swipeGeste === "x") {
if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
else { swipeAktivWrap.classList.remove("swiped"); }
if (swipeDX <= swipeVollAb(wrapJetzt)) {
// Ganz durchgezogen: jetzt handeln. Die Rueckfrage bleibt - geloescht
// werden hier Fahrten, Belege und Messwerte, die nicht wiederkommen.
schliesseAlleSwipes();
const knopf = wrapJetzt.querySelector("[data-swipe-loeschen]");
if (knopf) {
const [art, id] = knopf.dataset.swipeLoeschen.split(":");
bestaetigen(LOESCH_TEXT[art], LOESCH_HINWEIS[art], "Löschen", () => { eintragLoeschen(art, id); schliesseAlleSwipes(); });
}
}
else if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); wrapJetzt.classList.add("swiped"); }
else { wrapJetzt.classList.remove("swiped"); }
}
swipeAktivWrap = null; swipeGeste = null; swipeDX = 0;
// Der nachlaufende Klick derselben Geste feuert synchron noch in diesem
@@ -6170,7 +6362,7 @@ function ereignisseVerdrahten() {
smartdealPopupOffen = false; profilSpeichern(); render();
return;
}
if (e.target.closest("[data-bildgalerie]")) { bildWeiter(); return; }
if (e.target.closest("[data-bildgalerie]")) { if (galerieGewischt) { galerieGewischt = false; return; } bildWeiter(); return; }
if (e.target.closest("[data-sbneu]")) {
CAR.service.buch.unshift({ datum: dedat(heute()), km: CAR.odoBekannt ? CAR.odo : 0, art: SERVICE_ARTEN[0], werkstatt: CAR.service.autohaus.name, kosten: null, arbeiten: "" });
entwurfIndex = 0; go("sbuch", "0"); return;
@@ -7104,6 +7296,7 @@ class AudiDashboardPanel extends HTMLElement {
ereignisseVerdrahten();
randwischenVerdrahten();
standortMenuVerdrahten();
galerieWischenVerdrahten();
themeIcon();
setInterval(standAlterTicken, 1000);
@@ -25,7 +25,7 @@
--line:rgba(255,255,255,.13);
--line-strong:rgba(255,255,255,.24);
--fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93;
--ok:#30D158; --warn:#FFD60A; --bad:#FF453A;
--ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015;
/* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown-
Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht,
egal wie dunkel die App selbst ist - das liess z. B. "Modell" beim
@@ -45,7 +45,7 @@
auf 4,75:1 bzw. 5,30:1, ohne den ruhigen Gesamteindruck zu aendern.
Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */
--fg:#000000; --fg2:#3C3C43; --fg3:#6B6B70;
--ok:#34C759; --warn:#FF9F0A; --bad:#FF3B30;
--ok:#34C759; --warn:#FF9F0A; --bad:#D70015; --bad-flaeche:#D70015;
--ios-sep:rgba(60,60,67,.16);
--ios-fill:rgba(120,120,128,.16);
--tile-deckend:#FFFFFF;
@@ -396,3 +396,30 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
/* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */
.row dd .dot{width:8px;height:8px}
/* --- Apple HIG: 44x44 pt als kleinstes Tippziel -------------------------
Wortgleich mit rahmen.css/screens.css der App (04.09.2026 gemessen).
Das Overlay laesst das Bild unveraendert und vergroessert nur, was der
Finger trifft; Mindesthoehen dort, wo das Element ohnehin Flaeche ist. */
#back, .ringsbtn, .icon-btn, .zahnrad {
position: relative;
}
#back::after, .ringsbtn::after, .icon-btn::after, .zahnrad::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: var(--r-pill);
}
.feld input, .feld select, .seg button {
min-height: 44px;
}
input[type="range"] {
min-height: 44px;
}
@@ -1103,7 +1103,7 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
.swipe-wrap.swiped .swipe-delete { visibility: visible; }
.swipe-loeschen {
width: 100%;
background: var(--bad); color: #fff; border: none;
background: var(--bad-flaeche, var(--bad)); color: #fff; border: none;
font-family: inherit; font-size: 13px;
letter-spacing: normal;
cursor: pointer;
@@ -1701,3 +1701,8 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
.debugknopf:active {
color: var(--fg2);
}
/* Voll durchgewischt: die Flaeche wird kraeftiger, damit vor dem Loslassen
ablesbar ist, dass jetzt geloescht wird und nicht nur aufgeklappt.
Wortgleich mit .dm-wisch__loeschen--scharf in der App. */
.swipe-delete--scharf { filter: brightness(1.12); }
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.22",
"version": "2026.9.4.23",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [