diff --git a/README.md b/README.md index 0f69602..6c9cfdc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Lokaler CMS-Prototyp für PHP-Webhosting mit erstem visuellen Editor. Symfony 7. Die übernommenen Originaldokumente sind `AGENTS.md` und `ENTWICKLUNGSPLAN.md`. Den tatsächlichen Stand enthält `docs/STATUS.md`, Entscheidungen und Datenmodell stehen in `docs/ARCHITECTURE.md`. +## Slider auf dem iPhone (ab 1.11.2) + +Safari bricht sanftes Weiterblättern in einem einrastenden Slider ab und springt zur vorigen Folie zurück; die Automatik und die Pfeile stotterten deshalb auf dem iPhone. Während der Fahrt ist das Einrasten jetzt ausgeschaltet und wird bei Ankunft auf der Folie wieder eingeschaltet. Die Punkte unter dem Slider sind echte Punkte, der aktive gefüllt, bei unveränderter Trefffläche von 44 px. + ## Kopfbereich beim Scrollen und gleich hohe Karten (ab 1.11.1) Der Kopfbereich kann für die gescrollte Seite eigene Farben tragen, wie der transparente Header in Flatsome: Kopfbereich im Editor öffnen, die Sektion wählen, **Design → Hintergrund**. Dort steht unter „Hintergrundfarbe“ die Zeile **„Hintergrundfarbe beim Scrollen“**; dasselbe gibt es für Textfarbe, Rahmenfarbe, Schattenfarbe und Deckkraft. Der normale Wert gilt am Seitenanfang, der Scroll-Wert sobald die Seite mehr als 40 px bewegt wurde, mit weichem Übergang. Für einen durchsichtigen Kopf am Anfang `#ffffff00` als normale Hintergrundfarbe eintragen. Damit der Kopf über der ersten Sektion liegt statt darüber zu stehen, unter **Einstellungen** „Kopfbereich über dem Inhalt schweben lassen“ einschalten; zusammen mit „oben halten“ bleibt er beim Scrollen sichtbar. Ohne JavaScript behält der Kopf seine normalen Farben. diff --git a/VERSION b/VERSION index b0f61c5..0c9cb69 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.11.1 \ No newline at end of file +1.11.2 \ No newline at end of file diff --git a/docs/STATUS.md b/docs/STATUS.md index 1bf1f48..60df131 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,13 @@ # Projektstatus +## 1.11.2 – Slider stottert nicht mehr auf dem iPhone (14.09.2026) + +**Anlass:** Umuts iPhone-Screenshot des Hero-Sliders („das buggt“). Safari bricht `scrollTo({behavior:'smooth'})` in einem Container mit `scroll-snap-type: x mandatory` ab und rastet auf die vorige Folie zurück; Pfeile und Automatik ruckeln. + +**Umgesetzt:** `slider.js` schaltet das Einrasten für die Fahrt aus und stellt es bei Ankunft am Zielwert wieder her (spätestens nach zwei Sekunden, weil ein Hintergrund-Tab sanftes Scrollen pausiert); bei reduzierter Bewegung weiter sofortiges Springen. Die Punkte der Steuerung sind gezeichnete Punkte (aktiv gefüllt) statt eines Kastens um den Text, Trefffläche bleibt 44 px. + +**Geprüft:** Playwright auf der Pilotkopie bei 375 und 1600 px: „Nächste Folie“ landet exakt auf dem Folienanfang (359 bzw. 666 px), Einrasten danach wieder `x mandatory`, Automatik blättert nach 7 s selbst weiter; 33 JavaScript-Tests. Der Safari-Fall selbst ist hier nicht ausführbar; die Maßnahme ist die dokumentierte Umgehung dieses WebKit-Verhaltens. + ## 1.11.1 – Kopfbereich mit Scroll-Farben, gleich hohe Karten (14.09.2026) **Anlass:** Umut sah auf der Startseite ungleich hohe Karten und wünschte sich für den Kopfbereich einstellbare Transparenz-Effekte beim Scrollen, wie in Flatsome. diff --git a/public/assets/site.css b/public/assets/site.css index 3ffa8d6..d0c617a 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -203,7 +203,7 @@ body.consent-open{padding-bottom:260px} .slider-dots{position:relative} @supports selector(::scroll-marker){.slider-dots:not(.slider-controlled){scroll-marker-group:after;scroll-behavior:smooth;padding-bottom:0;scrollbar-width:none;scrollbar-gutter:auto}.slider-dots:not(.slider-controlled)::-webkit-scrollbar{display:none}.slider-dots:not(.slider-controlled)>*::scroll-marker{content:'';width:12px;height:12px;border-radius:50%;background:color-mix(in srgb,currentColor 30%,transparent);margin:0 6px;cursor:pointer}.slider-dots>*::scroll-marker:target-current{background:var(--accent)}.slider-dots::scroll-marker-group{display:flex;justify-content:center;align-items:center;padding:14px 0 2px}} .slider-controlled{scrollbar-width:none;scrollbar-gutter:auto}.slider-controlled::-webkit-scrollbar{display:none} -.slider-controls{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;min-height:48px;margin-top:8px;padding:4px;font:14px/1.4 system-ui,sans-serif}.slider-controls button{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:40px;padding:8px 12px;border:1px solid #b2c2b8;border-radius:7px;background:#fff;color:#193c2d;cursor:pointer;font:inherit}.slider-controls button:focus-visible{outline:3px solid #b55b12;outline-offset:3px}.slider-controls button:hover{background:#e4efe7}.slider-controls button:disabled{opacity:.6;cursor:default}.slider-position{font-variant-numeric:tabular-nums;min-width:44px;text-align:center}.slider-control-dots{display:flex;flex-wrap:wrap;gap:3px}.slider-control-dots button{min-width:30px;padding:5px;border:0;background:transparent;color:inherit;opacity:.5}.slider-control-dots button[aria-pressed=true]{opacity:1;outline:1px solid currentColor;outline-offset:-5px}.slider-controls[hidden]{display:none} +.slider-controls{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;min-height:48px;margin-top:8px;padding:4px;font:14px/1.4 system-ui,sans-serif}.slider-controls button{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:40px;padding:8px 12px;border:1px solid #b2c2b8;border-radius:7px;background:#fff;color:#193c2d;cursor:pointer;font:inherit}.slider-controls button:focus-visible{outline:3px solid #b55b12;outline-offset:3px}.slider-controls button:hover{background:#e4efe7}.slider-controls button:disabled{opacity:.6;cursor:default}.slider-position{font-variant-numeric:tabular-nums;min-width:44px;text-align:center}.slider-control-dots{display:flex;flex-wrap:wrap;gap:3px}.slider-control-dots button{min-width:32px;padding:0;border:0;background:transparent;color:inherit;font-size:0}.slider-control-dots button::before{content:'';width:10px;height:10px;border-radius:50%;border:2px solid currentColor;background:transparent;transition:background-color .2s}.slider-control-dots button[aria-pressed=true]::before{background:currentColor}.slider-control-dots button:hover::before{background:color-mix(in srgb,currentColor 50%,transparent)}.slider-controls[hidden]{display:none} @media(max-width:650px){.slider-controls button{min-height:44px;min-width:44px}} .slide-desktop-1{grid-auto-columns:100%}.slide-desktop-2{grid-auto-columns:calc(50% - 10px)} diff --git a/public/assets/slider.js b/public/assets/slider.js index 4a580e7..91b9310 100644 --- a/public/assets/slider.js +++ b/public/assets/slider.js @@ -28,9 +28,25 @@ export function initSliders(doc = document) { available().forEach(i => { const delta = Math.abs(left(slides[i]) - slider.scrollLeft); if (delta < distance) { distance = delta; index = i; } }); return index; }; + let settle = null; const go = index => { if (!available().includes(index)) return; - slider.scrollTo({ left: Math.max(0, Math.min(slider.scrollWidth - slider.clientWidth, left(slides[index]))), behavior: reduced() ? 'instant' : 'smooth' }); + const target = Math.max(0, Math.min(slider.scrollWidth - slider.clientWidth, left(slides[index]))); + if (reduced()) { slider.scrollTo({ left: target, behavior: 'instant' }); return; } + // Safari cancels a smooth scroll on a snapping container and snaps back to the slide it + // came from - the slider stutters instead of moving on. Snapping is switched off for the + // ride and put back once the movement has settled, on the exact start of a slide, so + // restoring it moves nothing. + clearInterval(settle); + const snap = slider.style.scrollSnapType; + slider.style.scrollSnapType = 'none'; + slider.scrollTo({ left: target, behavior: 'smooth' }); + // Restored on arrival, or after two seconds if the ride was interrupted - a background tab + // pauses smooth scrolling, and snapping must not come back halfway. + let ticks = 0; + settle = setInterval(() => { + if (Math.abs(slider.scrollLeft - target) < 1 || ++ticks > 25) { clearInterval(settle); settle = null; slider.style.scrollSnapType = snap; } + }, 80); }; const step = direction => { const visible = available(), position = visible.indexOf(active()); @@ -82,7 +98,7 @@ export function initSliders(doc = document) { const resize = new view.ResizeObserver(update); resize.observe(slider); preference.addEventListener('change', () => { if (reduced()) pause(); update(); }, { signal }); schedule(); - cleanups.push(() => { clearTimeout(timer); events.abort(); observer.disconnect(); resize.disconnect(); controls.remove(); slider.classList.remove('slider-controlled'); delete slider.dataset.enhanced; }); + cleanups.push(() => { clearTimeout(timer); clearInterval(settle); slider.style.scrollSnapType = ''; events.abort(); observer.disconnect(); resize.disconnect(); controls.remove(); slider.classList.remove('slider-controlled'); delete slider.dataset.enhanced; }); }); return () => cleanups.forEach(cleanup => cleanup()); }