From dd6c8a313afd57e4ed51ce255348a685942879f7 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 6 Apr 2026 10:10:01 +0200 Subject: [PATCH] fix(pwa): fix remaining iOS scroll bleed and safe-area height overflow (#16) Root cause 1 (scroll bleed): padding-top was applied to body in standalone mode. Since .app-shell has height: 100dvh, body-padding shifted the shell beyond the viewport bottom - enabling body-level scrolling. Fix: moved padding-top from body to .app-shell in the standalone media query. Root cause 2 (content overflow): fixed-height page containers (Calendar, Shopping, Meals, Notes, Budget, Contacts) calculated height as 100dvh - nav-bottom - safe-area-inset-bottom, but never subtracted the top safe area. This caused each page to overflow .app-content by exactly env(safe-area-inset-top) pixels in standalone mode. Fix: added --safe-area-inset-top token and subtracted it in all 6 height calculations. Service worker cache bumped to v27/v26. --- CHANGELOG.md | 8 ++++++++ public/styles/budget.css | 2 +- public/styles/calendar.css | 2 +- public/styles/contacts.css | 2 +- public/styles/meals.css | 2 +- public/styles/notes.css | 2 +- public/styles/pwa.css | 13 +++++++++---- public/styles/shopping.css | 2 +- public/styles/tokens.css | 1 + public/sw.js | 6 +++--- 10 files changed, 27 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 456b9df..09b36da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.14.3] - 2026-04-06 + +### Fixed +- PWA iOS: scroll bleed fully resolved - `padding-top: env(safe-area-inset-top)` moved from `body` to `.app-shell`; body-padding was pushing `.app-shell` (height: 100dvh) beyond the viewport bottom, allowing the page body itself to scroll (#16) +- PWA iOS: all fixed-height page containers (Calendar, Shopping, Meals, Notes, Budget, Contacts) now subtract `--safe-area-inset-top` from their height calculation so they no longer overflow `.app-content` in standalone mode (#16) +- Added `--safe-area-inset-top` CSS token (mirrors `env(safe-area-inset-top, 0px)`) for consistent use across all page layout calculations (#16) +- Service worker cache bumped to v27/v26 to ensure CSS changes are picked up on next update (#16) + ## [0.14.2] - 2026-04-06 ### Fixed diff --git a/public/styles/budget.css b/public/styles/budget.css index eeeb8d9..0b946a5 100644 --- a/public/styles/budget.css +++ b/public/styles/budget.css @@ -15,7 +15,7 @@ .budget-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/calendar.css b/public/styles/calendar.css index f536ec1..cb4a1dc 100644 --- a/public/styles/calendar.css +++ b/public/styles/calendar.css @@ -15,7 +15,7 @@ .calendar-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/contacts.css b/public/styles/contacts.css index 93cb795..2a7a56f 100644 --- a/public/styles/contacts.css +++ b/public/styles/contacts.css @@ -15,7 +15,7 @@ .contacts-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/meals.css b/public/styles/meals.css index fde8256..534967a 100644 --- a/public/styles/meals.css +++ b/public/styles/meals.css @@ -15,7 +15,7 @@ .meals-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/notes.css b/public/styles/notes.css index e950a88..121a2db 100644 --- a/public/styles/notes.css +++ b/public/styles/notes.css @@ -15,7 +15,7 @@ .notes-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/pwa.css b/public/styles/pwa.css index 64f2025..65de8c8 100644 --- a/public/styles/pwa.css +++ b/public/styles/pwa.css @@ -16,9 +16,11 @@ html, body { } /* ── Safe Area Insets (Notch, Dynamic Island, Gesture Bar) ── - * Nur horizontale Safe Areas auf body - vertikale werden von - * Standalone-Modus (padding-top) und Nav/Seiten (padding-bottom) gehandhabt. - * Kein body padding-top/bottom hier, sonst doppelt mit Seiten-Berechnungen. */ + * Nur horizontale Safe Areas auf body. Vertikale Safe Areas werden über + * .app-shell (padding-top im Standalone-Modus) und die Seiten-/Nav-Berechnungen + * (padding-bottom via --safe-area-inset-bottom) gehandhabt. + * KEIN body padding-top/bottom - das würde .app-shell (height: 100dvh) + * um den Safe-Area-Betrag aus dem Viewport schieben und Scroll-Bleed erzeugen. */ body { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); @@ -59,7 +61,10 @@ nav, /* ── Standalone-Modus: Status-Bar-Bereich berücksichtigen ── */ @media (display-mode: standalone) { - body { + /* padding-top auf .app-shell (nicht body): So bleibt app-shell exakt 100dvh + * hoch und überläuft den Viewport nicht - body-padding hätte .app-shell + * nach unten verschoben und Scroll-Bleed verursacht. */ + .app-shell { padding-top: env(safe-area-inset-top); } diff --git a/public/styles/shopping.css b/public/styles/shopping.css index d994746..ba00e95 100644 --- a/public/styles/shopping.css +++ b/public/styles/shopping.css @@ -15,7 +15,7 @@ .shopping-page { display: flex; flex-direction: column; - height: calc(100dvh - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); + height: calc(100dvh - var(--safe-area-inset-top) - var(--nav-bottom-height) - var(--safe-area-inset-bottom)); max-width: var(--content-max-width); margin: 0 auto; overflow: hidden; diff --git a/public/styles/tokens.css b/public/styles/tokens.css index 243b62e..e9db487 100644 --- a/public/styles/tokens.css +++ b/public/styles/tokens.css @@ -230,6 +230,7 @@ --sidebar-bg: var(--neutral-100); --sidebar-shadow-light: rgba(255, 255, 255, 0.6); --sidebar-shadow-dark: rgba(0, 0, 0, 0.08); + --safe-area-inset-top: env(safe-area-inset-top, 0px); --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); /* -------------------------------------------------------- diff --git a/public/sw.js b/public/sw.js index 84d9641..1125b31 100644 --- a/public/sw.js +++ b/public/sw.js @@ -12,9 +12,9 @@ * API: Immer Netzwerk (kein Caching von Nutzerdaten) */ -const SHELL_CACHE = 'oikos-shell-v26'; -const PAGES_CACHE = 'oikos-pages-v25'; -const ASSETS_CACHE = 'oikos-assets-v25'; +const SHELL_CACHE = 'oikos-shell-v27'; +const PAGES_CACHE = 'oikos-pages-v26'; +const ASSETS_CACHE = 'oikos-assets-v26'; const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE]; // App-Shell: sofort benötigt für ersten Render