feat(web): restore tall hero + carousel slide + viewport-fixed scroll cue
All checks were successful
Deploy to Production / deploy (push) Successful in 1m0s

Three coordinated tweaks to the landing-page above-the-fold:

1. **Hero padding restored to py-14/sm:py-20/md:py-28** (was py-12/14/16).
   Compressing it for the scroll-cue position fight made the hero feel
   cramped and gave the ParticleHero background less room to breathe.
   With the cue moved out (see #3), there's no reason to shrink the hero.

2. **Step rotator switches to carousel-style horizontal slide.** The
   AnimatePresence transition was a fade+y-shift cross-fade — clean but
   sequential. Now the leaving card slides left out (x:-220) while the
   entering card slides right in (x:220→0), both coexisting in the same
   3D-space and inheriting the same mouse-tilt. The container gets
   `min-h-[240px]` so the absolutely-positioned cards have layout to
   anchor to (claude_desktop_config.json is the tallest at 7 lines).
   Reduced-motion still gets the opacity-only cross-fade — sliding
   content sideways is exactly the kind of motion that preference is
   meant to suppress.

3. **`<ScrollCue>` extracted into its own client component**, fixed-
   positioned at viewport bottom (bottom-5) with a frosted pill style.
   Fades to opacity:0 once `window.scrollY > 80`, so it doesn't shadow
   the rest of the page. Lives next to `<section>` in page.tsx rather
   than inside the hero — that way it anchors to the loadscreen's
   natural bottom edge whether the hero is short or tall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi 2026-05-27 12:11:42 +02:00
parent e4e437c44c
commit 0cf9c66b6b
3 changed files with 73 additions and 19 deletions

View File

@ -1,9 +1,9 @@
import { HeroStepRotator } from '@/components/hero-step-rotator';
import { JsonLd } from '@/components/json-ld';
import { ParticleHero } from '@/components/particle-hero';
import { ScrollCue } from '@/components/scroll-cue';
import { StaticCodeBlock } from '@/components/static-code-block';
import { FAQ, faqJsonLd } from '@/lib/seo';
import { ChevronDown } from 'lucide-react';
import Link from 'next/link';
const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace.
@ -99,7 +99,7 @@ export default function Landing() {
for pointermove on window itself, so the ring still tracks
the cursor through the content above. */}
<ParticleHero />
<div className="relative z-10 mx-auto grid max-w-6xl gap-10 px-6 py-12 sm:py-14 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12 md:py-16">
<div className="relative z-10 mx-auto grid max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12 md:py-28">
<div className="min-w-0">
<span className="mono inline-block rounded-full border border-[--color-border] bg-[--color-bg-elevated] px-2.5 py-0.5 text-[11px] tracking-wide text-[--color-fg-muted]">
v0.1 updated 2026-05-20
@ -149,16 +149,12 @@ export default function Landing() {
<HeroStepRotator />
</div>
</div>
{/* Scroll cue — hints the video section sits directly below. */}
<a
href="#flow"
aria-label="See the flow in action"
className="absolute inset-x-0 bottom-2 z-10 mx-auto flex w-fit items-center gap-1 text-[11px] uppercase tracking-[0.18em] text-[--color-fg-subtle] transition-colors hover:text-[--color-fg-muted]"
>
<span>see it run</span>
<ChevronDown size={12} className="animate-bounce" />
</a>
</section>
{/* Scroll cue fixed at the bottom of the loadscreen rather than
inside the hero, so it sits at the natural lower edge of the
first viewport regardless of how tall the hero ends up. Fades
out once the user has scrolled past the loadscreen. */}
<ScrollCue targetId="flow" />
{/* Flow video full-width edge-to-edge under the hero. The clip
shows the real flow (prompt server schematic live connection

View File

@ -109,23 +109,38 @@ export function HeroStepRotator() {
return (
<div className="flex flex-col items-center gap-5">
{/* Container is relative + has a min-height so the absolutely-
positioned cards inside (during the slide) overlap cleanly
without collapsing the layout. min-h is sized for the tallest
card (claude_desktop_config.json at 7 lines). */}
<div
ref={containerRef}
className="relative w-full max-w-md"
className="relative w-full max-w-md min-h-[240px]"
style={{ perspective: 1200 }}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
onMouseMove={onMove}
>
<AnimatePresence mode="wait" initial={false}>
<AnimatePresence initial={false}>
<motion.div
key={step}
initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 12 }}
animate={reduced ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.97, y: -8 }}
transition={{ duration: reduced ? 0.15 : 0.5, ease: [0.16, 1, 0.3, 1] }}
style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}
className="relative overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated] shadow-2xl shadow-black/50"
// Carousel: current card slides left-out, next slides right-in.
// Both cards coexist briefly in the same 3D-space and inherit
// the same tilt — reads as a single tile that's swapping its
// contents, not two discrete tiles. Reduced-motion collapses
// the slide to a plain opacity cross-fade.
initial={reduced ? { opacity: 0 } : { x: 220, opacity: 0 }}
animate={reduced ? { opacity: 1 } : { x: 0, opacity: 1 }}
exit={reduced ? { opacity: 0 } : { x: -220, opacity: 0 }}
transition={{ duration: reduced ? 0.15 : 0.55, ease: [0.16, 1, 0.3, 1] }}
style={{
rotateX,
rotateY,
position: 'absolute',
inset: 0,
transformStyle: 'preserve-3d',
}}
className="overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated] shadow-2xl shadow-black/50"
>
{/* Cursor-following glow — sits behind the content, additive. */}
<motion.div

View File

@ -0,0 +1,43 @@
'use client';
import { ChevronDown } from 'lucide-react';
import { useEffect, useState } from 'react';
/**
* Scroll cue a fixed pill anchored to the bottom of the viewport that
* points the visitor down to the flow video below the hero. Fades out
* once the user has scrolled past the loadscreen so it doesn't follow
* them around the page.
*
* Lives at z-30 so it sits above the hero content but below modals.
* The frosted pill (backdrop-blur + border) reads clearly against the
* particle background without stealing focus from the H1.
*/
export function ScrollCue({ targetId }: { targetId: string }) {
const [visible, setVisible] = useState(true);
useEffect(() => {
function onScroll() {
setVisible(window.scrollY < 80);
}
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<a
href={`#${targetId}`}
aria-label="See the flow in action"
className={`fixed inset-x-0 bottom-5 z-30 mx-auto flex w-fit items-center gap-1.5 rounded-full border border-[--color-border] px-3 py-1.5 text-[10.5px] uppercase tracking-[0.18em] text-[--color-fg-subtle] backdrop-blur transition-opacity duration-500 hover:text-[--color-fg] ${
visible ? 'opacity-100' : 'pointer-events-none opacity-0'
}`}
style={{
backgroundColor: 'color-mix(in oklab, var(--color-bg-elevated) 75%, transparent)',
}}
>
<span>see it run</span>
<ChevronDown size={12} className="animate-bounce" />
</a>
);
}