Ziehen zum Aktualisieren, Nachtmodus-Zeilen, Parkplatznamen (2026.8.30.10)

Ziehen zum Aktualisieren funktionierte auf keinem Geraet, in keiner der
beiden Anwendungen - und der Grund entwertet zugleich, wie es bisher geprueft
wurde. Beide trieben die Geste ueber pointermove und riefen dort
preventDefault(). Das verhindert laut Spezifikation kein Scrollen; darueber
entscheidet allein touch-action. Auf echtem Touch uebernimmt der Browser die
senkrechte Geste, schickt pointercancel und stellt pointermove ein - der Zug
wird zurueckgesetzt, lange bevor er die Schwelle erreicht. Eine Maus kennt
diese Uebernahme nicht, synthetische PointerEvents ebenso wenig: genau
deshalb lief die Geste 2026-08-17 im Test sauber durch und am Geraet nie.

Jetzt laeuft sie auf Touch-Ereignissen, touchmove zwingend mit passive:false
(sonst ist preventDefault() wirkungslos); der Zeigerweg bleibt fuer Maus und
Stift und ignoriert pointerType "touch". Mit echten TouchEvents geprueft,
inklusive defaultPrevented - dem einzigen Signal, das belegt, dass das native
Ueberscrollen wirklich unterbunden ist.

Der zuletzt nicht nachstellbare Befund war nachtmodus-spezifisch: --tile ist
dort ein durchscheinender Schleier, und die Wischzeilen malten ihn ein
zweites Mal auf die Kachel, auf der sie ohnehin liegen. Am Tag sind beide
deckend weiss, deshalb war nichts zu sehen. Das Panel nutzt an dieser Stelle
--tile-deckend; dieses Token fehlte im Design-System und ist jetzt da.

Ausserdem: steht das Auto auf einem benannten Platz, zeigen beide dessen
Namen statt der naechsten Hausnummer - Nominatim liefert ihn mit, die
Pruefung auf die Art der Flaeche verhindert, dass irgendein benanntes
Gebaeude die Anschrift verdraengt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 23:22:52 +02:00
parent a08922e9a0
commit 0fd81df700
9 changed files with 224 additions and 45 deletions
+65 -18
View File
@@ -53,12 +53,8 @@ function useZiehenAktualisieren(
const [laedt, setzeLaedt] = useState(false)
const start = useRef<number | null>(null)
if (!beiAktualisieren) {
return { zug: 0, laedt: false, handler: {} as Record<string, never> }
}
const beenden = () => {
if (start.current === null) return
if (start.current === null || !beiAktualisieren) return
start.current = null
if (zug >= PTR_SCHWELLE) {
setzeLaedt(true)
@@ -71,26 +67,77 @@ function useZiehenAktualisieren(
}
}
const starten = (y: number) => {
if (laedt) return
if ((inhalt.current?.scrollTop ?? 0) > 0) return
start.current = y
setzeZug(0)
}
/* Gibt true zurück, wenn die Geste uns gehört — der Aufrufer muss dann
preventDefault() rufen. */
const bewegen = (y: number): boolean => {
if (start.current === null) return false
const dy = y - start.current
if (dy > 0 && (inhalt.current?.scrollTop ?? 0) <= 0) {
// Widerstand wie bei iOS: halbe Strecke, gedeckelt bei 90px.
setzeZug(Math.min(90, dy * 0.5))
return true
}
start.current = null
setzeZug(0)
return false
}
/* Auf dem Finger läuft die Geste über Touch-Ereignisse, nicht über Pointer.
Grund: preventDefault() auf einem Pointer-Ereignis verhindert laut
Spezifikation kein Scrollen — darüber entscheidet allein touch-action. Auf
einem echten Gerät übernimmt der Browser die senkrechte Geste deshalb
sofort, schickt pointercancel und stellt pointermove ein; der Zug wurde
zurückgesetzt, bevor er die Schwelle erreichte. Am Rechner fällt das nie
auf, eine Maus kennt keine Scroll-Übernahme. Der Listener muss von Hand
und mit passive:false hängen — React registriert touchmove passiv, dort
wäre preventDefault() wirkungslos. */
useEffect(() => {
const el = inhalt.current
// Der Aussteiger sitzt IM Effekt, nicht davor: ein vorgezogenes return
// wuerde diesen Hook je nach Bildschirmbreite mal aufrufen und mal nicht
// und damit die Hook-Reihenfolge brechen.
if (!el || !beiAktualisieren) return undefined
const beiStart = (e: TouchEvent) => {
if (e.touches.length === 1) starten(e.touches[0]!.clientY)
}
const beiBewegung = (e: TouchEvent) => {
if (e.touches.length !== 1) return
if (bewegen(e.touches[0]!.clientY)) e.preventDefault()
}
el.addEventListener("touchstart", beiStart, { passive: true })
el.addEventListener("touchmove", beiBewegung, { passive: false })
el.addEventListener("touchend", beenden)
el.addEventListener("touchcancel", beenden)
return () => {
el.removeEventListener("touchstart", beiStart)
el.removeEventListener("touchmove", beiBewegung)
el.removeEventListener("touchend", beenden)
el.removeEventListener("touchcancel", beenden)
}
})
if (!beiAktualisieren) {
return { zug: 0, laedt: false, handler: {} as Record<string, never> }
}
return {
zug,
laedt,
handler: {
// Nur Maus/Stift — der Finger läuft über die Touch-Ereignisse oben.
onPointerDown: (e: ReactPointerEvent) => {
if (!e.isPrimary || laedt) return
if ((inhalt.current?.scrollTop ?? 0) > 0) return
start.current = e.clientY
setzeZug(0)
if (!e.isPrimary || e.pointerType === "touch") return
starten(e.clientY)
},
onPointerMove: (e: ReactPointerEvent) => {
if (start.current === null || !e.isPrimary) return
const dy = e.clientY - start.current
if (dy > 0 && (inhalt.current?.scrollTop ?? 0) <= 0) {
// Widerstand wie bei iOS: halbe Strecke, gedeckelt bei 90px.
setzeZug(Math.min(90, dy * 0.5))
} else {
start.current = null
setzeZug(0)
}
if (!e.isPrimary || e.pointerType === "touch") return
bewegen(e.clientY)
},
onPointerUp: beenden,
onPointerCancel: beenden,
+27 -1
View File
@@ -23,6 +23,20 @@ export interface Koordinate {
const CACHE_SCHLUESSEL = "dm360.geocache"
/* Arten, bei denen der Eigenname aussagekräftiger ist als die Anschrift
(Feld type der Nominatim-Antwort). */
const PLATZ_ARTEN = new Set([
"parking",
"parking_space",
"parking_entrance",
"bicycle_parking",
"motorcycle_parking",
"rest_area",
"services",
"fuel",
"charging_station",
])
function cacheLesen(): Record<string, Koordinate | string | null> {
try {
const roh = localStorage.getItem(CACHE_SCHLUESSEL)
@@ -75,13 +89,25 @@ export async function koordinatenAufloesen(lat: number, lon: number): Promise<st
{ headers: { "Accept-Language": "de" } },
)
const daten = (await antwort.json()) as {
name?: string
type?: string
address?: Record<string, string>
display_name?: string
}
const a = daten.address ?? {}
const strasse = [a["road"], a["house_number"]].filter(Boolean).join(" ")
const ort = [a["postcode"], a["city"] ?? a["town"] ?? a["village"]].filter(Boolean).join(" ")
const adresse = [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null
/* Steht das Auto auf einem benannten Platz — Parkplatz, Parkhaus,
Rastplatz, Ladesäule —, sagt dessen Name mehr als die nächstgelegene
Hausnummer: "Parkhaus Westentor" statt "Rathausstraße 4". Nominatim
liefert ihn im Feld name, zusammen mit der Art der getroffenen Fläche;
ohne die Prüfung auf diese Art würde auch jedes beliebige Gebäude, das
zufällig einen Namen trägt, die Anschrift verdrängen. Der Ort bleibt
angehängt, damit erkennbar ist, wo der Platz liegt. */
const platz = PLATZ_ARTEN.has(daten.type ?? "") ? (daten.name ?? "").trim() : ""
const adresse = platz
? [platz, ort].filter(Boolean).join(", ")
: [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null
// Nur echte Treffer merken: eine gedrosselte Antwort ist kein "hier gibt
// es keine Adresse", die soll beim nächsten Mal erneut versucht werden.
if (adresse) adresseMerken(schluessel, adresse)