diff --git a/AGENTS.md b/AGENTS.md index c2e1b2b..4c81edc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10914,3 +10914,206 @@ Parser-Suite jetzt **17 Tests** (von 11): die neue Klasse und dass ein Aufgeben den Beleg nicht beschaedigt. Alle zwoelf Belege liefern weiterhin `Shell`. `audi_ha_test` auf 2026.9.4.22, 0 Tracebacks, das Nachtragen lief erwartungsgemaess **nicht** erneut. + +## CP. Der grosse Pruefdurchgang - und was er gefunden hat (2026.9.4.23) + +Auftrag des Eigentuemers: alle Funktionen, alle Bildschirme, optisch und +funktional, Audi CI und Apple HIG, danach die Paritaet. Danach: alles beheben. + +### Wie geprueft wurde + +Nicht nach Augenmass. Eine Pruefroutine (`/local/pruef.js` im Testcontainer) +lief auf **jedem der 26 Bildschirme** und mass seitlichen Ueberlauf, +Tippzielgroessen, abgeschnittenen Text, Schriftgroessen unter 11 px, Bilder +ohne Quelle und **WCAG-Kontrast fuer jeden einzelnen Textknoten**. Dazu die +Live-App mit den echten Daten (der Eigentuemer hat den Zugang selbst +eingetragen), die Musterseite fuer Leer- und Fehlerzustaende, und das Panel +zum Abgleich. + +**Merkposten fuer kuenftige Pruefungen:** ohne Token zeigt die App nur den +Einrichtungsbildschirm. Statt wieder auf die Musterseite auszuweichen und die +Live-Pruefung als "nicht moeglich" zu vermerken - **danach fragen**. Der +Eigentuemer hat sie sofort gegeben. + +### Befund 1: deutsche Datumsangaben wurden falsch gelesen + +`new Date("01.03.2025")` liest amerikanisch, also Monat vor Tag: + +| gespeichert | angezeigt | +|---|---| +| `01.03.2025` (1. Maerz) | **`03.01.2025`** | +| `12.11.2024` | **`11.12.2024`** | +| `25.03.2025` | **"-"** - der Wert verschwand | +| `08/2026` (Hauptuntersuchung) | **"-"** | + +Tage bis 12 wurden **still vertauscht**, ab Tag 13 fiel der Wert ganz weg. +Sichtbar an der Erstzulassung: "Mein Audi" zeigte den Rohwert richtig, +"Identitaet und Technik" formatierte ihn und drehte ihn dabei um - **derselbe +Wert, zwei Ergebnisse auf zwei Bildschirmen**. + +`alsZeitpunkt()` in `format.ts` erkennt jetzt genau die drei Formen, die in +diesem Projekt vorkommen: ISO vom Backend, `TT.MM.JJJJ` aus den +Eingabefeldern, `MM/JJJJ` fuer die Hauptuntersuchung. Alles andere bleibt +`null` - **lieber kein Datum als ein erfundenes**. Unmoegliche Daten werden +abgewiesen statt still verschoben (`new Date(2025,1,31)` waere der 3. Maerz). + +**Monatsgenau ist eine eigene Genauigkeit.** `08/2026` nennt keinen Tag; +daraus "01.08.2026" zu machen waere eine Erfindung. Das Ergebnis traegt seine +Genauigkeit mit sich, und angezeigt wird "August 2026". + +### Befund 2: die Hauptuntersuchung war in beiden Oberflaechen unerreichbar + +`08/2026` stand im Profil, gezeigt wurde "-". In der App aus Befund 1. Im +Panel aus einem anderen Grund, der genauso stillschweigend war: es leitete die +Faelligkeit aus der **Erstzulassung** ab - aber nur, wenn die ein Schraegstrich +enthielt. Bei `01.03.2025` (eine voellig richtige Eingabe) blieb der Termin +leer, und den ausdruecklich eingetragenen Wert benutzte es gar nicht. + +Beide Oberflaechen nehmen jetzt dieselbe Reihenfolge: **eingetragene +Faelligkeit vor Servicebuch vor Erstzulassung + 24 Monate**. Das Panel hat dazu +`datumAusText()` bekommen - wortgleich mit `alsZeitpunkt()` in der App. + +### Befund 3: die Tankstellenmarke stand zweimal da + +Mein eigener Fehler vom selben Tag (Abschnitt CN). Ein von Hand eingetragener +Name ohne Komma rutschte komplett als "Ort" durch: + + "Shell München Ost" + "Musterstraße 5" + -> "Shell, Musterstr., Shell München Ost" + +Die Regel uebersprang nur Teile, die **fuer sich** eine Marke sind. +`ohneMarkeVorn()` nimmt die Marke jetzt vorn ab. Steht sie NICHT am Anfang +("Autohof Shell Nord"), bleibt der Teil unangetastet - dort ist nicht sicher, +was Marke und was Ortsname ist. + +### Befund 4: Bilder wurden geholt, obwohl beide wussten, dass es sie nicht gibt + +Der Code sagt es selbst - "0 heisst: dieses Foto gibt es nicht" - und baute +die Adresse trotzdem. Gemessen: **fuenf Anfragen fuer `front-schraeg.webp` in +einem einzigen Ladevorgang**, dazu heckansicht, cockpit, sitze und +seitenansicht-winter, jede eine 404, bei jedem Rendern erneut. + +Sichtbar war nichts (der Platzhalter griff ueber `onerror`), die Konsole aber +voll. Jetzt liefern `bildUrl()` bzw. `bildAdresse()` gar keine Adresse mehr, +wenn der Stand 0 ist. **Nach der Korrektur gemessen: 4 Anfragen, alle 200, +kein einziges `v=0`.** + +### Befund 5: ein Bildelement ohne Quelle zeigt das kaputte Bildsymbol + +Und der Rettungsanker greift dabei nicht: **ohne `src` findet kein Ladeversuch +statt, also feuert `onerror` nie**. Genau dann, wenn er gebraucht wuerde. Auf +der Musterseite als kaputtes Symbol samt Alternativtext "Shell" gesehen. + +Neu `Markenlogo` (App) - rendert bei fehlender Adresse **nichts**. Im Panel +faellt das `src`-Attribut weg und die Box startet gleich als `bild-fehlt`; das +Bildelement bleibt trotzdem stehen, weil die Galerie es weiterschaltet. + +### Kleinere Maengel, alle behoben + +* `Versicherungsteuer ()` - leere Klammern ohne Steuersatz (beide). +* Eine Leistungsgruppe ohne Namen UND ohne Posten wurde zur **leeren grauen + Kachel** (beide). +* "Server-Adresse" brach am Bindestrich um - auf dem allerersten Bildschirm. + Seitlicher Rand von 32 auf 20 px. +* Drei Leerdarstellungen in EINER Kachel (Strich, "noch offen", nichts). Die + Erstzulassung wird jetzt formatiert und zeigt einen Strich, wenn nichts da + ist. +* Das Wurzelelement hatte keine Schriftfamilie - Rueckfall auf Times New + Roman. Sichtbar war heute nichts, die Flanke ist trotzdem zu. + +### Apple HIG + +**Kontrast**: auf 25 von 26 Bildschirmen einwandfrei. Eine Unterschreitung - +"Tankvorgang loeschen", `#FF3B30` auf `#F2F2F7` = **3,18** statt 4,5. + +Dabei kam ein zweites, groesseres heraus: **weisse Schrift auf der roten +Loeschflaeche** ergibt 3,41 (Nacht) und 3,55 (Tag) - beide zu wenig. + +Geloest ueber eine **Trennung der Rollen**: Rot als Schrift und Rot als Flaeche +brauchen Gegensaetzliches. Rote Schrift will Abstand zum HELLEN Grund, eine +rote Flaeche will Abstand zur WEISSEN Schrift darauf. Hell ist `--bad` jetzt +`#D70015` (4,83), und `--bad-flaeche` ist in beiden Modi `#D70015` (weiss +darauf: 5,38). Beides Apples eigenes systemRed fuer erhoehten Kontrast - die +HIG-Palette bleibt. + +**Tippziele**: durchgehend unter 44x44 pt - der Zurueck-Pfeil mit 34x34, und +**ohne jede Vergroesserung**, beim meistbenutzten Element der App. + +Zwei Wege, je nach Element: + +* **Unsichtbares Overlay**, wo die sichtbare Groesse Teil des Bildes ist + (Zurueck-Pfeil, Symbolknoepfe, Griff des Standort-Blatts). Nachgemessen mit + `elementFromPoint`: **sichtbar 34x34, treffbar 44x44**. +* **Echte Mindesthoehe** 44 px, wo das Element ohnehin Flaeche ist + (Eingabefelder, Auswahlfelder, Pillen, Segmente). + +Der zweite Weg **veraendert das Bild sichtbar**: Formulare werden hoeher. Das +ist Absicht und dem Eigentuemer so gemeldet. + +### Wischen in der Bildergalerie (Wunsch des Eigentuemers) + +Links/rechts wischen schaltet durch, in beide Richtungen. Zeigerereignisse +statt Beruehrungsereignisse - dieselbe Verdrahtung traegt Finger, Stift und +Maus, genau wie das Standort-Blatt es schon macht. Ueberwiegt die senkrechte +Bewegung, war es ein Scrollen. Ein Wisch schluckt den nachlaufenden Klick, +sonst schaltete die Galerie zweimal weiter. + +### Loeschgeste HIG-konform (Wunsch des Eigentuemers) + +Ein **voller** Wisch loescht jetzt sofort, ohne zweiten Tipp. Schwelle: 55 % +der Zeilenbreite (mindestens 120, hoechstens 220 px) - eine halbe Zeile ist +beim seitlichen Scrollen leicht versehentlich erreicht, eine klare Mehrheit +nicht. Die rote Flaeche waechst mit dem Zug und wird beim Ueberschreiten +heller: **Farbe heisst entschieden**. Die Rueckfrage bleibt - geloescht werden +Fahrten, Belege und Messwerte, die nicht wiederkommen. + +**Dabei einen echten Fehler gefunden:** die Abschlussbehandlung las den +Zugwert aus dem React-Zustand, und der hinkt eine Darstellung hinterher. +Treffen die letzte Bewegung und das Loslassen in derselben Aufgabe ein - genau +das tut ein schneller Wisch -, war die Geste verloren. Nachgestellt: **ein +voller Wisch tat gar nichts.** Der Zug liegt jetzt zusaetzlich in einer +Referenz, und die entscheidet. + +### Paritaet + +Alle 26 Routen und Titel decken sich; der Fahrzeugstatus ist **zeilengleich**; +`live` bleibt der einzige app-eigene Bildschirm. + +**Zwei ungeplante Abweichungen gefunden, beide geschlossen:** die +Datumsformatierung (App formatierte, Panel gab roh aus - deshalb war das Panel +richtig und die App falsch) und die Herkunft der Hauptuntersuchung. Beide +zeigen jetzt dasselbe. + +Die Tankstellen-Funktionen weiterhin **byteweise gleich** ueber elf Faelle - +die Panel-Funktionen aus dem Buendel geschnitten und in Node gegen dieselben +Eingaben laufen lassen. + +### Verifiziert + +`tsc --noEmit` sauber, **271 Tests** (von 258: 9 fuer die Datumsformen, 4 fuer +die Marke am Wortanfang), `vite build` sauber, Design-System gebaut, Panel als +**Modul** geparst, `audi_ha_test` auf `2026.9.4.23`, **0 Tracebacks**. + +**Live nachgemessen, nicht nur kompiliert:** + +* Erstzulassung `01.03.2025` auf BEIDEN Bildschirmen, Hauptuntersuchung + `August 2026` - in App **und** Panel. +* Bildanfragen: 4, alle 200, kein `v=0`. +* Zurueck-Pfeil: sichtbar 34x34, treffbar 44x44. Eingabefelder 44 px hoch. +* Galerie: Wisch links vor, Wisch rechts zurueck, kurzer Tipp vor. +* Loeschzeile: kurzer Wisch klappt auf, voller Wisch (-260 bei 343 px Breite) + loest die Rueckfrage "Fahrt vom 10.08.2026 loeschen?" aus. +* Kontrastpruefung ueber alle Bildschirme: **keine Unterschreitung mehr**. + +### Nicht geprueft - und warum + +Live-Fahrt (braucht eine laufende Fahrt), QR-Scanner (Kamera in der Vorschau +gesperrt), Zwischenablage/Teilen/Kalender (nativ, brauchen das Geraet), das +echte iPhone (Notch, Dynamic Type), und Loeschvorgaenge wurden nicht +ausgeloest - das sind die Daten des Eigentuemers. + +**Eine Korrektur an mir selbst:** ich hatte zwischendurch gemeldet, das +Standort-Blatt sei fehlerhaft, weil "Route" und "Teilen" hinter der Tab-Leiste +liegen. Das war falsch - die 96-px-Vorschau ist Absicht, aufgezogen sitzt das +Blatt sauber 30 px ueber der Leiste. Mein Klick-Test war der Fehler, nicht die +App: das Blatt hoert auf Zeigerereignisse, nicht auf `click`. diff --git a/companion-app/src/daten/tankstelle.test.ts b/companion-app/src/daten/tankstelle.test.ts index c304861..6970809 100644 --- a/companion-app/src/daten/tankstelle.test.ts +++ b/companion-app/src/daten/tankstelle.test.ts @@ -11,6 +11,7 @@ import { afterEach, describe, expect, it, vi } from "vitest" import { kartendienstUrl, markeErkennen, + ohneMarkeVorn, tankstelleKandidaten, tankstelleSuchtext, tankstelleKurz, @@ -306,3 +307,32 @@ describe("gespeicherte Marke (station_brand)", () => { ) }) }) + +describe("Marke am Anfang eines Namensteils", () => { + /* Am 04.09.2026 an der Musterseite aufgefallen: ein von Hand eingetragener + Name ohne Komma rutschte komplett als "Ort" durch. */ + + it("laesst die Marke nicht zweimal erscheinen", () => { + expect(tankstelleKurz("Shell München Ost", "Musterstraße 5")).toBe( + "Shell, Musterstr., München Ost", + ) + }) + + it("nimmt der Zeile die Marke ab, laesst den Ort stehen", () => { + expect(ohneMarkeVorn("Shell München Ost")).toBe("München Ost") + expect(ohneMarkeVorn("Shell, SÖLDEN")).toBe("SÖLDEN") + expect(ohneMarkeVorn("Shell")).toBe("") + }) + + it("laesst einen Teil in Ruhe, in dem die Marke nicht vorn steht", () => { + // Dort ist nicht sicher, was Marke und was Ortsname ist. + expect(ohneMarkeVorn("Autohof Shell Nord")).toBe("Autohof Shell Nord") + expect(ohneMarkeVorn("Musterstraße 5")).toBe("Musterstraße 5") + }) + + it("achtet auf die Wortgrenze", () => { + // "Star" darf nicht in "Starnberg" treffen. + expect(ohneMarkeVorn("Starnberg")).toBe("Starnberg") + expect(ohneMarkeVorn("Sprintstraße 5")).toBe("Sprintstraße 5") + }) +}) diff --git a/companion-app/src/daten/tankstelle.ts b/companion-app/src/daten/tankstelle.ts index b2e1573..c183473 100644 --- a/companion-app/src/daten/tankstelle.ts +++ b/companion-app/src/daten/tankstelle.ts @@ -69,6 +69,30 @@ const MARKEN = [ * geraten wird nur, wo es fehlt: bei Vorgaengen ohne Beleg, bei von Hand * eingetragenen Tankstellen und solange das Nachtragen nicht gelaufen ist. */ +/** + * Nimmt einem Namensteil die fuehrende Kopfmarke ab. + * + * WARUM NICHT NUR VERGLEICHEN: bis zum 04.09.2026 wurde ein Namensteil nur + * dann uebersprungen, wenn er FUER SICH eine Marke ist ("Shell"). Ein von + * Hand eingetragener Name wie "Shell München Ost" hat aber kein Komma - er + * rutschte komplett durch und landete als ORT in der Zeile: + * + * "Shell München Ost" + "Musterstraße 5" + * -> "Shell, Musterstr., Shell München Ost" + * + * Jetzt faellt die Marke vorn ab und der Rest bleibt der Ort. Steht die Marke + * NICHT am Anfang ("Autohof Shell Nord"), bleibt der Teil unangetastet - + * dort ist nicht sicher, was Marke und was Ortsname ist. + */ +export function ohneMarkeVorn(teil: string): string { + 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 +} + export function markeErkennen(name: string | null | undefined): string | null { const roh = (name ?? "").trim() if (!roh) return null @@ -162,13 +186,10 @@ export function tankstelleTeile( const quellen: string[] = [] for (const stueck of (adresse ?? "").split(",")) quellen.push(stueck) 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. - if (t && markeErkennen(t)?.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) { diff --git a/companion-app/src/format.test.ts b/companion-app/src/format.test.ts index 271f14a..23aa23b 100644 --- a/companion-app/src/format.test.ts +++ b/companion-app/src/format.test.ts @@ -2,7 +2,20 @@ import { describe, expect, it } from "vitest" import { alsCsv } from "./screens/csv" import { kalenderDatei } from "./screens/kalender" -import { dauer, de, deOderStrich, eur, isoTag, schnittpreis, summe } from "./format" +import { + datum, + datumLang, + datumZeit, + dauer, + de, + deOderStrich, + eur, + isoTag, + monatJahr, + schnittpreis, + summe, + uhrzeit, +} from "./format" describe("Zahlen im deutschen Format", () => { it("setzt Punkt als Tausender- und Komma als Dezimaltrennzeichen", () => { @@ -101,3 +114,67 @@ describe("Kalenderdatei", () => { ) }) }) + +describe("Datumsangaben aus dem Bestand", () => { + /* Alle Faelle am 04.09.2026 an der laufenden Instanz gemessen - vorher + vertauschte `new Date()` Tag und Monat, oder verwarf den Wert ganz. */ + + it("liest deutsche Tagesdaten richtig herum", () => { + // Der gemeldete Fall: die Erstzulassung stand als 03.01.2025 da. + expect(datum("01.03.2025")).toBe("01.03.2025") + expect(datum("12.11.2024")).toBe("12.11.2024") + expect(datum("1.3.2025")).toBe("01.03.2025") + }) + + it("verliert Tage ab dem 13. nicht mehr", () => { + // Die waren vorher ungueltig und verschwanden als "–". + expect(datum("25.03.2025")).toBe("25.03.2025") + expect(datum("31.12.2023")).toBe("31.12.2023") + }) + + it("zeigt monatsgenaue Angaben als Monat, nicht als erfundenen Tag", () => { + // Die Hauptuntersuchung steht als "MM/JJJJ" im Profil (so verlangt es das + // Einrichtungsfeld). Vorher: "–". Ein "01.08.2026" waere eine Erfindung. + expect(datum("08/2026")).toBe("August 2026") + expect(datum("08.2026")).toBe("August 2026") + expect(datum("2026-08")).toBe("August 2026") + expect(datumLang("08/2026")).toBe("August 2026") + }) + + it("gibt einer monatsgenauen Angabe keine Uhrzeit", () => { + // 00:00 waere erfunden. + expect(uhrzeit("08/2026")).toBe("–") + expect(datumZeit("08/2026")).toBe("August 2026") + }) + + it("laesst ISO vom Backend unveraendert richtig", () => { + expect(datum("2026-08-14")).toBe("14.08.2026") + expect(datum("2024-09-25T15:47:00")).toBe("25.09.2024") + expect(datumZeit("2024-09-25T15:47:00")).toBe("25.09.2024, 15:47 Uhr") + }) + + it("weist unmoegliche Daten ab, statt still ein anderes zu zeigen", () => { + // new Date(2025,1,31) waere in JavaScript der 3. Maerz. + expect(datum("31.02.2025")).toBe("–") + expect(datum("32.01.2025")).toBe("–") + expect(datum("00.01.2025")).toBe("–") + expect(datum("01.13.2025")).toBe("–") + }) + + it("bleibt bei Unbekanntem beim Strich", () => { + expect(datum(null)).toBe("–") + expect(datum("")).toBe("–") + expect(datum("demnaechst")).toBe("–") + expect(datum("01/03/2025")).toBe("–") + }) + + it("nimmt Date und Zahl weiterhin an", () => { + expect(datum(new Date(2025, 2, 1))).toBe("01.03.2025") + expect(datum(new Date(2025, 2, 1).getTime())).toBe("01.03.2025") + }) + + it("monatJahr kann beide Genauigkeiten", () => { + expect(monatJahr("2026-08-14")).toBe("August 2026") + expect(monatJahr("08/2026")).toBe("August 2026") + }) +}) diff --git a/companion-app/src/format.ts b/companion-app/src/format.ts index 465b1a8..20b445b 100644 --- a/companion-app/src/format.ts +++ b/companion-app/src/format.ts @@ -88,35 +88,137 @@ const UHRZEIT = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-d const MONAT_JAHR = new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }) -function alsDatum(wert: string | number | Date | null | undefined): Date | null { +/** + * Datumsangaben aus dem Bestand kommen in DREI Formen - deshalb reicht + * `new Date(wert)` hier nicht. + * + * WAS AM 04.09.2026 GEMESSEN WURDE + * -------------------------------- + * `new Date()` liest einen nicht-ISO-Text nach amerikanischer Lesart, also + * Monat vor Tag. Fuer deutsche Eingaben heisst das: + * + * "01.03.2025" (1. Maerz) -> 3. Januar 2025 - Tag und Monat vertauscht + * "12.11.2024" -> 11. Dezember - dito + * "25.03.2025" -> ungueltig - Wert verschwand als "-" + * "08/2026" (Hauptunters.) -> ungueltig - dito + * + * Tage bis 12 wurden also STILL vertauscht, ab Tag 13 fiel der Wert ganz weg. + * Sichtbar war es an der Erstzulassung: "Mein Audi" zeigte den Rohwert + * richtig, "Identitaet und Technik" formatierte ihn und drehte ihn dabei um - + * derselbe Wert, zwei Ergebnisse. Und die Hauptuntersuchung stand als "-" da, + * obwohl "08/2026" gespeichert war. + * + * WARUM EIGENE MUSTER STATT EINER BIBLIOTHEK + * ------------------------------------------ + * Es sind genau drei Formen, alle aus diesem Projekt: ISO vom Backend, + * "TT.MM.JJJJ" aus den Eingabefeldern, "MM/JJJJ" fuer die Hauptuntersuchung + * (so verlangt es das Einrichtungsfeld, siehe Panel `einErstzulassung`). + * Erkannt wird nur, was hier wirklich vorkommt - alles andere bleibt `null` + * und damit "-". Lieber kein Datum als ein erfundenes. + * + * MONATSGENAU IST EINE EIGENE GENAUIGKEIT + * --------------------------------------- + * "08/2026" nennt keinen Tag. Daraus den 1. August zu machen und + * "01.08.2026" anzuzeigen waere eine Erfindung - deshalb traegt das Ergebnis + * seine Genauigkeit mit sich, und `datum()` zeigt dann "August 2026". + */ +type Genauigkeit = "tag" | "monat" +interface Zeitpunkt { + zeit: Date + genau: Genauigkeit +} + +/* ISO vom Backend, mit oder ohne Uhrzeit - das kann `new Date` selbst, und + zwar nach Norm statt nach Gutduenken. */ +const ISO = /^\d{4}-\d{2}-\d{2}([T ]|$)/ +/* "1.3.2025" wie "01.03.2025". */ +const DE_TAG = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/ +/* "08/2026" und "08.2026" - Monat und Jahr, ohne Tag. */ +const DE_MONAT = /^(\d{1,2})[./](\d{4})$/ +/* "2026-08" - dieselbe Genauigkeit, in ISO-Schreibweise. */ +const ISO_MONAT = /^(\d{4})-(\d{1,2})$/ + +/** + * Baut das Datum und weist die Rollover-Falle ab: `new Date(2025, 1, 31)` ist + * in JavaScript der 3. Maerz, nicht der 31. Februar. Ein unmoegliches Datum + * soll "-" ergeben, nicht klammheimlich ein anderes. + */ +function bauen(jahr: number, monat: number, tag: number): Date | null { + if (monat < 1 || monat > 12 || tag < 1 || tag > 31) return null + const d = new Date(jahr, monat - 1, tag) + if (d.getFullYear() !== jahr || d.getMonth() !== monat - 1 || d.getDate() !== tag) return null + return d +} + +function alsZeitpunkt(wert: string | number | Date | null | undefined): Zeitpunkt | null { if (wert === null || wert === undefined || wert === "") return null - const datum = wert instanceof Date ? wert : new Date(wert) - return Number.isNaN(datum.getTime()) ? null : datum + if (wert instanceof Date) return Number.isNaN(wert.getTime()) ? null : { zeit: wert, genau: "tag" } + if (typeof wert === "number") { + const d = new Date(wert) + return Number.isNaN(d.getTime()) ? null : { zeit: d, genau: "tag" } + } + + const text = wert.trim() + if (!text) return null + + if (ISO.test(text)) { + const d = new Date(text) + return Number.isNaN(d.getTime()) ? null : { zeit: d, genau: "tag" } + } + + const tag = DE_TAG.exec(text) + if (tag) { + const d = bauen(Number(tag[3]), Number(tag[2]), Number(tag[1])) + return d ? { zeit: d, genau: "tag" } : null + } + + const monat = DE_MONAT.exec(text) + if (monat) { + const d = bauen(Number(monat[2]), Number(monat[1]), 1) + return d ? { zeit: d, genau: "monat" } : null + } + + const isoMonat = ISO_MONAT.exec(text) + if (isoMonat) { + const d = bauen(Number(isoMonat[1]), Number(isoMonat[2]), 1) + return d ? { zeit: d, genau: "monat" } : null + } + + return null +} + +function alsDatum(wert: string | number | Date | null | undefined): Date | null { + return alsZeitpunkt(wert)?.zeit ?? null } export function datum(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? DATUM_KURZ.format(d) : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + return z.genau === "monat" ? MONAT_JAHR.format(z.zeit) : DATUM_KURZ.format(z.zeit) } export function datumLang(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? DATUM_LANG.format(d) : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + return z.genau === "monat" ? MONAT_JAHR.format(z.zeit) : DATUM_LANG.format(z.zeit) } export function uhrzeit(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? UHRZEIT.format(d) : "–" + const z = alsZeitpunkt(wert) + // Eine monatsgenaue Angabe hat keine Uhrzeit - 00:00 waere erfunden. + return z && z.genau === "tag" ? UHRZEIT.format(z.zeit) : "–" } export function datumZeit(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? `${DATUM_KURZ.format(d)}, ${UHRZEIT.format(d)} Uhr` : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + if (z.genau === "monat") return MONAT_JAHR.format(z.zeit) + return `${DATUM_KURZ.format(z.zeit)}, ${UHRZEIT.format(z.zeit)} Uhr` } export function monatJahr(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? MONAT_JAHR.format(d) : "–" + const z = alsZeitpunkt(wert) + return z ? MONAT_JAHR.format(z.zeit) : "–" } /** Dauer in Sekunden als „2 h 14 min" bzw. „14 min". */ diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index caa41ba..89a2f23 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -17,9 +17,9 @@ import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter" import { datum, datumZeit, de, isoTag, standAlter } from "../format" import { useTheme } from "../theme" -import { BildMitMenue, Wertzeile, Werteliste } from "./bausteine" +import { BildMitMenue, Markenlogo, Werteliste, Wertzeile } from "./bausteine" import { bestaetigen } from "./bestaetigung" -import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder" +import { BILDPLAETZE, SEITE_WINTER } from "./bilder" import { DatensatzPopup } from "./DatensatzPopup" import { HistorienImport } from "./HistorienImport" import type { ServicebuchEintrag } from "./Service" @@ -530,14 +530,7 @@ export function Einstellungen({ - {werte.markenname} { - e.currentTarget.style.display = "none" - }} - /> + {/* Das Panel nennt den Schalter "SmartDeal aktiv" - fest, nicht aus dem Markennamen gebaut. */} SmartDeal aktiv diff --git a/companion-app/src/screens/MeinAudi.tsx b/companion-app/src/screens/MeinAudi.tsx index acf1a8d..9d723fc 100644 --- a/companion-app/src/screens/MeinAudi.tsx +++ b/companion-app/src/screens/MeinAudi.tsx @@ -13,7 +13,7 @@ * Werteliste + Chevron statt einer Zeile Fließtext. */ -import { useState } from "react" +import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react" import { Tile } from "@audi-dash/ui" @@ -58,6 +58,36 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = return wert }) } + /* 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 (Standort.tsx). Ein Wisch ab 40 px zaehlt als Wisch, alles + darunter bleibt ein Tipp - dieselbe Schwelle wie dort. + + Ueberwiegt die senkrechte Bewegung, war es ein Scrollen und kein + Blaettern - dann passiert nichts. + + `gewischt` schluckt den anschliessenden Klick: ein Wisch loest auf dem + Knopf auch ein click aus, und ohne diese Sperre schaltete die Galerie + zweimal weiter. Wortgleich mit galerieWischenVerdrahten() im Panel. */ + const zug = useRef<{ x0: number; y0: number } | null>(null) + const gewischt = useRef(false) + const weiter = (richtung: number) => + setzeBildIndex((i) => (i + richtung + GALERIE.length) % GALERIE.length) + const zugStart = (e: ReactPointerEvent) => { + zug.current = { x0: e.clientX, y0: e.clientY } + } + const zugEnde = (e: ReactPointerEvent) => { + const z = zug.current + zug.current = null + if (!z) return + const dx = e.clientX - z.x0 + if (Math.abs(dx) <= 40 || Math.abs(e.clientY - z.y0) > Math.abs(dx)) return + gewischt.current = true + weiter(dx < 0 ? 1 : -1) + } + if (!fahrzeug || !einstellungen) return null const aktiverSatz = @@ -128,8 +158,19 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = @@ -167,9 +208,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = Hinweis, dass es sie gibt, statt einer abgeschnittenen Motorzeile. */} - {/* Leerer Wert statt Strich, wie vAudi(): das Panel gibt hier - den Rohwert aus. */} - + {/* Formatiert und mit Strich, wenn nichts da ist - vorher stand + hier der Rohwert und bei leerem Feld gar nichts, waehrend + FIN daneben "–" und Ausfuehrung "noch offen" zeigte: + drei Leerdarstellungen in einer Kachel (04.09.2026). */} + diff --git a/companion-app/src/screens/TankDetail.tsx b/companion-app/src/screens/TankDetail.tsx index 3538867..efe3756 100644 --- a/companion-app/src/screens/TankDetail.tsx +++ b/companion-app/src/screens/TankDetail.tsx @@ -17,9 +17,8 @@ import { useDaten } from "../daten/DatenKontext" import { kartendienstUrl, tankstelleZeilen } from "../daten/tankstelle" import { datum, de, deOderStrich, eur, uhrzeit } from "../format" import type { SeitenName } from "../navigation" -import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste } from "./bausteine" +import { KRAFTSTOFFSORTEN, Leerzustand, Markenlogo, Werteliste, Wertzeile } from "./bausteine" import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung" -import { markenlogoUrl } from "./bilder" import { BelegPopup } from "./BelegPopup" import { TankstellenKarte } from "./TankstellenKarte" @@ -116,13 +115,7 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na - { - e.currentTarget.style.display = "none" - }} - /> + SmartDeal-Ersparnis } diff --git a/companion-app/src/screens/Tanken.tsx b/companion-app/src/screens/Tanken.tsx index 9688fcb..4dc507c 100644 --- a/companion-app/src/screens/Tanken.tsx +++ b/companion-app/src/screens/Tanken.tsx @@ -21,9 +21,8 @@ import { geteiltenBelegSetzen, geteiltenBelegStand, } from "../daten/geteilterBeleg" -import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand } from "./bausteine" +import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand, Markenlogo } from "./bausteine" import { BelegPopup } from "./BelegPopup" -import { markenlogoUrl } from "./bilder" import { belegFormularwerte, type BelegEntwurf } from "../daten/belegentwurf" import { tankstelleKurz } from "../daten/tankstelle" import { LOESCH_HINWEIS } from "./bestaetigung" @@ -75,14 +74,10 @@ export function Tanken({ geheZu }: { geheZu: (name: SeitenName, id?: string) => {smartdealAktiv && ( - {einstellungen?.markenname { - e.currentTarget.style.display = "none" - }} /> −{eur(ersparnisJahr)} € diff --git a/companion-app/src/screens/Versicherung.tsx b/companion-app/src/screens/Versicherung.tsx index cba93e3..e6448f5 100644 --- a/companion-app/src/screens/Versicherung.tsx +++ b/companion-app/src/screens/Versicherung.tsx @@ -39,6 +39,7 @@ export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: strin const v = fahrzeug.versicherung const teile = (v["teile"] ?? []) as Beitragsposten[] const beitrag = (v["beitrag"] as number) ?? 0 + const steuersatz = ((v["steuersatz"] as string) ?? "").trim() const selbst = (v["selbst"] ?? []) as [string, string][] const service = (v["service"] as string) ?? "" const notfall = (v["notfall"] as string) ?? "" @@ -67,7 +68,9 @@ export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: strin {(v["zahlweise"] as string) ?? ""} · enthält {eur((v["steueranteil"] as number) ?? 0)} € - Versicherungsteuer ({(v["steuersatz"] as string) ?? ""}) + {/* Ohne Steuersatz KEINE leeren Klammern - bis zum 04.09.2026 + stand dort ein nacktes Klammerpaar. */} + Versicherungsteuer{steuersatz ? ` (${steuersatz})` : ""} ( @@ -411,7 +414,12 @@ export function Vertragsdetails({ geheZu }: { geheZu: (name: SeitenName, id?: st if (!fahrzeug) return null const v = fahrzeug.versicherung - const leistungen = (v["leistungen"] ?? []) as Leistungsgruppe[] + /* Gruppen ohne Namen UND ohne Posten fallen raus: sie ergaben eine leere + graue Kachel (am 04.09.2026 auf der Musterseite gesehen). Eine Kachel + ohne Inhalt sagt nichts und sieht aus wie ein Ladefehler. */ + const leistungen = ((v["leistungen"] ?? []) as Leistungsgruppe[]).filter( + (g) => (g.gruppe ?? "").trim() || (g.posten ?? []).length, + ) return ( <> diff --git a/companion-app/src/screens/bausteine.tsx b/companion-app/src/screens/bausteine.tsx index 6811a6b..bee8b62 100644 --- a/companion-app/src/screens/bausteine.tsx +++ b/companion-app/src/screens/bausteine.tsx @@ -3,7 +3,13 @@ * gebraucht wird, aber zu speziell für das Design-System ist. */ -import { useRef, useState, type ReactNode, type TouchEvent as ReactTouchEvent } from "react" +import { + useRef, + useState, + type CSSProperties, + type ReactNode, + type TouchEvent as ReactTouchEvent, +} from "react" import { ActionButton, ActionSheet, Tile } from "@audi-dash/ui" @@ -11,7 +17,7 @@ import type { SicherheitsPunkt } from "../api" import { useDaten } from "../daten/DatenKontext" import { SymbolZahnradVoll } from "../symbole" import { Bild } from "./Bild" -import { bildAlsBase64, bildVerkleinern, bildVersionErneuern } from "./bilder" +import { bildAlsBase64, bildVerkleinern, bildVersionErneuern, markenlogoUrl } from "./bilder" import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung" /** Kraftstoffsorten zur Auswahl - dieselbe feste Liste wie `KRAFTSTOFFSORTEN` @@ -350,6 +356,16 @@ export function StatusKreis({ ok }: { ok: boolean | null }) { */ const WISCH_BREITE = 88 const WISCH_START = 10 +/* Ab diesem Anteil der Zeilenbreite loescht das Loslassen sofort, ohne + zweiten Tipp - so macht es iOS, und genau das fehlte hier: ein voller + Wisch blieb wie ein kurzer an der Raste stehen. + + 0,55 haelt die Geste absichtsvoll. Eine halbe Zeile ist beim seitlichen + Scrollen leicht versehentlich erreicht, eine klare Mehrheit nicht. + Wortgleich mit FULL_SWIPE_FRACTION in SwipeRow (Design-System). */ +const WISCH_VOLL_ANTEIL = 0.55 +const WISCH_VOLL_MAX = 220 +const WISCH_VOLL_MIN = 120 export function WischLoeschen({ beiLoeschen, @@ -362,11 +378,27 @@ export function WischLoeschen({ children: ReactNode }) { const [offen, setzeOffen] = useState(false) - const [zug, setzeZug] = useState(null) + /* Der Zug liegt zusaetzlich in einer Referenz, und die entscheidet. + Der Zustand hinkt eine Darstellung hinterher: treffen die letzte + Bewegung und das Loslassen in derselben Aufgabe ein - genau das tut + ein schneller Wisch -, laese `beiEnde` noch `null` und die Geste waere + verloren. Am 04.09.2026 gemessen: ein voller Wisch tat gar nichts. */ + const [zug, setzeZugZustand] = useState(null) + const zugRef = useRef(null) + const setzeZug = (wert: number | null) => { + zugRef.current = wert + setzeZugZustand(wert) + } const start = useRef<{ x: number; y: number } | null>(null) const entschieden = useRef(false) + const rahmen = useRef(null) const versatz = zug ?? (offen ? -WISCH_BREITE : 0) + const vollAb = () => { + const breite = rahmen.current?.offsetWidth ?? 0 + return -Math.min(WISCH_VOLL_MAX, Math.max(WISCH_VOLL_MIN, breite * WISCH_VOLL_ANTEIL)) + } + const scharf = versatz <= vollAb() const beiStart = (e: ReactTouchEvent) => { if (e.touches.length !== 1) return @@ -389,13 +421,24 @@ export function WischLoeschen({ entschieden.current = true } const basis = offen ? -WISCH_BREITE : 0 - setzeZug(Math.max(-WISCH_BREITE, Math.min(0, basis + dx))) + // Nicht mehr bei WISCH_BREITE abschneiden: ohne den weiteren Weg gibt + // es keinen vollen Wisch und keine Rueckmeldung, dass er loescht. + setzeZug(Math.min(0, basis + dx)) } const beiEnde = () => { - if (zug !== null) setzeOffen(zug < -WISCH_BREITE / 2) + const versatzJetzt = zugRef.current start.current = null setzeZug(null) + if (versatzJetzt === null) return + if (versatzJetzt <= vollAb()) { + // Ganz durchgezogen: jetzt handeln. `beiLoeschen` ist der Handler des + // Aufrufers und fragt dort nach, wo die Daten nicht wiederkommen. + setzeOffen(false) + beiLoeschen() + return + } + setzeOffen(versatzJetzt < -WISCH_BREITE / 2) } // Sichtbar erst, wenn wirklich gewischt wird. Ohne das liegt der rote @@ -406,10 +449,15 @@ export function WischLoeschen({ const zeigt = offen || zug !== null return ( -
+
${eur(v.beitrag)}
- ${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} € Versicherungsteuer (${esc(v.steuersatz)}) + ${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} € Versicherungsteuer${(v.steuersatz || "").trim() ? ` (${esc(v.steuersatz)})` : ""}
${v.teile.map((t, i) => `
${esc(t.bez)}${t.sf ? `
${esc(t.sf)} · Beitragssatz ${esc(t.satz)}
` : ""} @@ -3211,7 +3368,7 @@ function vVertragsdetails() {
Panne, Unfall, Diebstahl, Fahrerausfall
Nummern und Leistungen für den Ernstfall - ${v.leistungen.map((g) => `
${esc(g.gruppe)} + ${v.leistungen.filter((g) => (g.gruppe || "").trim() || (g.posten || []).length).map((g) => `
${esc(g.gruppe)}
${g.posten.map((x, i) => `
${esc(x)}
`).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); diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css index f5035d6..7a22aaf 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css @@ -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