feat(web): restore tall hero + carousel slide + viewport-fixed scroll cue
All checks were successful
Deploy to Production / deploy (push) Successful in 1m0s
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:
parent
e4e437c44c
commit
0cf9c66b6b
@ -1,9 +1,9 @@
|
|||||||
import { HeroStepRotator } from '@/components/hero-step-rotator';
|
import { HeroStepRotator } from '@/components/hero-step-rotator';
|
||||||
import { JsonLd } from '@/components/json-ld';
|
import { JsonLd } from '@/components/json-ld';
|
||||||
import { ParticleHero } from '@/components/particle-hero';
|
import { ParticleHero } from '@/components/particle-hero';
|
||||||
|
import { ScrollCue } from '@/components/scroll-cue';
|
||||||
import { StaticCodeBlock } from '@/components/static-code-block';
|
import { StaticCodeBlock } from '@/components/static-code-block';
|
||||||
import { FAQ, faqJsonLd } from '@/lib/seo';
|
import { FAQ, faqJsonLd } from '@/lib/seo';
|
||||||
import { ChevronDown } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace.
|
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
|
for pointermove on window itself, so the ring still tracks
|
||||||
the cursor through the content above. */}
|
the cursor through the content above. */}
|
||||||
<ParticleHero />
|
<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">
|
<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]">
|
<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
|
v0.1 — updated 2026-05-20
|
||||||
@ -149,16 +149,12 @@ export default function Landing() {
|
|||||||
<HeroStepRotator />
|
<HeroStepRotator />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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
|
{/* Flow video — full-width edge-to-edge under the hero. The clip
|
||||||
shows the real flow (prompt → server schematic → live connection
|
shows the real flow (prompt → server schematic → live connection
|
||||||
|
|||||||
@ -109,23 +109,38 @@ export function HeroStepRotator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-5">
|
<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
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="relative w-full max-w-md"
|
className="relative w-full max-w-md min-h-[240px]"
|
||||||
style={{ perspective: 1200 }}
|
style={{ perspective: 1200 }}
|
||||||
onMouseEnter={onEnter}
|
onMouseEnter={onEnter}
|
||||||
onMouseLeave={onLeave}
|
onMouseLeave={onLeave}
|
||||||
onMouseMove={onMove}
|
onMouseMove={onMove}
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
<motion.div
|
<motion.div
|
||||||
key={step}
|
key={step}
|
||||||
initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 12 }}
|
// Carousel: current card slides left-out, next slides right-in.
|
||||||
animate={reduced ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
|
// Both cards coexist briefly in the same 3D-space and inherit
|
||||||
exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.97, y: -8 }}
|
// the same tilt — reads as a single tile that's swapping its
|
||||||
transition={{ duration: reduced ? 0.15 : 0.5, ease: [0.16, 1, 0.3, 1] }}
|
// contents, not two discrete tiles. Reduced-motion collapses
|
||||||
style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}
|
// the slide to a plain opacity cross-fade.
|
||||||
className="relative overflow-hidden rounded-lg border border-[--color-border-strong] bg-[--color-bg-elevated] shadow-2xl shadow-black/50"
|
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. */}
|
{/* Cursor-following glow — sits behind the content, additive. */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
43
apps/web/components/scroll-cue.tsx
Normal file
43
apps/web/components/scroll-cue.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user