From d5bfd1be3d73cf9cc1222609df45b48c0b084f41 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:17:36 +0000 Subject: [PATCH] App-Ansicht iOS: Chat-Eingabe springt nicht mehr weg, kein Zoom beim Tippen Auf dem iPhone sprang die Seite beim Tippen ins Chatfeld weg (nur die Fusszeile war zu sehen), zoomte hinein und liess sich danach seitlich schieben. Beides sind bekannte Safari-Effekte: - window.scrollTo(0, 0) bei offener Tastatur kaempft gegen Safaris eigenes Hochschieben des Feldes und ist entfernt, ebenso das Verkleinern des Chats auf die sichtbare Hoehe, das das schon hochgeschobene Feld wieder aus dem Bild rutschen liess. Der Chat nutzt jetzt 100dvh: auf Android folgt das der Tastatur (interactive-widget=resizes-content), auf iOS bleibt die Hoehe stabil. - Auf iOS aendert sich beim Fokus keine Hoehe mehr (die Tab-Leiste wird nur optisch ausgeblendet). Ist die Chat-Eingabe nach der Tastatur-Animation doch verdeckt, wird sie gezielt nachgeschoben (gemessen am visualViewport). - iOS zoomt beim Fokus, wenn die Schrift effektiv unter 16 px liegt, z. B. bei Safari-Seitenzoom unter 100 %. maximum-scale=1 nur auf iOS verhindert diesen automatischen Zoom; mit zwei Fingern zoomen bleibt moeglich (WebKit ignoriert die Sperre dafuer seit iOS 10). Android bleibt unveraendert, dort wuerde es das Zoomen sperren. Im Ruhezustand pixelgleich zu main (28 Ansichten). Co-Authored-By: Claude Opus 5.5 --- public/app.js | 48 +++++++++++++++++++++++++++++++--------- public/kundenbereich.css | 12 ++++++---- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/public/app.js b/public/app.js index 9845510..236b8ac 100644 --- a/public/app.js +++ b/public/app.js @@ -1379,6 +1379,22 @@ function onAppViewShown(name) { updateAppBar() } +/* Chat-Eingabe ueber der Tastatur halten: nur nachschieben, wenn sie wirklich verdeckt ist, gemessen am + sichtbaren Bereich (visualViewport). Die Seite nicht nach oben pinnen – das kaempft auf iOS gegen + Safaris eigenes Hochschieben und laesst die Eingabe aus dem Bild springen. */ +function revealChatComposer() { + const composer = document.getElementById('chat-composer') + const vv = window.visualViewport + if (!composer || !vv || !isAppLayout() || !composer.contains(document.activeElement)) return + const rect = composer.getBoundingClientRect() + const visibleTop = vv.offsetTop + const visibleBottom = vv.offsetTop + vv.height + let delta = 0 + if (rect.bottom > visibleBottom - 8) delta = rect.bottom - visibleBottom + 8 + else if (rect.top < visibleTop + 8) delta = rect.top - visibleTop - 8 + if (Number.isFinite(delta) && Math.abs(delta) > 1) window.scrollBy(0, delta) +} + /* Wartungs-Tab: Hinweis bei fehlgeschlagener Zahlung oder offener Rückfrage zur Gutschrift */ function updateWartungBadge() { const card = document.getElementById('abo-card') @@ -1563,9 +1579,16 @@ function initAppShell() { }) }, { passive: true }) - // Tastatur offen (Touch): Tab-Leiste ausblenden, damit Eingabe und Chat Platz haben + // Tastatur offen (Touch): Tab-Leiste ausblenden. Auf Android schrumpft die Seite mit der Tastatur + // (interactive-widget=resizes-content), dort nutzt der Chat den frei werdenden Platz. Auf iOS liegt + // die Tastatur ueber der Seite und Safari schiebt das Feld selbst hoch – dort darf sich beim Fokus + // keine Hoehe aendern (kb-ios, siehe kundenbereich.css), sonst rutscht das Feld wieder aus dem Bild. + if (isIosDevice()) document.body.classList.add('kb-ios') document.addEventListener('focusin', (ev) => { - if (coarsePointer.matches && ev.target.matches?.(TEXT_INPUT)) document.body.classList.add('kb-typing') + if (!coarsePointer.matches || !ev.target.matches?.(TEXT_INPUT)) return + document.body.classList.add('kb-typing') + // Nach der Tastatur-Animation (iOS ca. 350 ms) pruefen, ob die Chat-Eingabe sichtbar ist + if (ev.target.closest('#chat-composer')) setTimeout(revealChatComposer, 350) }) document.addEventListener('focusout', () => { setTimeout(() => { @@ -1573,7 +1596,6 @@ function initAppShell() { }, 80) }) - // Sichtbare Hoehe (ohne Tastatur) fuer den Chat. // Schliesst die Tastatur, ohne dass das Feld den Fokus verliert (Android-Zurueck-Geste), // kommt die Tab-Leiste zurueck; tippt man erneut hinein, verschwindet sie wieder. const vv = window.visualViewport @@ -1581,8 +1603,7 @@ function initAppShell() { let fullHeight = vv.height let width = vv.width let keyboardOpen = false - const syncHeight = () => { - document.documentElement.style.setProperty('--kb-vvh', `${Math.round(vv.height)}px`) + vv.addEventListener('resize', () => { if (Math.abs(vv.width - width) > 1) { width = vv.width fullHeight = vv.height @@ -1594,11 +1615,8 @@ function initAppShell() { if (!open) document.body.classList.remove('kb-typing') else if (coarsePointer.matches && document.activeElement?.matches?.(TEXT_INPUT)) document.body.classList.add('kb-typing') } - // Chat mit Tastatur: iOS schiebt die Seite hoch; App-Leiste, Verlauf und Eingabe passen genau hinein - if (keyboardOpen && appShell.view === 'chat' && isAppLayout()) window.scrollTo(0, 0) - } - vv.addEventListener('resize', syncHeight) - syncHeight() + if (open) requestAnimationFrame(() => requestAnimationFrame(revealChatComposer)) + }) } // Wird der Verlauf kleiner (Tastatur, Tab-Leiste), bleibt die neueste Nachricht sichtbar – sofern // man unten war (data-stick setzt ChatComposer.trackScroll) @@ -1650,6 +1668,16 @@ function initAppShell() { if (isAppLayout() && getQueryParam('abo') === 'success') showView('wartung') } +// iOS zoomt beim Tippen in ein Feld hinein, wenn dessen Schrift effektiv unter 16 px liegt – z. B. bei +// verkleinertem Seitenzoom in Safari, obwohl die Felder 16 px haben. maximum-scale=1 verhindert nur +// diesen automatischen Zoom; mit zwei Fingern zoomen bleibt auf iOS moeglich (WebKit ignoriert die +// Sperre dafuer seit iOS 10). Nur iOS: Android wuerde damit jedes Zoomen sperren. +function preventIosFocusZoom() { + const meta = document.querySelector('meta[name="viewport"]') + if (meta && isIosDevice() && !/maximum-scale/.test(meta.content)) meta.content += ', maximum-scale=1' +} +preventIosFocusZoom() + // App-Installation (Homescreen) und Offline-Seite; zwischengespeichert wird nur die Offline-Seite (sw.js) if ('serviceWorker' in navigator && window.isSecureContext) { window.addEventListener('load', () => { diff --git a/public/kundenbereich.css b/public/kundenbereich.css index 5d6b6f9..aa9954e 100644 --- a/public/kundenbereich.css +++ b/public/kundenbereich.css @@ -441,7 +441,8 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; } -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; } - .kb-page.kb-typing { --kb-tabbar-h: 0px; } + /* Tastatur offen: auf Android wird der Platz der Tab-Leiste frei; auf iOS keine Hoehenaenderung (app.js) */ + .kb-page.kb-typing:not(.kb-ios) { --kb-tabbar-h: 0px; } html.kb-sheet-open { overflow: hidden; } .kb-desk-only { display: none; } .kb-app-only { display: inline; } @@ -588,10 +589,12 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; } .kb-invoices .inv-actions .btn-link:first-child { background: rgba(157, 61, 254, 0.18); border-color: var(--kb-purple-line); } .kb-invoices .inv-actions .btn-link:first-child:not(:last-child) { flex: 1.7; } - /* Chat: fuellt den Platz zwischen App-Leiste und Tab-Leiste (bzw. Tastatur) */ + /* Chat: fuellt den Platz zwischen App-Leiste und Tab-Leiste. 100dvh folgt auf Android der Tastatur + (interactive-widget=resizes-content); auf iOS bleibt die Hoehe, Safari schiebt das Feld selbst hoch */ #view-chat.active { display: flex; flex-direction: column; - height: calc(var(--kb-vvh, 100dvh) - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px); + height: calc(100vh - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px); + height: calc(100dvh - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px); } #view-chat .kb-head { flex: none; margin-bottom: 12px; } #view-chat .chat-box { flex: 1 1 auto; min-height: 0; height: auto; padding: 10px; border-radius: 20px; } @@ -631,7 +634,8 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; } .kb-sheet { inset: auto 0 0 0; width: 100%; max-width: none; margin: 0; } .kb-sheet::backdrop { background: rgba(0, 0, 0, 0.55); } .kb-sheet-panel { - max-height: calc(var(--kb-vvh, 100dvh) - env(safe-area-inset-top) - 24px); overscroll-behavior: contain; + max-height: calc(100vh - env(safe-area-inset-top) - 24px); max-height: calc(100dvh - env(safe-area-inset-top) - 24px); + overscroll-behavior: contain; padding: 8px max(18px, env(safe-area-inset-right)) calc(18px + env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left)); border-radius: 24px 24px 0 0; border-bottom: none; box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.5); transition: transform 0.24s ease-in;