Add /widerruf and /agb legal pages, fix newsletter modal scroll jank

- New /widerruf page: CMS-driven content via getLegalPage("widerruf"),
  sidebar TOC + "Nachhaltig handeln" callout, TrustRow, and a download
  card for the Muster-Widerrufsformular PDF attachment.
- New /agb page, same legal-page shell pattern as Impressum/Datenschutz.
- lib/payload.ts: getLegalPage now also resolves the optional `attachment`
  media relation (depth=1) for legal pages that have a downloadable file.
- NewsletterModal: replaced the body overflow/position scroll-lock hacks
  (which fought with the sticky Navbar and the global scroll-smooth CSS)
  with a wheel/touch event interceptor that never touches scroll position
  at all, plus `preventScroll: true` on all modal/drawer focus() calls to
  stop the browser's implicit scrollIntoView from nudging the page for
  elements near the sticky navbar's reserved scroll-padding-top zone.
  Same preventScroll fix applied to VersandModal and the Navbar mobile
  drawer for consistency.
- globals.css: scrollbar-gutter: stable, so a modal's overflow:hidden
  lock never causes a scrollbar-width layout shift.
- Divider: stagger its reveal animation slightly (delay 0.3s) behind the
  section above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-19 19:57:28 +00:00
parent 1b434fe23e
commit 7a3ae0ea53
9 changed files with 270 additions and 20 deletions
+43 -7
View File
@@ -34,13 +34,40 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
const dialogRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
// Body scroll lock while open.
// Background scroll lock while open — intercepts and cancels the wheel/
// touch input that would cause scrolling, instead of toggling
// overflow/position on body or html. Two earlier approaches (plain
// overflow:hidden on body; position:fixed with a negative top offset to
// compensate) each fixed one symptom while causing another — overflow
// alone reset scrollY to 0 for a frame when opened away from the top of
// the page (e.g. parked at #ueber-bjoern), and position:fixed took body
// out of normal flow, which detached the Navbar's `position: sticky`
// from its scrolling container and made it visibly snap. Neither is a
// risk here: scrollY, overflow and layout are never touched at all, so
// there's nothing that can jump, reflow, or need restoring on close —
// the events that would move the page just never get to.
// `{ passive: false }` is required for preventDefault() to have any
// effect on wheel/touchmove. Events that originate inside the dialog
// (which has its own overflow-y-auto) are let through untouched, so the
// modal's own content still scrolls normally.
useEffect(() => {
if (!open) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const isInsideDialog = (target: EventTarget | null) =>
target instanceof Node && !!dialogRef.current?.contains(target);
const onWheel = (e: WheelEvent) => {
if (!isInsideDialog(e.target)) e.preventDefault();
};
const onTouchMove = (e: TouchEvent) => {
if (!isInsideDialog(e.target)) e.preventDefault();
};
document.addEventListener("wheel", onWheel, { passive: false });
document.addEventListener("touchmove", onTouchMove, { passive: false });
return () => {
document.body.style.overflow = prevOverflow;
document.removeEventListener("wheel", onWheel);
document.removeEventListener("touchmove", onTouchMove);
};
}, [open]);
@@ -50,7 +77,16 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
useEffect(() => {
if (!open) return;
closeButtonRef.current?.focus();
// preventScroll: true — otherwise the browser's implicit
// scrollIntoView on focus() sees this button sitting close to the
// viewport's top edge and "corrects" for the global
// scroll-padding-top (reserved for the sticky Navbar, see
// globals.css) by nudging the whole page upward — pointless here
// since the modal is position:fixed and always fully in view
// regardless of document scroll, but that nudge is exactly the
// "page scrolls up a bit and the modal lands in a weird position"
// jank on open.
closeButtonRef.current?.focus({ preventScroll: true });
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
@@ -67,10 +103,10 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
first.focus({ preventScroll: true });
}
};