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:
@@ -25,7 +25,10 @@ function Arrow() {
|
||||
|
||||
export function Divider() {
|
||||
return (
|
||||
<Reveal className="flex items-center justify-center flex-wrap gap-x-8 gap-y-3 pb-5 pt-12 px-[var(--layout-padding-x)] w-full bg-bg-base text-center">
|
||||
<Reveal
|
||||
delay={0.3}
|
||||
className="flex items-center justify-center flex-wrap gap-x-8 gap-y-3 pb-5 pt-12 px-[var(--layout-padding-x)] w-full bg-bg-base text-center"
|
||||
>
|
||||
|
||||
{/* Word + its trailing icon are grouped into one shrink-0 flex unit
|
||||
so flex-wrap only ever breaks BETWEEN pairs, never leaving an
|
||||
|
||||
@@ -33,12 +33,15 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
{/* Social handle — Lora Regular (weight 400) */}
|
||||
<p
|
||||
className="font-normal text-bg-white text-h-small text-center leading-[1.2] whitespace-nowrap shrink-0"
|
||||
<a
|
||||
href="https://www.instagram.com/einfach.produktiv/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-normal text-bg-white text-h-small text-center leading-[1.2] whitespace-nowrap shrink-0 hover:text-brand transition-colors"
|
||||
style={{ fontFamily: "var(--font-lora)", fontWeight: 400 }}
|
||||
>
|
||||
@einfach.produktiv
|
||||
</p>
|
||||
</a>
|
||||
|
||||
{/* Legal links — wrap + center below md instead of a rigid row */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 shrink-0">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NewsletterModal } from "./NewsletterModal";
|
||||
import { useCartFly } from "./CartFly";
|
||||
|
||||
const NAVBAR_HEIGHT = 100; // px — matches h-[6.25rem]
|
||||
const SCROLL_DURATION = 800; // ms
|
||||
const SCROLL_DURATION = 200; // ms
|
||||
|
||||
function smoothScrollTo(targetY: number) {
|
||||
const startY = window.scrollY;
|
||||
@@ -224,13 +224,13 @@ export function Navbar() {
|
||||
const focusables = panel?.querySelectorAll<HTMLElement>(
|
||||
'a[href], button:not([disabled])'
|
||||
);
|
||||
focusables?.[0]?.focus();
|
||||
focusables?.[0]?.focus({ preventScroll: true });
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
setMobileOpen(false);
|
||||
hamburgerRef.current?.focus();
|
||||
hamburgerRef.current?.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Tab" || !focusables || focusables.length === 0) return;
|
||||
@@ -238,10 +238,10 @@ export function Navbar() {
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export function VersandModal({ open, onClose }: { open: boolean; onClose: () =>
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
closeButtonRef.current?.focus();
|
||||
closeButtonRef.current?.focus({ preventScroll: true });
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
@@ -43,10 +43,10 @@ export function VersandModal({ 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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user