Kundenansicht neu: Leiste im Chat am iPhone wieder ganz unten

Die Messung am iPhone (iOS 26.2, App vom Home-Bildschirm) zeigt die
Ursache: Auf der Startseite ist das Fenster 1002 px hoch. Im Chat
schrumpft die Spalte auf den Kopf (alles andere steht fest), die Seite
wird kürzer als der Bildschirm, und iOS hält unten rund 60 pt für eine
Safari-Leiste frei, die es in der App nicht gibt: 932 px, Leiste und
Eingabe stehen zu hoch, darunter ein weißer Streifen. Mit der Tastatur
hat das nichts zu tun.

Am Handy hat die Spalte im Chat jetzt wie in den anderen Bereichen
mindestens volle Bildschirmhöhe (100vh, dann 100lvh). Ohne
Browserleisten scrollt die Seite dadurch nicht; in Chromium sind alle
Chat-Bilder am Handy und Desktop pixelgleich zu main.

Der Höhen-Wächter aus 0fa8fdb (Hülle aus- und einblenden) hat nicht
geholfen und ist wieder raus. Die Messung bleibt für die Kontrolle am
iPhone und wird danach entfernt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-29 11:21:13 +00:00
1 parent cca29a4aef
commit 17bce304eb
6 files changed
+28 -224

No files matched your search

+2 -3
View File
@@ -1,12 +1,12 @@
/* VORÜBERGEHENDE MESSUNG (2026-10-29), wird nach der Klärung wieder entfernt: Am iPhone (App vom Home-Bildschirm) steht die Leiste zu hoch.
Schickt Bildschirmwerte (Fenster, visualViewport, Bildschirm, Safe-Area, vh-Einheiten, Lage der Leiste, Zustand von hoehe.js) an
Schickt Bildschirmwerte (Fenster, visualViewport, Bildschirm, Safe-Area, vh-Einheiten, Lage der Leiste) an
/api/diag/hoehe; der Server schreibt sie ins Log (server/routes/diagHoehe.js). Keine Inhalte, keine Eingaben. Höchstens MAX Meldungen je Seite. */
const MAX = 80
const ZIEL = '/api/diag/hoehe'
const rund = (x) => (Number.isFinite(x) ? Math.round(x * 10) / 10 : null)
export function starteDiagHoehe({ win = window, doc = document, hoehe = null }) {
export function starteDiagHoehe({ win = window, doc = document }) {
const t0 = Date.now()
let n = 0
const probe = (stil) => {
@@ -38,7 +38,6 @@ export function starteDiagHoehe({ win = window, doc = document, hoehe = null })
unten: rund(unten.getBoundingClientRect().bottom),
leiste: leiste ? [rund(leiste.top), rund(leiste.bottom), rund(leiste.height)] : null,
huelle: huelle ? [rund(huelle.top), rund(huelle.height)] : null,
hoehe: hoehe?.stand?.() ?? null,
app: Boolean(win.matchMedia?.('(display-mode: standalone)').matches), ns: win.navigator?.standalone ?? null,
ori: win.screen?.orientation?.type ?? null,
fokus: aktiv && aktiv !== doc.body ? aktiv.tagName : null,
-92
View File
@@ -1,92 +0,0 @@
/* Kundenansicht neu: iOS-Fehler der App vom Home-Bildschirm. Nach dem Schließen der Tastatur bleibt die Fensterhöhe oft kleiner
(WebKit, bekannt seit iOS 17, in iOS 26 wieder da): alles, was unten fest steht (Leiste, Chat-Eingabe), sitzt dann rund 60 px zu hoch,
darunter bleibt ein leerer weißer Streifen, bis die App neu startet. Abhilfe: die Hülle (mindestens volle Fensterhöhe) kurz aus- und
wieder einblenden, das zwingt WebKit, die Höhe neu zu messen. Scrollstände und Fokus bleiben. Nur in der App vom Home-Bildschirm auf
iOS (das entscheidet `aktiv`) und nur, wenn die Höhe wirklich hängt. */
export const LUFT = 4 // so viel darf die Höhe schwanken, ohne dass sie als hängend gilt
export const PRUEF_MS = [150, 600] // nach dem Schließen der Tastatur: früh und noch einmal, wenn die Animation sicher durch ist
export const MAX_VERSUCHE = 2 // je Anlass (Tastatur zu, App wieder vorne); hilft es nicht, bleibt es bis zum nächsten Anlass dabei
/** Reiner Zustand { voll, breite }: größte innere Höhe bei dieser Breite. Ein Breitenwechsel (Drehen, iPad-Fenster) fängt neu an. */
export function hoeheZustand(alt, { hoehe, breite }) {
if (!alt || Math.abs(breite - alt.breite) > 1) return { voll: hoehe, breite }
return { voll: Math.max(alt.voll, hoehe), breite: alt.breite }
}
/** Hängt die Höhe? Kleiner als die größte bei gleicher Breite und kein Textfeld im Fokus (eine offene Tastatur ist kein Hängen). */
export function haengt(zustand, { hoehe, breite, textfeldAktiv }) {
if (!zustand || textfeldAktiv) return false
if (Math.abs(breite - zustand.breite) > 1) return false
return zustand.voll - hoehe > LUFT
}
/**
* Verdrahtung: merkt sich die volle Höhe (resize), prüft nach dem Verlassen eines Feldes (focusout), nach einem resize ohne Textfeld
* und wenn die App wieder nach vorne kommt (visibilitychange), jeweils zu den Zeiten PRUEF_MS. onHeile({ vorher, nachher, voll, versuche }) nach
* jedem Eingriff. Rückgabe { pruefe, stand, beende }.
*/
export function beobachteHoehe({ win = window, doc = document, element, aktiv = () => true, istTextfeld = () => false, onHeile = () => {} }) {
const messe = () => ({ hoehe: win.innerHeight, breite: win.innerWidth })
let z = hoeheZustand(null, messe())
let versuche = 0
let timer = []
const merke = () => { z = hoeheZustand(z, messe()) }
/** Hülle aus- und einblenden, mit synchronem Umbruch dazwischen; vorher gescrollte Kästen, Fensterscroll und Fokus zurücksetzen. */
function heile() {
const fensterX = win.scrollX ?? 0
const fensterY = win.scrollY ?? 0
const staende = []
for (const el of element.querySelectorAll('*')) {
if (el.scrollTop || el.scrollLeft) staende.push([el, el.scrollTop, el.scrollLeft])
}
const fokus = doc.activeElement && doc.activeElement !== doc.body && element.contains(doc.activeElement) ? doc.activeElement : null
const vorher = element.style.display
element.style.display = 'none'
void element.offsetHeight
element.style.display = vorher
void element.offsetHeight
for (const [el, top, left] of staende) { el.scrollTop = top; el.scrollLeft = left }
if (fensterX || fensterY) win.scrollTo(fensterX, fensterY)
if (fokus && doc.activeElement !== fokus) fokus.focus?.({ preventScroll: true })
}
function pruefe() {
if (!aktiv() || !element) return false
const m = messe()
if (!haengt(z, { ...m, textfeldAktiv: istTextfeld(doc.activeElement) })) { merke(); return false }
if (versuche >= MAX_VERSUCHE) return false
versuche += 1
const vorher = win.innerHeight
heile()
merke()
try { onHeile({ vorher, nachher: win.innerHeight, voll: z.voll, versuche }) } catch { /* nur Beobachter */ }
return true
}
const warte = (fn, ms) => (win.setTimeout ? win.setTimeout(fn, ms) : setTimeout(fn, ms))
const stoppe = (id) => (win.clearTimeout ? win.clearTimeout(id) : clearTimeout(id))
const plane = () => {
for (const id of timer) stoppe(id)
timer = PRUEF_MS.map((ms) => warte(pruefe, ms))
}
const neuerAnlass = () => { versuche = 0; plane() }
const beiResize = () => { merke(); if (!istTextfeld(doc.activeElement)) plane() }
const beiUnfokus = () => neuerAnlass()
const beiSicht = () => { if (doc.visibilityState === 'visible') neuerAnlass() }
win.addEventListener('resize', beiResize)
doc.addEventListener('focusout', beiUnfokus)
doc.addEventListener('visibilitychange', beiSicht)
return {
pruefe,
stand: () => ({ ...z, versuche }),
beende() {
for (const id of timer) stoppe(id)
win.removeEventListener('resize', beiResize)
doc.removeEventListener('focusout', beiUnfokus)
doc.removeEventListener('visibilitychange', beiSicht)
},
}
}
+4 -1
View File
@@ -185,7 +185,10 @@
#chat-body { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; }
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
.kn-spalte:has(#bereich-chat:not([hidden])) { display: block; height: auto; }
/* Mindestens volle Bildschirmhöhe wie die anderen Bereiche (.kn-spalte, 100vh): ist die Seite kürzer, hält iOS 26 in der App vom Home-Bildschirm
unten rund 60 pt für eine Safari-Leiste frei, die es dort nicht gibt. Das Fenster wird kleiner (gemessen am iPhone 2026-10-29: 1002 → 932 px),
Leiste und Eingabe stehen zu hoch, darunter ein weißer Streifen. lvh = Höhe ohne Browserleisten; die Seite scrollt dadurch nicht. */
.kn-spalte:has(#bereich-chat:not([hidden])) { display: block; height: auto; min-height: 100vh; min-height: 100lvh; }
.kn-haupt:has(#bereich-chat:not([hidden])) { position: fixed; inset: calc(var(--kn-banner-h) + var(--kn-kopf-h) + env(safe-area-inset-top)) 0 var(--kn-unten, var(--kn-zeile-h)); padding: 0 max(16px, env(safe-area-inset-right)) 6px max(16px, env(safe-area-inset-left)); background: var(--kn-bg); }
#bereich-chat > .kn-titel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* bleibt für Screenreader die Überschrift der Seite */
.kn:has(#bereich-chat:not([hidden])) .kn-kopf { height: calc(var(--kn-kopf-h) + env(safe-area-inset-top)); border-bottom-color: var(--kn-line); } /* feste Höhe wie der Rand oben des Inhalts: im Querformat wäre der Kopf mit Haarlinie 1 px höher */
+2 -6
View File
@@ -7,8 +7,7 @@ import { h, leeren, text } from './dom.js'
import { api as apiStandard } from './api.js'
import * as routerStandard from './router.js'
import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js'
import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, istTextfeld, leisteSichtbar } from './leiste.js'
import { beobachteHoehe } from './hoehe.js'
import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js'
import { starteDiagHoehe } from './diag-hoehe.js'
import { baueEtage } from './etage.js'
import { erzeugeZaehler } from './zaehler.js'
@@ -279,11 +278,8 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan
if (handyMq.addEventListener) handyMq.addEventListener('change', beiMedien)
else handyMq.addListener?.(beiMedien)
beobachteTastatur({ win, doc, onChange: (z) => doc.body.classList.toggle('kn-tastatur', !leisteSichtbar(z)) })
// iOS-App vom Home-Bildschirm: hängt nach der Tastatur die Fensterhöhe, steht die Leiste zu hoch (hoehe.js); dann einmal neu vermessen
let diag = null
const hoehe = beobachteHoehe({ win, doc, element: doc.querySelector('.kn-huelle'), aktiv: () => istIos() && standalone() && istHandy(), istTextfeld, onHeile: (werte) => diag?.sende('heile', werte) })
// VORÜBERGEHENDE MESSUNG (2026-10-29, diag-hoehe.js): Bildschirmwerte am iPhone ins Server-Log, wird nach der Klärung entfernt
if (istIos() && istHandy() && !viewAs) diag = starteDiagHoehe({ win, doc, hoehe })
if (istIos() && istHandy() && !viewAs) starteDiagHoehe({ win, doc })
// 7. Fristen und Zähler laden parallel und brechen den Start nie ab
ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() })