more animated visuals

This commit is contained in:
Alan Daniel committed 2026-03-13 15:25:08 -04:00
1 parent 5b3f362a2c
commit 433b7484d5
15 files changed
+587 -192

No files matched your search

@@ -103,7 +103,7 @@ export function BuiltWithSupabaseSection() {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="flex items-end justify-between pt-32 pb-8">
<div className="flex items-end justify-between pt-40 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Kickstart your next project <br />
<span className="text-foreground">with production ready templates</span>
@@ -77,7 +77,7 @@ export function CustomerStoriesSection() {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="flex items-end justify-between pt-16 pb-8">
<div className="flex items-end justify-between pt-40 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
How industry leaders <br />{' '}
<span className="text-foreground">are building with Supabase</span>
@@ -87,7 +87,7 @@ export function FeaturesSection() {
{/* Header */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="pt-32 pb-8">
<div className="pt-40 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Everything you need
<br />
@@ -40,7 +40,7 @@ export function HighlightsSection() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
{/* Header */}
<div className="pt-32 pb-8 border-b border-border px-6">
<div className="pt-40 pb-8 border-b border-border px-6">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Built for scale
<br />
@@ -40,7 +40,7 @@ export function SqlEditorSection() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
{/* Header */}
<div className="px-6 pt-32 pb-8 border-b border-border">
<div className="px-6 pt-40 pb-8 border-b border-border">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
The power of
<br />
@@ -41,7 +41,7 @@ export function TableEditorSection() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
{/* Header */}
<div className="px-6 pt-32 pb-8 border-b border-border">
<div className="px-6 pt-40 pb-8 border-b border-border">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Easy to use
<br />
@@ -41,7 +41,7 @@ function TerminalAnimation() {
<span className="ml-2 text-xs text-foreground-muted font-mono uppercase">Terminal</span>
</div>
{/* Lines */}
<div className="px-4 py-8 md:px-6 md:py-10 font-mono text-xs md:text-sm leading-relaxed min-h-[240px]">
<div className="px-4 md:px-6 font-mono text-xs md:text-sm leading-relaxed h-[280px] flex flex-col justify-center">
<AnimatePresence>
{TERMINAL_LINES.slice(0, visibleCount).map((line, i) => (
<motion.div
@@ -21,7 +21,7 @@ export function ExamplesSection() {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="flex items-end justify-between pt-32 pb-8">
<div className="flex items-end justify-between pt-40 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
What you can build <br />
<span className="text-foreground">with Edge Functions</span>
@@ -76,7 +76,7 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl pt-32 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl pt-40 pb-8">
Integrates with the <span className="text-foreground">Supabase ecosystem</span>
</h3>
</div>
@@ -2,6 +2,7 @@
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useState } from 'react'
import { cn } from 'ui'
const LocalDXImage = dynamic(() => import('~/components/Products/Functions/LocalDXImage'))
@@ -9,75 +10,124 @@ const ParityImage = dynamic(() => import('~/components/Products/Functions/Parity
const NpmEcosystem = dynamic(() => import('~/components/Products/Functions/NpmEcosystem'))
const CI = dynamic(() => import('~/components/Products/Functions/CI'))
const cards = [
{
id: 'localDX',
label: 'First-class local dev experience',
paragraph:
'Write code with hot code reloading, a fast Language server for autocompletion, type checking and linting',
image: <LocalDXImage />,
imageClassname: 'pb-8 md:pb-12',
},
{
id: 'parity',
label: 'Dev and Prod parity',
paragraph: (
<>
The open source{' '}
<Link
href="https://github.com/supabase/edge-runtime/"
className="underline hover:text-foreground-light transition-colors"
target="_blank"
>
Edge runtime
</Link>{' '}
runs locally in dev and powers functions in production
</>
),
image: <ParityImage />,
imageClassname: 'pb-8 md:pb-12 pt-7',
},
{
id: 'ecosystem',
label: 'Use any NPM module',
paragraph: 'Tap into the 2+ million modules in the Deno and NPM ecosystem',
image: <NpmEcosystem />,
},
{
id: 'ci',
label: 'Continuous Integration',
paragraph: (
<>
Use the{' '}
<Link
href="https://supabase.com/docs/guides/functions/cicd-workflow"
className="underline hover:text-foreground-light transition-colors"
target="_blank"
>
Supabase CLI with GitHub actions
</Link>{' '}
to preview and deploy your functions along with the rest of your application
</>
),
image: <CI />,
imageClassname: 'pl-2 pt-8 md:pt-12',
},
]
function EcosystemCard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className={cn('flex flex-col border-b border-border', 'col-span-1 md:row-span-2 !border-b-0')}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex-1 relative overflow-hidden min-h-[320px] md:min-h-0">
<NpmEcosystem isHovered={isHovered} />
</div>
<div className="flex flex-col gap-1 px-6 py-5 border-t border-border bg-surface-200">
<h4 className="text-foreground text-sm font-medium">Use any NPM module</h4>
<p className="text-foreground-lighter text-sm">
Tap into the 2+ million modules in the Deno and NPM ecosystem
</p>
</div>
</div>
)
}
const GRID_CLASS: Record<string, string> = {
localDX: 'col-span-1 md:border-r',
parity: 'col-span-1 xl:border-r',
ecosystem: 'col-span-1 md:row-span-2 !border-b-0',
ci: 'col-span-1 md:col-span-2 xl:border-r !border-b-0',
}
function ParityCard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className={cn('flex flex-col border-b border-border', GRID_CLASS['parity'])}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="aspect-[3/2] w-full flex items-center justify-center overflow-hidden">
<ParityImage isHovered={isHovered} />
</div>
<div className="flex flex-col gap-1 px-6 py-5 border-t border-border bg-surface-200">
<h4 className="text-foreground text-sm font-medium">Dev and Prod parity</h4>
<p className="text-foreground-lighter text-sm">
The open source{' '}
<Link
href="https://github.com/supabase/edge-runtime/"
className="underline hover:text-foreground-light transition-colors"
target="_blank"
>
Edge runtime
</Link>{' '}
runs locally in dev and powers functions in production
</p>
</div>
</div>
)
}
function CICard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className={cn('flex flex-col border-b border-border', GRID_CLASS['ci'])}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex-1 flex items-center justify-center overflow-hidden pl-2 pt-8 md:pt-12">
<CI isHovered={isHovered} />
</div>
<div className="flex flex-col gap-1 px-6 py-5 border-t border-border bg-surface-200">
<h4 className="text-foreground text-sm font-medium">Continuous Integration</h4>
<p className="text-foreground-lighter text-sm">
Use the{' '}
<Link
href="https://supabase.com/docs/guides/functions/cicd-workflow"
className="underline hover:text-foreground-light transition-colors"
target="_blank"
>
Supabase CLI with GitHub actions
</Link>{' '}
to preview and deploy your functions along with the rest of your application
</p>
</div>
</div>
)
}
function LocalDXCard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className={cn('flex flex-col border-b border-border', GRID_CLASS['localDX'])}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="aspect-[3/2] w-full flex items-center justify-center overflow-hidden">
<LocalDXImage isHovered={isHovered} />
</div>
<div className="flex flex-col gap-1 px-6 py-5 border-t border-border bg-surface-200">
<h4 className="text-foreground text-sm font-medium">First-class local dev experience</h4>
<p className="text-foreground-lighter text-sm">
Write code with hot code reloading, a fast Language server for autocompletion, type
checking and linting
</p>
</div>
</div>
)
}
export function LocalDXSection() {
return (
<div>
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end pt-32 pb-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end pt-40 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Delightful DX from <br />
<span className="text-foreground">local to production</span>
@@ -100,26 +150,11 @@ export function LocalDXSection() {
{/* Bento grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3">
{cards.map((card, i) => (
<div
key={card.id}
className={cn('flex flex-col border-b border-border', GRID_CLASS[card.id])}
>
<div
className={cn(
'flex-1 flex items-center justify-center overflow-hidden',
card.imageClassname
)}
>
{card.image}
</div>
<div className="flex flex-col gap-1 px-6 py-5 border-t border-border bg-surface-200">
<h4 className="text-foreground text-sm font-medium">{card.label}</h4>
<p className="text-foreground-lighter text-sm">{card.paragraph}</p>
</div>
</div>
))}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 grid-flow-dense">
<LocalDXCard />
<ParityCard />
<CICard />
<EcosystemCard />
</div>
</div>
</div>
@@ -59,7 +59,7 @@ export function ObservabilitySection() {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl pt-32 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl pt-40 pb-8">
Built-in <span className="text-foreground">observability</span>
</h3>
</div>
+126 -32
View File
@@ -1,36 +1,130 @@
import React from 'react'
import CodeWindow from '~/components/CodeWindow'
'use client'
const ciCode = `jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: supabase/setup-cli@v1
with:
version: latest
- run: supabase functions deploy
`
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
const CI = () => (
<div className="w-full h-full relative pl-4 xl:-mb-0 pt-4 sm:pt-4 border-b xl:border-none overflow-hidden">
<CodeWindow
code={ciCode}
lang="yaml"
className="
rounded-r-none rounded-b-none border-r-0 border-b-0
h-full !text-[13px] xl:!text-sm leading-4
[&_.synthax-highlighter]:rounded-b-none
[&_.synthax-highlighter]:rounded-r-none
[&_.synthax-highlighter]:border-r-0
[&_.synthax-highlighter]:border-b-0
[&_.synthax-highlighter]:!pb-8
[&_.synthax-highlighter]:xl:min-h-[240px]
pr-0 pb-0 -bottom-2 xl:-bottom-4
"
/>
</div>
)
const CODE_LINES = [
'jobs:',
' deploy:',
' runs-on: ubuntu-latest',
'',
' steps:',
' - uses: actions/checkout@v4',
' - uses: supabase/setup-cli@v1',
' with:',
' version: latest',
' - run: supabase functions deploy',
]
type TokenType = 'key' | 'value' | 'punct' | 'dim' | 'plain'
interface Token {
text: string
type: TokenType
}
// Colors from supabase-light / supabase-dark shiki themes
const TOKEN_CLASS: Record<TokenType, string> = {
key: '[color:#6b35dc] dark:[color:#bda4ff]',
value: '[color:#f1a10d] dark:[color:#ffcda1]',
punct: '[color:#a0a0a0] dark:[color:#ffffff99]',
dim: '[color:#7e7e7e]',
plain: '[color:#525252] dark:[color:#ffffff]',
}
function tokenizeLine(line: string): Token[] {
if (!line.trim()) return [{ text: '\u00a0', type: 'plain' }]
const indent = line.match(/^(\s*)/)?.[1] ?? ''
const trimmed = line.trimStart()
const tokens: Token[] = []
if (indent) tokens.push({ text: indent, type: 'plain' })
// List item: "- ..."
if (trimmed.startsWith('- ')) {
const rest = trimmed.slice(2)
const ci = rest.indexOf(': ')
tokens.push({ text: '- ', type: 'dim' })
if (ci >= 0) {
tokens.push({ text: rest.slice(0, ci), type: 'key' })
tokens.push({ text: ': ', type: 'punct' })
tokens.push({ text: rest.slice(ci + 2), type: 'value' })
} else {
tokens.push({ text: rest, type: 'value' })
}
return tokens
}
// "key: value"
const ci = trimmed.indexOf(': ')
if (ci >= 0) {
tokens.push({ text: trimmed.slice(0, ci), type: 'key' })
tokens.push({ text: ': ', type: 'punct' })
tokens.push({ text: trimmed.slice(ci + 2), type: 'value' })
return tokens
}
// "key:" only
if (trimmed.endsWith(':')) {
tokens.push({ text: trimmed.slice(0, -1), type: 'key' })
tokens.push({ text: ':', type: 'punct' })
return tokens
}
tokens.push({ text: trimmed, type: 'plain' })
return tokens
}
const containerVariants = {
visible: { transition: { staggerChildren: 0.08 } },
}
const lineVariants = {
hidden: { opacity: 0, x: -8 },
visible: { opacity: 1, x: 0, transition: { duration: 0.15, ease: 'easeOut' } },
}
const CI = ({ isHovered = false }: { isHovered?: boolean }) => {
const [hoverCount, setHoverCount] = useState(0)
useEffect(() => {
if (isHovered) setHoverCount((c) => c + 1)
}, [isHovered])
return (
<div className="w-full h-full relative pl-4 xl:-mb-0 pt-4 sm:pt-4 border-b xl:border-none overflow-hidden">
{/* CodeWindow shell */}
<div className="relative rounded-2xl rounded-r-none rounded-b-none shadow-lg pt-0 w-full h-full bg-alternative-200 border border-r-0 border-b-0 flex flex-col">
{/* Title bar */}
<div className="w-full px-4 py-3 flex items-center gap-1.5 border-b">
<div className="w-2 h-2 bg-border rounded-full" />
<div className="w-2 h-2 bg-border rounded-full" />
<div className="w-2 h-2 bg-border rounded-full" />
</div>
{/* Code area */}
<div className="flex-1 rounded-lg rounded-r-none rounded-b-none overflow-hidden p-4 text-[13px] xl:text-sm font-mono leading-6 bg-surface-75">
<motion.div
key={hoverCount}
initial="hidden"
animate="visible"
variants={containerVariants}
>
{CODE_LINES.map((line, i) => (
<motion.div key={i} variants={lineVariants}>
{tokenizeLine(line).map((token, j) => (
<span key={j} className={TOKEN_CLASS[token.type]}>
{token.text}
</span>
))}
</motion.div>
))}
</motion.div>
</div>
</div>
</div>
)
}
export default CI
@@ -1,31 +1,63 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Check, Copy } from 'lucide-react'
import Image from 'next/image'
import { useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
const LocalDXImage = () => {
const COMMAND = 'supabase functions serve'
const OUTPUT = 'Functions listening on 127.0.0.1:54321/functions/v1/'
const LocalDXImage = ({ isHovered = false }: { isHovered?: boolean }) => {
const [copied, setCopied] = useState(false)
const [visibleChars, setVisibleChars] = useState(COMMAND.length)
const [showOutput, setShowOutput] = useState(false)
const visibleCharsRef = useRef(COMMAND.length)
const timeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([])
const clearTimeouts = () => {
timeoutsRef.current.forEach(clearTimeout)
timeoutsRef.current = []
}
const setChars = (n: number) => {
visibleCharsRef.current = n
setVisibleChars(n)
}
useEffect(() => {
clearTimeouts()
if (isHovered) {
setShowOutput(false)
setChars(0)
COMMAND.split('').forEach((_, i) => {
const t = setTimeout(() => setChars(i + 1), i * 40)
timeoutsRef.current.push(t)
})
// Show output line shortly after typing completes
const t = setTimeout(() => setShowOutput(true), COMMAND.length * 40 + 120)
timeoutsRef.current.push(t)
} else {
setShowOutput(false)
const current = visibleCharsRef.current
for (let i = current; i < COMMAND.length; i++) {
const t = setTimeout(() => setChars(i + 1), (i - current) * 18)
timeoutsRef.current.push(t)
}
}
return clearTimeouts
}, [isHovered])
const handleCopy = () => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1000)
setTimeout(() => setCopied(false), 1000)
}
return (
<div className="relative w-full h-full flex-1 flex items-center justify-center">
<div className="relative w-full h-full flex-1 flex items-center justify-center px-4 xl:px-6">
<Image
src="/images/product/functions/grid-gradient-dark.svg"
alt=""
fill
sizes="100%"
aria-hidden
draggable={false}
className="object-cover absolute z-0 inset-0 hidden dark:block"
/>
<Image
src="/images/product/functions/grid-gradient-light.svg"
src="/images/product/functions/lines-gradient-light.svg"
alt=""
fill
sizes="100%"
@@ -33,14 +65,60 @@ const LocalDXImage = () => {
draggable={false}
className="object-cover absolute z-0 inset-0 dark:hidden block"
/>
<Image
src="/images/product/functions/lines-gradient-dark.svg"
alt=""
fill
sizes="100%"
aria-hidden
draggable={false}
className="object-cover absolute z-0 inset-0 hidden dark:block"
/>
<CopyToClipboard text="supabase functions serve <function-name>">
<button
onClick={handleCopy}
className="p-3 relative z-10 group hover:border-strong flex gap-2 xl:gap-4 items-center bg-alternative-200 rounded-xl border"
className="p-3 relative z-10 w-full group hover:border-strong flex gap-2 items-center bg-alternative-200 rounded-xl border overflow-hidden"
>
<div className="text-foreground-muted text-sm font-mono">$</div>
<div className="text-foreground text-sm font-mono">supabase functions serve</div>
<div className="text-foreground rounded p-1.5 ml-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="text-foreground-muted text-sm font-mono shrink-0">$</div>
{/* Slot: command ↔ output, same position */}
<div className="relative flex-1 min-w-0 text-sm font-mono h-5">
{/* Ghost text stabilises width */}
<span className="invisible select-none whitespace-nowrap">{COMMAND}</span>
<AnimatePresence mode="wait" initial={false}>
{!showOutput ? (
<motion.span
key="cmd"
className="absolute inset-0 flex items-center text-foreground"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15 } }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
{COMMAND.slice(0, visibleChars)}
{isHovered && visibleChars < COMMAND.length && (
<motion.span
animate={{ opacity: [1, 0] }}
transition={{ repeat: Infinity, duration: 0.6, ease: 'linear' }}
className="inline-block w-[1.5px] h-[14px] bg-foreground align-middle ml-px shrink-0"
/>
)}
</motion.span>
) : (
<motion.span
key="output"
className="absolute inset-0 flex items-center gap-1.5 text-foreground-muted truncate"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.2, ease: 'easeOut' } }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
<span className="truncate">{OUTPUT}</span>
</motion.span>
)}
</AnimatePresence>
</div>
<div className="text-foreground rounded p-1.5 ml-2 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
{copied ? (
<span className="text-brand">
<Check className="w-3.5 h-3.5" />
@@ -1,4 +1,6 @@
import React from 'react'
'use client'
import { motion } from 'framer-motion'
const npmList = [
'fetch',
@@ -29,11 +31,17 @@ const npmList = [
'js-yaml',
'typescript',
'jest',
'vitest',
'winston',
'pino',
'debug',
'eslint',
'prettier',
'nodemon',
'tsx',
'dotenv',
'zod',
'valibot',
'@octokit/rest',
'cross-env',
'commander',
@@ -42,82 +50,167 @@ const npmList = [
'chalk',
'colors',
'ora',
'kleur',
'picocolors',
'@aws-sdk',
'@google-cloud/storage',
'axios',
'got',
'ky',
'passport',
'nodemailer',
'resend',
'@sendgrid/mail',
'@supabase/auth-helpers-react',
'mongoose',
'prisma',
'drizzle-orm',
'kysely',
'openai',
'@anthropic-ai/sdk',
'langchain',
'jwt',
'react',
'solid-js',
'svelte',
'mocha',
'autoprefixer',
'@supabase/auth-ui-react',
'gray-matter',
'marked',
'unified',
'rehype',
'remark',
'request',
'prop-types',
'react-dom',
'bluebird',
'p-limit',
'p-queue',
'vue',
'nuxt',
'next',
'tslib',
'ts-node',
'inquirer',
'prompts',
'webpack',
'esbuild',
'rollup',
'classnames',
'clsx',
'body-parser',
'rxjs',
'xstate',
'babel-runtime',
'jquery',
'fetch',
'crypto',
'eslint',
'atob',
'moment',
'express',
'fastify',
'socket.io',
'async',
'lodash',
'underscore',
'ramda',
'validator',
'yup',
'day.js',
'date-fns',
'jsonwebtoken',
'bcrypt',
'minimist',
'chalk',
'stripe',
'@clerk/clerk-js',
'posthog-js',
'sentry',
'datadog-metrics',
'sharp',
'jimp',
'pdf-lib',
'archiver',
'node-cron',
'bull',
'ioredis',
'cheerio',
'playwright',
'puppeteer',
'stytch',
'twilio',
'vonage',
'@slack/web-api',
'discord.js',
'telegraf',
'hono',
'oak',
'itty-router',
'zx',
'execa',
'nanoid',
'ms',
'qs',
'node-fetch',
'undici',
'msw',
'jose',
'qrcode',
'jsdom',
'xlsx',
'csv-parse',
'papaparse',
'yaml',
'bullmq',
'lru-cache',
'keyv',
'deepmerge',
'immer',
'zustand',
'jotai',
]
const NpmEcosystem = () => {
const center = Math.floor(npmList.length / 2)
const maxDistance = center
const NpmEcosystem = ({ isHovered = false }: { isHovered?: boolean }) => {
return (
<div className="relative w-full h-full flex justify-center overflow-hidden group">
<div
className="absolute flex flex-wrap items-start gap-2 opacity-80 dark:opacity-50"
style={{ left: -50, right: -150 }}
>
{npmList.map((module, i) => (
<span
key={`${module}-${i}`}
className="py-1 px-2 rounded-md bg-surface-75 border text-foreground-muted"
>
{module}
</span>
))}
<div className="absolute inset-0 flex justify-center overflow-hidden">
{/* Pills */}
<div className="absolute flex flex-wrap items-start gap-2" style={{ left: -50, right: -150 }}>
{npmList.map((module, i) => {
const distance = Math.abs(i - center)
return (
<motion.span
key={`${module}-${i}`}
className="py-1 px-2 rounded-md bg-surface-75 border text-foreground-muted text-sm"
initial={{ opacity: 0, scale: 0.95 }}
animate={
isHovered
? {
opacity: 1,
scale: 1,
transition: {
delay: distance * 0.012,
duration: 0.2,
ease: 'easeOut',
},
}
: {
opacity: 0,
scale: 0.85,
transition: {
delay: (maxDistance - distance) * 0.008,
duration: 0.15,
ease: 'easeIn',
},
}
}
>
{module}
</motion.span>
)
})}
</div>
{/* Radial gradient fade */}
<div
className="absolute inset-0"
className="absolute inset-0 z-10"
style={{
background: `radial-gradient(70% 50% at 50% 50%, transparent, hsl(var(--background-surface-75))`,
background: `radial-gradient(70% 50% at 50% 50%, transparent, hsl(var(--background-default)))`,
}}
/>
<div className="absolute z-10 inset-0 flex flex-col items-center justify-center font-bold text-7xl uppercase text-foreground-light group-hover:text-foreground transition-colors">
{/* npm logo */}
<div className="absolute z-20 inset-0 flex flex-col items-center justify-center">
<svg
width="111"
height="42"
viewBox="0 0 111 42"
fill="currentColor"
className="text-foreground-light"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M36.1763 37.4452H49.2591V30.8616H62.342V4.52698H36.1763V37.4452ZM49.2591 11.1106H55.8006V24.2779H49.2591V11.1106ZM67.5752 4.52698V30.8616H80.6581V11.1106H87.1995V30.8616H93.7409V11.1106H100.282V30.8616H106.824V4.52698H67.5752ZM4.77734 30.8616H17.8602V11.1106H24.4017V30.8616H30.9431V4.52698H4.77734V30.8616Z" />
@@ -1,7 +1,57 @@
import Image from 'next/image'
import React from 'react'
'use client'
const ParityImage = () => (
import { motion } from 'framer-motion'
import Image from 'next/image'
import { cn } from 'ui'
const SYNC_WIDTH = 48
function SyncLines({ isHovered }: { isHovered: boolean }) {
const shimmerTransition = (delay: number) =>
isHovered
? { duration: 0.7, repeat: Infinity, ease: 'linear' as const, repeatDelay: 1.8, delay }
: { duration: 0.4, ease: 'easeOut' as const }
const tipMask = 'linear-gradient(to right, transparent, black 28%, black 72%, transparent)'
return (
<div
className="flex flex-col gap-1.5 items-center justify-center"
style={{ width: SYNC_WIDTH, transform: 'skewX(-12deg)' }}
>
{/* → green shimmer */}
<div
className="relative w-full h-px bg-border overflow-hidden"
style={{ maskImage: tipMask, WebkitMaskImage: tipMask }}
>
<motion.div
className="absolute inset-0"
style={{
background: 'linear-gradient(to right, transparent 0%, #3ecf8e 50%, transparent 100%)',
}}
animate={isHovered ? { x: ['-100%', '100%'] } : { x: '-100%' }}
transition={shimmerTransition(0)}
/>
</div>
{/* ← purple shimmer */}
<div
className="relative w-full h-px bg-border overflow-hidden"
style={{ maskImage: tipMask, WebkitMaskImage: tipMask }}
>
<motion.div
className="absolute inset-0"
style={{
background: 'linear-gradient(to left, transparent 0%, #bda4ff 50%, transparent 100%)',
}}
animate={isHovered ? { x: ['100%', '-100%'] } : { x: '100%' }}
transition={shimmerTransition(1.0)}
/>
</div>
</div>
)
}
const ParityImage = ({ isHovered = false }: { isHovered?: boolean }) => (
<div className="relative w-full h-full flex items-center justify-center text-sm">
<Image
src="/images/product/functions/lines-gradient-light.svg"
@@ -21,22 +71,67 @@ const ParityImage = () => (
draggable={false}
className="object-cover absolute z-0 inset-0 hidden dark:block"
/>
<div className="relative z-10 p-4 font-mono bg-surface-200 border-2 border-dashed rounded-2xl justify-center items-center gap-1 flex">
<div className="py-2 px-4 bg-alternative-200 rounded-lg shadow border flex-col justify-center items-center">
<div className="text-foreground uppercase tracking-wide">Dev</div>
</div>
<svg width="32" height="7" viewBox="0 0 32 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M27 5.74425C27 6.0204 27.3382 6.21566 27.5774 6.07759L31.4226 3.8575C31.6618 3.71943 31.6618 3.32891 31.4226 3.19084L27.5774 0.970752C27.3382 0.832681 27 1.02794 27 1.30409V5.74425ZM5 1.30409C5 1.02794 4.6618 0.832681 4.42265 0.970752L0.57735 3.19084C0.338204 3.32891 0.338204 3.71943 0.57735 3.8575L4.42265 6.07759C4.6618 6.21566 5 6.0204 5 5.74425V1.30409ZM27.5 3.02417H4.5V4.02417H27.5V3.02417Z"
fill="currentColor"
/>
<path
d="M5 1.30409C5 1.02794 4.6618 0.832681 4.42265 0.970752L0.57735 3.19084C0.338204 3.32891 0.338204 3.71943 0.57735 3.8575L4.42265 6.07759C4.6618 6.21566 5 6.0204 5 5.74425V1.30409ZM27 5.74425C27 6.0204 27.3382 6.21566 27.5774 6.07759L31.4226 3.8575C31.6618 3.71943 31.6618 3.32891 31.4226 3.19084L27.5774 0.970752C27.3382 0.832681 27 1.02794 27 1.30409V5.74425ZM4.5 4.02417H27.5V3.02417H4.5V4.02417Z"
fill="currentColor"
<div className="relative z-10 p-4 font-mono bg-surface-200 rounded-2xl justify-center items-center gap-3 flex">
{/* Animated dashed border */}
<svg
className={cn(
'absolute inset-0 w-full h-full pointer-events-none overflow-visible transition-colors duration-200 text-border'
)}
>
<motion.rect
x="0"
y="0"
width="100%"
height="100%"
rx="16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeDasharray="6 4"
animate={{ strokeDashoffset: isHovered ? [0, -10] : 0 }}
transition={
isHovered
? { duration: 0.4, repeat: Infinity, ease: 'linear' }
: { duration: 0.4, ease: 'easeOut' }
}
/>
</svg>
<div className="py-2 px-4 bg-alternative-200 rounded-lg shadow border flex-col justify-center items-center">
{/* Dev badge — green border + shadow on hover */}
<div
className="py-2 px-4 bg-alternative-200 rounded-lg flex-col justify-center items-center transition-all duration-300"
style={
isHovered
? {
border: '1px solid #3ecf8e',
boxShadow: '0 0 12px 0 #3ecf8e40',
}
: {
border: '1px solid hsl(var(--border-default))',
boxShadow: 'none',
}
}
>
<div className="text-foreground uppercase tracking-wide">Dev</div>
</div>
<SyncLines isHovered={isHovered} />
{/* Prod badge — purple border + shadow on hover */}
<div
className="py-2 px-4 bg-alternative-200 rounded-lg flex-col justify-center items-center transition-all duration-300"
style={
isHovered
? {
border: '1px solid #bda4ff',
boxShadow: '0 0 12px 0 #bda4ff40',
}
: {
border: '1px solid hsl(var(--border-default))',
boxShadow: 'none',
}
}
>
<div className="text-foreground uppercase tracking-wide">Prod</div>
</div>
</div>