diff --git a/packages/website/public/blank-page.svg b/packages/website/public/blank-page.svg new file mode 100644 index 0000000000..9ee048ce2c --- /dev/null +++ b/packages/website/public/blank-page.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index f1a0733d32..4204a8d036 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -1,15 +1,95 @@ +import { motion } from "framer-motion"; +import { useCallback, useState } from "react"; +import { + DEFAULT_MOCKUP_STATE, + DESIGN_HEIGHT, + DESIGN_WIDTH, + MOCKUP_STATES, + type MockupStateId, + MockupWindow, +} from "~/components/mockup"; + +const ALT = + "Paseo desktop app with coding agents, a conversation, and a code diff open side by side"; + +const ASPECT_STYLE = { aspectRatio: `${DESIGN_WIDTH} / ${DESIGN_HEIGHT}` }; + +// `tan(atan2(a, b))` is the CSS way to divide one length by another, so the scale +// factor tracks the container with no JS, no layout thrash, and a correct +// server-rendered first paint. +const SCALE_STYLE = { + width: DESIGN_WIDTH, + height: DESIGN_HEIGHT, + transform: `scale(tan(atan2(100cqw, ${DESIGN_WIDTH}px)))`, +}; + +const PILL_TRANSITION = { duration: 0.34, ease: [0.22, 0.61, 0.36, 1] as const }; + export function HeroMockup() { + const [state, setState] = useState(DEFAULT_MOCKUP_STATE); + return ( -
- Paseo desktop app with coding agents, a conversation, and a code diff open side by side +
+
+ {MOCKUP_STATES.map((option) => ( + + ))} +
+ +
+ {/* The window is authored at DESIGN_WIDTH and scaled to fit the hero column. */} +
+
+
+ +
+
+
+
); } + +function StatePill({ + id, + label, + selected, + onSelect, +}: { + id: MockupStateId; + label: string; + selected: boolean; + onSelect: (id: MockupStateId) => void; +}) { + const select = useCallback(() => onSelect(id), [onSelect, id]); + return ( + + ); +} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 87f12f69d9..9443c4f6c7 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -27,20 +27,23 @@ const DURATION_05: Transition = { duration: 0.5 }; const VIEWPORT_60 = { once: true, margin: "-60px" }; const SVG_OVERFLOW_VISIBLE_STYLE = { overflow: "visible" as const }; -const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 1200 }; +// A ~240px-wide phone rotated 15° only foreshortens a couple percent at +// perspective 1200 — it reads as a flat, skewed card. The side phones already +// sit on a correctly projecting plane (the frame and its scaled interior share +// one flattened texture), so the interior just needs the projection to be +// strong enough to see: a tighter perspective gives the trio a real book-fold. +const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 700 }; import { CursorFieldProvider } from "~/components/butterfly"; import { CommandDialog } from "~/components/command-dialog"; import { AGENT_PAGES } from "~/data/agent-pages"; import { appStoreUrl, playStoreUrl, - webAppUrl, getDownloadOptions, useDetectedPlatform, AppleIcon, PlayStoreIcon, TerminalIcon, - GlobeIcon, } from "~/downloads"; import { useRelease } from "~/routes/__root"; import { HeroMockup } from "~/components/hero-mockup"; @@ -52,7 +55,7 @@ import { PiIcon, } from "~/components/agent-icons"; import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; -import { ClaudeIcon } from "~/components/mockup"; +import { ClaudeIcon, MobileChat, MobileDiff, MobileSidebar, PhoneFrame } from "~/components/mockup"; import { FAQItem } from "~/components/faq-item"; import { SiteFooter } from "~/components/site-footer"; import { SiteHeader } from "~/components/site-header"; @@ -68,7 +71,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { {/* Hero section with background image */}
-
+