diff --git a/packages/website/src/components/butterfly.tsx b/packages/website/src/components/butterfly.tsx index cbc1a7eaa..deee8ad70 100644 --- a/packages/website/src/components/butterfly.tsx +++ b/packages/website/src/components/butterfly.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useRef, useState } from "react"; +import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; // --- Vector math utilities --- @@ -107,10 +107,21 @@ function ButterflyVisual({ direction = "right", heading = 0, }: ButterflyVisualProps) { - const wingStyle = { - animationDelay: `${delay}s`, - animationDuration: `${duration}s`, - }; + const wingStyle = useMemo( + () => ({ + animationDelay: `${delay}s`, + animationDuration: `${duration}s`, + }), + [delay, duration], + ); + + const bodyStyle = useMemo( + () => ({ + animationDelay: `${delay}s`, + animationDuration: `${duration}s`, + }), + [delay, duration], + ); const scaleX = direction === "left" ? -1 : 1; @@ -119,24 +130,28 @@ function ButterflyVisual({ // -30° = horizontal, 0° = 30° down (base), +90° = 120° down (nearly straight down) const clampedHeading = Math.max(-30, Math.min(90, heading)); + const svgStyle = useMemo( + () => ({ + filter: "url(#painterly)", + transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`, + transition: "transform 0.3s ease-out", + }), + [scaleX, clampedHeading], + ); + + const farWingStyle = useMemo( + () => ({ ...wingStyle, animationDelay: `${delay + 0.08}s` }), + [wingStyle, delay], + ); + + const lowerWingStyle = useMemo( + () => ({ ...wingStyle, animationDelay: `${delay + 0.05}s` }), + [wingStyle, delay], + ); + return ( -
- +
+ @@ -153,7 +168,7 @@ function ButterflyVisual({ {/* Far side wings (darker, behind, slightly rotated) */} @@ -165,10 +180,7 @@ function ButterflyVisual({ - + @@ -358,17 +370,18 @@ export function FloatingButterfly({ }; }, [mounted]); + const containerStyle = useMemo( + () => ({ + ...style, + transform: `translate(${renderState.x}px, ${renderState.y}px)`, + }), + [style, renderState.x, renderState.y], + ); + if (!mounted) return null; return ( -
+
setOpen(false)} />
diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index 09c1ac6c0..8c74ac705 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -27,6 +27,9 @@ import { } from "lucide-react"; import { ClaudeIcon, CodexIcon, SourceControlIcon } from "~/components/mockup"; +const USER_BUBBLE_STYLE = { borderRadius: "16px 2px 16px 16px" }; +const NEW_BADGE_STYLE = { backgroundColor: "rgba(46, 160, 67, 0.2)" }; + // ── Stagger timing ────────────────────────────────────── const D = { @@ -477,7 +480,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
{item.text}
@@ -593,7 +596,7 @@ function ExplorerSidebar() { src/pages New @@ -658,7 +661,7 @@ function ExplorerSidebar() { {file.isNew && ( New @@ -683,53 +686,66 @@ const SYNCED_LOADER_DURATION_MS = 950; const DOT_COUNT = 6; const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step +interface SyncedLoaderDotProps { + dotIndex: number; + dotSize: number; + gap: number; +} + +function SyncedLoaderDot({ dotIndex, dotSize, gap }: SyncedLoaderDotProps) { + const col = dotIndex % 2; + const row = Math.floor(dotIndex / 2); + const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]); + const delayMs = -(sequenceIndex * STEP_MS); + const style = React.useMemo( + () => ({ + position: "absolute" as const, + left: col * (dotSize + gap), + top: row * (dotSize + gap), + width: dotSize, + height: dotSize, + borderRadius: "50%", + backgroundColor: "#f59e0b", + animationName: "synced-snake-dot", + animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`, + animationTimingFunction: "linear", + animationIterationCount: "infinite", + animationDelay: `${delayMs}ms`, + }), + [col, row, dotSize, gap, delayMs], + ); + return
; +} + function SyncedLoader({ size = 11 }: { size?: number }) { const gap = Math.max(1, Math.round(size * 0.12)); const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3)); const gridW = dotSize * 2 + gap; const gridH = dotSize * 3 + gap * 2; + const outerStyle = React.useMemo( + () => ({ + width: size, + height: size, + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + }), + [size], + ); + + const innerStyle = React.useMemo( + () => ({ position: "relative" as const, width: gridW, height: gridH }), + [gridW, gridH], + ); + return ( -
-
- {Array.from({ length: DOT_COUNT }).map((_, dotIndex) => { - const col = dotIndex % 2; - const row = Math.floor(dotIndex / 2); - // The snake sequence index for this dot: when is it the "head"? - const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]); - // Negative delay syncs the animation so this dot is the head at t=0 when sequenceIndex=0. - // At t=0, headIndex should be 0, meaning sequenceIndex=0 dot is the head. - // Each dot is shifted by sequenceIndex steps back in time. - const delayMs = -(sequenceIndex * STEP_MS); - return ( -
- ); - })} +
+
+ {Array.from({ length: DOT_COUNT }).map((_, dotIndex) => ( + + ))}
); @@ -869,15 +885,14 @@ function DesktopMockup() { return () => window.removeEventListener("resize", updateScale); }, []); + const containerStyle = React.useMemo(() => ({ height: `${1200 * (9 / 16) * scale}px` }), [scale]); + const innerStyle = React.useMemo(() => ({ width: 1200, transform: `scale(${scale})` }), [scale]); + return ( -
+
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 196bdeb4e..d35f9a795 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -1,5 +1,38 @@ import * as React from "react"; -import { motion, AnimatePresence, useInView, useScroll, useTransform } from "framer-motion"; +import { + motion, + AnimatePresence, + useInView, + useScroll, + useTransform, + type Transition, +} from "framer-motion"; + +// Shared motion presets — hoisted so every JSX site receives the same object +// reference and doesn't trigger jsx-no-new-object-as-prop. +const FADE_IN_UP = { opacity: 0, y: 20 }; +const FADE_IN = { opacity: 1, y: 0 }; +const FADE_IN_UP_SMALL = { opacity: 0, y: 16 }; +const FADE_IN_UP_TINY = { opacity: 0, y: -10 }; +const FADE_IN_UP_XL = { opacity: 0, y: 30 }; +const FADE_IN_UP_40 = { opacity: 0, y: 40 }; +const FADE_IN_UP_4 = { opacity: 0, y: 4 }; +const FADE_OUT_UP_4 = { opacity: 0, y: 4 }; + +const EASE_OUT_06: Transition = { duration: 0.6, ease: "easeOut" }; +const EASE_OUT_06_DELAY_015: Transition = { duration: 0.6, delay: 0.15, ease: "easeOut" }; +const EASE_OUT_06_DELAY_01: Transition = { duration: 0.6, delay: 0.1, ease: "easeOut" }; +const EASE_OUT_06_DELAY_04: Transition = { duration: 0.6, delay: 0.4, ease: "easeOut" }; +const EASE_OUT_08_DELAY_05: Transition = { duration: 0.8, delay: 0.5, ease: "easeOut" }; +const EASE_OUT_05: Transition = { duration: 0.5, ease: "easeOut" }; +const EASE_OUT_015: Transition = { duration: 0.15, ease: "easeOut" }; +const DURATION_05: Transition = { duration: 0.5 }; + +const VIEWPORT_60 = { once: true, margin: "-60px" }; +const VIEWPORT_40 = { once: true, margin: "-40px" }; + +const SVG_OVERFLOW_VISIBLE_STYLE = { overflow: "visible" as const }; +const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 1200 }; import { CursorFieldProvider } from "~/components/butterfly"; import { CommandDialog } from "~/components/command-dialog"; import { @@ -38,9 +71,9 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { {/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */}
@@ -203,17 +236,17 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string }) return (
{title} {subtitle} @@ -235,10 +268,10 @@ function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) { {hovered && ( {name} @@ -260,10 +293,10 @@ function FeatureSection({ }) { return (
@@ -278,14 +311,14 @@ function FeatureSection({ function PrinciplesSection() { return (

- Here's what's under the hood. + Here's what's under the hood.

); @@ -510,7 +543,7 @@ function SelfHostedDiagram() { {/* SVG curves */} {[...paths.left, ...paths.right].map( (d, i) => @@ -696,38 +729,41 @@ function ShortcutsSection() { ); } +interface VoiceBarProps { + index: number; + barCount: number; +} + +function VoiceBar({ index, barCount }: VoiceBarProps) { + const style = React.useMemo(() => { + const center = barCount / 2; + const dist = Math.abs(index - center) / center; + const envelope = 1 - dist * dist; + const minH = 4; + const maxH = 56; + const baseH = minH + (maxH - minH) * envelope; + const jitter = Math.sin(index * 2.3) * 0.3 + Math.cos(index * 1.7) * 0.2; + const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(index * 0.8)))); + return { + height: h, + animationName: "voice-bar", + animationDuration: `${800 + (index % 5) * 200}ms`, + animationTimingFunction: "ease-in-out", + animationIterationCount: "infinite", + animationDirection: "alternate" as const, + animationDelay: `${(index % 7) * 80}ms`, + }; + }, [index, barCount]); + return
; +} + function VoiceWaveform() { const barCount = 48; return (
- {Array.from({ length: barCount }).map((_, i) => { - // Create a natural-looking waveform envelope — louder in center, quieter at edges - const center = barCount / 2; - const dist = Math.abs(i - center) / center; - const envelope = 1 - dist * dist; // quadratic falloff - const minH = 4; - const maxH = 56; - const baseH = minH + (maxH - minH) * envelope; - // Vary per-bar so it doesn't look uniform - const jitter = Math.sin(i * 2.3) * 0.3 + Math.cos(i * 1.7) * 0.2; - const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(i * 0.8)))); - - return ( -
- ); - })} + {Array.from({ length: barCount }).map((_, i) => ( + + ))}
); } @@ -925,9 +961,9 @@ function LocalVoiceSection() { function GetStarted() { return (
@@ -1440,13 +1476,24 @@ function PhoneShowcase() { const leftX = useTransform(scrollYProgress, [0.2, 0.6], [0, -slideDistance]); const rightX = useTransform(scrollYProgress, [0.2, 0.6], [0, slideDistance]); + const leftPhoneStyle = React.useMemo( + () => ({ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }), + [sideOpacity, leftX], + ); + const rightPhoneStyle = React.useMemo( + () => ({ opacity: sideOpacity, x: rightX, rotateY: 15, scale: 0.97 }), + [sideOpacity, rightX], + ); + const centerPhoneAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]); + const textAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]); + return (
{/* Arrow + text */} {/* Left phone — rotated to face inward */} - + Paseo sessions list {/* Right phone — rotated to face inward */} - + Paseo diff view

FAQ

@@ -1587,8 +1628,8 @@ function FAQ() { speech providers if you configure them. - Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs as - they normally would. For remote access, you can use the optional{" "} + Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs + as they normally would. For remote access, you can use the optional{" "} end-to-end encrypted relay @@ -1596,7 +1637,7 @@ function FAQ() { Claude Code, Codex, and OpenCode. Each agent runs as its own process using its own CLI. - Paseo doesn't modify or wrap their behavior. + Paseo doesn't modify or wrap their behavior. No. You can run the daemon headless with{" "} @@ -1616,9 +1657,9 @@ function FAQ() { . - Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your traffic), - set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the daemon port - directly. See{" "} + Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your + traffic), set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the + daemon port directly. See{" "} configuration @@ -1626,17 +1667,17 @@ function FAQ() { No. Paseo works in any directory. Worktrees are optional and only relevant if you use git. - You can run agents anywhere you'd normally work. + You can run agents anywhere you'd normally work. -

We can't make promises on behalf of providers.

+

We can't make promises on behalf of providers.

That said, Paseo launches the official first-party CLIs (Claude Code, Codex, OpenCode) - as subprocesses. It doesn't extract tokens or call inference APIs directly. From the - provider's perspective, usage through Paseo is indistinguishable from running the CLI - yourself. + as subprocesses. It doesn't extract tokens or call inference APIs directly. From + the provider's perspective, usage through Paseo is indistinguishable from running + the CLI yourself.

-

I've been using Paseo with all providers for months without issue.

+

I've been using Paseo with all providers for months without issue.

When you launch an agent with the worktree option (from the app, desktop, or CLI), Paseo @@ -1655,17 +1696,17 @@ function FAQ() { function SponsorCTA() { return (

- I built Paseo because I wanted better tools for coding agents on my own setup. It's an - independent open source project, built around freedom of choice and real workflows. If you - like what I'm building, consider becoming a supporter. + I built Paseo because I wanted better tools for coding agents on my own setup. It's + an independent open source project, built around freedom of choice and real workflows. If + you like what I'm building, consider becoming a supporter.

- Mo

diff --git a/packages/website/src/routes/__root.tsx b/packages/website/src/routes/__root.tsx index 61f44db78..90dd98fa8 100644 --- a/packages/website/src/routes/__root.tsx +++ b/packages/website/src/routes/__root.tsx @@ -15,6 +15,10 @@ interface StarsContext { const ReleaseCtx = createContext({ version: "" }); const StarsCtx = createContext({ stars: "" }); +const PLAUSIBLE_INIT_SCRIPT = { + __html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`, +}; + export function useRelease(): ReleaseContext { return useContext(ReleaseCtx); } @@ -67,11 +71,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {