diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8aa67..456b9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.14.2] - 2026-04-06 + +### Fixed +- Modal: overlay tap now reliably closes the modal on iOS Safari / PWA - added `cursor: pointer` to the overlay (iOS requires this on non-interactive elements to fire click events) and a `touchend` fallback (#29) +- Modal: close button enlarged from 32px to 40px to meet Apple's 44px touch-target recommendation (#29) +- Modal: swipe-to-close no longer triggers when scrolling content inside the sheet - drag only activates from the top handle zone or when the panel is scrolled to the top (#29) + ## [0.14.1] - 2026-04-06 ### Fixed diff --git a/public/components/modal.js b/public/components/modal.js index 656a395..dde211e 100644 --- a/public/components/modal.js +++ b/public/components/modal.js @@ -115,14 +115,20 @@ function _wireSheetSwipe(panel) { let dragging = false; panel.addEventListener('touchstart', (e) => { - startY = e.touches[0].clientY; + // Nur von der Handle-Zone (obere 48px) oder wenn Panel ganz oben → Swipe erlauben + const touchY = e.touches[0].clientY; + const rect = panel.getBoundingClientRect(); + const isHandleZone = touchY - rect.top < 48; + const isScrolledToTop = panel.scrollTop <= 0; + if (!isHandleZone && !isScrolledToTop) return; + startY = touchY; dragging = true; }, { passive: true }); panel.addEventListener('touchmove', (e) => { if (!dragging) return; const dy = e.touches[0].clientY - startY; - if (dy < 0) return; // Kein Swipe nach oben + if (dy < 0) { dragging = false; return; } // Aufwärts-Scroll: Swipe abbrechen panel.style.transform = `translateY(${dy * 0.6}px)`; }, { passive: true }); @@ -224,6 +230,12 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {} if (e.target === activeOverlay) closeModal(); }); + // iOS PWA: click-Events auf non-interactive divs sind unzuverlässig → + // touchend als Fallback (passive, damit Scroll nicht blockiert wird) + activeOverlay.addEventListener('touchend', (e) => { + if (e.target === activeOverlay) closeModal(); + }, { passive: true }); + // Close-Button activeOverlay.querySelector('[data-action="close-modal"]') ?.addEventListener('click', closeModal); diff --git a/public/styles/layout.css b/public/styles/layout.css index 263441e..9ca52cd 100644 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -626,6 +626,8 @@ align-items: flex-end; justify-content: center; animation: modal-overlay-in 0.2s ease forwards; + /* iOS Safari: click-Events auf non-interactive divs erfordern cursor:pointer */ + cursor: pointer; } @media (min-width: 768px) { @@ -642,6 +644,8 @@ overflow-y: auto; border-radius: var(--radius-lg) var(--radius-lg) 0 0; animation: modal-slide-up 0.25s var(--ease-out) forwards; + /* Cursor vom Overlay zurücksetzen */ + cursor: default; } @media (min-width: 768px) { @@ -672,8 +676,8 @@ } .modal-panel__close { - width: var(--target-sm); - height: var(--target-sm); + width: var(--target-md); + height: var(--target-md); display: flex; align-items: center; justify-content: center; @@ -681,12 +685,7 @@ color: var(--color-text-secondary); transition: background-color var(--transition-fast); position: relative; -} - -.modal-panel__close::before { - content: ''; - position: absolute; - inset: -8px; + flex-shrink: 0; } .modal-panel__close:hover {