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.
This commit is contained in:
Ulas
2026-04-06 10:10:01 +02:00
parent f4268ce696
commit dd6c8a313a
10 changed files with 27 additions and 13 deletions
+8
View File
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.14.2] - 2026-04-06
### Fixed ### Fixed
+1 -1
View File
@@ -15,7 +15,7 @@
.budget-page { .budget-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+1 -1
View File
@@ -15,7 +15,7 @@
.calendar-page { .calendar-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+1 -1
View File
@@ -15,7 +15,7 @@
.contacts-page { .contacts-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+1 -1
View File
@@ -15,7 +15,7 @@
.meals-page { .meals-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+1 -1
View File
@@ -15,7 +15,7 @@
.notes-page { .notes-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+9 -4
View File
@@ -16,9 +16,11 @@ html, body {
} }
/* ── Safe Area Insets (Notch, Dynamic Island, Gesture Bar) ── /* ── Safe Area Insets (Notch, Dynamic Island, Gesture Bar) ──
* Nur horizontale Safe Areas auf body - vertikale werden von * Nur horizontale Safe Areas auf body. Vertikale Safe Areas werden über
* Standalone-Modus (padding-top) und Nav/Seiten (padding-bottom) gehandhabt. * .app-shell (padding-top im Standalone-Modus) und die Seiten-/Nav-Berechnungen
* Kein body padding-top/bottom hier, sonst doppelt mit Seiten-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 { body {
padding-left: env(safe-area-inset-left); padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right); padding-right: env(safe-area-inset-right);
@@ -59,7 +61,10 @@ nav,
/* ── Standalone-Modus: Status-Bar-Bereich berücksichtigen ── */ /* ── Standalone-Modus: Status-Bar-Bereich berücksichtigen ── */
@media (display-mode: standalone) { @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); padding-top: env(safe-area-inset-top);
} }
+1 -1
View File
@@ -15,7 +15,7 @@
.shopping-page { .shopping-page {
display: flex; display: flex;
flex-direction: column; 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); max-width: var(--content-max-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
+1
View File
@@ -230,6 +230,7 @@
--sidebar-bg: var(--neutral-100); --sidebar-bg: var(--neutral-100);
--sidebar-shadow-light: rgba(255, 255, 255, 0.6); --sidebar-shadow-light: rgba(255, 255, 255, 0.6);
--sidebar-shadow-dark: rgba(0, 0, 0, 0.08); --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); --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
/* -------------------------------------------------------- /* --------------------------------------------------------
+3 -3
View File
@@ -12,9 +12,9 @@
* API: Immer Netzwerk (kein Caching von Nutzerdaten) * API: Immer Netzwerk (kein Caching von Nutzerdaten)
*/ */
const SHELL_CACHE = 'oikos-shell-v26'; const SHELL_CACHE = 'oikos-shell-v27';
const PAGES_CACHE = 'oikos-pages-v25'; const PAGES_CACHE = 'oikos-pages-v26';
const ASSETS_CACHE = 'oikos-assets-v25'; const ASSETS_CACHE = 'oikos-assets-v26';
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE]; const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
// App-Shell: sofort benötigt für ersten Render // App-Shell: sofort benötigt für ersten Render