From bc2834e86713a6ea48971f913acb1a4c7be35c3b Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 13 Aug 2026 16:04:05 +0200 Subject: [PATCH] Bedienbarkeit, CSS-Robustheit und drei Anzeigefehler Befund 9: In Telefonbreite blendet die iOS-Auflage das Zahnrad aus, die Ringe waren dort der einzige Zugang zu den Einstellungen - als div mit pointer-events:none aber weder fokussierbar noch fuer Sprachausgabe erreichbar. Sie sind jetzt ein echter Knopf mit aria-label, in jeder Breite bedienbar und unabhaengig davon, ob die Auflage geladen wurde. Befund 10: .navmarke{display:none} stand nur in der iOS-Auflage. Ohne sie waere der Markenklon ein sechstes Element im fuenfspaltigen Raster und die Menueleiste zerfiele. Die Regel steht jetzt in der Basis-CSS, die Sichtbarkeit in der Auflage - genau andersherum als bisher. Befund 13: Das Setup-Fenster deklariert role=dialog aria-modal=true, hielt den Vertrag aber nicht ein. Escape schliesst jetzt (Reihenfolge: Auswahlliste, dann Fenster), der Tabulator wandert im Fenster im Kreis statt in den verdeckten Hintergrund, und der Filterschalter hat einen zugaenglichen Namen. CSS-Spezifitaet: main{padding} in der Auflage verlor gegen main#view in der Basis - auf grossen Bildschirmen blieb der Seitenrand bei 20px statt 44px. Der Rand liegt jetzt in einer Variablen, die auch der negative Rand der randlosen Standortkarte benutzt; sonst haette deren Ausgleich nach dem Fix nicht mehr gepasst. Drei Anzeigefehler, im Browser gegen die Testinstanz gefunden und behoben: ein unlesbares Datum ergab NaN/N statt eines Strichs (das Profil wird laut INSTALL.md von Hand gepflegt, ein ISO-Datum genuegt), ein fehlender Tanksensor ergab 0 Prozent statt unbekannt, und neben dem Typenschild stand die Ausfuehrung doppelt, wenn sie schon im Modellnamen steckt. Alles im echten Panel geprueft: #marke ist ein BUTTON mit aria-label, der Markenklon ist ohne die Auflage versteckt, der Seitenrand folgt der Variablen, und die drei Anzeigen zeigen jetzt Striche statt Rechenreste. --- homeassistant/www/audi-dashboard-app.js | 56 ++++++++++++++++++++---- homeassistant/www/audi-dashboard-ios.css | 6 +-- homeassistant/www/audi-dashboard.css | 28 +++++++++--- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index 5c02f2b..8b0b98e 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -108,6 +108,7 @@ function profilZuCar(p, status) { odo: status.km ?? 0, odoBekannt: status.km != null, tankPct: status.tankprozent ?? 0, + tankBekannt: status.tankprozent != null, reichweite: status.reichweite_km, batteriespannung: status.batteriespannung, gesichert: status.gesichert, @@ -308,8 +309,17 @@ function bildWeiter() { das tatsächliche Tagesdatum ersetzt werden muss. */ function heute() { return new Date(); } const dTag = 86400000; -const mmjj = (d) => String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getFullYear()).slice(2); -const dedat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }); +/* Ein unlesbares Datum ergibt einen Strich, keine Rechenreste. dat() erwartet + TT.MM.JJJJ; steht im Profil etwas anderes (es wird laut INSTALL.md von Hand + gepflegt, z. B. versehentlich im ISO-Format), lieferte mmjj() sonst + "NaN/N" - ein Wert, der wie eine Angabe aussieht und keine ist. */ +const gueltigesDatum = (d) => d instanceof Date && !Number.isNaN(d.getTime()); +const mmjj = (d) => (gueltigesDatum(d) + ? String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getFullYear()).slice(2) + : "–"); +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); } const MISCHDAUER = 180; @@ -866,7 +876,12 @@ function badge() { if (!praefix) return ""; const datei = BADGE_DATEIEN[praefix]; const modellZusatz = (CONFIG.fahrzeugtitel || "").slice(praefix.length).trim(); - const zusatz = [modellZusatz, CONFIG.ausfuehrung].filter(Boolean).join(" "); + /* Die Ausfuehrung steht oft schon im Modellnamen ("RS 4 Avant competition" + plus Ausfuehrung "competition") - dann nicht ein zweites Mal anhaengen, + sonst steht dort "Avant competition competition". */ + const ausfuehrung = (CONFIG.ausfuehrung || "").trim(); + const doppelt = ausfuehrung && modellZusatz.toLowerCase().includes(ausfuehrung.toLowerCase()); + const zusatz = [modellZusatz, doppelt ? "" : ausfuehrung].filter(Boolean).join(" "); const alt = esc(CONFIG.fahrzeugtitel) + " Badge"; return `
${alt} @@ -1017,10 +1032,10 @@ function vHome() {
${CAR.reichweite != null ? de(CAR.reichweite) : "–"}km
- ${pct} % + ${CAR.tankBekannt ? pct + " %" : "–"}
- Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""} + Reichweite${CAR.tankBekannt && liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}
Kilometerstand @@ -2860,8 +2875,8 @@ function vSetupPopup() { Setup Sensoren dieser Home-Assistant-Instanz den App-Funktionen zuordnen.
- - Nur passende Sensoren anzeigen + +
@@ -3027,6 +3042,31 @@ function serviceRufen(dienst, daten) { } function ereignisseVerdrahten() { + /* Tastaturbedienung fuer die beiden Ueberlagerungen. Bisher gab es in der + ganzen Datei keinen keydown-Handler: Das Setup-Fenster deklariert + role="dialog" aria-modal="true", liess sich aber weder mit Escape + schliessen noch verhinderte es, dass der Tabulator in den verdeckten + Hintergrund wandert. Beides gehoert zum Vertrag dieser Rolle. + Der Handler haengt am Dokument, weil Fokus im Shadow DOM auch ausserhalb + von ROOT liegen kann. */ + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + if (sheet) { sheetSchliessen(); e.preventDefault(); return; } + if (setupSucheOffen) { setupSucheOffen = null; setupSuchtext = ""; render(); e.preventDefault(); return; } + if (setupOffen) { setupSchliessen(); e.preventDefault(); return; } + return; + } + if (e.key !== "Tab" || !setupOffen) return; + /* Fokusfalle: innerhalb des Fensters im Kreis wandern statt dahinter. */ + const fokussierbar = ROOT.querySelectorAll( + '.setup-popup button:not([disabled]), .setup-popup input:not([disabled]), .setup-popup [tabindex]:not([tabindex="-1"])'); + if (!fokussierbar.length) return; + const erstes = fokussierbar[0]; + const letztes = fokussierbar[fokussierbar.length - 1]; + const aktiv = ROOT.activeElement; + if (e.shiftKey && (aktiv === erstes || !aktiv)) { letztes.focus(); e.preventDefault(); } + else if (!e.shiftKey && aktiv === letztes) { erstes.focus(); e.preventDefault(); } + }); ROOT.getElementById("tabbar").innerHTML = TABS.map((t) => `
+
Übersicht
diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css index f975bd7..ebf5c12 100644 --- a/homeassistant/www/audi-dashboard-ios.css +++ b/homeassistant/www/audi-dashboard-ios.css @@ -75,7 +75,7 @@ html,body{height:100%} /* Telefonbreite: die Ringe stehen oben rechts und oeffnen die Einstellungen (Stand nach dem Design-Audit). Das Zahnrad entfaellt dort, damit oben rechts nur ein Element sitzt. */ -.rings{order:9;margin-left:8px;margin-right:-10px;pointer-events:auto;cursor:pointer; +.rings{order:9;margin-left:8px;margin-right:-10px;cursor:pointer; min-width:44px;min-height:44px;height:44px;justify-content:flex-end;padding:0 10px} .rings svg{height:17px} .profilbtn{display:none} @@ -85,7 +85,7 @@ html,body{height:100%} .phone:has(.back.on) .title{font-size:17px;font-weight:400;letter-spacing:-.01em;text-align:center} .phone:has(.back.on) .sync{margin-top:2px;font-size:11.5px} -main{padding:0 16px 30px;scroll-behavior:smooth} +main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smooth} /* ---------------- Karten und Listen ---------------- */ .tile{padding:16px 18px;margin-bottom:14px;border-radius:var(--r-tile)} @@ -205,7 +205,7 @@ main{padding:0 16px 30px;scroll-behavior:smooth} .phone:has(.back.on) .title{font-size:26px;text-align:left} .title{font-size:34px} .ptr{grid-column:2;grid-row:2} - main{grid-column:2;grid-row:3;padding:8px 44px 48px;max-width:860px} + main#view{--seitenrand:44px;grid-column:2;grid-row:3;padding:8px var(--seitenrand) 48px;max-width:860px} .tabbar{grid-column:1;grid-row:1 / 4;display:flex;flex-direction:column;gap:4px; align-content:start;width:264px;padding:38px 14px 20px; diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css index 118178e..b3eadb5 100644 --- a/homeassistant/www/audi-dashboard.css +++ b/homeassistant/www/audi-dashboard.css @@ -165,10 +165,23 @@ .themebtn:hover { background: var(--tile-2); color: var(--fg); } .themebtn:active { background: var(--tile-2); color: var(--fg); transform: scale(.92); } .themebtn svg { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; } -/* D4: die Ringe sind Marke, kein Knopf - sie sitzen links neben dem - Fahrzeugnamen und nehmen keine Tipps an. */ -.rings { width: 42px; height: 24px; color: var(--fg); pointer-events: none; } +/* D4 war urspruenglich: die Ringe sind Marke, kein Knopf. Seit die + iOS-Auflage in Telefonbreite das Zahnrad ausblendet, sind sie dort aber der + einzige Zugang zu den Einstellungen - und als reines div mit + pointer-events:none war dieser Zugang fuer Tastatur und Sprachausgabe gar + nicht erreichbar. Sie sind deshalb jetzt ein echter Knopf, in jeder Breite + bedienbar. Auf grossen Bildschirmen gibt es damit zwei Wege in die + Einstellungen (Ringe und Zahnrad) - unschaedlich, und der Zugang haengt + nicht mehr davon ab, ob audi-dashboard-ios.css geladen wurde. */ +.rings { width: 42px; height: 24px; color: var(--fg); } .rings svg { width: 42px; height: auto; } + +/* Der Markenklon in der Seitenleiste wird vom Skript unbedingt in die + Menueleiste gehaengt; sichtbar macht ihn erst die iOS-Auflage ab 860px. + Das Verstecken gehoert deshalb hierher: laedt die Auflage nicht (404, + Cache), waere er sonst ein sechstes Element im fuenfspaltigen Raster und + die Menueleiste zerfiele. */ +.navmarke { display: none; } .profilbtn { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; @@ -195,11 +208,16 @@ @keyframes drehen { to { transform: rotate(360deg); } } main#view { + /* Der seitliche Rand steht als Variable, weil randlose Elemente ihn per + negativem Rand ausgleichen muessen (siehe .standort-vollbild). Wer das + Padding aendert, ohne die Variable zu benutzen, verschiebt die randlosen + Elemente unbemerkt. */ + --seitenrand: 20px; flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; - padding: 10px 20px 34px; + padding: 10px var(--seitenrand) 34px; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; scrollbar-width: none; @@ -691,7 +709,7 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } } @keyframes user-pulse { 0% { transform: scale(.4); opacity: .9; } 100% { transform: scale(1); opacity: 0; } } -.standort-vollbild { position: relative; height: min(640px, calc(100dvh - 190px)); border-radius: var(--r-tile); overflow: hidden; margin: 0 -20px; } +.standort-vollbild { position: relative; height: min(640px, calc(100dvh - 190px)); border-radius: var(--r-tile); overflow: hidden; margin: 0 calc(-1 * var(--seitenrand)); } .standort-karte { width: 100%; height: 100%; } .kartensteuerung {