Fix orange dot/hero image visibility, arrow alignment, and add a mobile legal-page TOC

- PopIn (Home Hero's brand dot) switched from whileInView to animate — its
  translate-based entrance could push the element off-screen on a narrow
  phone before the IntersectionObserver ever saw it as visible, leaving it
  stuck invisible permanently.
- Hero image: no longer wrapped in Reveal below lg: — whileInView's margin
  meant it stayed at opacity:0 (a white gap above the fold) on short mobile
  viewports until scrolled. Reveal's fade-in kept from lg: up.
- "→ Label" CTA links (Tools.tsx, Blog.tsx) now use a flex row with the
  arrow as its own span instead of a literal inline "→" character, which
  doesn't reliably align to the surrounding text's cap-height.
- Replaced icon-arrow-connector.svg with a new shared StepArrow component
  (inline SVG) across all three step sections — the old asset's color
  couldn't be overridden from outside the SVG file, so it could never
  actually become brand-orange. Bigger and better-shaped below the
  structural breakpoint per feedback.
- Added MobileSectionTOC (SectionTOC.tsx) — a <details> accordion shown
  below lg: on Impressum/Datenschutz/AGB/Widerruf/Versand, which previously
  had no on-page navigation aid at all below lg: (the sidebar TOC is
  `hidden` entirely there).
- Updated the figma-to-nextjs skill with 8 new dated Gotchas from this
  mobile-responsive pass, and expanded Step 6's verification checklist.
- README: new "Mobile responsive pass" section summarizing the above.
This commit is contained in:
Marco
2026-07-24 19:17:56 +00:00
parent bca29ab7a3
commit 6a6d50abdb
16 changed files with 264 additions and 97 deletions
+33
View File
@@ -1408,6 +1408,39 @@ session (low-stock digest, stale-unverified-accounts report, weekly revenue
report, expired-discount-code cleanup) needed no monitor changes of their
own; see the Payload README's "Jobs Queue" section.
## Mobile responsive pass (2026-07-24)
A full-site pass fixing concrete Mobile-width bugs across the Home Hero,
Navbar, Newsletter, cart, `/challenge`, `/todo-cards`, blog detail, and the
legal pages — found by testing on a real phone rather than just resizing a
desktop browser to 768px. The detailed technical lessons (aspect-ratio
distortion from mixing fluid/fixed sizing, `preserveAspectRatio="none"` SVGs,
`whileInView` failing to trigger on short/narrow viewports, etc.) are written
up as dated Gotchas in the `figma-to-nextjs` skill
(`~/.claude/skills/figma-to-nextjs/SKILL.md`) rather than duplicated here —
that's now the reference for "why" on any of this. Two changes are worth
calling out specifically since they add new shared components:
- **`app/components/StepArrow.tsx`** — a small inline-SVG connector arrow,
replacing `/icon-arrow-connector.svg` across all three "how it works" step
sections (`/challenge`, `/todo-cards`, `/newsletter`). The old asset's
color lived in a CSS custom property scoped to the SVG file itself, so it
could never actually become the brand orange used everywhere else once
loaded via `next/image` — converting it to a real component was the only
fix, and having one shared component means a future color/shape/size
tweak is one edit instead of three.
- **`app/components/SectionTOC.tsx`'s `MobileSectionTOC`** — the
Impressum/Datenschutz/AGB/Widerruf/Versand table-of-contents sidebar was
`hidden` entirely below `lg:` (a 360px sidebar genuinely doesn't fit next
to a readable content column below that), which meant Mobile/Tablet had no
on-page navigation aid at all on these often-long pages — exactly where
scanning a long legal document by scrolling is hardest. `MobileSectionTOC`
is a `<details>/<summary>` accordion (no extra JS state needed) sharing the
same scroll-spy "active section" logic (extracted into a `useActiveSection`
hook) as the desktop sidebar nav, rendered as its own element right after
each page's heading — not nested inside the sidebar's `hidden lg:...`
wrapper, which would hide it too regardless of its own `lg:hidden` class.
## Tests
`npm run test:unit` (Vitest, `node` environment, no jsdom/Next.js runtime
+8 -1
View File
@@ -7,7 +7,7 @@ import { Footer } from "../components/Footer";
import { TrustRow } from "../components/TrustRow";
import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC } from "../components/SectionTOC";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
export const metadata: Metadata = {
@@ -39,6 +39,13 @@ export default async function AgbPage() {
<p className="text-body text-text-muted">Stand: Juli 2026</p>
</Reveal>
{/* MobileSectionTOC below lg: only, see SectionTOC.tsx's own
comment. Outside the sidebar's `hidden lg:flex` wrapper below
(that wrapper's `hidden` would hide this too otherwise). */}
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
<MobileSectionTOC sections={headings} />
</div>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
<SectionTOC sections={headings} />
+8 -16
View File
@@ -4,6 +4,7 @@ import Image from "next/image";
import { draftMode } from "next/headers";
import { Footer } from "../components/Footer";
import { Reveal, RevealGroup, RevealItem } from "../components/Reveal";
import { StepArrow } from "../components/StepArrow";
import { TestimonialsGrid } from "../components/TestimonialsGrid";
import { LiveTestimonialsGrid } from "../components/LiveTestimonialsGrid";
import { getTestimonials } from "../lib/payload";
@@ -251,23 +252,14 @@ export default async function ChallengePage() {
</div>
</RevealItem>,
i < steps.length - 1 ? (
// Same /icon-arrow-connector.svg asset and rotate-on-stack
// pattern as todo-cards/newsletter's HowItWorks — this
// used to be its own hand-drawn SVG arrow, inconsistent
// with those two. Always visible (rotated 90° while
// stacked below lg, this page's own structural
// breakpoint) rather than hidden below lg like before.
// Shared StepArrow component (see its own file) — same
// rotate-on-stack pattern as todo-cards/newsletter's
// HowItWorks. Always visible (rotated 90° while stacked
// below lg, this page's own structural breakpoint) rather
// than hidden below lg like before. Bigger below lg:
// (w-8 h-8, was w-6 h-6) per explicit feedback.
<div key={`arrow-${i}`} className="flex items-center justify-center shrink-0 lg:mt-5">
{/* object-contain same preserveAspectRatio="none" fix
as todo-cards/newsletter's HowItWorks; the square
mobile box was stretching this into a blob. */}
<Image
alt=""
src="/icon-arrow-connector.svg"
width={24}
height={24}
className="w-6 h-6 object-contain rotate-90 lg:w-10 lg:h-3 lg:rotate-0"
/>
<StepArrow className="w-8 h-8 rotate-90 lg:w-10 lg:h-4 lg:rotate-0" />
</div>
) : null,
])}
+10 -4
View File
@@ -63,11 +63,14 @@ export async function Blog() {
</p>
</div>
</div>
{/* flex + arrow as its own span see Tools.tsx's comment on
this same fix ('s glyph baseline sits low next to text). */}
<Link
href={featured.href}
className="font-bold text-body text-text-primary whitespace-nowrap hover:text-brand transition-colors"
className="flex items-center gap-1 font-bold text-body text-text-primary whitespace-nowrap hover:text-brand transition-colors"
>
Zum Beitrag
<span aria-hidden></span>
<span>Zum Beitrag</span>
</Link>
</div>
</RevealItem>
@@ -109,11 +112,14 @@ export async function Blog() {
</p>
</div>
</div>
{/* flex + arrow as its own span see the featured post's
own Link above / Tools.tsx's comment on this same fix. */}
<Link
href={post.href}
className="font-bold text-body whitespace-nowrap hover:text-brand transition-colors"
className="flex items-center gap-1 font-bold text-body whitespace-nowrap hover:text-brand transition-colors"
>
Zum Beitrag
<span aria-hidden></span>
<span>Zum Beitrag</span>
</Link>
</div>
</RevealItem>
+37 -17
View File
@@ -2,6 +2,29 @@ import Link from "next/link";
import Image from "next/image";
import { PopIn, Reveal } from "./Reveal";
// Shared between the plain (below lg:) and Reveal-wrapped (lg:+) render —
// see the two call sites' own comment on why this needs two wrappers.
function HeroImage() {
return (
<Image
src="/hero.png"
alt=""
fill
priority
sizes="(min-width: 1024px) 58vw, 100vw"
className="object-cover"
style={{
WebkitMaskImage:
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
WebkitMaskComposite: "destination-in",
maskImage:
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
maskComposite: "intersect",
}}
/>
);
}
export function Hero() {
return (
<section className="bg-bg-base w-full overflow-hidden">
@@ -113,26 +136,23 @@ export function Hero() {
plain box-shadow reads as a hard rectangular edge against the
existing corner/right/bottom mask-gradient fade below, which
looked worse than no shadow at all tried and reverted. */}
{/* No Reveal (fade-in-on-scroll) below lg: whileInView's -80px
viewport margin means the image doesn't fade in until scrolled
that much further into view; on a short mobile viewport this
image sits right at the initial fold, so it stayed at
opacity:0 (a white gap, matching the section's own bg-bg-base)
above the fold until the user scrolled (reported 2026-07-24).
Plain, always-visible image below lg: instead; Reveal's fade
kept from lg: up, where the image is beside the text with
plenty of room and this was never an issue. */}
<div className="order-2 lg:hidden relative w-full aspect-[887/583] max-h-[16rem] md:max-h-[22rem] mt-0 md:-mt-6">
<HeroImage />
</div>
<Reveal
className="order-2 lg:order-none lg:col-span-7 relative w-full aspect-[887/583] max-h-[16rem] md:max-h-[22rem] lg:max-h-none mt-0 md:-mt-6 lg:mt-0"
className="hidden lg:block lg:col-span-7 relative w-full aspect-[887/583]"
delay={0.15}
>
<Image
src="/hero.png"
alt=""
fill
priority
sizes="(min-width: 1024px) 58vw, 100vw"
className="object-cover"
style={{
WebkitMaskImage:
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
WebkitMaskComposite: "destination-in",
maskImage:
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
maskComposite: "intersect",
}}
/>
<HeroImage />
</Reveal>
</div>
+11 -2
View File
@@ -120,8 +120,17 @@ export function PopIn({ children, className, delay = 0 }: RevealProps) {
<motion.span
className={className}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-80px" }}
// animate, not whileInView — its only caller (Hero.tsx's brand dot)
// sits above the fold, already visible on load, so there's no real
// "scrolls into view" moment to gate on. whileInView's -80px viewport
// margin also broke on some phones: the popIn variant's own "hidden"
// state translates x:+140, and on a narrow mobile viewport that could
// push the dot's pre-animation bounding box past the right edge —
// IntersectionObserver then never reports it as visible, so
// whileInView never fires and the dot stays stuck off-screen
// (reported 2026-07-24: dot invisible on a real phone). Firing on
// mount sidesteps that geometry entirely.
animate="show"
variants={popIn}
transition={{ delay }}
>
+69 -18
View File
@@ -10,24 +10,12 @@ export type TOCSection = { id: string; title: string };
// active state away from what was actually clicked.
const CLICK_OVERRIDE_MS = 1000;
// lg:-only sidebar — same "wide fixed-width block next to content" shape
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
// #5): a 360px TOC card plus a readable content column already exceeds
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
// split at Tablet widths.
//
// Generic over `sections` — originally written just for /versand
// (VersandTOC), generalized once /datenschutz needed the identical
// scroll-spy sidebar but driven by CMS-authored headings instead of a
// hardcoded array. Any future long legal/content page reuses this too.
//
// Not sticky itself — Impressum/Datenschutz put an extra card below this
// in the same sidebar column, and if only this <nav> were sticky, the
// card (a plain-flow sibling) would scroll away independently instead of
// travelling with it. The caller wraps whatever the sidebar column
// contains (this alone, or this + more) in `lg:sticky lg:top-32
// lg:self-start` so the whole column moves as one unit.
export function SectionTOC({ sections }: { sections: TOCSection[] }) {
// Shared between SectionTOC (desktop sidebar nav) and MobileSectionTOC
// (below lg: collapsible accordion, added 2026-07-24) — both need the same
// scroll-spy "active" state and click-override handling, just render it
// completely differently, so the logic lives here once instead of being
// duplicated per component.
function useActiveSection(sections: TOCSection[]) {
const [active, setActive] = useState<string>(sections[0]?.id ?? "");
// Not state — read inside the IntersectionObserver callback without
// needing to re-subscribe it on every click, and cleared by its own
@@ -67,6 +55,28 @@ export function SectionTOC({ sections }: { sections: TOCSection[] }) {
}, CLICK_OVERRIDE_MS);
}
return { active, handleClick };
}
// lg:-only sidebar — same "wide fixed-width block next to content" shape
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
// #5): a 360px TOC card plus a readable content column already exceeds
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
// split at Tablet widths.
//
// Generic over `sections` — originally written just for /versand
// (VersandTOC), generalized once /datenschutz needed the identical
// scroll-spy sidebar but driven by CMS-authored headings instead of a
// hardcoded array. Any future long legal/content page reuses this too.
//
// Not sticky itself — every caller wraps this in its own `hidden lg:flex
// ... lg:sticky lg:top-32 lg:self-start` div (Impressum/Datenschutz also
// stack a second "Nachhaltigkeit" card below this in that same wrapper, so
// the sticky behavior has to live on the wrapper for the two to travel
// together as one unit — putting it on this <nav> instead would leave
// that card behind as a plain-flow sibling scrolling past a now-fixed nav).
export function SectionTOC({ sections }: { sections: TOCSection[] }) {
const { active, handleClick } = useActiveSection(sections);
if (sections.length === 0) return null;
return (
@@ -92,3 +102,44 @@ export function SectionTOC({ sections }: { sections: TOCSection[] }) {
</nav>
);
}
// Below lg: only — a collapsible accordion instead of the sidebar nav
// (which is `hidden` entirely below lg:, see SectionTOC's own comment on
// why a real 2-column split doesn't fit there). Added 2026-07-24: these
// legal pages had no on-page navigation aid at all on Mobile/Tablet, which
// is exactly where scanning a long legal document by scrolling is hardest.
// Native <details>/<summary> — no extra open/close state needed, and it
// stays open after a click so jumping between sections doesn't require
// reopening it each time. Render this as its own element in the page
// (typically right after the heading, before the two-column content row),
// not nested inside a parent that's itself `hidden lg:...` — that would
// hide this too regardless of its own lg:hidden class.
export function MobileSectionTOC({ sections }: { sections: TOCSection[] }) {
const { active, handleClick } = useActiveSection(sections);
if (sections.length === 0) return null;
return (
<details className="lg:hidden w-full bg-bg-base border border-border rounded-md p-4 open:pb-2">
<summary className="text-label font-semibold text-text-muted uppercase tracking-wide cursor-pointer select-none">
Inhaltsübersicht
</summary>
<div className="flex flex-col gap-1 mt-3">
{sections.map(({ id, title }) => (
<a
key={id}
href={`#${id}`}
onClick={() => handleClick(id)}
className={
"px-3 py-2 rounded-sm text-body-sm transition-colors border-l-2 " +
(active === id
? "border-toc-active-border bg-bg-muted text-text-primary font-semibold"
: "border-transparent text-text-muted hover:text-text-primary")
}
>
{title}
</a>
))}
</div>
</details>
);
}
+24
View File
@@ -0,0 +1,24 @@
// Shared "how it works" step connector — used by Challenge's, /todo-cards's,
// and /newsletter's ("Impulse & Tipps") step sections. Used to be
// /icon-arrow-connector.svg (a thin gray line+chevron) loaded via next/image;
// replaced 2026-07-24 for two reasons that both needed an inline SVG to fix:
// 1. It read as a faint gray line, not a real arrow, even after the
// object-contain aspect-ratio fix — too thin/subtle at these sizes.
// 2. Its color lives in a `var(--stroke-0, #C9C9C9)` CSS custom property
// that's scoped to the SVG file's own document when loaded via <img
// src>/next/image — un-recolorable from the host page's CSS, so it could
// never actually become the brand orange used everywhere else (buttons,
// checkmarks, icons). Inline SVG sidesteps that entirely.
export function StepArrow({ className }: { className?: string }) {
return (
<svg width="40" height="16" viewBox="0 0 40 16" fill="none" aria-hidden="true" className={className}>
<path
d="M1 8H33M26 14.5L34.5 8L26 1.5"
stroke="#f6a701"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
+9 -2
View File
@@ -71,11 +71,18 @@ export async function Tools() {
{tool.description}
</p>
</div>
{/* flex items-center + arrow as its own span, not inline text
the glyph sits low relative to the surrounding text's
cap-height in the font used here, off-center against the
label if it's just part of the same text node (fixed
2026-07-24, same pattern ProductGrid.tsx's "Mehr
erfahren" link already uses). */}
<Link
href={tool.ctaHref}
className="font-bold leading-normal text-body whitespace-nowrap hover:text-brand transition-colors"
className="flex items-center gap-1 font-bold leading-normal text-body whitespace-nowrap hover:text-brand transition-colors"
>
{tool.ctaLabel}
<span aria-hidden></span>
<span>{tool.ctaLabel}</span>
</Link>
</div>
</RevealItem>
+8 -1
View File
@@ -6,7 +6,7 @@ import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC } from "../components/SectionTOC";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
export const metadata: Metadata = {
@@ -38,6 +38,13 @@ export default async function DatenschutzPage() {
<p className="text-body text-text-muted">Stand: Juli 2026</p>
</Reveal>
{/* MobileSectionTOC below lg: only, see SectionTOC.tsx's own
comment. Outside the sidebar's `hidden lg:flex` wrapper below
(that wrapper's `hidden` would hide this too otherwise). */}
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
<MobileSectionTOC sections={headings} />
</div>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
<SectionTOC sections={headings} />
+8 -1
View File
@@ -6,7 +6,7 @@ import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC } from "../components/SectionTOC";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage, getCompanySettings } from "../lib/payload";
import { AnbieterAngaben, anbieterAngabenHeadings } from "./components/AnbieterAngaben";
@@ -42,6 +42,13 @@ export default async function ImpressumPage() {
<p className="text-body text-text-muted">Angaben gemäß § 5 DDG</p>
</Reveal>
{/* MobileSectionTOC below lg: only, see SectionTOC.tsx's own
comment. Outside the sidebar's `hidden lg:flex` wrapper below
(that wrapper's `hidden` would hide this too otherwise). */}
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
<MobileSectionTOC sections={headings} />
</div>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
<SectionTOC sections={headings} />
+10 -16
View File
@@ -1,6 +1,7 @@
import { Fragment } from "react";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
import { StepArrow } from "../../components/StepArrow";
// Each icon's own real pixel dimensions (not uniformly square) — needed so
// the `h-16 w-auto` sizing below infers the correct aspect ratio instead of
@@ -71,22 +72,15 @@ export function HowItWorks() {
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
</RevealItem>
{i < steps.length - 1 && (
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
// (64px) icon above it — same margin-based centering
// technique as Challenge's step connector and todo-cards'
// identical HowItWorks, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
{/* object-contain see todo-cards/HowItWorks.tsx's own
comment on this same fix: the SVG's preserveAspectRatio
="none" stretched it to fill the square mobile box
instead of keeping its thin-arrow ratio. */}
<Image
alt=""
src="/icon-arrow-connector.svg"
width={24}
height={24}
className="w-6 h-6 object-contain rotate-90 md:w-10 md:h-3 md:rotate-0"
/>
// md:mt-[1.5rem] centers the arrow on the h-16 icon above it,
// same technique as todo-cards'/Challenge's own step
// connector. Below md: pulled up with a negative margin so it
// sits nearer the icon row above it instead of dead-center in
// the whole gap between steps (fixed 2026-07-24, consistency
// with Challenge's icon-at-top layout). Bigger below md:
// (w-8 h-8, was w-6 h-6) per explicit feedback.
<div className="flex items-center justify-center shrink-0 -mt-2 md:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
</div>
)}
</Fragment>
+6 -16
View File
@@ -1,6 +1,7 @@
import { Fragment } from "react";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
import { StepArrow } from "../../components/StepArrow";
// Each icon's own real pixel dimensions (not uniformly square, e.g.
// icon-step-2 is 180x168) — needed so the `h-16 w-auto` sizing below infers
@@ -79,23 +80,12 @@ export function HowItWorks() {
</RevealItem>
{i < steps.length - 1 && (
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
// (64px) icon above it — (64 - arrow's own 12px height) / 2
// (64px) icon above it — (64 - arrow's own 16px height) / 2
// — same margin-based centering technique as Challenge's
// step connector, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
{/* object-contain the SVG has preserveAspectRatio="none"
(viewBox 40x12) and stretches to fill whatever box
it's given; the square w-6 h-6 mobile box squished it
into a blob instead of a thin arrow. contain keeps its
real ratio, letterboxed inside the (still square, so
rotate-90 doesn't shift its footprint) box. */}
<Image
alt=""
src="/icon-arrow-connector.svg"
width={24}
height={24}
className="w-6 h-6 object-contain rotate-90 md:w-10 md:h-3 md:rotate-0"
/>
// step connector, not just the same icon asset. Bigger below
// md: (w-8 h-8, was w-6 h-6) per explicit feedback.
<div className="flex items-center justify-center shrink-0 md:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
</div>
)}
</Fragment>
+8 -1
View File
@@ -1,6 +1,13 @@
import { SectionTOC } from "../../components/SectionTOC";
import { SectionTOC, MobileSectionTOC } from "../../components/SectionTOC";
import { VERSAND_SECTION_IDS } from "./VersandSections";
export function VersandTOC() {
return <SectionTOC sections={[...VERSAND_SECTION_IDS]} />;
}
// Below lg: accordion counterpart — see SectionTOC.tsx's own comment on
// why this needs to be a separate element rather than nested inside
// VersandTOC/the page's `hidden lg:block` sidebar wrapper.
export function MobileVersandTOC() {
return <MobileSectionTOC sections={[...VERSAND_SECTION_IDS]} />;
}
+7 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { VersandSections } from "./components/VersandSections";
import { VersandTOC } from "./components/VersandTOC";
import { VersandTOC, MobileVersandTOC } from "./components/VersandTOC";
import { getShippingSettings } from "../lib/payload";
export const metadata: Metadata = {
@@ -36,6 +36,12 @@ export default async function VersandPage() {
</p>
</Reveal>
{/* MobileSectionTOC counterpart below lg: only, see
SectionTOC.tsx's own comment. */}
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
<MobileVersandTOC />
</div>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:block lg:sticky lg:top-32 lg:self-start">
<VersandTOC />
+8 -1
View File
@@ -7,7 +7,7 @@ import { Footer } from "../components/Footer";
import { TrustRow } from "../components/TrustRow";
import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC } from "../components/SectionTOC";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
export const metadata: Metadata = {
@@ -39,6 +39,13 @@ export default async function WiderrufPage() {
<p className="text-body text-text-muted">Stand: Juli 2026</p>
</Reveal>
{/* MobileSectionTOC below lg: only, see SectionTOC.tsx's own
comment. Outside the sidebar's `hidden lg:flex` wrapper below
(that wrapper's `hidden` would hide this too otherwise). */}
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
<MobileSectionTOC sections={headings} />
</div>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
<SectionTOC sections={headings} />