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:
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
.ads-action--destructive:hover:not(:disabled) {
|
||||
background: var(--bad);
|
||||
background: var(--bad-flaeche, var(--bad));
|
||||
color: #fff;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@@ -26,3 +26,28 @@
|
||||
opacity: .4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Apple HIG verlangt 44x44 pt als kleinstes Tippziel. Der Knopf SIEHT
|
||||
weiterhin 34x34 aus - die Flaeche liegt unsichtbar darum.
|
||||
|
||||
Warum nicht einfach groesser machen: die Groessen sind ueber viele Sitzungen
|
||||
am gerenderten Bild abgestimmt worden. Ein Overlay loest das Missverhaeltnis
|
||||
zwischen Sichtbarem und Treffbarem, ohne das Bild anzufassen.
|
||||
|
||||
Am 04.09.2026 gemessen: 34x34, teils 24x24 - beim Zurueck-Pfeil, also dem
|
||||
meistbenutzten Bedienelement der App. */
|
||||
.ads-icon-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ads-icon-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
/* Nur Trefferflaeche, kein Aussehen. */
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
position: absolute;
|
||||
inset: 0 0 0 auto;
|
||||
width: 84px;
|
||||
background: var(--bad);
|
||||
background: var(--bad-flaeche, var(--bad));
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
@@ -27,10 +27,17 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ads-swipe-wrap:has(.ads-swiped) .ads-swipe-delete {
|
||||
.ads-swipe-wrap:has(.ads-swiped) .ads-swipe-delete,
|
||||
.ads-swipe-wrap.ads-swiping .ads-swipe-delete {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Voll durchgewischt: die Flaeche wird kraeftiger, damit vor dem Loslassen
|
||||
ablesbar ist, dass jetzt geloescht wird und nicht nur aufgeklappt. */
|
||||
.ads-swipe-delete--armed {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.ads-swipe-delete:active {
|
||||
filter: brightness(.88);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
const SWIPE_THRESHOLD = -40;
|
||||
/** Past this the row snaps open and stays there, waiting for a tap. */
|
||||
const SNAP_OPEN_AT = -40;
|
||||
/** How far the opened row rests. */
|
||||
const SWIPE_OPEN = -84;
|
||||
/**
|
||||
* Past this fraction of the row's width, releasing deletes straight away —
|
||||
* no second tap. That is what iOS does, and what this component was missing:
|
||||
* a full swipe used to stop at the resting position like a short one.
|
||||
*
|
||||
* 0.55 keeps the gesture deliberate. Half a row is easy to reach by accident
|
||||
* while scrolling a list sideways; a clear majority of the row is not.
|
||||
*/
|
||||
const FULL_SWIPE_FRACTION = 0.55;
|
||||
/** Never ask for more than this, so a very wide row stays reachable. */
|
||||
const FULL_SWIPE_MAX = 220;
|
||||
|
||||
/** Module-wide pub/sub so opening one row closes every other open row (mirrors the source app's schliesseAlleSwipes()). */
|
||||
const listeners = new Set<() => void>();
|
||||
@@ -33,6 +46,21 @@ export function SwipeRow({
|
||||
const isControlled = swiped !== undefined;
|
||||
const isSwiped = isControlled ? swiped : internalSwiped;
|
||||
|
||||
/** Live offset while a finger is down — null when nothing is being dragged.
|
||||
*
|
||||
* Kept in a ref as well as in state, and the ref is the one that decides.
|
||||
* State lags by a render: when the last pointermove and the pointerup
|
||||
* arrive in the same task — which is exactly what a fast flick does —
|
||||
* the release handler would still read `null` and the swipe would be
|
||||
* lost. Measured on 2026-09-04: a full swipe did nothing at all. */
|
||||
const [drag, setDragState] = React.useState<number | null>(null);
|
||||
const dragRef = React.useRef<number | null>(null);
|
||||
const setDrag = (value: number | null) => {
|
||||
dragRef.current = value;
|
||||
setDragState(value);
|
||||
};
|
||||
const wrapRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const setSwiped = React.useCallback(
|
||||
(next: boolean) => {
|
||||
if (!isControlled) setInternalSwiped(next);
|
||||
@@ -50,43 +78,80 @@ export function SwipeRow({
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const fullSwipeAt = () => {
|
||||
const width = wrapRef.current?.offsetWidth ?? 0;
|
||||
return -Math.min(FULL_SWIPE_MAX, Math.max(120, width * FULL_SWIPE_FRACTION));
|
||||
};
|
||||
|
||||
const dragStartX = React.useRef<number | null>(null);
|
||||
const decided = React.useRef(false);
|
||||
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
dragStartX.current = e.clientX;
|
||||
decided.current = false;
|
||||
};
|
||||
|
||||
const onPointerMove = (e: React.PointerEvent) => {
|
||||
if (dragStartX.current === null) return;
|
||||
const delta = e.clientX - dragStartX.current;
|
||||
if (delta < SWIPE_THRESHOLD) {
|
||||
closeAllExcept(close);
|
||||
setSwiped(true);
|
||||
dragStartX.current = null;
|
||||
} else if (delta > -SWIPE_THRESHOLD) {
|
||||
setSwiped(false);
|
||||
if (!decided.current) {
|
||||
if (Math.abs(delta) < 8) return;
|
||||
decided.current = true;
|
||||
if (delta < 0) closeAllExcept(close);
|
||||
}
|
||||
// Follow the finger instead of jumping to the resting position: without a
|
||||
// live offset there is no way to tell a short swipe from a full one, and
|
||||
// no feedback that pulling further will delete.
|
||||
const base = isSwiped ? SWIPE_OPEN : 0;
|
||||
setDrag(Math.min(0, base + delta));
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
const finish = () => {
|
||||
const offset = dragRef.current;
|
||||
dragStartX.current = null;
|
||||
decided.current = false;
|
||||
setDrag(null);
|
||||
if (offset === null) return;
|
||||
if (offset <= fullSwipeAt()) {
|
||||
// Pulled all the way through: act now. `onDelete` is the caller's
|
||||
// handler and asks for confirmation where the data cannot come back.
|
||||
setSwiped(false);
|
||||
onDelete();
|
||||
return;
|
||||
}
|
||||
setSwiped(offset <= SNAP_OPEN_AT);
|
||||
};
|
||||
|
||||
const offset = drag ?? (isSwiped ? SWIPE_OPEN : 0);
|
||||
const armed = offset <= fullSwipeAt();
|
||||
const showing = isSwiped || drag !== null;
|
||||
|
||||
const classes = ["ads-swipe-wrap"];
|
||||
if (showing) classes.push("ads-swiping");
|
||||
if (className) classes.push(className);
|
||||
|
||||
return (
|
||||
<div className={classes.join(" ")}>
|
||||
<button type="button" className="ads-swipe-delete" onClick={onDelete}>
|
||||
<div className={classes.join(" ")} ref={wrapRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={armed ? "ads-swipe-delete ads-swipe-delete--armed" : "ads-swipe-delete"}
|
||||
// The panel grows with the drag so the row fills with red the further
|
||||
// it is pulled — the same read as on iOS: colour means committed.
|
||||
style={{ width: Math.max(84, -offset) }}
|
||||
onClick={onDelete}
|
||||
>
|
||||
{deleteLabel}
|
||||
</button>
|
||||
<div
|
||||
className={isSwiped ? "ads-swipe-content ads-swiped" : "ads-swipe-content"}
|
||||
style={{ transform: isSwiped ? `translateX(${SWIPE_OPEN}px)` : undefined }}
|
||||
style={{
|
||||
transform: offset ? `translateX(${offset}px)` : undefined,
|
||||
transition: drag === null ? undefined : "none",
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
onPointerUp={finish}
|
||||
onPointerCancel={finish}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,17 @@
|
||||
--ok: #30D158;
|
||||
--warn: #FFD60A;
|
||||
--bad: #FF453A;
|
||||
/* Rot als FLAECHE, nicht als Schrift.
|
||||
|
||||
Weisse Schrift auf --bad ergab am 04.09.2026 gemessen 3,41 (Nacht) bzw.
|
||||
3,55 (Tag) - unter den 4,5, die WCAG AA fuer 16px verlangt. Auf
|
||||
#D70015 sind es 5,38, und das ist Apples eigenes systemRed fuer erhoehten
|
||||
Kontrast, bleibt also innerhalb der HIG-Palette.
|
||||
|
||||
Getrennt vom Schrift-Rot, weil beide Rollen Gegensaetzliches brauchen:
|
||||
rote Schrift will Abstand zum HELLEN Grund, eine rote Flaeche will
|
||||
Abstand zur WEISSEN Schrift darauf. */
|
||||
--bad-flaeche: #D70015;
|
||||
--shade: rgba(255, 255, 255, .05);
|
||||
/* Graue Füllfläche für Bedienelemente (Seg-Spur, Eingabe-/Auswahl-Boxen) -
|
||||
live am Panel gemessen (--ios-fill, audi-dashboard-ios.css). */
|
||||
@@ -73,7 +84,22 @@
|
||||
--fg3: #6B6B70;
|
||||
--ok: #34C759;
|
||||
--warn: #FF9F0A;
|
||||
--bad: #FF3B30;
|
||||
/* #FF3B30 (Apples systemRed) auf #F2F2F7 ergab 3,18 - unter den 4,5 fuer
|
||||
16px-Schrift. #D70015 ist Apples systemRed fuer erhoehten Kontrast und
|
||||
kommt auf 4,83 (auf Weiss 5,38). Gemessen am 04.09.2026 am
|
||||
Loesch-Knopf des Einzelbelegs. */
|
||||
--bad: #D70015;
|
||||
/* Rot als FLAECHE, nicht als Schrift.
|
||||
|
||||
Weisse Schrift auf --bad ergab am 04.09.2026 gemessen 3,41 (Nacht) bzw.
|
||||
3,55 (Tag) - unter den 4,5, die WCAG AA fuer 16px verlangt. Auf
|
||||
#D70015 sind es 5,38, und das ist Apples eigenes systemRed fuer erhoehten
|
||||
Kontrast, bleibt also innerhalb der HIG-Palette.
|
||||
|
||||
Getrennt vom Schrift-Rot, weil beide Rollen Gegensaetzliches brauchen:
|
||||
rote Schrift will Abstand zum HELLEN Grund, eine rote Flaeche will
|
||||
Abstand zur WEISSEN Schrift darauf. */
|
||||
--bad-flaeche: #D70015;
|
||||
--shade: rgba(0, 0, 0, .04);
|
||||
--ios-fill: rgba(120, 120, 128, .16);
|
||||
--tile-deckend: #FFFFFF;
|
||||
|
||||
Reference in New Issue
Block a user