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.
This commit is contained in:
Paul Nothaft
2026-08-13 16:04:05 +02:00
parent 7a43be9764
commit bc2834e867
3 changed files with 74 additions and 16 deletions
+48 -8
View File
@@ -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 `<div class="badge">
<img class="positive" src="/local/badges/${datei}-positive.svg" alt="${alt}">
@@ -1017,10 +1032,10 @@ function vHome() {
<div class="tile flat" style="margin-bottom:22px">
<div class="tankzeile">
<div class="fig" style="font-size:60px">${CAR.reichweite != null ? de(CAR.reichweite) : ""}<span class="unit">km</span></div>
<span class="pct">${pct} %</span>
<span class="pct">${CAR.tankBekannt ? pct + " %" : ""}</span>
</div>
<div class="balken"><i style="width:${pct}%;background:${balkenFarbe}"></i></div>
<span class="label" style="margin-top:12px">Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}</span>
<span class="label" style="margin-top:12px">Reichweite${CAR.tankBekannt && liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}</span>
</div>
<div class="duo">
<div class="tile"><span class="label">Kilometerstand</span>
@@ -2860,8 +2875,8 @@ function vSetupPopup() {
<strong>Setup</strong>
<span>Sensoren dieser Home-Assistant-Instanz den App-Funktionen zuordnen.</span>
<div class="setup-kopf-schalter">
<label class="switch"><input type="checkbox" data-setup-nurpassend ${setupNurPassend ? "checked" : ""}><span class="track"></span></label>
<span>Nur passende Sensoren anzeigen</span>
<label class="switch"><input type="checkbox" data-setup-nurpassend aria-label="Nur passende Sensoren anzeigen" ${setupNurPassend ? "checked" : ""}><span class="track"></span></label>
<span aria-hidden="true">Nur passende Sensoren anzeigen</span>
</div>
</div>
<div class="setup-koerper">
@@ -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) =>
`<button class="tab" data-tab="${t.id}" aria-current="false">
<span class="tabpille"><svg viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"${ICONS_VOLL.has(t.id) ? ' class="voll"' : ""}>${ICONS[t.id]}</svg></span><span>${t.label}</span>
@@ -3949,7 +3989,7 @@ class AudiDashboardPanel extends HTMLElement {
wrapper.innerHTML = `
<div class="phone">
<div class="topbar">
<button class="back" id="back" aria-label="Zurück"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="m10.21 12 5.12-5.08-.7-.71L8.79 12l5.84 5.8.7-.72L10.22 12Z"/></svg></button> <div class="rings" id="marke" aria-label="Audi"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 289 100" fill="currentColor" aria-hidden="true"><defs><clipPath id="audiRingsClip" clipPathUnits="userSpaceOnUse"><path d="M0 100h289V0H0z"/></clipPath></defs><g clip-path="url(#audiRingsClip)" transform="matrix(1 0 0 -1 0 100.00040017)"><path d="M239 11.1914c-8.885 0-17.075 3.008-23.624 8.05 6.646 8.489 10.624 19.165 10.624 30.759 0 11.593-3.978 22.269-10.624 30.759 6.549 5.041 14.739 8.05 23.624 8.05 21.399 0 38.809-17.41 38.809-38.809 0-21.4-17.41-38.809-38.809-38.809m-86.625 8.05c6.647 8.489 10.625 19.165 10.625 30.759 0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.885 0 17.075-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.549-5.042-14.739-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-63 0C96.022 27.7304 100 38.4064 100 50.0004c0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.884 0 17.076-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-78.184 30.759c0 21.399 17.41 38.809 38.809 38.809 8.884 0 17.076-3.009 23.624-8.05C66.978 72.2694 63 61.5934 63 50.0004c0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-21.399 0-38.809 17.409-38.809 38.809M81.5 27.3734c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m31.5 72.627c-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.568 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-27.57 0-50-22.43-50-50 0-27.571 22.43-50 50-50 11.931 0 22.896 4.206 31.5 11.207C90.104 4.2064 101.068.0004 113 .0004c11.931 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 11.932 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 27.57 0 50 22.429 50 50 0 27.57-22.43 50-50 50"/></g></svg></div>
<button class="back" id="back" aria-label="Zurück"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="m10.21 12 5.12-5.08-.7-.71L8.79 12l5.84 5.8.7-.72L10.22 12Z"/></svg></button> <button type="button" class="rings" id="marke" aria-label="Einstellungen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 289 100" fill="currentColor" aria-hidden="true"><defs><clipPath id="audiRingsClip" clipPathUnits="userSpaceOnUse"><path d="M0 100h289V0H0z"/></clipPath></defs><g clip-path="url(#audiRingsClip)" transform="matrix(1 0 0 -1 0 100.00040017)"><path d="M239 11.1914c-8.885 0-17.075 3.008-23.624 8.05 6.646 8.489 10.624 19.165 10.624 30.759 0 11.593-3.978 22.269-10.624 30.759 6.549 5.041 14.739 8.05 23.624 8.05 21.399 0 38.809-17.41 38.809-38.809 0-21.4-17.41-38.809-38.809-38.809m-86.625 8.05c6.647 8.489 10.625 19.165 10.625 30.759 0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.885 0 17.075-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.549-5.042-14.739-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-63 0C96.022 27.7304 100 38.4064 100 50.0004c0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.884 0 17.076-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-78.184 30.759c0 21.399 17.41 38.809 38.809 38.809 8.884 0 17.076-3.009 23.624-8.05C66.978 72.2694 63 61.5934 63 50.0004c0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-21.399 0-38.809 17.409-38.809 38.809M81.5 27.3734c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m31.5 72.627c-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.568 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-27.57 0-50-22.43-50-50 0-27.571 22.43-50 50-50 11.931 0 22.896 4.206 31.5 11.207C90.104 4.2064 101.068.0004 113 .0004c11.931 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 11.932 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 27.57 0 50 22.429 50 50 0 27.57-22.43 50-50 50"/></g></svg></button>
<div class="head"><div class="eyebrow" id="eyebrow"></div><div class="title" id="title">Übersicht</div>
<div class="sync" id="sync"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="m18.65 4.95 2.56 2.55-2.56 2.55-.7-.7L19.29 8H7.2A5.24 5.24 0 0 0 2 13.38V14H1v-.6A6.24 6.24 0 0 1 7.2 7h12.1l-1.34-1.35.7-.7ZM23 11.59A6.24 6.24 0 0 1 16.8 18H4.7l1.35 1.35-.7.7-2.56-2.55 2.56-2.55.7.7L4.71 17H16.8a5.24 5.24 0 0 0 5.2-5.38V11h1v.6Z"/></svg><span></span></div></div>
<button class="themebtn" id="theme" hidden aria-hidden="true"><svg id="themeicon" viewBox="0 0 20 20" width="15" height="15"></svg></button>
+3 -3
View File
@@ -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;
+23 -5
View File
@@ -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 {